Refactor PacePlan to CoursePace

fixes LS-2989, LS-2988
flag=pace_plans

test plan:
- Specs should pass
- Enable the course paces account feature flag
- Enable the course paces setting on a course
- All current pacing related features should work as expected

Change-Id: Ibd1d9f3295f624a3f2411c72ad5ace9410d965ca
Reviewed-on: https://gerrit.instructure.com/c/canvas-lms/+/286519
Migration-Review: Jeremy Stanley <jeremy@instructure.com>
Product-Review: Eric Saupe <eric.saupe@instructure.com>
Reviewed-by: Robin Kuss <rkuss@instructure.com>
QA-Review: Robin Kuss <rkuss@instructure.com>
Tested-by: Service Cloud Jenkins <svc.cloudjenkins@instructure.com>
This commit is contained in:
Eric Saupe 2022-03-07 16:35:34 -08:00
parent af20e417dd
commit 6ba814bed7
119 changed files with 1984 additions and 1827 deletions

View File

@ -155,7 +155,7 @@ class BlackoutDatesController < ApplicationController
def require_feature_flag
account = @context.is_a?(Account) ? @context : @context.account
not_found unless account.feature_enabled?(:pace_plans)
not_found unless account.feature_enabled?(:course_paces)
end
def load_blackout_date

View File

@ -333,9 +333,9 @@ class ContextModulesController < ApplicationController
m.save_without_touching_context
Canvas::LiveEvents.module_updated(m) if m.position != order_before[m.id]
end
# Update pace plans if enabled
if @context.account.feature_enabled?(:pace_plans) && @context.enable_pace_plans
@context.pace_plans.primary.find_each(&:create_publish_progress)
# Update course paces if enabled
if @context.account.feature_enabled?(:course_paces) && @context.enable_course_paces
@context.course_paces.primary.find_each(&:create_publish_progress)
end
@context.touch

View File

@ -17,26 +17,26 @@
# You should have received a copy of the GNU Affero General Public License along
# with this program. If not, see <http://www.gnu.org/licenses/>.
class PacePlansController < ApplicationController
class CoursePacesController < ApplicationController
before_action :load_context
before_action :load_course
before_action :require_feature_flag
before_action :authorize_action
before_action :load_pace_plan, only: %i[api_show publish update]
before_action :load_course_pace, only: %i[api_show publish update]
include Api::V1::Course
include Api::V1::Progress
include K5Mode
def index
@pace_plan = @context.pace_plans.primary.first
@course_pace = @context.course_paces.primary.first
if @pace_plan.nil?
@pace_plan = @context.pace_plans.new
if @course_pace.nil?
@course_pace = @context.course_paces.new
@context.context_module_tags.not_deleted.each do |module_item|
next unless module_item.assignment
@pace_plan.pace_plan_module_items.new module_item: module_item, duration: 0
@course_pace.course_pace_module_items.new module_item: module_item, duration: 0
end
end
@ -47,7 +47,7 @@ class PacePlansController < ApplicationController
if progress.delayed_job.present?
progress.delayed_job.update(run_at: Time.now)
else
progress = publish_pace_plan
progress = publish_course_pace
end
end
progress_json = progress_json(progress, @current_user, session)
@ -58,33 +58,33 @@ class PacePlansController < ApplicationController
COURSE: course_json(@context, @current_user, session, [], nil),
ENROLLMENTS: enrollments_json(@context),
SECTIONS: sections_json(@context),
PACE_PLAN: PacePlanPresenter.new(@pace_plan).as_json,
PACE_PLAN_PROGRESS: progress_json,
COURSE_PACE: CoursePacePresenter.new(@course_pace).as_json,
COURSE_PACE_PROGRESS: progress_json,
VALID_DATE_RANGE: CourseDateRange.new(@context)
})
js_bundle :pace_plans
css_bundle :pace_plans
js_bundle :course_paces
css_bundle :course_paces
end
def api_show
progress = latest_progress
progress_json = progress_json(progress, @current_user, session) if progress
render json: {
pace_plan: PacePlanPresenter.new(@pace_plan).as_json,
course_pace: CoursePacePresenter.new(@course_pace).as_json,
progress: progress_json
}
end
def new
@pace_plan = case @context
when Course
@context.pace_plans.primary.not_deleted.take
when CourseSection
@course.pace_plans.for_section(@context).not_deleted.take
when Enrollment
@course.pace_plans.for_user(@context.user).not_deleted.take
end
if @pace_plan.nil?
@course_pace = case @context
when Course
@context.course_paces.primary.not_deleted.take
when CourseSection
@course.course_paces.for_section(@context).not_deleted.take
when Enrollment
@course.course_paces.for_user(@context.user).not_deleted.take
end
if @course_pace.nil?
params = case @context
when Course
{ course_section_id: nil, user_id: nil }
@ -94,73 +94,73 @@ class PacePlansController < ApplicationController
{ user_id: @context.user }
end
# Duplicate a published plan if one exists for the plan or for the course
published_pace_plan = @course.pace_plans.published.where(params).take || @course.pace_plans.primary.published.take
if published_pace_plan
@pace_plan = published_pace_plan.duplicate(params)
published_course_pace = @course.course_paces.published.where(params).take || @course.course_paces.primary.published.take
if published_course_pace
@course_pace = published_course_pace.duplicate(params)
else
@pace_plan = @course.pace_plans.new(params)
@course_pace = @course.course_paces.new(params)
@course.context_module_tags.can_have_assignment.not_deleted.each do |module_item|
@pace_plan.pace_plan_module_items.new module_item: module_item, duration: 0
@course_pace.course_pace_module_items.new module_item: module_item, duration: 0
end
end
end
render json: { pace_plan: PacePlanPresenter.new(@pace_plan).as_json }
render json: { course_pace: CoursePacePresenter.new(@course_pace).as_json }
end
def publish
publish_pace_plan
publish_course_pace
render json: progress_json(@progress, @current_user, session)
end
def create
@pace_plan = @context.pace_plans.new(create_params)
@course_pace = @context.course_paces.new(create_params)
if @pace_plan.save
publish_pace_plan
if @course_pace.save
publish_course_pace
render json: {
pace_plan: PacePlanPresenter.new(@pace_plan).as_json,
course_pace: CoursePacePresenter.new(@course_pace).as_json,
progress: progress_json(@progress, @current_user, session)
}
else
render json: { success: false, errors: @pace_plan.errors.full_messages }, status: :unprocessable_entity
render json: { success: false, errors: @course_pace.errors.full_messages }, status: :unprocessable_entity
end
end
def update
if @pace_plan.update(update_params)
# Force the updated_at to be updated, because if the update just changed the items the pace plan's
if @course_pace.update(update_params)
# Force the updated_at to be updated, because if the update just changed the items the course pace's
# updated_at doesn't get modified
@pace_plan.touch
@course_pace.touch
publish_pace_plan
publish_course_pace
render json: {
pace_plan: PacePlanPresenter.new(@pace_plan).as_json,
course_pace: CoursePacePresenter.new(@course_pace).as_json,
progress: progress_json(@progress, @current_user, session)
}
else
render json: { success: false, errors: @pace_plan.errors.full_messages }, status: :unprocessable_entity
render json: { success: false, errors: @course_pace.errors.full_messages }, status: :unprocessable_entity
end
end
def compress_dates
@pace_plan = @course.pace_plans.new(create_params)
unless @pace_plan.valid?
return render json: { success: false, errors: @pace_plan.errors.full_messages }, status: :unprocessable_entity
@course_pace = @course.course_paces.new(create_params)
unless @course_pace.valid?
return render json: { success: false, errors: @course_pace.errors.full_messages }, status: :unprocessable_entity
end
@pace_plan.course = @course
start_date = params.dig(:pace_plan, :start_date).present? ? Date.parse(params.dig(:pace_plan, :start_date)) : @pace_plan.start_date
@course_pace.course = @course
start_date = params.dig(:course_pace, :start_date).present? ? Date.parse(params.dig(:course_pace, :start_date)) : @course_pace.start_date
if @pace_plan.end_date && start_date && @pace_plan.end_date < start_date
if @course_pace.end_date && start_date && @course_pace.end_date < start_date
return render json: { success: false, errors: "End date cannot be before start date" }, status: :unprocessable_entity
end
compressed_module_items = @pace_plan.compress_dates(save: false, start_date: start_date)
.sort_by { |ppmi| ppmi.module_item.position }
.group_by { |ppmi| ppmi.module_item.context_module }
.sort_by { |context_module, _items| context_module.position }
.to_h.values.flatten
compressed_dates = PacePlanDueDatesCalculator.new(@pace_plan).get_due_dates(compressed_module_items, start_date: start_date)
compressed_module_items = @course_pace.compress_dates(save: false, start_date: start_date)
.sort_by { |ppmi| ppmi.module_item.position }
.group_by { |ppmi| ppmi.module_item.context_module }
.sort_by { |context_module, _items| context_module.position }
.to_h.values.flatten
compressed_dates = CoursePaceDueDatesCalculator.new(@course_pace).get_due_dates(compressed_module_items, start_date: start_date)
render json: compressed_dates.to_json
end
@ -168,7 +168,7 @@ class PacePlansController < ApplicationController
private
def latest_progress
progress = Progress.order(created_at: :desc).find_by(context: @pace_plan, tag: "pace_plan_publish")
progress = Progress.order(created_at: :desc).find_by(context: @course_pace, tag: "course_pace_publish")
progress&.workflow_state == "completed" ? nil : progress
end
@ -205,11 +205,11 @@ class PacePlansController < ApplicationController
end
def require_feature_flag
not_found unless @course.account.feature_enabled?(:pace_plans) && @course.enable_pace_plans
not_found unless @course.account.feature_enabled?(:course_paces) && @course.enable_course_paces
end
def load_pace_plan
@pace_plan = @context.pace_plans.find(params[:id])
def load_course_pace
@course_pace = @context.course_paces.find(params[:id])
end
def load_context
@ -227,19 +227,19 @@ class PacePlansController < ApplicationController
end
def update_params
params.require(:pace_plan).permit(
params.require(:course_pace).permit(
:course_section_id,
:user_id,
:end_date,
:exclude_weekends,
:hard_end_dates,
:workflow_state,
pace_plan_module_items_attributes: %i[id duration module_item_id root_account_id]
course_pace_module_items_attributes: %i[id duration module_item_id root_account_id]
)
end
def create_params
params.require(:pace_plan).permit(
params.require(:course_pace).permit(
:course_id,
:course_section_id,
:user_id,
@ -247,11 +247,11 @@ class PacePlansController < ApplicationController
:exclude_weekends,
:hard_end_dates,
:workflow_state,
pace_plan_module_items_attributes: %i[duration module_item_id root_account_id]
course_pace_module_items_attributes: %i[duration module_item_id root_account_id]
)
end
def publish_pace_plan
@progress = @pace_plan.create_publish_progress(run_at: Time.now)
def publish_course_pace
@progress = @course_pace.create_publish_progress(run_at: Time.now)
end
end

View File

@ -1684,7 +1684,7 @@ class CoursesController < ApplicationController
:homeroom_course_id,
:course_color,
:friendly_name,
:enable_pace_plans
:enable_course_paces
)
changes = changed_settings(@course.changes, @course.settings, old_settings)
@course.delay_if_production(priority: Delayed::LOW_PRIORITY)
@ -2855,10 +2855,10 @@ class CoursesController < ApplicationController
# Elementary account, it will be shown instead of the course name. This setting takes priority over
# course nicknames defined by individual users.
#
# @argument course[enable_pace_plans] [Boolean]
# Enable or disable Pace Plans for the course. This setting only has an effect when the Pace Plans feature flag is
# enabled for the sub-account. Otherwise, Pace Plans are always disabled.
# Note: Pace Plans is in active development.
# @argument course[enable_course_paces] [Boolean]
# Enable or disable Course Pacing for the course. This setting only has an effect when the Course Pacing feature flag is
# enabled for the sub-account. Otherwise, Course Pacing are always disabled.
# Note: Course Pacing is in active development.
#
# @example_request
# curl https://<canvas>/api/v1/courses/<course_id> \
@ -3927,7 +3927,7 @@ class CoursesController < ApplicationController
:locale, :integration_id, :hide_final_grades, :hide_distribution_graphs, :hide_sections_on_course_users_page, :lock_all_announcements, :public_syllabus,
:quiz_engine_selected, :public_syllabus_to_auth, :course_format, :time_zone, :organize_epub_by_content_type, :enable_offline_web_export,
:show_announcements_on_home_page, :home_page_announcement_limit, :allow_final_grade_override, :filter_speed_grader_by_student_group, :homeroom_course,
:template, :course_color, :homeroom_course_id, :sync_enrollments_from_homeroom, :friendly_name, :enable_pace_plans, :default_due_time
:template, :course_color, :homeroom_course_id, :sync_enrollments_from_homeroom, :friendly_name, :enable_course_paces, :default_due_time
)
end
end

View File

@ -65,7 +65,7 @@ class ContentTag < ActiveRecord::Base
after_save :clear_discussion_stream_items
after_save :send_items_to_stream
after_save :clear_total_outcomes_cache
after_save :update_pace_plan_module_items
after_save :update_course_pace_module_items
after_create :update_outcome_contexts
include CustomValidations
@ -700,22 +700,22 @@ class ContentTag < ActiveRecord::Base
OutcomeFriendlyDescription.active.find_by(context: context, learning_outcome_id: content_id)&.destroy
end
def update_pace_plan_module_items
def update_course_pace_module_items
course = context.is_a?(Course) ? context : context.try(:course)
return unless course
course.pace_plans.primary.find_each do |pace_plan|
ppmi = pace_plan.pace_plan_module_items.find_by(module_item_id: id)
ppmi ||= pace_plan.pace_plan_module_items.create(module_item_id: id, duration: 0) unless deleted?
# Pace plans takes over how and when assignment overrides are managed so if we are deleting an assignment from
course.course_paces.primary.find_each do |course_pace|
ppmi = course_pace.course_pace_module_items.find_by(module_item_id: id)
ppmi ||= course_pace.course_pace_module_items.create(module_item_id: id, duration: 0) unless deleted?
# Course paces takes over how and when assignment overrides are managed so if we are deleting an assignment from
# a module we need to reset it back to an untouched state with regards to overrides.
if deleted?
ppmi&.destroy
ppmi&.module_item&.assignment&.assignment_overrides&.destroy_all
end
# Republish the pace plan if changes were made
pace_plan.create_publish_progress if deleted? || ppmi.saved_change_to_id? || saved_change_to_position?
# Republish the course pace if changes were made
course_pace.create_publish_progress if deleted? || ppmi.saved_change_to_id? || saved_change_to_position?
end
end
end

View File

@ -254,7 +254,7 @@ class Course < ActiveRecord::Base
has_many :comment_bank_items, inverse_of: :course
has_many :pace_plans
has_many :course_paces
has_many :blackout_dates, as: :context, inverse_of: :context
prepend Profile::Association
@ -2943,7 +2943,7 @@ class Course < ActiveRecord::Base
TAB_COLLABORATIONS_NEW = 17
TAB_RUBRICS = 18
TAB_SCHEDULE = 19
TAB_PACE_PLANS = 20
TAB_COURSE_PACES = 20
CANVAS_K6_TAB_IDS = [TAB_HOME, TAB_ANNOUNCEMENTS, TAB_GRADES, TAB_MODULES].freeze
COURSE_SUBJECT_TAB_IDS = [TAB_HOME, TAB_SCHEDULE, TAB_MODULES, TAB_GRADES, TAB_GROUPS].freeze
@ -3103,27 +3103,27 @@ class Course < ActiveRecord::Base
def uncached_tabs_available(user, opts)
# make sure t() is called before we switch to the secondary, in case we update the user's selected locale in the process
course_subject_tabs = elementary_subject_course? && opts[:course_subject_tabs]
pace_plans_allowed = false
course_paces_allowed = false
default_tabs = if elementary_homeroom_course?
Course.default_homeroom_tabs
elsif course_subject_tabs
Course.course_subject_tabs
elsif elementary_subject_course?
pace_plans_allowed = true
course_paces_allowed = true
Course.elementary_course_nav_tabs
else
pace_plans_allowed = true
course_paces_allowed = true
Course.default_tabs
end
# can't manage people in template courses
default_tabs.delete_if { |t| t[:id] == TAB_PEOPLE } if template?
# only show pace plans if enabled
if pace_plans_allowed && account.feature_enabled?(:pace_plans) && enable_pace_plans
# only show course paces if enabled
if course_paces_allowed && account.feature_enabled?(:course_paces) && enable_course_paces
default_tabs.insert(default_tabs.index { |t| t[:id] == TAB_MODULES } + 1, {
id: TAB_PACE_PLANS,
label: t("#tabs.pace_plans", "Pace Plans"),
css_class: "pace_plans",
href: :course_pace_plans_path,
id: TAB_COURSE_PACES,
label: t("#tabs.course_paces", "Course Pacing"),
css_class: "course_paces",
href: :course_course_paces_path,
visibility: "admins"
})
end
@ -3241,7 +3241,7 @@ class Course < ActiveRecord::Base
# remove tabs that the user doesn't have access to
unless opts[:for_reordering]
delete_unless.call([TAB_HOME, TAB_ANNOUNCEMENTS, TAB_PAGES, TAB_OUTCOMES, TAB_CONFERENCES, TAB_COLLABORATIONS, TAB_MODULES, TAB_PACE_PLANS], :read, :manage_content)
delete_unless.call([TAB_HOME, TAB_ANNOUNCEMENTS, TAB_PAGES, TAB_OUTCOMES, TAB_CONFERENCES, TAB_COLLABORATIONS, TAB_MODULES, TAB_COURSE_PACES], :read, :manage_content)
member_only_tabs = tabs.select { |t| t[:visibility] == "members" }
tabs -= member_only_tabs if member_only_tabs.present? && !check_for_permission.call(:participate_as_student, :read_as_admin)
@ -3423,7 +3423,7 @@ class Course < ActiveRecord::Base
add_setting :syllabus_course_summary, boolean: true, default: true
add_setting :syllabus_updated_at
add_setting :enable_pace_plans, boolean: true, default: false
add_setting :enable_course_paces, boolean: true, default: false
add_setting :usage_rights_required, boolean: true, default: false, inherited: true

View File

@ -18,17 +18,17 @@
# with this program. If not, see <http://www.gnu.org/licenses/>.
#
class PacePlan < ActiveRecord::Base
class CoursePace < ActiveRecord::Base
include Workflow
include Canvas::SoftDeletable
extend RootAccountResolver
resolves_root_account through: :course
belongs_to :course, inverse_of: :pace_plans
has_many :pace_plan_module_items, dependent: :destroy
belongs_to :course, inverse_of: :course_paces
has_many :course_pace_module_items, dependent: :destroy
accepts_nested_attributes_for :pace_plan_module_items, allow_destroy: true
accepts_nested_attributes_for :course_pace_module_items, allow_destroy: true
belongs_to :course_section
belongs_to :user
@ -70,25 +70,25 @@ class PacePlan < ActiveRecord::Base
published_at: nil,
workflow_state: "unpublished"
}
pace_plan = dup
pace_plan.attributes = default_opts.merge(opts)
course_pace = dup
course_pace.attributes = default_opts.merge(opts)
pace_plan_module_items.each do |pace_plan_module_item|
pace_plan.pace_plan_module_items.new(
module_item_id: pace_plan_module_item.module_item_id,
duration: pace_plan_module_item.duration,
root_account_id: pace_plan_module_item.root_account_id
course_pace_module_items.each do |course_pace_module_item|
course_pace.course_pace_module_items.new(
module_item_id: course_pace_module_item.module_item_id,
duration: course_pace_module_item.duration,
root_account_id: course_pace_module_item.root_account_id
)
end
pace_plan
course_pace
end
def create_publish_progress(run_at: Setting.get("pace_plan_publish_interval", "300").to_i.seconds.from_now)
progress = Progress.create!(context: self, tag: "pace_plan_publish")
def create_publish_progress(run_at: Setting.get("course_pace_publish_interval", "300").to_i.seconds.from_now)
progress = Progress.create!(context: self, tag: "course_pace_publish")
progress.process_job(self, :publish, {
run_at: run_at,
singleton: "pace_plan_publish:#{id}",
singleton: "course_pace_publish:#{id}",
on_conflict: :overwrite
})
progress
@ -99,20 +99,20 @@ class PacePlan < ActiveRecord::Base
Assignment.suspend_due_date_caching do
Assignment.suspend_grading_period_grade_recalculation do
progress&.calculate_completion!(0, student_enrollments.size)
ordered_module_items = pace_plan_module_items.not_deleted
.sort_by { |ppmi| ppmi.module_item.position }
.group_by { |ppmi| ppmi.module_item.context_module }
.sort_by { |context_module, _items| context_module.position }
.to_h.values.flatten
ordered_module_items = course_pace_module_items.not_deleted
.sort_by { |ppmi| ppmi.module_item.position }
.group_by { |ppmi| ppmi.module_item.context_module }
.sort_by { |context_module, _items| context_module.position }
.to_h.values.flatten
student_enrollments.each do |enrollment|
dates =
PacePlanDueDatesCalculator.new(self).get_due_dates(ordered_module_items, enrollment)
pace_plan_module_items.each do |pace_plan_module_item|
content_tag = pace_plan_module_item.module_item
CoursePaceDueDatesCalculator.new(self).get_due_dates(ordered_module_items, enrollment)
course_pace_module_items.each do |course_pace_module_item|
content_tag = course_pace_module_item.module_item
assignment = content_tag.assignment
next unless assignment
due_at = dates[pace_plan_module_item.id]
due_at = dates[course_pace_module_item.id]
user_id = enrollment.user_id
# Check for an old override
@ -177,9 +177,9 @@ class PacePlan < ActiveRecord::Base
end
def compress_dates(save: true, start_date: self.start_date)
PacePlanHardEndDateCompressor.compress(
CoursePaceHardEndDateCompressor.compress(
self,
pace_plan_module_items,
course_pace_module_items,
save: save,
start_date: start_date
)
@ -190,25 +190,25 @@ class PacePlan < ActiveRecord::Base
if user_id
course.student_enrollments.where(user_id: user_id)
elsif course_section_id
student_pace_plan_user_ids = course.pace_plans.where.not(user_id: nil).pluck(:user_id)
course_section.student_enrollments.where.not(user_id: student_pace_plan_user_ids)
student_course_pace_user_ids = course.course_paces.where.not(user_id: nil).pluck(:user_id)
course_section.student_enrollments.where.not(user_id: student_course_pace_user_ids)
else
student_pace_plan_user_ids = course.pace_plans.where.not(user_id: nil).pluck(:user_id)
course_section_pace_plan_section_ids =
course.pace_plans.where.not(course_section: nil).pluck(:course_section_id)
student_course_pace_user_ids = course.course_paces.where.not(user_id: nil).pluck(:user_id)
course_section_course_pace_section_ids =
course.course_paces.where.not(course_section: nil).pluck(:course_section_id)
course
.student_enrollments
.where
.not(user_id: student_pace_plan_user_ids)
.not(user_id: student_course_pace_user_ids)
.where
.not(course_section_id: course_section_pace_plan_section_ids)
.not(course_section_id: course_section_course_pace_section_ids)
end
end
def start_date
student_enrollment = course.student_enrollments.find_by(user_id: user_id) if user_id
# always put pace plan dates in the course time zone
# always put course pace dates in the course time zone
Time.at(
(
student_enrollment&.start_at || course_section&.start_at || course.start_at ||

View File

@ -18,15 +18,15 @@
# with this program. If not, see <http://www.gnu.org/licenses/>.
#
class PacePlanModuleItem < ActiveRecord::Base
belongs_to :pace_plan
class CoursePaceModuleItem < ActiveRecord::Base
belongs_to :course_pace
belongs_to :module_item, class_name: "ContentTag"
belongs_to :root_account, class_name: "Account"
extend RootAccountResolver
resolves_root_account through: :pace_plan
resolves_root_account through: :course_pace
validates :pace_plan, presence: true
validates :course_pace, presence: true
validate :assignable_module_item
scope :active, -> { joins(:module_item).merge(ContentTag.active) }

View File

@ -175,7 +175,7 @@ module Importers
Importers::WikiPageImporter.process_migration_course_outline(data, migration)
Importers::CalendarEventImporter.process_migration(data, migration)
Importers::LtiResourceLinkImporter.process_migration(data, migration)
Importers::PacePlanImporter.process_migration(data, migration)
Importers::CoursePaceImporter.process_migration(data, migration)
everything_selected = !migration.copy_options || migration.is_set?(migration.copy_options[:everything])

View File

@ -20,29 +20,29 @@
require_dependency "importers"
module Importers
class PacePlanImporter < Importer
self.item_class = PacePlan
class CoursePaceImporter < Importer
self.item_class = CoursePace
def self.process_migration(data, migration)
pace_plans = data["pace_plans"] || []
pace_plans.each do |pace_plan|
import_from_migration(pace_plan, migration.context, migration)
course_paces = data["course_paces"] || []
course_paces.each do |course_pace|
import_from_migration(course_pace, migration.context, migration)
end
end
def self.import_from_migration(hash, context, migration)
hash = hash.with_indifferent_access
return unless migration.import_object?("pace_plans", hash[:migration_id])
return unless migration.import_object?("course_paces", hash[:migration_id])
pace_plan = context.pace_plans.primary.where(workflow_state: hash[:workflow_state]).take
pace_plan ||= context.pace_plans.create
course_pace = context.course_paces.primary.find_by(workflow_state: hash[:workflow_state])
course_pace ||= context.course_paces.create
pace_plan.workflow_state = hash[:workflow_state]
pace_plan.end_date = Canvas::Migration::MigratorHelper.get_utc_time_from_timestamp(hash[:end_date])
pace_plan.published_at = Canvas::Migration::MigratorHelper.get_utc_time_from_timestamp(hash[:published_at])
pace_plan.exclude_weekends = hash[:exclude_weekends]
pace_plan.hard_end_dates = hash[:hard_end_dates]
pace_plan.save!
course_pace.workflow_state = hash[:workflow_state]
course_pace.end_date = Canvas::Migration::MigratorHelper.get_utc_time_from_timestamp(hash[:end_date])
course_pace.published_at = Canvas::Migration::MigratorHelper.get_utc_time_from_timestamp(hash[:published_at])
course_pace.exclude_weekends = hash[:exclude_weekends]
course_pace.hard_end_dates = hash[:hard_end_dates]
course_pace.save!
# preload mapping from content tag migration id to id
module_items_by_migration_id = context.context_module_tags.not_deleted
@ -53,9 +53,9 @@ module Importers
module_item_id = module_items_by_migration_id[pp_module_item[:module_item_migration_id]]&.id
next unless module_item_id
pace_plan_module_item = pace_plan.pace_plan_module_items.find_or_create_by(module_item_id: module_item_id)
pace_plan_module_item.duration = pp_module_item[:duration]
pace_plan_module_item.save!
course_pace_module_item = course_pace.course_pace_module_items.find_or_create_by(module_item_id: module_item_id)
course_pace_module_item.duration = pp_module_item[:duration]
course_pace_module_item.save!
end
end
end

View File

@ -21,7 +21,7 @@
class Progress < ActiveRecord::Base
belongs_to :context, polymorphic:
[:content_migration, :course, :account, :group_category, :content_export,
:assignment, :attachment, :epub_export, :sis_batch, :pace_plan,
:assignment, :attachment, :epub_export, :sis_batch, :course_pace,
{ context_user: "User", quiz_statistics: "Quizzes::QuizStatistics" }]
belongs_to :user
belongs_to :delayed_job, class_name: "::Delayed::Job", optional: true

View File

@ -28,7 +28,7 @@ class StudentEnrollment < Enrollment
e.user.enrollments.where.not(id: e.id).active.where(course_id: e.course_id).exists?)
}
after_save :restore_submissions_and_scores
after_save :republish_pace_plan_if_needed
after_save :republish_course_pace_if_needed
def student?
true
@ -132,17 +132,17 @@ class StudentEnrollment < Enrollment
StudentEnrollment.restore_deleted_scores_for_enrollments([self])
end
def republish_pace_plan_if_needed
def republish_course_pace_if_needed
return unless saved_change_to_id? || saved_change_to_start_at?
return unless course.enable_pace_plans?
return unless course.enable_course_paces?
pace_plan = course.pace_plans.published.for_user(user).take || course.pace_plans.published.primary.take
return unless pace_plan
course_pace = course.course_paces.published.for_user(user).take || course.course_paces.published.primary.take
return unless course_pace
pace_plan
course_pace
.delay(
run_at: Setting.get("pace_plan_enrollment_update_republish_interval", "300").to_i.seconds.from_now,
singleton: "pace_plan_republish:#{pace_plan.global_course_id}:#{pace_plan.global_user_id}",
run_at: Setting.get("course_pace_enrollment_update_republish_interval", "300").to_i.seconds.from_now,
singleton: "course_pace_republish:#{course_pace.global_course_id}:#{course_pace.global_user_id}",
on_conflict: :overwrite
)
.publish

View File

@ -0,0 +1,110 @@
# frozen_string_literal: true
#
# Copyright (C) 2021 - present Instructure, Inc.
#
# This file is part of Canvas.
#
# Canvas is free software: you can redistribute it and/or modify it under
# the terms of the GNU Affero General Public License as published by the Free
# Software Foundation, version 3 of the License.
#
# Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# You should have received a copy of the GNU Affero General Public License along
# with this program. If not, see <http://www.gnu.org/licenses/>.
class CoursePacePresenter
include Rails.application.routes.url_helpers
attr_reader :course_pace
def initialize(course_pace)
@course_pace = course_pace
end
def as_json
{
id: course_pace.id,
course_id: course_pace.course_id,
course_section_id: course_pace.course_section_id,
user_id: course_pace.user_id,
workflow_state: course_pace.workflow_state,
start_date: course_pace.start_date,
end_date: course_pace.end_date,
exclude_weekends: course_pace.exclude_weekends,
hard_end_dates: course_pace.hard_end_dates,
created_at: course_pace.created_at,
updated_at: course_pace.updated_at,
published_at: course_pace.published_at,
root_account_id: course_pace.root_account_id,
modules: modules_json,
context_id: context_id,
context_type: context_type
}
end
private
def modules_json
course_pace_module_items.map do |context_module, items|
{
id: context_module.id,
name: context_module.name,
position: context_module.position,
items: items_json(items),
}
end
end
def items_json(items)
return [] unless items
items.map do |ppmi|
module_item = ppmi.module_item
{
id: ppmi.id,
duration: ppmi.duration,
course_pace_id: ppmi.course_pace_id,
root_account_id: ppmi.root_account_id,
module_item_id: module_item.id,
assignment_title: module_item.title,
points_possible: TextHelper.round_if_whole(module_item.try_rescue(:assignment).try_rescue(:points_possible)),
assignment_link: "#{course_url(course_pace.course, only_path: true)}/modules/items/#{module_item.id}",
position: module_item.position,
module_item_type: module_item.content_type,
published: module_item.published?
}
end
end
def context_id
course_pace.user_id || course_pace.course_section_id || course_pace.course_id
end
def context_type
if course_pace.user_id
"Enrollment"
elsif course_pace.course_section_id
"Section"
else
"Course"
end
end
def course_pace_module_items
@course_pace_module_items ||= if course_pace.persisted?
course_pace.course_pace_module_items.joins(:module_item)
.preload(module_item: [:context_module])
.order("content_tags.position ASC")
else
course_pace.course_pace_module_items.sort do |a, b|
a.module_item.position <=> b.module_item.position
end
end.group_by { |ppmi| ppmi.module_item.context_module }
.sort_by { |context_module, _items| context_module.position }
end
end

View File

@ -1,110 +0,0 @@
# frozen_string_literal: true
#
# Copyright (C) 2021 - present Instructure, Inc.
#
# This file is part of Canvas.
#
# Canvas is free software: you can redistribute it and/or modify it under
# the terms of the GNU Affero General Public License as published by the Free
# Software Foundation, version 3 of the License.
#
# Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# You should have received a copy of the GNU Affero General Public License along
# with this program. If not, see <http://www.gnu.org/licenses/>.
class PacePlanPresenter
include Rails.application.routes.url_helpers
attr_reader :pace_plan
def initialize(pace_plan)
@pace_plan = pace_plan
end
def as_json
{
id: pace_plan.id,
course_id: pace_plan.course_id,
course_section_id: pace_plan.course_section_id,
user_id: pace_plan.user_id,
workflow_state: pace_plan.workflow_state,
start_date: pace_plan.start_date,
end_date: pace_plan.end_date,
exclude_weekends: pace_plan.exclude_weekends,
hard_end_dates: pace_plan.hard_end_dates,
created_at: pace_plan.created_at,
updated_at: pace_plan.updated_at,
published_at: pace_plan.published_at,
root_account_id: pace_plan.root_account_id,
modules: modules_json,
context_id: context_id,
context_type: context_type
}
end
private
def modules_json
pace_plan_module_items.map do |context_module, items|
{
id: context_module.id,
name: context_module.name,
position: context_module.position,
items: items_json(items),
}
end
end
def items_json(items)
return [] unless items
items.map do |ppmi|
module_item = ppmi.module_item
{
id: ppmi.id,
duration: ppmi.duration,
pace_plan_id: ppmi.pace_plan_id,
root_account_id: ppmi.root_account_id,
module_item_id: module_item.id,
assignment_title: module_item.title,
points_possible: TextHelper.round_if_whole(module_item.try_rescue(:assignment).try_rescue(:points_possible)),
assignment_link: "#{course_url(pace_plan.course, only_path: true)}/modules/items/#{module_item.id}",
position: module_item.position,
module_item_type: module_item.content_type,
published: module_item.published?
}
end
end
def context_id
pace_plan.user_id || pace_plan.course_section_id || pace_plan.course_id
end
def context_type
if pace_plan.user_id
"Enrollment"
elsif pace_plan.course_section_id
"Section"
else
"Course"
end
end
def pace_plan_module_items
@pace_plan_module_items ||= if pace_plan.persisted?
pace_plan.pace_plan_module_items.joins(:module_item)
.preload(module_item: [:context_module])
.order("content_tags.position ASC")
else
pace_plan.pace_plan_module_items.sort do |a, b|
a.module_item.position <=> b.module_item.position
end
end.group_by { |ppmi| ppmi.module_item.context_module }
.sort_by { |context_module, _items| context_module.position }
end
end

View File

@ -20,18 +20,18 @@
// This is a workaround for an InstUI v7 issue with the Menu component not being
// properly themeable. This should be removed once we're on InstUI v8
[data-position-content='pace-plan-menu'] [role='menu'] {
[data-position-content='course-pace-menu'] [role='menu'] {
max-width: 20rem;
}
// This allows us to animate on height and width, which InstUI Transitions don't
// handle very well.
.pace-plans-collapse {
.course-paces-collapse {
transition: max-height 500ms ease, width 500ms ease;
white-space: nowrap;
}
.pace-plans-module-table {
.course-paces-module-table {
// This prevents the focus outline on the ToggleDetails button from being
// overlapped by the modules Table underneath it
& button {
@ -81,10 +81,10 @@
}
}
#pace-plans-required-end-date-input > span {
#course-paces-required-end-date-input > span {
width: 100%;
}
.pace-plans-assignment-row-points-possible {
.course-paces-assignment-row-points-possible {
color: $ic-color-medium-darker;
}

View File

@ -408,7 +408,7 @@ ul.user_list div.enrollment_type.pending {
.course-conclude-at-row,
.course-participation-row,
.language-row,
.pace-plans-row,
.course-paces-row,
.friendly-name-row {
align-items: flex-start;
padding-top: 0.5rem;
@ -472,7 +472,7 @@ td.center-text {
margin-#{direction(left)}: 18px;
}
.pace-plans-caution-text {
.course-paces-caution-text {
display: flex;
& i {
color: $ic-color-danger;

View File

@ -17,9 +17,9 @@
%>
<%
provide :page_title, t("Pace Plans")
set_active_tab "pace_plans"
js_bundle :pace_plans
provide :page_title, t("Course Pacing")
set_active_tab "course_paces"
js_bundle :course_paces
%>
<div id="pace_plans"></div>
<div id="course_paces"></div>

View File

@ -513,23 +513,23 @@
</div>
</div>
<% end %>
<% if @context.account.feature_enabled?(:pace_plans) %>
<div class="form-row pace-plans-row">
<div class="form-label nobr"><%= f.blabel :pace_plans, :en => "Pace Plans" %></div>
<% if @context.account.feature_enabled?(:course_paces) %>
<div class="form-row course-paces-row">
<div class="form-label nobr"><%= f.blabel :course_paces, :en => "Course Pacing" %></div>
<div class="tall-row">
<div class="nobr">
<%= f.check_box :enable_pace_plans, :disabled => !can_manage %>
<%= f.label :enable_pace_plans, :en => "Enable Pace Plans" %><br/>
<%= f.check_box :enable_course_paces, :disabled => !can_manage %>
<%= f.label :enable_course_paces, :en => "Enable Course Pacing" %><br/>
</div>
<div id="pace_plans_caution_text" class="aside palign pace-plans-caution-text">
<div id="course_paces_caution_text" class="aside palign course-paces-caution-text">
<i class='icon-warning-borderless'></i>
<div>
<p>
<%= t ("Pace Plans is in active development.")%>
<%= t ("Course Pacing is in active development.")%>
</p>
<p>
<%= t do %>
Learn more about this feature in the <a href="<%= t(:'#community.pace_plans') %>" target="_blank">Pace Plans User Group</a>.
Learn more about this feature in the <a href="<%= t(:'#community.pace_plans') %>" target="_blank">Course Pacing User Group</a>.
<% end %>
</p>
</div>

View File

@ -25,13 +25,13 @@ auto_show_cc:
Automatically show closed captions in the user's current
language (if available) when playing canvas media.
applies_to: User
pace_plans:
course_paces:
state: hidden
display_name: 'Pace Plans'
description: Pace Plans adds support for rolling enrollments to Canvas;
display_name: 'Course Pacing'
description: Course Pacing adds support for rolling enrollments to Canvas;
allowing magical distribution of due dates for students with different
start dates based on an instructor's defined pace. Learn more and give
feedback by joining the Pace Plans User Group.
feedback by joining the Course Pacing User Group.
applies_to: Account
beta: true
create_course_subaccount_picker:

View File

@ -455,8 +455,8 @@ CanvasRails::Application.routes.draw do
end
end
get "pace_plans" => "pace_plans#index"
get "blackout_dates" => "blackout_dates#index"
get "course_paces" => "course_paces#index"
post "collapse_all_modules" => "context_modules#toggle_collapse_all"
resources :content_exports, only: %i[create index destroy show]
@ -2383,13 +2383,13 @@ CanvasRails::Application.routes.draw do
put "courses/:course_id/apply_score_to_ungraded_submissions", action: "apply_score_to_ungraded_submissions"
end
scope(controller: :pace_plans) do
post "courses/:course_id/pace_plans", action: :create
get "courses/:course_id/pace_plans/new", action: :new
get "courses/:course_id/pace_plans/:id", action: :api_show
put "courses/:course_id/pace_plans/:id", action: :update
post "courses/:course_id/pace_plans/:id/publish", action: :publish
post "courses/:course_id/pace_plans/compress_dates", action: :compress_dates
scope(controller: :course_paces) do
post "courses/:course_id/course_paces", action: :create
get "courses/:course_id/course_paces/new", action: :new
get "courses/:course_id/course_paces/:id", action: :api_show
put "courses/:course_id/course_paces/:id", action: :update
post "courses/:course_id/course_paces/:id/publish", action: :publish
post "courses/:course_id/course_paces/compress_dates", action: :compress_dates
end
scope(controller: :blackout_dates) do

View File

@ -21,11 +21,15 @@ class AddReplicaIdentityForPacePlans < ActiveRecord::Migration[6.0]
disable_ddl_transaction!
def up
return unless defined?(PacePlan)
add_replica_identity "PacePlan", :root_account_id, 0
remove_index :pace_plans, column: :root_account_id, if_exists: true
end
def down
return unless defined?(PacePlan)
add_index :pace_plans, :root_account_id, algorithm: :concurrently, if_not_exists: true
remove_replica_identity "PacePlan"
end

View File

@ -21,11 +21,15 @@ class AddReplicaIdentityForPacePlanModuleItems < ActiveRecord::Migration[6.0]
disable_ddl_transaction!
def up
return unless defined?(PacePlanModuleItem)
add_replica_identity "PacePlanModuleItem", :root_account_id, 0
remove_index :pace_plan_module_items, column: :root_account_id, if_exists: true
end
def down
return unless defined?(PacePlanModuleItem)
add_index :pace_plan_module_items, :root_account_id, algorithm: :concurrently, if_not_exists: true
remove_replica_identity "PacePlanModuleItem"
end

View File

@ -0,0 +1,48 @@
# frozen_string_literal: true
#
# Copyright (C) 2022 - present Instructure, Inc.
#
# This file is part of Canvas.
#
# Canvas is free software: you can redistribute it and/or modify it under
# the terms of the GNU Affero General Public License as published by the Free
# Software Foundation, version 3 of the License.
#
# Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# You should have received a copy of the GNU Affero General Public License along
# with this program. If not, see <http://www.gnu.org/licenses/>.
#
class CreateCoursePaces < ActiveRecord::Migration[6.0]
tag :predeploy
def up
create_table :course_paces do |t|
t.belongs_to :course, null: false, foreign_key: true
t.references :course_section, null: true, index: false
t.references :user, null: true, index: false
t.string :workflow_state, default: "unpublished", null: false, limit: 255
t.date :end_date
t.boolean :exclude_weekends, null: false, default: true
t.boolean :hard_end_dates, null: false, default: false
t.timestamps
t.datetime :published_at
t.references :root_account, foreign_key: { to_table: :accounts }, limit: 8, null: false, index: false
t.index [:course_id], unique: true, where: "course_section_id IS NULL AND user_id IS NULL AND workflow_state='active'", name: "course_paces_unique_primary_plan_index"
t.index [:course_section_id], unique: true, where: "workflow_state='active'"
t.index [:course_id, :user_id], unique: true, where: "workflow_state='active'"
end
add_replica_identity "CoursePace", :root_account_id, 0
end
def down
drop_table :course_paces
end
end

View File

@ -0,0 +1,39 @@
# frozen_string_literal: true
#
# Copyright (C) 2022 - present Instructure, Inc.
#
# This file is part of Canvas.
#
# Canvas is free software: you can redistribute it and/or modify it under
# the terms of the GNU Affero General Public License as published by the Free
# Software Foundation, version 3 of the License.
#
# Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# You should have received a copy of the GNU Affero General Public License along
# with this program. If not, see <http://www.gnu.org/licenses/>.
#
class CreateCoursePaceModuleItems < ActiveRecord::Migration[6.0]
tag :predeploy
def up
create_table :course_pace_module_items do |t|
t.belongs_to :course_pace, foreign_key: true, index: true
t.integer :duration, null: false, default: 0
t.references :module_item, foreign_key: { to_table: :content_tags }
t.references :root_account, foreign_key: { to_table: :accounts }, limit: 8, null: false, index: false
t.timestamps
end
add_replica_identity "CoursePaceModuleItem", :root_account_id, 0
end
def down
drop_table :course_pace_module_items
end
end

View File

@ -0,0 +1,56 @@
# frozen_string_literal: true
#
# Copyright (C) 2022 - present Instructure, Inc.
#
# This file is part of Canvas.
#
# Canvas is free software: you can redistribute it and/or modify it under
# the terms of the GNU Affero General Public License as published by the Free
# Software Foundation, version 3 of the License.
#
# Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# You should have received a copy of the GNU Affero General Public License along
# with this program. If not, see <http://www.gnu.org/licenses/>.
#
class RemovePacePlans < ActiveRecord::Migration[6.0]
tag :postdeploy
def up
drop_table :pace_plan_module_items
drop_table :pace_plans
end
def down
create_table :pace_plans do |t|
t.belongs_to :course, null: false, foreign_key: true
t.references :course_section, null: true, index: false
t.references :user, null: true, index: false
t.string :workflow_state, default: "unpublished", null: false, limit: 255
t.date :end_date
t.boolean :exclude_weekends, null: false, default: true
t.boolean :hard_end_dates, null: false, default: false
t.timestamps
t.datetime :published_at
t.references :root_account, foreign_key: { to_table: "accounts" }, limit: 8, null: false
t.index [:course_id], unique: true, where: "course_section_id IS NULL AND user_id IS NULL AND workflow_state='active'", name: "pace_plans_unique_primary_plan_index"
t.index [:course_section_id], unique: true, where: "workflow_state='active'"
t.index [:course_id, :user_id], unique: true, where: "workflow_state='active'"
end
add_replica_identity("PacePlan", :root_account_id, 0) if defined?(PacePlan)
create_table :pace_plan_module_items do |t|
t.belongs_to :pace_plans, foreign_key: true, index: true
t.integer :duration, null: false, default: 0
t.references :module_item, foreign_key: { to_table: "content_tags" }
t.references :root_account, foreign_key: { to_table: "accounts" }, limit: 8, null: false
end
add_replica_identity("PacePlan", :root_account_id, 0) if defined?(PacePlanModuleItem)
end
end

View File

@ -21,7 +21,7 @@ module Canvas::Migration::Helpers
class SelectiveContentFormatter
COURSE_SETTING_TYPE = -> { I18n.t("lib.canvas.migration.course_settings", "Course Settings") }
COURSE_SYLLABUS_TYPE = -> { I18n.t("lib.canvas.migration.syllabus_body", "Syllabus Body") }
PACE_PLAN_TYPE = -> { I18n.t("Pace Plan") }
COURSE_PACE_TYPE = -> { I18n.t("Course Pace") }
SELECTIVE_CONTENT_TYPES = [
["context_modules", -> { I18n.t("lib.canvas.migration.context_modules", "Modules") }],
["assignments", -> { I18n.t("lib.canvas.migration.assignments", "Assignments") }],
@ -122,8 +122,8 @@ module Canvas::Migration::Helpers
content_list << { type: "syllabus_body", property: "#{property_prefix}[all_syllabus_body]", title: COURSE_SYLLABUS_TYPE.call }
end
end
if course_data["pace_plans"]
content_list << { type: "pace_plans", property: "#{property_prefix}[all_pace_plans]", title: PACE_PLAN_TYPE.call }
if course_data["course_paces"]
content_list << { type: "course_paces", property: "#{property_prefix}[all_course_paces]", title: COURSE_PACE_TYPE.call }
end
SELECTIVE_CONTENT_TYPES.each do |type2, title|
next unless course_data[type2] && course_data[type2].count > 0
@ -323,7 +323,7 @@ module Canvas::Migration::Helpers
else
content_list << { type: "course_settings", property: "#{property_prefix}[all_course_settings]", title: COURSE_SETTING_TYPE.call }
content_list << { type: "syllabus_body", property: "#{property_prefix}[all_syllabus_body]", title: COURSE_SYLLABUS_TYPE.call }
content_list << { type: "pace_plans", property: "#{property_prefix}[all_pace_plans]", title: PACE_PLAN_TYPE.call } if source.pace_plans.primary.not_deleted.any?
content_list << { type: "course_paces", property: "#{property_prefix}[all_course_paces]", title: COURSE_PACE_TYPE.call } if source.course_paces.primary.not_deleted.any?
SELECTIVE_CONTENT_TYPES.each do |type2, title|
next if type2 == "groups"

View File

@ -27,7 +27,7 @@ module CC
include LearningOutcomes
include Rubrics
include Events
include PacePlans
include CoursePaces
include WebResources
def add_canvas_non_cc_data
@ -40,7 +40,7 @@ module CC
resources = []
resources << run_and_set_progress(:create_course_settings, nil, I18n.t("course_exports.errors.course_settings", "Failed to export course settings"), migration_id) if export_symbol?(:all_course_settings)
resources << run_and_set_progress(:create_module_meta, nil, I18n.t("course_exports.errors.module_meta", "Failed to export module meta data"))
resources << run_and_set_progress(:create_pace_plans, nil, I18n.t("Failed to export pace plans"))
resources << run_and_set_progress(:create_course_paces, nil, I18n.t("Failed to export course paces"))
resources << run_and_set_progress(:create_external_feeds, nil, I18n.t("course_exports.errors.external_feeds", "Failed to export external feeds"))
resources << run_and_set_progress(:create_assignment_groups, nil, I18n.t("course_exports.errors.assignment_groups", "Failed to export assignment groups"))
resources << run_and_set_progress(:create_grading_standards, 20, I18n.t("course_exports.errors.grading_standards", "Failed to export grading standards"))

View File

@ -87,7 +87,7 @@ module CC
LEARNING_OUTCOMES = "learning_outcomes.xml"
MANIFEST = "imsmanifest.xml"
MODULE_META = "module_meta.xml"
PACE_PLANS = "pace_plans.xml"
COURSE_PACES = "course_paces.xml"
RUBRICS = "rubrics.xml"
EXTERNAL_TOOLS = "external_tools.xml"
FILES_META = "files_meta.xml"

View File

@ -18,39 +18,39 @@
# with this program. If not, see <http://www.gnu.org/licenses/>.
#
module CC
module PacePlans
def create_pace_plans(document = nil)
return nil unless @course.pace_plans.primary.not_deleted.any?
module CoursePaces
def create_course_paces(document = nil)
return nil unless @course.course_paces.primary.not_deleted.any?
if document
meta_file = nil
rel_path = nil
else
meta_file = File.new(File.join(@canvas_resource_dir, CCHelper::PACE_PLANS), "w")
rel_path = File.join(CCHelper::COURSE_SETTINGS_DIR, CCHelper::PACE_PLANS)
meta_file = File.new(File.join(@canvas_resource_dir, CCHelper::COURSE_PACES), "w")
rel_path = File.join(CCHelper::COURSE_SETTINGS_DIR, CCHelper::COURSE_PACES)
document = Builder::XmlMarkup.new(target: meta_file, indent: 2)
end
document.instruct!
document.pace_plans(
document.course_paces(
"xmlns" => CCHelper::CANVAS_NAMESPACE,
"xmlns:xsi" => "http://www.w3.org/2001/XMLSchema-instance",
"xsi:schemaLocation" => "#{CCHelper::CANVAS_NAMESPACE} #{CCHelper::XSD_URI}"
) do |pace_plans_node|
@course.pace_plans.primary.not_deleted.each do |pace_plan|
next unless export_object?(pace_plan)
) do |course_paces_node|
@course.course_paces.primary.not_deleted.each do |course_pace|
next unless export_object?(course_pace)
pace_plans_node.pace_plan(identifier: create_key(pace_plan)) do |pace_plan_node|
pace_plan_node.workflow_state pace_plan.workflow_state
pace_plan_node.end_date CCHelper.ims_date(pace_plan.end_date) if pace_plan.end_date
pace_plan_node.published_at CCHelper.ims_datetime(pace_plan.published_at) if pace_plan.published_at
pace_plan_node.exclude_weekends pace_plan.exclude_weekends
pace_plan_node.hard_end_dates pace_plan.hard_end_dates
pace_plan_node.module_items do |module_items_node|
pace_plan.pace_plan_module_items.ordered.each do |pace_plan_module_item|
course_paces_node.course_pace(identifier: create_key(course_pace)) do |course_pace_node|
course_pace_node.workflow_state course_pace.workflow_state
course_pace_node.end_date CCHelper.ims_date(course_pace.end_date) if course_pace.end_date
course_pace_node.published_at CCHelper.ims_datetime(course_pace.published_at) if course_pace.published_at
course_pace_node.exclude_weekends course_pace.exclude_weekends
course_pace_node.hard_end_dates course_pace.hard_end_dates
course_pace_node.module_items do |module_items_node|
course_pace.course_pace_module_items.ordered.each do |course_pace_module_item|
module_items_node.module_item do |module_item_node|
module_item_node.duration pace_plan_module_item.duration
module_item_node.module_item_identifierref create_key(pace_plan_module_item.module_item)
module_item_node.duration course_pace_module_item.duration
module_item_node.module_item_identifierref create_key(course_pace_module_item.module_item)
end
end
end

View File

@ -18,34 +18,34 @@
# with this program. If not, see <http://www.gnu.org/licenses/>.
#
module CC::Importer::Canvas
module PacePlansConverter
module CoursePacesConverter
include CC::Importer
def convert_pace_plans(doc)
pace_plans = []
return pace_plans unless doc
def convert_course_paces(doc)
course_paces = []
return course_paces unless doc
doc.css("pace_plan").each do |pace_plan_node|
pace_plan = {}
pace_plan[:migration_id] = pace_plan_node["identifier"]
pace_plan[:workflow_state] = get_node_val(pace_plan_node, "workflow_state")
pace_plan[:end_date] = get_time_val(pace_plan_node, "end_date")
pace_plan[:published_at] = get_time_val(pace_plan_node, "published_at")
pace_plan[:exclude_weekends] = get_bool_val(pace_plan_node, "exclude_weekends")
pace_plan[:hard_end_dates] = get_bool_val(pace_plan_node, "hard_end_dates")
doc.css("course_pace").each do |course_pace_node|
course_pace = {}
course_pace[:migration_id] = course_pace_node["identifier"]
course_pace[:workflow_state] = get_node_val(course_pace_node, "workflow_state")
course_pace[:end_date] = get_time_val(course_pace_node, "end_date")
course_pace[:published_at] = get_time_val(course_pace_node, "published_at")
course_pace[:exclude_weekends] = get_bool_val(course_pace_node, "exclude_weekends")
course_pace[:hard_end_dates] = get_bool_val(course_pace_node, "hard_end_dates")
pace_plan[:module_items] = []
pace_plan_node.css("module_item").each do |item_node|
course_pace[:module_items] = []
course_pace_node.css("module_item").each do |item_node|
item = {}
item[:duration] = get_int_val(item_node, "duration")
item[:module_item_migration_id] = get_node_val(item_node, "module_item_identifierref")
pace_plan[:module_items] << item
course_pace[:module_items] << item
end
pace_plans << pace_plan
course_paces << course_pace
end
pace_plans
course_paces
end
end
end

View File

@ -23,7 +23,7 @@ module CC::Importer::Canvas
include LearningOutcomesConverter
include RubricsConverter
include ModuleConverter
include PacePlansConverter
include CoursePacesConverter
def settings_doc(file, html = false)
path = @package_root.item_path(COURSE_SETTINGS_DIR, file)
@ -48,7 +48,7 @@ module CC::Importer::Canvas
@course[:grading_standards] = convert_grading_standards(settings_doc(GRADING_STANDARDS))
@course[:learning_outcomes] = convert_learning_outcomes(settings_doc(LEARNING_OUTCOMES))
@course[:modules] = convert_modules(settings_doc(MODULE_META))
@course[:pace_plans] = convert_pace_plans(settings_doc(PACE_PLANS))
@course[:course_paces] = convert_course_paces(settings_doc(COURSE_PACES))
@course[:rubrics] = convert_rubrics(settings_doc(RUBRICS))
@course[:calendar_events] = convert_events(settings_doc(EVENTS))
@course[:late_policy] = convert_late_policy(settings_doc(LATE_POLICY))

View File

@ -396,10 +396,10 @@
</xs:complexType>
</xs:element>
<xs:element name="pace_plans">
<xs:element name="course_paces">
<xs:complexType>
<xs:sequence minOccurs="0" maxOccurs="unbounded">
<xs:element name="pace_plan">
<xs:element name="course_pace">
<xs:complexType>
<xs:all minOccurs="0">
<xs:element name="workflow_state" type="xs:string" minOccurs="0"/>

View File

@ -17,33 +17,33 @@
# You should have received a copy of the GNU Affero General Public License along
# with this program. If not, see <http://www.gnu.org/licenses/>.
#
class PacePlanDueDatesCalculator
attr_reader :pace_plan
class CoursePaceDueDatesCalculator
attr_reader :course_pace
def initialize(pace_plan)
@pace_plan = pace_plan
def initialize(course_pace)
@course_pace = course_pace
end
def get_due_dates(items, enrollment = nil, start_date: nil)
due_dates = {}
start_date = start_date || enrollment&.start_at&.to_date || pace_plan.start_date
start_date = start_date || enrollment&.start_at&.to_date || course_pace.start_date
# We have to make sure we start counting on a day that is enabled
unless PacePlansDateHelpers.day_is_enabled?(start_date, pace_plan.exclude_weekends, blackout_dates)
start_date = PacePlansDateHelpers.first_enabled_day(start_date, pace_plan.exclude_weekends, blackout_dates)
unless CoursePacesDateHelpers.day_is_enabled?(start_date, course_pace.exclude_weekends, blackout_dates)
start_date = CoursePacesDateHelpers.first_enabled_day(start_date, course_pace.exclude_weekends, blackout_dates)
end
items.each do |item|
due_date = PacePlansDateHelpers.add_days(
due_date = CoursePacesDateHelpers.add_days(
start_date,
item.duration,
pace_plan.exclude_weekends,
course_pace.exclude_weekends,
blackout_dates
)
# If the pace plan hasn't been committed yet we need to group the items from their module_item_id or we will
# If the course pace hasn't been committed yet we need to group the items from their module_item_id or we will
# end up grouping them by nil and losing the data for each item as it gets overwritten by the next item.
key = pace_plan.persisted? ? item.id : item.module_item_id
key = course_pace.persisted? ? item.id : item.module_item_id
due_dates[key] = due_date.to_date
start_date = due_date # The next item's start date is this item's due date
end
@ -54,6 +54,6 @@ class PacePlanDueDatesCalculator
private
def blackout_dates
@blackout_dates ||= pace_plan.course.blackout_dates
@blackout_dates ||= course_pace.course.blackout_dates
end
end

View File

@ -18,60 +18,60 @@
# with this program. If not, see <http://www.gnu.org/licenses/>.
#
class PacePlanHardEndDateCompressor
# Takes a list of pace plan module items, compresses them by a specified percentage, and
class CoursePaceHardEndDateCompressor
# Takes a list of course pace module items, compresses them by a specified percentage, and
# validates that they don't extend past the plan length.
ROUNDING_BREAKPOINT = 0.75 # Determines when we round up
# @param pace_plan [PacePlan] the plan you want to compress
# @param items [PacePlanModuleItem[]] The module items you want to compress
# @param course_pace [CoursePace] the plan you want to compress
# @param items [CoursePaceModuleItem[]] The module items you want to compress
# @param enrollment [Enrollment] The enrollment you want to compress the plan for
# @param compress_items_after [integer] an optional integer representing the position of that you want to start at when
# compressing items, rather than compressing them all
# @params save [boolean] set to yes if you want the items saved after being modified.
# @params start_date [Date] the start date of the plan. Used to calculate the number of days to the hard end date
def self.compress(pace_plan, items, enrollment: nil, compress_items_after: nil, save: false, start_date: nil)
def self.compress(course_pace, items, enrollment: nil, compress_items_after: nil, save: false, start_date: nil)
return if compress_items_after && compress_items_after >= items.length - 1
return items if items.empty?
start_date_of_item_group = start_date || enrollment&.start_at || pace_plan.start_date
end_date = pace_plan.end_date || pace_plan.course.end_at&.to_date || pace_plan.course.enrollment_term&.end_at&.to_date
due_dates = PacePlanDueDatesCalculator.new(pace_plan).get_due_dates(items, enrollment, start_date: start_date_of_item_group)
start_date_of_item_group = start_date || enrollment&.start_at || course_pace.start_date
end_date = course_pace.end_date || course_pace.course.end_at&.to_date || course_pace.course.enrollment_term&.end_at&.to_date
due_dates = CoursePaceDueDatesCalculator.new(course_pace).get_due_dates(items, enrollment, start_date: start_date_of_item_group)
if compress_items_after
starting_item = items[compress_items_after]
# The group should start one day after the due date of the previous item
start_date_of_item_group = PacePlansDateHelpers.add_days(
start_date_of_item_group = CoursePacesDateHelpers.add_days(
due_dates[starting_item.id],
1,
pace_plan.exclude_weekends,
pace_plan.course.blackout_dates
course_pace.exclude_weekends,
course_pace.course.blackout_dates
)
items = items[compress_items_after + 1..]
end
# This is how much time the Hard End Date plan should take up
actual_plan_length = PacePlansDateHelpers.days_between(
actual_plan_length = CoursePacesDateHelpers.days_between(
start_date_of_item_group,
end_date,
pace_plan.exclude_weekends,
blackout_dates: pace_plan.course.blackout_dates
course_pace.exclude_weekends,
blackout_dates: course_pace.course.blackout_dates
)
# If the pace plan hasn't been committed yet we are grouping the items by their module_item_id since the item.id is
# If the course pace hasn't been committed yet we are grouping the items by their module_item_id since the item.id is
# not set yet.
key = pace_plan.persisted? ? items[-1].id : items[-1].module_item_id
key = course_pace.persisted? ? items[-1].id : items[-1].module_item_id
final_item_due_date = due_dates[key]
# Return if we are already within the end of the pace plan
# Return if we are already within the end of the course pace
return items if end_date.blank? || final_item_due_date < end_date
# This is how much time we're currently using
plan_length_with_items = PacePlansDateHelpers.days_between(
plan_length_with_items = CoursePacesDateHelpers.days_between(
start_date_of_item_group,
start_date_of_item_group > final_item_due_date ? start_date_of_item_group : final_item_due_date,
pace_plan.exclude_weekends,
blackout_dates: pace_plan.course.blackout_dates
course_pace.exclude_weekends,
blackout_dates: course_pace.course.blackout_dates
)
# This is the percentage that we should modify the plan by, so it hits our specified end date
@ -82,19 +82,19 @@ class PacePlanHardEndDateCompressor
items = update_item_durations(items, rounded_durations, save)
# when compressing heavily, the final due date can end up being after the pace plan hard end date
# when compressing heavily, the final due date can end up being after the course pace hard end date
# adjust later module items
new_due_dates = PacePlanDueDatesCalculator.new(pace_plan).get_due_dates(items, enrollment, start_date: start_date_of_item_group)
# If the pace plan hasn't been committed yet we are grouping the items by their module_item_id since the item.id is
new_due_dates = CoursePaceDueDatesCalculator.new(course_pace).get_due_dates(items, enrollment, start_date: start_date_of_item_group)
# If the course pace hasn't been committed yet we are grouping the items by their module_item_id since the item.id is
# not set yet.
key = pace_plan.persisted? ? items[-1].id : items[-1].module_item_id
key = course_pace.persisted? ? items[-1].id : items[-1].module_item_id
if new_due_dates[key] > end_date
days_over = PacePlansDateHelpers.days_between(
days_over = CoursePacesDateHelpers.days_between(
end_date,
new_due_dates[key],
pace_plan.exclude_weekends,
course_pace.exclude_weekends,
inclusive_end: false,
blackout_dates: pace_plan.course.blackout_dates
blackout_dates: course_pace.course.blackout_dates
)
adjusted_durations = shift_durations_down(rounded_durations, days_over)
items = update_item_durations(items, adjusted_durations, save)
@ -178,12 +178,12 @@ class PacePlanHardEndDateCompressor
end
# Iterates through array of durations, decreasing each one until either it
# or the number of days the over the pace plan end date reaches 0. This weights
# or the number of days the over the course pace end date reaches 0. This weights
# the reduction heaviest on the last module item, then the next in reverse order,
# and so on.
#
# @param durations [Duration[]] The array of Durations used to calculate the pace plan item due dates
# @param days_over [Integer] The number of days over the pace plan hard end date the durations totaled
# @param durations [Duration[]] The array of Durations used to calculate the course pace item due dates
# @param days_over [Integer] The number of days over the course pace hard end date the durations totaled
def self.shift_durations_down(durations, days_over)
durations.reverse.map do |duration|
while duration.duration > 0 && days_over > 0

View File

@ -17,7 +17,7 @@
# You should have received a copy of the GNU Affero General Public License along
# with this program. If not, see <http://www.gnu.org/licenses/>.
#
module PacePlansDateHelpers
module CoursePacesDateHelpers
class << self
def add_days(start_date, duration, exclude_weekends, blackout_dates = [])
return nil unless start_date && duration

View File

@ -639,7 +639,7 @@ describe ContentExportsApiController, type: :request do
@cm = @course.context_modules.create!(name: "some module")
@att = Attachment.create!(filename: "first.txt", uploaded_data: StringIO.new("ohai"), folder: Folder.unfiled_folder(@course), context: @course)
@wiki = @course.wiki_pages.create!(title: "wiki", body: "ohai")
@course.pace_plans.create!
@course.course_paces.create!
@quiz = @course.quizzes.create!(title: "quizz")
@quiz.did_edit
@ -653,9 +653,9 @@ describe ContentExportsApiController, type: :request do
{ "type" => "assignments", "property" => "select[all_assignments]", "title" => "Assignments", "count" => 1, "sub_items_url" => "http://www.example.com/api/v1/courses/#{@course.id}/content_list?type=assignments" },
{ "type" => "attachments", "property" => "select[all_attachments]", "title" => "Files", "count" => 1, "sub_items_url" => "http://www.example.com/api/v1/courses/#{@course.id}/content_list?type=attachments" },
{ "type" => "context_modules", "property" => "select[all_context_modules]", "title" => "Modules", "count" => 1, "sub_items_url" => "http://www.example.com/api/v1/courses/#{@course.id}/content_list?type=context_modules" },
{ "type" => "course_paces", "property" => "select[all_course_paces]", "title" => "Course Pace" },
{ "type" => "course_settings", "property" => "select[all_course_settings]", "title" => "Course Settings" },
{ "type" => "discussion_topics", "property" => "select[all_discussion_topics]", "title" => "Discussion Topics", "count" => 1, "sub_items_url" => "http://www.example.com/api/v1/courses/#{@course.id}/content_list?type=discussion_topics" },
{ "type" => "pace_plans", "property" => "select[all_pace_plans]", "title" => "Pace Plan" },
{ "type" => "quizzes", "property" => "select[all_quizzes]", "title" => "Quizzes", "count" => 1, "sub_items_url" => "http://www.example.com/api/v1/courses/#{@course.id}/content_list?type=quizzes" },
{ "type" => "syllabus_body", "property" => "select[all_syllabus_body]", "title" => "Syllabus Body" },
{ "type" => "wiki_pages", "property" => "select[all_wiki_pages]", "title" => "Pages", "count" => 2, "sub_items_url" => "http://www.example.com/api/v1/courses/#{@course.id}/content_list?type=wiki_pages" }

View File

@ -22,9 +22,9 @@ describe BlackoutDatesController, type: :controller do
before :once do
course_with_teacher(active_all: true)
@course.enable_pace_plans = true
@course.enable_course_paces = true
@course.save!
@course.account.enable_feature!(:pace_plans)
@course.account.enable_feature!(:course_paces)
@blackout_date = @course.blackout_dates.create!(start_date: "2022-02-14", end_date: "2022-02-18", event_title: "Test Week Off")
end

View File

@ -17,20 +17,20 @@
# You should have received a copy of the GNU Affero General Public License along
# with this program. If not, see <http://www.gnu.org/licenses/>.
describe PacePlansController, type: :controller do
describe CoursePacesController, type: :controller do
let(:valid_update_params) do
{
end_date: 1.year.from_now.strftime("%Y-%m-%d"),
workflow_state: "active",
pace_plan_module_items_attributes: [
course_pace_module_items_attributes: [
{
id: @pace_plan.pace_plan_module_items.first.id,
module_item_id: @pace_plan.pace_plan_module_items.first.module_item_id,
id: @course_pace.course_pace_module_items.first.id,
module_item_id: @course_pace.course_pace_module_items.first.module_item_id,
duration: 1,
},
{
id: @pace_plan.pace_plan_module_items.second.id,
module_item_id: @pace_plan.pace_plan_module_items.second.module_item_id,
id: @course_pace.course_pace_module_items.second.id,
module_item_id: @course_pace.course_pace_module_items.second.module_item_id,
duration: 10,
},
],
@ -41,7 +41,7 @@ describe PacePlansController, type: :controller do
course_with_teacher(active_all: true)
@course.update(start_at: "2021-09-30")
student_in_course(active_all: true)
pace_plan_model(course: @course)
course_pace_model(course: @course)
@student_enrollment = @student.enrollments.first
@mod1 = @course.context_modules.create! name: "M1"
@ -55,28 +55,28 @@ describe PacePlansController, type: :controller do
@mod2.add_item id: @a3.id, type: "assignment"
@mod2.add_item type: "external_url", title: "External URL", url: "http://localhost"
@pace_plan.pace_plan_module_items.each_with_index do |ppmi, i|
@course_pace.course_pace_module_items.each_with_index do |ppmi, i|
ppmi.update! duration: i * 2
end
@course.enable_pace_plans = true
@course.enable_course_paces = true
@course.save!
@course.account.enable_feature!(:pace_plans)
@course.account.enable_feature!(:course_paces)
@course_section = @course.course_sections.first
@valid_params = {
end_date: 1.year.from_now.strftime("%Y-%m-%d"),
workflow_state: "active",
pace_plan_module_items_attributes: [
course_pace_module_items_attributes: [
{
id: @pace_plan.pace_plan_module_items.first.id,
module_item_id: @pace_plan.pace_plan_module_items.first.module_item_id,
id: @course_pace.course_pace_module_items.first.id,
module_item_id: @course_pace.course_pace_module_items.first.module_item_id,
duration: 1,
},
{
id: @pace_plan.pace_plan_module_items.second.id,
module_item_id: @pace_plan.pace_plan_module_items.second.module_item_id,
id: @course_pace.course_pace_module_items.second.id,
module_item_id: @course_pace.course_pace_module_items.second.module_item_id,
duration: 10,
},
],
@ -88,14 +88,14 @@ describe PacePlansController, type: :controller do
end
describe "GET #index" do
it "populates js_env with course, enrollment, sections, and pace_plan details" do
it "populates js_env with course, enrollment, sections, and course_pace details" do
@section = @course.course_sections.first
@student_enrollment = @course.enrollments.find_by(user_id: @student.id)
@progress = @pace_plan.create_publish_progress
@progress = @course_pace.create_publish_progress
get :index, { params: { course_id: @course.id } }
expect(response).to be_successful
expect(assigns[:js_bundles].flatten).to include(:pace_plans)
expect(assigns[:js_bundles].flatten).to include(:course_paces)
js_env = controller.js_env
expect(js_env[:BLACKOUT_DATES]).to eq([])
expect(js_env[:COURSE]).to match(hash_including({
@ -120,56 +120,56 @@ describe PacePlansController, type: :controller do
start_at: @section.start_at,
end_at: @section.end_at
}))
expect(js_env[:PACE_PLAN]).to match(hash_including({
id: @pace_plan.id,
course_id: @course.id,
course_section_id: nil,
user_id: nil,
workflow_state: "active",
exclude_weekends: true,
hard_end_dates: true,
context_id: @course.id,
context_type: "Course"
}))
expect(js_env[:PACE_PLAN][:modules].length).to be(2)
expect(js_env[:PACE_PLAN][:modules][0][:items].length).to be(1)
expect(js_env[:PACE_PLAN][:modules][1][:items].length).to be(2)
expect(js_env[:PACE_PLAN][:modules][1][:items][1]).to match(hash_including({
assignment_title: @a3.title,
module_item_type: "Assignment",
duration: 4
}))
expect(js_env[:COURSE_PACE]).to match(hash_including({
id: @course_pace.id,
course_id: @course.id,
course_section_id: nil,
user_id: nil,
workflow_state: "active",
exclude_weekends: true,
hard_end_dates: true,
context_id: @course.id,
context_type: "Course"
}))
expect(js_env[:COURSE_PACE][:modules].length).to be(2)
expect(js_env[:COURSE_PACE][:modules][0][:items].length).to be(1)
expect(js_env[:COURSE_PACE][:modules][1][:items].length).to be(2)
expect(js_env[:COURSE_PACE][:modules][1][:items][1]).to match(hash_including({
assignment_title: @a3.title,
module_item_type: "Assignment",
duration: 4
}))
expect(js_env[:PACE_PLAN_PROGRESS]).to match(hash_including({
id: @progress.id,
context_id: @progress.context_id,
context_type: "PacePlan",
tag: "pace_plan_publish",
workflow_state: "queued"
}))
expect(js_env[:COURSE_PACE_PROGRESS]).to match(hash_including({
id: @progress.id,
context_id: @progress.context_id,
context_type: "CoursePace",
tag: "course_pace_publish",
workflow_state: "queued"
}))
end
it "does not create a pace plan if no primary pace plans are available" do
@pace_plan.update(user_id: @student)
expect(@course.pace_plans.count).to eq(1)
expect(@course.pace_plans.primary).to be_empty
it "does not create a course pace if no primary course paces are available" do
@course_pace.update(user_id: @student)
expect(@course.course_paces.count).to eq(1)
expect(@course.course_paces.primary).to be_empty
get :index, params: { course_id: @course.id }
pace_plan = @controller.instance_variable_get(:@pace_plan)
expect(pace_plan).not_to be_nil
expect(pace_plan.pace_plan_module_items.size).to eq(3)
expect(@course.pace_plans.count).to eq(1)
expect(@course.pace_plans.primary.count).to eq(0)
course_pace = @controller.instance_variable_get(:@course_pace)
expect(course_pace).not_to be_nil
expect(course_pace.course_pace_module_items.size).to eq(3)
expect(@course.course_paces.count).to eq(1)
expect(@course.course_paces.primary.count).to eq(0)
end
it "responds with not found if the pace_plans feature is disabled" do
@course.account.disable_feature!(:pace_plans)
it "responds with not found if the course_paces feature is disabled" do
@course.account.disable_feature!(:course_paces)
assert_page_not_found do
get :index, params: { course_id: @course.id }
end
end
it "responds with not found if the enable_pace_plans setting is disabled" do
@course.enable_pace_plans = false
it "responds with not found if the enable_course_paces setting is disabled" do
@course.enable_course_paces = false
@course.save!
assert_page_not_found do
get :index, params: { course_id: @course.id }
@ -184,7 +184,7 @@ describe PacePlansController, type: :controller do
context "progress" do
it "starts the progress' delayed job if queued" do
progress = @pace_plan.create_publish_progress
progress = @course_pace.create_publish_progress
delayed_job = progress.delayed_job
original_run_at = delayed_job.run_at
get :index, { params: { course_id: @course.id } }
@ -195,50 +195,50 @@ describe PacePlansController, type: :controller do
end
describe "GET #api_show" do
it "renders the specified pace plan" do
get :api_show, params: { course_id: @course.id, id: @pace_plan.id }
it "renders the specified course pace" do
get :api_show, params: { course_id: @course.id, id: @course_pace.id }
expect(response).to be_successful
expect(JSON.parse(response.body)["pace_plan"]["id"]).to eq(@pace_plan.id)
expect(JSON.parse(response.body)["course_pace"]["id"]).to eq(@course_pace.id)
end
it "renders the latest progress object associated with publishing" do
Progress.create!(context: @pace_plan, tag: "pace_plan_publish", workflow_state: "failed")
Progress.create!(context: @pace_plan, tag: "pace_plan_publish", workflow_state: "running")
Progress.create!(context: @course_pace, tag: "course_pace_publish", workflow_state: "failed")
Progress.create!(context: @course_pace, tag: "course_pace_publish", workflow_state: "running")
get :api_show, params: { course_id: @course.id, id: @pace_plan.id }
get :api_show, params: { course_id: @course.id, id: @course_pace.id }
expect(response).to be_successful
expect(JSON.parse(response.body)["progress"]["workflow_state"]).to eq("running")
end
it "renders a nil progress object if the most recent progress was completed" do
Progress.create!(context: @pace_plan, tag: "pace_plan_publish", workflow_state: "failed")
Progress.create!(context: @pace_plan, tag: "pace_plan_publish", workflow_state: "completed")
Progress.create!(context: @course_pace, tag: "course_pace_publish", workflow_state: "failed")
Progress.create!(context: @course_pace, tag: "course_pace_publish", workflow_state: "completed")
get :api_show, params: { course_id: @course.id, id: @pace_plan.id }
get :api_show, params: { course_id: @course.id, id: @course_pace.id }
expect(response).to be_successful
expect(JSON.parse(response.body)["progress"]).to be_nil
end
end
describe "PUT #update" do
it "updates the PacePlan" do
put :update, params: { course_id: @course.id, id: @pace_plan.id, pace_plan: valid_update_params }
it "updates the CoursePace" do
put :update, params: { course_id: @course.id, id: @course_pace.id, course_pace: valid_update_params }
expect(response).to be_successful
expect(@pace_plan.reload.end_date.to_s).to eq(valid_update_params[:end_date])
expect(@pace_plan.workflow_state).to eq(valid_update_params[:workflow_state])
expect(@course_pace.reload.end_date.to_s).to eq(valid_update_params[:end_date])
expect(@course_pace.workflow_state).to eq(valid_update_params[:workflow_state])
expect(
@pace_plan.pace_plan_module_items.joins(:module_item).find_by(content_tags: { content_id: @a1.id }).duration
).to eq(valid_update_params[:pace_plan_module_items_attributes][0][:duration])
@course_pace.course_pace_module_items.joins(:module_item).find_by(content_tags: { content_id: @a1.id }).duration
).to eq(valid_update_params[:course_pace_module_items_attributes][0][:duration])
expect(
@pace_plan.pace_plan_module_items.joins(:module_item).find_by(content_tags: { content_id: @a2.id }).duration
).to eq(valid_update_params[:pace_plan_module_items_attributes][1][:duration])
@course_pace.course_pace_module_items.joins(:module_item).find_by(content_tags: { content_id: @a2.id }).duration
).to eq(valid_update_params[:course_pace_module_items_attributes][1][:duration])
response_body = JSON.parse(response.body)
expect(response_body["pace_plan"]["id"]).to eq(@pace_plan.id)
expect(response_body["course_pace"]["id"]).to eq(@course_pace.id)
# Pace plan's publish should be queued
# Course pace's publish should be queued
progress = Progress.last
expect(progress.context).to eq(@pace_plan)
expect(progress.context).to eq(@course_pace)
expect(progress.workflow_state).to eq("queued")
expect(response_body["progress"]["id"]).to eq(progress.id)
end
@ -247,33 +247,33 @@ describe PacePlansController, type: :controller do
describe "POST #create" do
let(:create_params) { valid_update_params.merge(course_id: @course.id, user_id: @student.id) }
it "creates the PacePlan and all the PacePlanModuleItems" do
pace_plan_count_before = PacePlan.count
pace_plan_module_item_count_before = PacePlanModuleItem.count
it "creates the CoursePace and all the CoursePaceModuleItems" do
course_pace_count_before = CoursePace.count
course_pace_module_item_count_before = CoursePaceModuleItem.count
post :create, params: { course_id: @course.id, pace_plan: create_params }
post :create, params: { course_id: @course.id, course_pace: create_params }
expect(response).to be_successful
expect(PacePlan.count).to eq(pace_plan_count_before + 1)
expect(PacePlanModuleItem.count).to eq(pace_plan_module_item_count_before + 2)
expect(CoursePace.count).to eq(course_pace_count_before + 1)
expect(CoursePaceModuleItem.count).to eq(course_pace_module_item_count_before + 2)
pace_plan = PacePlan.last
course_pace = CoursePace.last
response_body = JSON.parse(response.body)
expect(response_body["pace_plan"]["id"]).to eq(pace_plan.id)
expect(response_body["course_pace"]["id"]).to eq(course_pace.id)
expect(pace_plan.end_date.to_s).to eq(valid_update_params[:end_date])
expect(pace_plan.workflow_state).to eq(valid_update_params[:workflow_state])
expect(course_pace.end_date.to_s).to eq(valid_update_params[:end_date])
expect(course_pace.workflow_state).to eq(valid_update_params[:workflow_state])
expect(
pace_plan.pace_plan_module_items.joins(:module_item).find_by(content_tags: { content_id: @a1.id }).duration
).to eq(valid_update_params[:pace_plan_module_items_attributes][0][:duration])
course_pace.course_pace_module_items.joins(:module_item).find_by(content_tags: { content_id: @a1.id }).duration
).to eq(valid_update_params[:course_pace_module_items_attributes][0][:duration])
expect(
pace_plan.pace_plan_module_items.joins(:module_item).find_by(content_tags: { content_id: @a2.id }).duration
).to eq(valid_update_params[:pace_plan_module_items_attributes][1][:duration])
expect(pace_plan.pace_plan_module_items.count).to eq(2)
# Pace plan's publish should be queued
course_pace.course_pace_module_items.joins(:module_item).find_by(content_tags: { content_id: @a2.id }).duration
).to eq(valid_update_params[:course_pace_module_items_attributes][1][:duration])
expect(course_pace.course_pace_module_items.count).to eq(2)
# Course pace's publish should be queued
progress = Progress.last
expect(progress.context).to eq(pace_plan)
expect(progress.context).to eq(course_pace)
expect(progress.workflow_state).to eq("queued")
expect(response_body["progress"]["id"]).to eq(progress.id)
end
@ -281,28 +281,28 @@ describe PacePlansController, type: :controller do
describe "GET #new" do
context "course" do
it "returns a created pace plan if one already exists" do
it "returns a created course pace if one already exists" do
get :new, { params: { course_id: @course.id } }
expect(response).to be_successful
expect(JSON.parse(response.body)["pace_plan"]["id"]).to eq(@pace_plan.id)
expect(JSON.parse(response.body)["pace_plan"]["published_at"]).not_to be_nil
expect(JSON.parse(response.body)["course_pace"]["id"]).to eq(@course_pace.id)
expect(JSON.parse(response.body)["course_pace"]["published_at"]).not_to be_nil
end
it "returns an instantiated pace plan if one is not already available" do
@pace_plan.destroy
expect(@course.pace_plans.not_deleted.count).to eq(0)
it "returns an instantiated course pace if one is not already available" do
@course_pace.destroy
expect(@course.course_paces.not_deleted.count).to eq(0)
get :new, { params: { course_id: @course.id } }
expect(response).to be_successful
expect(@course.pace_plans.not_deleted.count).to eq(0)
expect(@course.course_paces.not_deleted.count).to eq(0)
json_response = JSON.parse(response.body)
expect(json_response["pace_plan"]["id"]).to eq(nil)
expect(json_response["pace_plan"]["published_at"]).to eq(nil)
expect(json_response["pace_plan"]["modules"].count).to eq(2)
m1 = json_response["pace_plan"]["modules"].first
expect(json_response["course_pace"]["id"]).to eq(nil)
expect(json_response["course_pace"]["published_at"]).to eq(nil)
expect(json_response["course_pace"]["modules"].count).to eq(2)
m1 = json_response["course_pace"]["modules"].first
expect(m1["items"].count).to eq(1)
expect(m1["items"].first["duration"]).to eq(0)
expect(m1["items"].first["published"]).to eq(true)
m2 = json_response["pace_plan"]["modules"].second
m2 = json_response["course_pace"]["modules"].second
expect(m2["items"].count).to eq(2)
expect(m2["items"].first["duration"]).to eq(0)
expect(m2["items"].first["published"]).to eq(true)
@ -312,28 +312,28 @@ describe PacePlansController, type: :controller do
end
context "course_section" do
it "returns a draft pace plan" do
it "returns a draft course pace" do
get :new, { params: { course_id: @course.id, course_section_id: @course_section.id } }
expect(response).to be_successful
expect(JSON.parse(response.body)["pace_plan"]["id"]).to eq(nil)
expect(JSON.parse(response.body)["pace_plan"]["published_at"]).to eq(nil)
expect(JSON.parse(response.body)["course_pace"]["id"]).to eq(nil)
expect(JSON.parse(response.body)["course_pace"]["published_at"]).to eq(nil)
end
it "returns an instantiated pace plan if one is not already available" do
expect(@course.pace_plans.unpublished.for_section(@course_section).count).to eq(0)
it "returns an instantiated course pace if one is not already available" do
expect(@course.course_paces.unpublished.for_section(@course_section).count).to eq(0)
get :new, { params: { course_id: @course.id, course_section_id: @course_section.id } }
expect(response).to be_successful
expect(@course.pace_plans.unpublished.for_section(@course_section).count).to eq(0)
expect(@course.course_paces.unpublished.for_section(@course_section).count).to eq(0)
json_response = JSON.parse(response.body)
expect(json_response["pace_plan"]["id"]).to eq(nil)
expect(json_response["pace_plan"]["published_at"]).to eq(nil)
expect(json_response["pace_plan"]["course_section_id"]).to eq(@course_section.id)
expect(json_response["pace_plan"]["modules"].count).to eq(2)
m1 = json_response["pace_plan"]["modules"].first
expect(json_response["course_pace"]["id"]).to eq(nil)
expect(json_response["course_pace"]["published_at"]).to eq(nil)
expect(json_response["course_pace"]["course_section_id"]).to eq(@course_section.id)
expect(json_response["course_pace"]["modules"].count).to eq(2)
m1 = json_response["course_pace"]["modules"].first
expect(m1["items"].count).to eq(1)
expect(m1["items"].first["duration"]).to eq(0)
expect(m1["items"].first["published"]).to eq(true)
m2 = json_response["pace_plan"]["modules"].second
m2 = json_response["course_pace"]["modules"].second
expect(m2["items"].count).to eq(2)
expect(m2["items"].first["duration"]).to eq(2)
expect(m2["items"].first["published"]).to eq(true)
@ -343,29 +343,29 @@ describe PacePlansController, type: :controller do
end
context "enrollment" do
it "returns a draft pace plan" do
it "returns a draft course pace" do
get :new, { params: { course_id: @course.id, enrollment_id: @course.student_enrollments.first.id } }
expect(response).to be_successful
expect(JSON.parse(response.body)["pace_plan"]["id"]).to eq(nil)
expect(JSON.parse(response.body)["pace_plan"]["published_at"]).to eq(nil)
expect(JSON.parse(response.body)["pace_plan"]["user_id"]).to eq(@student.id)
expect(JSON.parse(response.body)["course_pace"]["id"]).to eq(nil)
expect(JSON.parse(response.body)["course_pace"]["published_at"]).to eq(nil)
expect(JSON.parse(response.body)["course_pace"]["user_id"]).to eq(@student.id)
end
it "returns an instantiated pace plan if one is not already available" do
expect(@course.pace_plans.unpublished.for_user(@student).count).to eq(0)
it "returns an instantiated course pace if one is not already available" do
expect(@course.course_paces.unpublished.for_user(@student).count).to eq(0)
get :new, { params: { course_id: @course.id, enrollment_id: @student_enrollment.id } }
expect(response).to be_successful
expect(@course.pace_plans.unpublished.for_user(@student).count).to eq(0)
expect(@course.course_paces.unpublished.for_user(@student).count).to eq(0)
json_response = JSON.parse(response.body)
expect(json_response["pace_plan"]["id"]).to eq(nil)
expect(json_response["pace_plan"]["published_at"]).to eq(nil)
expect(json_response["pace_plan"]["user_id"]).to eq(@student.id)
expect(json_response["pace_plan"]["modules"].count).to eq(2)
m1 = json_response["pace_plan"]["modules"].first
expect(json_response["course_pace"]["id"]).to eq(nil)
expect(json_response["course_pace"]["published_at"]).to eq(nil)
expect(json_response["course_pace"]["user_id"]).to eq(@student.id)
expect(json_response["course_pace"]["modules"].count).to eq(2)
m1 = json_response["course_pace"]["modules"].first
expect(m1["items"].count).to eq(1)
expect(m1["items"].first["duration"]).to eq(0)
expect(m1["items"].first["published"]).to eq(true)
m2 = json_response["pace_plan"]["modules"].second
m2 = json_response["course_pace"]["modules"].second
expect(m2["items"].count).to eq(2)
expect(m2["items"].first["duration"]).to eq(2)
expect(m2["items"].first["published"]).to eq(true)
@ -376,56 +376,56 @@ describe PacePlansController, type: :controller do
end
describe "POST #publish" do
it "starts a new background job to publish the pace plan" do
post :publish, params: { course_id: @course.id, id: @pace_plan.id }
it "starts a new background job to publish the course pace" do
post :publish, params: { course_id: @course.id, id: @course_pace.id }
expect(response).to be_successful
json_response = JSON.parse(response.body)
expect(json_response["context_type"]).to eq("PacePlan")
expect(json_response["context_type"]).to eq("CoursePace")
expect(json_response["workflow_state"]).to eq("queued")
end
end
describe "POST #compress_dates" do
it "returns a compressed list of dates" do
pace_plan_params = @valid_params.merge(end_date: @pace_plan.start_date + 5.days)
post :compress_dates, params: { course_id: @course.id, pace_plan: pace_plan_params }
course_pace_params = @valid_params.merge(end_date: @course_pace.start_date + 5.days)
post :compress_dates, params: { course_id: @course.id, course_pace: course_pace_params }
expect(response).to be_successful
json_response = JSON.parse(response.body)
expect(json_response.values).to eq(%w[2021-09-30 2021-10-05])
end
it "supports changing durations and start dates" do
pace_plan_params = @valid_params.merge(start_date: "2021-11-01", end_date: "2021-11-05")
post :compress_dates, params: { course_id: @course.id, pace_plan: pace_plan_params }
course_pace_params = @valid_params.merge(start_date: "2021-11-01", end_date: "2021-11-05")
post :compress_dates, params: { course_id: @course.id, course_pace: course_pace_params }
expect(response).to be_successful
json_response = JSON.parse(response.body)
expect(json_response.values).to eq(%w[2021-11-01 2021-11-05])
end
it "squishes proportionally and ends on the end date" do
pace_plan_params = @valid_params.merge(
course_pace_params = @valid_params.merge(
start_date: "2021-12-27",
end_date: "2021-12-31",
pace_plan_module_items_attributes: [
course_pace_module_items_attributes: [
{
id: @pace_plan.pace_plan_module_items.first.id,
module_item_id: @pace_plan.pace_plan_module_items.first.module_item_id,
id: @course_pace.course_pace_module_items.first.id,
module_item_id: @course_pace.course_pace_module_items.first.module_item_id,
duration: 2,
},
{
id: @pace_plan.pace_plan_module_items.second.id,
module_item_id: @pace_plan.pace_plan_module_items.second.module_item_id,
id: @course_pace.course_pace_module_items.second.id,
module_item_id: @course_pace.course_pace_module_items.second.module_item_id,
duration: 4,
},
{
id: @pace_plan.pace_plan_module_items.third.id,
module_item_id: @pace_plan.pace_plan_module_items.third.module_item_id,
id: @course_pace.course_pace_module_items.third.id,
module_item_id: @course_pace.course_pace_module_items.third.module_item_id,
duration: 6,
},
]
)
post :compress_dates, params: { course_id: @course.id, pace_plan: pace_plan_params }
post :compress_dates, params: { course_id: @course.id, course_pace: course_pace_params }
expect(response).to be_successful
json_response = JSON.parse(response.body)
expect(json_response.values).to eq(%w[2021-12-28 2021-12-29 2021-12-31])
@ -435,53 +435,53 @@ describe PacePlansController, type: :controller do
assignment = @course.assignments.create! name: "A4", workflow_state: "active"
@mod1.add_item id: assignment.id, type: "assignment"
tag = @mod1.add_item id: assignment.id, type: "assignment"
@pace_plan.pace_plan_module_items.create! module_item: tag, duration: 8
@course_pace.course_pace_module_items.create! module_item: tag, duration: 8
pace_plan_params = @valid_params.merge(
course_pace_params = @valid_params.merge(
start_date: "2021-12-13",
end_date: "2022-01-12",
exclude_weekends: true,
pace_plan_module_items_attributes: [
course_pace_module_items_attributes: [
{
id: @pace_plan.pace_plan_module_items.first.id,
module_item_id: @pace_plan.pace_plan_module_items.first.module_item_id,
id: @course_pace.course_pace_module_items.first.id,
module_item_id: @course_pace.course_pace_module_items.first.module_item_id,
duration: 7,
},
{
id: @pace_plan.pace_plan_module_items.second.id,
module_item_id: @pace_plan.pace_plan_module_items.second.module_item_id,
id: @course_pace.course_pace_module_items.second.id,
module_item_id: @course_pace.course_pace_module_items.second.module_item_id,
duration: 6,
},
{
id: @pace_plan.pace_plan_module_items.third.id,
module_item_id: @pace_plan.pace_plan_module_items.third.module_item_id,
id: @course_pace.course_pace_module_items.third.id,
module_item_id: @course_pace.course_pace_module_items.third.module_item_id,
duration: 5,
},
{
id: @pace_plan.pace_plan_module_items.third.id,
module_item_id: @pace_plan.pace_plan_module_items.fourth.module_item_id,
id: @course_pace.course_pace_module_items.third.id,
module_item_id: @course_pace.course_pace_module_items.fourth.module_item_id,
duration: 5,
},
]
)
post :compress_dates, params: { course_id: @course.id, pace_plan: pace_plan_params }
post :compress_dates, params: { course_id: @course.id, course_pace: course_pace_params }
expect(response).to be_successful
json_response = JSON.parse(response.body)
expect(json_response.values).to eq(%w[2021-12-22 2021-12-28 2022-01-05 2022-01-12])
end
it "returns an error if the start date is after the end date" do
pace_plan_params = @valid_params.merge(start_date: "2022-01-27", end_date: "2022-01-20")
post :compress_dates, params: { course_id: @course.id, pace_plan: pace_plan_params }
course_pace_params = @valid_params.merge(start_date: "2022-01-27", end_date: "2022-01-20")
post :compress_dates, params: { course_id: @course.id, course_pace: course_pace_params }
expect(response).not_to be_successful
json_response = JSON.parse(response.body)
expect(json_response["errors"]).to eq("End date cannot be before start date")
end
it "returns uncompressed items if the end date is not set" do
pace_plan_params = @valid_params.merge(start_date: "2022-01-27", end_date: nil)
post :compress_dates, params: { course_id: @course.id, pace_plan: pace_plan_params }
course_pace_params = @valid_params.merge(start_date: "2022-01-27", end_date: nil)
post :compress_dates, params: { course_id: @course.id, course_pace: course_pace_params }
expect(response).to be_successful
json_response = JSON.parse(response.body)
expect(json_response.values).to eq(%w[2022-01-28 2022-02-11])
@ -494,7 +494,7 @@ describe PacePlansController, type: :controller do
@mod3.add_item id: assignment.id, type: "assignment"
end
pace_plan_module_items_attributes = @pace_plan.pace_plan_module_items.order(:id).map do |ppmi|
course_pace_module_items_attributes = @course_pace.course_pace_module_items.order(:id).map do |ppmi|
{
id: ppmi.id,
module_item_id: ppmi.module_item_id,
@ -502,15 +502,15 @@ describe PacePlansController, type: :controller do
}
end
pace_plan_params = @valid_params.merge(
course_pace_params = @valid_params.merge(
start_date: "2021-11-01",
end_date: "2021-11-06",
pace_plan_module_items_attributes: pace_plan_module_items_attributes
course_pace_module_items_attributes: course_pace_module_items_attributes
)
post :compress_dates, params: { course_id: @course.id, pace_plan: pace_plan_params }
post :compress_dates, params: { course_id: @course.id, course_pace: course_pace_params }
expect(response).to be_successful
json_response = JSON.parse(response.body)
expect(json_response.keys).to eq(pace_plan_module_items_attributes.map { |i| i[:module_item_id].to_s })
expect(json_response.keys).to eq(course_pace_module_items_attributes.map { |i| i[:module_item_id].to_s })
end
end
end

View File

@ -19,12 +19,12 @@
#
module Factories
def pace_plan_model(opts = {})
def course_pace_model(opts = {})
course = opts.delete(:course) || opts[:context] || course_model(reusable: true)
@pace_plan = factory_with_protected_attributes(course.pace_plans, valid_pace_plan_attributes.merge(opts))
@course_pace = factory_with_protected_attributes(course.course_paces, valid_course_pace_attributes.merge(opts))
end
def valid_pace_plan_attributes
def valid_course_pace_attributes
{
workflow_state: "active",
end_date: "2021-09-30",

View File

@ -387,13 +387,13 @@ def generate_course_with_dated_assignments
puts "Teacher ID is #{@teacher.id}"
end
def generate_pace_plan_course
puts "Generate a pace plan course with module and assignments"
def generate_course_pace_course
puts "Generate a course pace course with module and assignments"
course_with_teacher_enrolled
course_with_students_enrolled
@root_account.enable_feature!(:pace_plans)
@course.update(enable_pace_plans: true)
@root_account.enable_feature!(:course_paces)
@course.update(enable_course_paces: true)
module1 = create_module(@course)
assignment1 = create_assignment(@course, "Assignment 1")
@ -435,8 +435,8 @@ def create_all_the_available_data
generate_course_with_outcome_rubric
@course_name = save_course_name + " (course with assignment groups)"
generate_course_assignment_groups
@course_name = save_course_name + " (pace plan course)"
generate_pace_plan_course
@course_name = save_course_name + " (course pace course)"
generate_course_pace_course
end
# rubocop:enable Specs/ScopeHelperModules
@ -447,7 +447,7 @@ option_parser = OptionParser.new do |opts|
opts.on("-a", "--all_data", "Create all the available data with defaults")
opts.on("-b", "--basic_course", "Course with teacher and students")
opts.on("-c", "--course_name=COURSENAME", "Course Name")
opts.on("-e", "--pace_plan", "Place Plan Course")
opts.on("-e", "--course_pace", "Course Pacing Course")
opts.on("-d", "--dated_assignments", "Course with Dated Assignments")
opts.on("-g", "--assignment_groups", "Course with Assignments in assignment groups")
opts.on("-i", "--account_id=ACCOUNTID", "Id Number of the root account")
@ -515,8 +515,8 @@ options.each_key do |key|
generate_course_and_submissions
when :sections
generate_sections
when :pace_plan
generate_pace_plan_course
when :course_pace
generate_course_pace_course
else raise "should never get here -- BIG FAIL"
end
end

View File

@ -133,8 +133,8 @@ describe "Common Cartridge exporting" do
@event1 = @course.calendar_events.create!(title: "event2", start_at: 2.weeks.from_now)
@bank = @course.assessment_question_banks.create!(title: "bank")
@bank2 = @course.assessment_question_banks.create!(title: "bank2")
@pp1 = @course.pace_plans.create! workflow_state: "unpublished"
@pp2 = @course.pace_plans.create! workflow_state: "active"
@pp1 = @course.course_paces.create! workflow_state: "unpublished"
@pp2 = @course.course_paces.create! workflow_state: "active"
# only select one of each type
@ce.selected_content = {
@ -152,7 +152,7 @@ describe "Common Cartridge exporting" do
wiki_pages: { mig_id(@wiki) => "1", mig_id(@wiki2) => "0" },
calendar_events: { mig_id(@event) => "1", mig_id(@event2) => "0" },
assessment_question_banks: { mig_id(@bank) => "1", mig_id(@bank2) => "0" },
pace_plans: { mig_id(@pp1) => "1", mig_id(@pp2) => "0" }
course_paces: { mig_id(@pp1) => "1", mig_id(@pp2) => "0" }
}
@ce.save!
@ -205,9 +205,9 @@ describe "Common Cartridge exporting" do
expect(doc.at_css("event[identifier=#{mig_id(@event2)}]")).to be_nil
expect(ccc_schema.validate(doc)).to be_empty
doc = Nokogiri::XML.parse(@zip_file.read("course_settings/pace_plans.xml"))
expect(doc.at_css("pace_plan[identifier=#{mig_id(@pp1)}]")).not_to be_nil
expect(doc.at_css("pace_plan[identifier=#{mig_id(@pp2)}]")).to be_nil
doc = Nokogiri::XML.parse(@zip_file.read("course_settings/course_paces.xml"))
expect(doc.at_css("course_pace[identifier=#{mig_id(@pp1)}]")).not_to be_nil
expect(doc.at_css("course_pace[identifier=#{mig_id(@pp2)}]")).to be_nil
expect(ccc_schema.validate(doc)).to be_empty
end

View File

@ -18,48 +18,48 @@
# with this program. If not, see <http://www.gnu.org/licenses/>.
#
describe PacePlanDueDatesCalculator do
describe CoursePaceDueDatesCalculator do
before :once do
course_with_student active_all: true
@course.update start_at: "2021-09-01"
@module = @course.context_modules.create!
@assignment = @course.assignments.create!
@tag = @assignment.context_module_tags.create! context_module: @module, context: @course, tag_type: "context_module"
@pace_plan = @course.pace_plans.create! workflow_state: "active", end_date: "2021-09-30"
@pace_plan_module_item = @pace_plan.pace_plan_module_items.create! module_item: @tag
@pace_plan_module_items = @pace_plan.pace_plan_module_items.active
@calculator = PacePlanDueDatesCalculator.new(@pace_plan)
@course_pace = @course.course_paces.create! workflow_state: "active", end_date: "2021-09-30"
@course_pace_module_item = @course_pace.course_pace_module_items.create! module_item: @tag
@course_pace_module_items = @course_pace.course_pace_module_items.active
@calculator = CoursePaceDueDatesCalculator.new(@course_pace)
end
context "get_due_dates" do
it "returns the next due date" do
expect(@calculator.get_due_dates(@pace_plan_module_items)).to eq(
{ @pace_plan_module_item.id => Date.parse("2021-09-01") }
expect(@calculator.get_due_dates(@course_pace_module_items)).to eq(
{ @course_pace_module_item.id => Date.parse("2021-09-01") }
)
end
it "respects blackout dates" do
@course.blackout_dates.create! event_title: "Blackout test", start_date: "2021-09-01", end_date: "2021-09-08"
expect(@calculator.get_due_dates(@pace_plan_module_items)).to eq(
{ @pace_plan_module_item.id => Date.parse("2021-09-09") }
expect(@calculator.get_due_dates(@course_pace_module_items)).to eq(
{ @course_pace_module_item.id => Date.parse("2021-09-09") }
)
end
it "respects skipping weekends" do
@course.blackout_dates.create! event_title: "Blackout test", start_date: "2021-09-01", end_date: "2021-09-03"
expect(@calculator.get_due_dates(@pace_plan_module_items)).to eq(
{ @pace_plan_module_item.id => Date.parse("2021-09-06") }
expect(@calculator.get_due_dates(@course_pace_module_items)).to eq(
{ @course_pace_module_item.id => Date.parse("2021-09-06") }
)
@pace_plan.update exclude_weekends: false
expect(@calculator.get_due_dates(@pace_plan_module_items)).to eq(
{ @pace_plan_module_item.id => Date.parse("2021-09-04") }
@course_pace.update exclude_weekends: false
expect(@calculator.get_due_dates(@course_pace_module_items)).to eq(
{ @course_pace_module_item.id => Date.parse("2021-09-04") }
)
end
it "calculates from a given enrollment start date" do
enrollment = Enrollment.new(start_at: Date.parse("2021-09-09"))
expect(@calculator.get_due_dates(@pace_plan_module_items, enrollment)).to eq(
{ @pace_plan_module_item.id => Date.parse("2021-09-09") }
expect(@calculator.get_due_dates(@course_pace_module_items, enrollment)).to eq(
{ @course_pace_module_item.id => Date.parse("2021-09-09") }
)
end
end

View File

@ -18,11 +18,11 @@
# with this program. If not, see <http://www.gnu.org/licenses/>.
#
describe PacePlanHardEndDateCompressor do
describe CoursePaceHardEndDateCompressor do
before :once do
course_with_student active_all: true
@course.update start_at: "2021-09-01"
@pace_plan = @course.pace_plans.create! workflow_state: "active", end_date: "2021-09-10"
@course_pace = @course.course_paces.create! workflow_state: "active", end_date: "2021-09-10"
@module = @course.context_modules.create!
end
@ -31,54 +31,54 @@ describe PacePlanHardEndDateCompressor do
before :once do
assignment = @course.assignments.create!
assignment.context_module_tags.create! context_module: @module, context: @course, tag_type: "context_module"
@pace_plan.pace_plan_module_items.last.update! duration: 10
@course_pace.course_pace_module_items.last.update! duration: 10
assignment = @course.assignments.create!
assignment.context_module_tags.create! context_module: @module, context: @course, tag_type: "context_module"
@pace_plan.pace_plan_module_items.last.update! duration: 0
@course_pace.course_pace_module_items.last.update! duration: 0
assignment = @course.assignments.create!
assignment.context_module_tags.create! context_module: @module, context: @course, tag_type: "context_module"
@pace_plan.pace_plan_module_items.last.update! duration: 6
@course_pace.course_pace_module_items.last.update! duration: 6
end
it "compresses the plan items by the required percentage to reach the hard end date" do
compressed = PacePlanHardEndDateCompressor.compress(@pace_plan, @pace_plan.pace_plan_module_items)
compressed = CoursePaceHardEndDateCompressor.compress(@course_pace, @course_pace.course_pace_module_items)
expect(compressed.pluck(:duration)).to eq([5, 0, 2])
end
it "does nothing if the duration of the pace plan is within the end date" do
@pace_plan.update(end_date: "2022-09-10")
compressed = PacePlanHardEndDateCompressor.compress(@pace_plan, @pace_plan.pace_plan_module_items)
it "does nothing if the duration of the course pace is within the end date" do
@course_pace.update(end_date: "2022-09-10")
compressed = CoursePaceHardEndDateCompressor.compress(@course_pace, @course_pace.course_pace_module_items)
expect(compressed.pluck(:duration)).to eq([10, 0, 6])
end
it "compresses to end on the hard end date" do
@course.update(start_at: "2021-12-27")
@pace_plan.update(end_date: "2021-12-31", exclude_weekends: true, hard_end_dates: true)
@pace_plan.pace_plan_module_items.each_with_index do |item, index|
@course_pace.update(end_date: "2021-12-31", exclude_weekends: true, hard_end_dates: true)
@course_pace.course_pace_module_items.each_with_index do |item, index|
item.update(duration: (index + 1) * 2)
end
compressed = PacePlanHardEndDateCompressor.compress(@pace_plan, @pace_plan.pace_plan_module_items)
compressed = CoursePaceHardEndDateCompressor.compress(@course_pace, @course_pace.course_pace_module_items)
expect(compressed.pluck(:duration)).to eq([1, 1, 2])
end
context "implicit end dates" do
before :once do
@course.update(start_at: "2021-12-27")
@pace_plan.update(end_date: nil, exclude_weekends: true)
@pace_plan.pace_plan_module_items.each_with_index do |item, index|
@course_pace.update(end_date: nil, exclude_weekends: true)
@course_pace.course_pace_module_items.each_with_index do |item, index|
item.update(duration: (index + 1) * 2)
end
end
it "supports implicit end dates from the course's term" do
@course.enrollment_term.update(end_at: "2021-12-31")
compressed = PacePlanHardEndDateCompressor.compress(@pace_plan, @pace_plan.pace_plan_module_items)
compressed = CoursePaceHardEndDateCompressor.compress(@course_pace, @course_pace.course_pace_module_items)
expect(compressed.pluck(:duration)).to eq([1, 1, 2])
end
it "supports implicit end dates from the course" do
@course.update(conclude_at: "2021-12-31")
compressed = PacePlanHardEndDateCompressor.compress(@pace_plan, @pace_plan.pace_plan_module_items)
compressed = CoursePaceHardEndDateCompressor.compress(@course_pace, @course_pace.course_pace_module_items)
expect(compressed.pluck(:duration)).to eq([1, 1, 2])
end
end
@ -89,8 +89,8 @@ describe PacePlanHardEndDateCompressor do
assignment = @course.assignments.create!
assignment.context_module_tags.create! context_module: @module, context: @course, tag_type: "context_module"
end
@pace_plan.pace_plan_module_items.update(duration: 1)
compressed = PacePlanHardEndDateCompressor.compress(@pace_plan, @pace_plan.pace_plan_module_items)
@course_pace.course_pace_module_items.update(duration: 1)
compressed = CoursePaceHardEndDateCompressor.compress(@course_pace, @course_pace.course_pace_module_items)
expect(compressed.pluck(:duration)).to eq([1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0])
end
end
@ -99,19 +99,19 @@ describe PacePlanHardEndDateCompressor do
context "duration is >= 1" do
context "the remainder is greater than the breakpoint" do
it "rounds up if doing so would not cause an overallocation" do
rounded = PacePlanHardEndDateCompressor.round_durations([7.8], 78)
rounded = CoursePaceHardEndDateCompressor.round_durations([7.8], 78)
expect(rounded.map(&:duration)).to eq([8])
end
it "rounds down if rounding up would cause an overallocation" do
rounded = PacePlanHardEndDateCompressor.round_durations([7.8, 7.8, 7.8, 7.8, 7.8, 7.8, 7.8, 7.8, 7.8, 7.8], 78)
rounded = CoursePaceHardEndDateCompressor.round_durations([7.8, 7.8, 7.8, 7.8, 7.8, 7.8, 7.8, 7.8, 7.8, 7.8], 78)
expect(rounded.map(&:duration)).to eq([8, 8, 8, 8, 8, 8, 8, 8, 7, 7])
end
end
context "the remainder is less than the breakpoint" do
it "rounds down" do
rounded = PacePlanHardEndDateCompressor.round_durations([2.5], 2)
rounded = CoursePaceHardEndDateCompressor.round_durations([2.5], 2)
expect(rounded.map(&:duration)).to eq([2])
end
end
@ -119,7 +119,7 @@ describe PacePlanHardEndDateCompressor do
context "duration is < 1" do
it "does calculates the groups based off their remainders" do
rounded = PacePlanHardEndDateCompressor.round_durations([0.2, 0.2, 0.1, 0.2, 0.2, 0.5, 0.5, 0.5, 0.5, 0.5], 3)
rounded = CoursePaceHardEndDateCompressor.round_durations([0.2, 0.2, 0.1, 0.2, 0.2, 0.5, 0.5, 0.5, 0.5, 0.5], 3)
expect(rounded.map(&:duration)).to eq([1, 0, 0, 0, 0, 1, 0, 1, 0, 0])
end
end
@ -128,21 +128,21 @@ describe PacePlanHardEndDateCompressor do
describe ".shift_durations_down" do
context "all days over can be absorbed by last item" do
it "decreases last item duration by the number of days over" do
rounded = PacePlanHardEndDateCompressor.shift_durations_down([Duration.new(5), Duration.new(5), Duration.new(5)], 3)
rounded = CoursePaceHardEndDateCompressor.shift_durations_down([Duration.new(5), Duration.new(5), Duration.new(5)], 3)
expect(rounded.map(&:duration)).to eq([5, 5, 2])
end
end
context "number of days over is greater than last item duration" do
it "decreases last and subsequent item durations" do
rounded = PacePlanHardEndDateCompressor.shift_durations_down([Duration.new(1), Duration.new(1), Duration.new(1)], 2)
rounded = CoursePaceHardEndDateCompressor.shift_durations_down([Duration.new(1), Duration.new(1), Duration.new(1)], 2)
expect(rounded.map(&:duration)).to eq([1, 0, 0])
end
end
context "number of days over is greater than the sum of durations" do
it "decreases all durations to 0" do
rounded = PacePlanHardEndDateCompressor.shift_durations_down([Duration.new(1), Duration.new(1), Duration.new(1)], 5)
rounded = CoursePaceHardEndDateCompressor.shift_durations_down([Duration.new(1), Duration.new(1), Duration.new(1)], 5)
expect(rounded.map(&:duration)).to eq([0, 0, 0])
end
end

View File

@ -20,29 +20,29 @@
require_relative "course_copy_helper"
describe ContentMigration do
context "pace plans" do
context "course paces" do
include_context "course copy"
it "copies pace plan attributes" do
pace_plan = @copy_from.pace_plans.new
pace_plan.workflow_state = "active"
pace_plan.end_date = 1.day.from_now.beginning_of_day
pace_plan.published_at = Time.now.utc
pace_plan.exclude_weekends = false
pace_plan.hard_end_dates = true
pace_plan.save!
it "copies course pace attributes" do
course_pace = @copy_from.course_paces.new
course_pace.workflow_state = "active"
course_pace.end_date = 1.day.from_now.beginning_of_day
course_pace.published_at = Time.now.utc
course_pace.exclude_weekends = false
course_pace.hard_end_dates = true
course_pace.save!
run_course_copy
expect(@copy_to.pace_plans.count).to eq 1
expect(@copy_to.course_paces.count).to eq 1
pace_plan_to = @copy_to.pace_plans.take
course_pace_to = @copy_to.course_paces.take
expect(pace_plan_to.workflow_state).to eq "active"
expect(pace_plan_to.start_date).to eq pace_plan.start_date
expect(pace_plan_to.end_date).to eq pace_plan.end_date
expect(pace_plan_to.published_at.to_i).to eq pace_plan.published_at.to_i
expect(pace_plan_to.exclude_weekends).to eq false
expect(pace_plan_to.hard_end_dates).to eq true
expect(course_pace_to.workflow_state).to eq "active"
expect(course_pace_to.start_date).to eq course_pace.start_date
expect(course_pace_to.end_date).to eq course_pace.end_date
expect(course_pace_to.published_at.to_i).to eq course_pace.published_at.to_i
expect(course_pace_to.exclude_weekends).to eq false
expect(course_pace_to.hard_end_dates).to eq true
end
context "module items" do
@ -54,31 +54,31 @@ describe ContentMigration do
@mod2 = @copy_from.context_modules.create! name: "module2"
@tag2 = @mod2.add_item(type: "assignment", id: @a2.id)
@pace_plan = @copy_from.pace_plans.create!
@pace_plan.pace_plan_module_items.create! duration: 1, module_item_id: @tag1.id
@pace_plan.pace_plan_module_items.create! duration: 2, module_item_id: @tag2.id
@course_pace = @copy_from.course_paces.create!
@course_pace.course_pace_module_items.create! duration: 1, module_item_id: @tag1.id
@course_pace.course_pace_module_items.create! duration: 2, module_item_id: @tag2.id
end
it "copies pace plan module item durations" do
it "copies course pace module item durations" do
run_course_copy
tag1_to = @copy_to.context_module_tags.where(migration_id: mig_id(@tag1)).take
tag2_to = @copy_to.context_module_tags.where(migration_id: mig_id(@tag2)).take
pace_plan_to = @copy_to.pace_plans.where(workflow_state: "unpublished").take
course_pace_to = @copy_to.course_paces.where(workflow_state: "unpublished").take
expect(pace_plan_to.pace_plan_module_items.find_by(module_item_id: tag1_to.id).duration).to eq 1
expect(pace_plan_to.pace_plan_module_items.find_by(module_item_id: tag2_to.id).duration).to eq 2
expect(course_pace_to.course_pace_module_items.find_by(module_item_id: tag1_to.id).duration).to eq 1
expect(course_pace_to.course_pace_module_items.find_by(module_item_id: tag2_to.id).duration).to eq 2
end
it "copies a subset of module items in selective migrations" do
@cm.copy_options = {
all_pace_plans: true,
all_course_paces: true,
context_modules: { mig_id(@mod1) => true }
}
run_course_copy
pace_plan_to = @copy_to.pace_plans.where(workflow_state: "unpublished").take
expect(pace_plan_to.pace_plan_module_items.count).to eq 1
course_pace_to = @copy_to.course_paces.where(workflow_state: "unpublished").take
expect(course_pace_to.course_pace_module_items.count).to eq 1
end
end
end

View File

@ -911,30 +911,30 @@ describe ContentTag do
end
end
describe "#update_pace_plan_module_items" do
describe "#update_course_pace_module_items" do
before do
course_factory
@context_module = @course.context_modules.create!
@assignment = @course.assignments.create!
@pace_plan = @course.pace_plans.create!
@course_pace = @course.course_paces.create!
@context_module.add_item(id: @assignment.id, type: "assignment")
@tag = @context_module.content_tags.first
end
it "creates a pace plan module item if a new content tag is created" do
it "creates a course pace module item if a new content tag is created" do
assignment = @course.assignments.create!
@context_module.add_item(id: assignment.id, type: "assignment")
tag = @context_module.content_tags.find_by(content_id: assignment.id)
tag.update_pace_plan_module_items
expect(@pace_plan.pace_plan_module_items.where(module_item_id: tag.id).exists?).to eq(true)
tag.update_course_pace_module_items
expect(@course_pace.course_pace_module_items.where(module_item_id: tag.id).exists?).to eq(true)
end
it "deletes a PacePlanModuleItem if a content tag is deleted" do
@tag.update_pace_plan_module_items
expect(@pace_plan.pace_plan_module_items.where(module_item_id: @tag.id).exists?).to eq(true)
it "deletes a CoursePaceModuleItem if a content tag is deleted" do
@tag.update_course_pace_module_items
expect(@course_pace.course_pace_module_items.where(module_item_id: @tag.id).exists?).to eq(true)
@tag.destroy
@tag.update_pace_plan_module_items
expect(@pace_plan.pace_plan_module_items.where(module_item_id: @tag.id).exists?).to eq(false)
@tag.update_course_pace_module_items
expect(@course_pace.course_pace_module_items.where(module_item_id: @tag.id).exists?).to eq(false)
end
end
end

View File

@ -19,7 +19,7 @@
#
require_relative "../spec_helper"
describe PacePlanModuleItem do
describe CoursePaceModuleItem do
before :once do
course_with_student active_all: true
@ -33,19 +33,19 @@ describe PacePlanModuleItem do
@a3 = @course.assignments.create! name: "A3", workflow_state: "unpublished"
@mod2.add_item id: @a3.id, type: "assignment"
@pace_plan = @course.pace_plans.create!
@course_pace = @course.course_paces.create!
@course.context_module_tags.each do |tag|
@pace_plan.pace_plan_module_items.create! module_item: tag
@course_pace.course_pace_module_items.create! module_item: tag
end
end
context "associations" do
before :once do
@item = @pace_plan.pace_plan_module_items.take
@item = @course_pace.course_pace_module_items.take
end
it "has a functioning pace_plan association" do
expect(@item.pace_plan).to eq @pace_plan
it "has a functioning course_pace association" do
expect(@item.course_pace).to eq @course_pace
end
it "has a functioning module_item association" do
@ -55,21 +55,21 @@ describe PacePlanModuleItem do
context "scopes" do
it "can filter on active module item status" do
expect(@pace_plan.pace_plan_module_items.active).to be_empty
expect(@course_pace.course_pace_module_items.active).to be_empty
@a3.publish!
items = @pace_plan.pace_plan_module_items.active
items = @course_pace.course_pace_module_items.active
expect(items.size).to eq 1
expect(items.first.module_item.content).to eq @a3
end
it "can order based on module progression order" do
expect(@pace_plan.pace_plan_module_items.ordered.map { |item| item.module_item.content }).to eq([@a1, @a2, @a3])
expect(@course_pace.course_pace_module_items.ordered.map { |item| item.module_item.content }).to eq([@a1, @a2, @a3])
end
end
context "root_account_id" do
it "infers root_account_id from pace_plan" do
expect(@pace_plan.pace_plan_module_items.first.root_account).to eq @course.root_account
it "infers root_account_id from course_pace" do
expect(@course_pace.course_pace_module_items.first.root_account).to eq @course.root_account
end
end
@ -78,12 +78,12 @@ describe PacePlanModuleItem do
quiz = @course.quizzes.create!
quiz_tag = @mod2.add_item id: quiz.id, type: "quiz"
header_tag = @mod2.add_item type: "context_module_sub_header", title: "not an assignment"
expect(@pace_plan.pace_plan_module_items.build(module_item: quiz_tag)).not_to be_valid
expect(@pace_plan.pace_plan_module_items.build(module_item: header_tag)).not_to be_valid
expect(@course_pace.course_pace_module_items.build(module_item: quiz_tag)).not_to be_valid
expect(@course_pace.course_pace_module_items.build(module_item: header_tag)).not_to be_valid
quiz.save! # Save again to create associated assignment
quiz_tag.reload
expect(@pace_plan.pace_plan_module_items.build(module_item: quiz_tag)).to be_valid
expect(@pace_plan.pace_plan_module_items.build(module_item: header_tag)).not_to be_valid
expect(@course_pace.course_pace_module_items.build(module_item: quiz_tag)).to be_valid
expect(@course_pace.course_pace_module_items.build(module_item: header_tag)).not_to be_valid
end
end
end

View File

@ -19,7 +19,7 @@
#
require_relative "../spec_helper"
describe PacePlan do
describe CoursePace do
before :once do
course_with_student active_all: true
@course.update start_at: "2021-09-01"
@ -27,46 +27,46 @@ describe PacePlan do
@assignment = @course.assignments.create!
@course_section = @course.course_sections.first
@tag = @assignment.context_module_tags.create! context_module: @module, context: @course, tag_type: "context_module"
@pace_plan = @course.pace_plans.create! workflow_state: "active"
@pace_plan_module_item = @pace_plan.pace_plan_module_items.create! module_item: @tag
@course_pace = @course.course_paces.create! workflow_state: "active"
@course_pace_module_item = @course_pace.course_pace_module_items.create! module_item: @tag
@unpublished_assignment = @course.assignments.create! workflow_state: "unpublished"
@unpublished_tag = @unpublished_assignment.context_module_tags.create! context_module: @module, context: @course, tag_type: "context_module", workflow_state: "unpublished"
end
context "associations" do
it "has functioning course association" do
expect(@course.pace_plans).to match_array([@pace_plan])
expect(@pace_plan.course).to eq @course
expect(@course.course_paces).to match_array([@course_pace])
expect(@course_pace.course).to eq @course
end
it "has functioning pace_plan_module_items association" do
expect(@pace_plan.pace_plan_module_items.map(&:module_item)).to match_array([@tag, @unpublished_tag])
it "has functioning course_pace_module_items association" do
expect(@course_pace.course_pace_module_items.map(&:module_item)).to match_array([@tag, @unpublished_tag])
end
end
context "scopes" do
before :once do
@other_section = @course.course_sections.create! name: "other_section"
@section_plan = @course.pace_plans.create! course_section: @other_section
@student_plan = @course.pace_plans.create! user: @student
@section_plan = @course.course_paces.create! course_section: @other_section
@student_plan = @course.course_paces.create! user: @student
end
it "has a working primary scope" do
expect(@course.pace_plans.primary).to match_array([@pace_plan])
expect(@course.course_paces.primary).to match_array([@course_pace])
end
it "has a working for_user scope" do
expect(@course.pace_plans.for_user(@student)).to match_array([@student_plan])
expect(@course.course_paces.for_user(@student)).to match_array([@student_plan])
end
it "has a working for_section scope" do
expect(@course.pace_plans.for_section(@other_section)).to match_array([@section_plan])
expect(@course.course_paces.for_section(@other_section)).to match_array([@section_plan])
end
end
context "pace_plan_context" do
context "course_pace_context" do
it "requires a course" do
bad_plan = PacePlan.create
bad_plan = CoursePace.create
expect(bad_plan).not_to be_valid
bad_plan.course = course_factory
@ -75,7 +75,7 @@ describe PacePlan do
it "disallows a user and section simultaneously" do
course_with_student
bad_plan = @course.pace_plans.build(user: @student, course_section: @course.default_section)
bad_plan = @course.course_paces.build(user: @student, course_section: @course.default_section)
expect(bad_plan).not_to be_valid
bad_plan.course_section = nil
@ -84,63 +84,63 @@ describe PacePlan do
end
context "constraints" do
it "has a unique constraint on course for active primary pace plans" do
expect { @course.pace_plans.create! workflow_state: "active" }.to raise_error(ActiveRecord::RecordNotUnique)
it "has a unique constraint on course for active primary course paces" do
expect { @course.course_paces.create! workflow_state: "active" }.to raise_error(ActiveRecord::RecordNotUnique)
end
it "has a unique constraint for active section pace plans" do
@course.pace_plans.create! course_section: @course.default_section, workflow_state: "active"
it "has a unique constraint for active section course paces" do
@course.course_paces.create! course_section: @course.default_section, workflow_state: "active"
expect do
@course.pace_plans.create! course_section: @course.default_section, workflow_state: "active"
@course.course_paces.create! course_section: @course.default_section, workflow_state: "active"
end.to raise_error(ActiveRecord::RecordNotUnique)
end
it "has a unique constraint for active student pace plans" do
@course.pace_plans.create! user: @student, workflow_state: "active"
it "has a unique constraint for active student course paces" do
@course.course_paces.create! user: @student, workflow_state: "active"
expect do
@course.pace_plans.create! user: @student, workflow_state: "active"
@course.course_paces.create! user: @student, workflow_state: "active"
end.to raise_error(ActiveRecord::RecordNotUnique)
end
end
context "root_account" do
it "infers root_account_id from course" do
expect(@pace_plan.root_account).to eq @course.root_account
expect(@course_pace.root_account).to eq @course.root_account
end
end
context "duplicate" do
it "returns an initialized duplicate of the pace plan" do
duplicate_pace_plan = @pace_plan.duplicate
expect(duplicate_pace_plan.class).to eq(PacePlan)
expect(duplicate_pace_plan.persisted?).to eq(false)
expect(duplicate_pace_plan.id).to eq(nil)
it "returns an initialized duplicate of the course pace" do
duplicate_course_pace = @course_pace.duplicate
expect(duplicate_course_pace.class).to eq(CoursePace)
expect(duplicate_course_pace.persisted?).to eq(false)
expect(duplicate_course_pace.id).to eq(nil)
end
it "supports passing in options" do
opts = { user_id: 1 }
duplicate_pace_plan = @pace_plan.duplicate(opts)
expect(duplicate_pace_plan.user_id).to eq(opts[:user_id])
expect(duplicate_pace_plan.course_section_id).to eq(opts[:course_section_id])
duplicate_course_pace = @course_pace.duplicate(opts)
expect(duplicate_course_pace.user_id).to eq(opts[:user_id])
expect(duplicate_course_pace.course_section_id).to eq(opts[:course_section_id])
end
end
context "publish" do
before :once do
@pace_plan.update! end_date: "2021-09-30"
@course_pace.update! end_date: "2021-09-30"
end
it "creates an override for students" do
expect(@assignment.due_at).to eq(nil)
expect(@unpublished_assignment.due_at).to eq(nil)
expect(@pace_plan.publish).to eq(true)
expect(@course_pace.publish).to eq(true)
expect(AssignmentOverride.count).to eq(2)
end
it "creates assignment overrides for the pace plan user" do
@pace_plan.update(user_id: @student)
it "creates assignment overrides for the course pace user" do
@course_pace.update(user_id: @student)
expect(AssignmentOverride.count).to eq(0)
expect(@pace_plan.publish).to eq(true)
expect(@course_pace.publish).to eq(true)
expect(AssignmentOverride.count).to eq(2)
@course.assignments.each do |assignment|
assignment_override = assignment.assignment_overrides.first
@ -162,12 +162,12 @@ describe PacePlan do
assignment_override.assignment_override_students << AssignmentOverrideStudent.new(user_id: @student, no_enrollment: false)
assignment_override.assignment_override_students << AssignmentOverrideStudent.new(user_id: student2, no_enrollment: false)
@pace_plan.update(user_id: @student)
@course_pace.update(user_id: @student)
expect(@assignment.assignment_overrides.count).to eq(1)
assignment_override = @assignment.assignment_overrides.first
expect(assignment_override.due_at).to eq(Date.parse("2021-09-05").end_of_day)
expect(assignment_override.assignment_override_students.pluck(:user_id)).to eq([@student.id, student2.id])
expect(@pace_plan.publish).to eq(true)
expect(@course_pace.publish).to eq(true)
expect(@assignment.assignment_overrides.count).to eq(2)
expect(assignment_override.due_at).to eq(Date.parse("2021-09-05").end_of_day)
expect(assignment_override.assignment_override_students.pluck(:user_id)).to eq([student2.id])
@ -176,10 +176,10 @@ describe PacePlan do
expect(assignment_override2.assignment_override_students.pluck(:user_id)).to eq([@student.id])
end
it "creates assignment overrides for the pace plan course section" do
@pace_plan.update(course_section: @course_section)
it "creates assignment overrides for the course pace course section" do
@course_pace.update(course_section: @course_section)
expect(@assignment.assignment_overrides.count).to eq(0)
expect(@pace_plan.publish).to eq(true)
expect(@course_pace.publish).to eq(true)
expect(@assignment.assignment_overrides.count).to eq(1)
assignment_override = @assignment.assignment_overrides.first
expect(assignment_override.due_at).to eq(Date.parse("2021-09-01").end_of_day)
@ -187,21 +187,21 @@ describe PacePlan do
end
it "updates overrides that are already present if the days have changed" do
@pace_plan.publish
@course_pace.publish
assignment_override = @assignment.assignment_overrides.first
expect(assignment_override.due_at).to eq(Date.parse("2021-09-01").end_of_day)
@pace_plan_module_item.update duration: 2
@pace_plan.publish
@course_pace_module_item.update duration: 2
@course_pace.publish
assignment_override.reload
expect(assignment_override.due_at).to eq(Date.parse("2021-09-03").end_of_day)
end
it "updates user overrides that are already present if the days have changed" do
@pace_plan.update(user_id: @student)
@pace_plan.publish
@course_pace.update(user_id: @student)
@course_pace.publish
expect(@assignment.assignment_overrides.active.count).to eq(1)
@pace_plan_module_item.update duration: 2
@pace_plan.publish
@course_pace_module_item.update duration: 2
@course_pace.publish
expect(@assignment.assignment_overrides.active.count).to eq(1)
assignment_override = @assignment.assignment_overrides.active.first
expect(assignment_override.due_at).to eq(Date.parse("2021-09-03").end_of_day)
@ -209,25 +209,25 @@ describe PacePlan do
end
it "updates course section overrides that are already present if the days have changed" do
@pace_plan.update(course_section: @course_section)
@pace_plan.publish
@course_pace.update(course_section: @course_section)
@course_pace.publish
expect(@assignment.assignment_overrides.active.count).to eq(1)
@pace_plan_module_item.update duration: 2
@pace_plan.publish
@course_pace_module_item.update duration: 2
@course_pace.publish
expect(@assignment.assignment_overrides.active.count).to eq(1)
assignment_override = @assignment.assignment_overrides.active.first
expect(assignment_override.due_at).to eq(Date.parse("2021-09-03").end_of_day)
expect(assignment_override.assignment_override_students.first.user_id).to eq(@student.id)
end
it "does not change assignment due date when user pace plan is published if an assignment override already exists" do
@pace_plan.publish
it "does not change assignment due date when user course pace is published if an assignment override already exists" do
@course_pace.publish
assignment_override = @assignment.assignment_overrides.first
expect(assignment_override.due_at).to eq(Date.parse("2021-09-01").end_of_day)
expect(@assignment.assignment_overrides.active.count).to eq(1)
student_pace_plan = @course.pace_plans.create!(user: @student, workflow_state: "active")
student_pace_plan.publish
student_course_pace = @course.course_paces.create!(user: @student, workflow_state: "active")
student_course_pace.publish
assignment_override.reload
expect(assignment_override.due_at).to eq(Date.parse("2021-09-01").end_of_day)
expect(@assignment.assignment_overrides.active.count).to eq(1)
@ -236,66 +236,66 @@ describe PacePlan do
it "sets overrides for graded discussions" do
topic = graded_discussion_topic(context: @course)
topic_tag = @module.add_item type: "discussion_topic", id: topic.id
@pace_plan.pace_plan_module_items.create! module_item: topic_tag
@course_pace.course_pace_module_items.create! module_item: topic_tag
expect(topic.assignment.assignment_overrides.count).to eq 0
expect(@pace_plan.publish).to eq(true)
expect(@course_pace.publish).to eq(true)
expect(topic.assignment.assignment_overrides.count).to eq 1
end
it "does not change overrides for students that have pace plans if the course pace plan is published" do
expect(@pace_plan.publish).to eq(true)
it "does not change overrides for students that have course paces if the course pace is published" do
expect(@course_pace.publish).to eq(true)
expect(@assignment.assignment_overrides.active.count).to eq(1)
assignment_override = @assignment.assignment_overrides.active.first
expect(assignment_override.due_at).to eq(Date.parse("2021-09-01").end_of_day)
# Publish student specific pace plan and verify dates have changed
student_pace_plan = @course.pace_plans.create! user: @student, workflow_state: "active"
# Publish student specific course pace and verify dates have changed
student_course_pace = @course.course_paces.create! user: @student, workflow_state: "active"
@course.student_enrollments.find_by(user: @student).update(start_at: "2021-09-06")
student_pace_plan.pace_plan_module_items.create! module_item: @tag
expect(student_pace_plan.publish).to eq(true)
student_course_pace.course_pace_module_items.create! module_item: @tag
expect(student_course_pace.publish).to eq(true)
assignment_override.reload
expect(assignment_override.due_at).to eq(Date.parse("2021-09-06").end_of_day)
# Republish course pace plan and verify dates have not changed on student specific override
@pace_plan.instance_variable_set(:@student_enrollments, nil)
expect(@pace_plan.publish).to eq(true)
# Republish course pace and verify dates have not changed on student specific override
@course_pace.instance_variable_set(:@student_enrollments, nil)
expect(@course_pace.publish).to eq(true)
assignment_override.reload
expect(assignment_override.due_at).to eq(Date.parse("2021-09-06").end_of_day)
end
it "does not change overrides for sections that have pace plans if the course pace plan is published" do
expect(@pace_plan.publish).to eq(true)
it "does not change overrides for sections that have course paces if the course pace is published" do
expect(@course_pace.publish).to eq(true)
expect(@assignment.assignment_overrides.active.count).to eq(1)
assignment_override = @assignment.assignment_overrides.active.first
expect(assignment_override.due_at).to eq(Date.parse("2021-09-01").end_of_day)
# Publish course section specific pace plan and verify dates have changed
# Publish course section specific course pace and verify dates have changed
@course_section.update(start_at: "2021-09-06")
section_pace_plan = @course.pace_plans.create! course_section: @course_section, workflow_state: "active"
section_pace_plan.pace_plan_module_items.create! module_item: @tag
expect(section_pace_plan.publish).to eq(true)
section_course_pace = @course.course_paces.create! course_section: @course_section, workflow_state: "active"
section_course_pace.course_pace_module_items.create! module_item: @tag
expect(section_course_pace.publish).to eq(true)
assignment_override.reload
expect(assignment_override.due_at).to eq(Date.parse("2021-09-06").end_of_day)
# Republish course pace plan and verify dates have not changed on student specific override
@pace_plan.instance_variable_set(:@student_enrollments, nil)
expect(@pace_plan.publish).to eq(true)
# Republish course pace and verify dates have not changed on student specific override
@course_pace.instance_variable_set(:@student_enrollments, nil)
expect(@course_pace.publish).to eq(true)
assignment_override.reload
expect(assignment_override.due_at).to eq(Date.parse("2021-09-06").end_of_day)
end
it "does not change overrides for students that have pace plans if the course section pace plan is published" do
@pace_plan.update(course_section: @course_section)
expect(@pace_plan.publish).to eq(true)
it "does not change overrides for students that have course paces if the course section course pace is published" do
@course_pace.update(course_section: @course_section)
expect(@course_pace.publish).to eq(true)
expect(@assignment.assignment_overrides.active.count).to eq(1)
assignment_override = @assignment.assignment_overrides.active.first
expect(assignment_override.due_at).to eq(Date.parse("2021-09-01").end_of_day)
# Publish student specific pace plan and verify dates have changed
# Publish student specific course pace and verify dates have changed
@course.student_enrollments.find_by(user: @student).update(start_at: "2021-09-06")
student_pace_plan = @course.pace_plans.create! user: @student, workflow_state: "active"
student_pace_plan.pace_plan_module_items.create! module_item: @tag
expect(student_pace_plan.publish).to eq(true)
student_course_pace = @course.course_paces.create! user: @student, workflow_state: "active"
student_course_pace.course_pace_module_items.create! module_item: @tag
expect(student_course_pace.publish).to eq(true)
assignment_override.reload
expect(assignment_override.due_at).to eq(Date.parse("2021-09-06").end_of_day)
# Republish course pace plan and verify dates have not changed on student specific override
@pace_plan.instance_variable_set(:@student_enrollments, nil)
expect(@pace_plan.publish).to eq(true)
# Republish course pace and verify dates have not changed on student specific override
@course_pace.instance_variable_set(:@student_enrollments, nil)
expect(@course_pace.publish).to eq(true)
assignment_override.reload
expect(assignment_override.due_at).to eq(Date.parse("2021-09-06").end_of_day)
end
@ -304,35 +304,35 @@ describe PacePlan do
describe "default plan start_at" do
before do
@course.update start_at: nil
@pace_plan.user_id = nil
@course_pace.user_id = nil
end
it "returns student enrollment date, if working on behalf of a student" do
student3 = user_model
enrollment = StudentEnrollment.create!(user: student3, course: @course)
enrollment.update start_at: "2022-01-29"
@pace_plan.user_id = student3.id
expect(@pace_plan.start_date.to_date).to eq(Date.parse("2022-01-29"))
@course_pace.user_id = student3.id
expect(@course_pace.start_date.to_date).to eq(Date.parse("2022-01-29"))
end
it "returns section start if available" do
other_section = @course.course_sections.create! name: "other_section", start_at: "2022-01-30"
section_plan = @course.pace_plans.create! course_section: other_section
section_plan = @course.course_paces.create! course_section: other_section
expect(section_plan.start_date.to_date).to eq(Date.parse("2022-01-30"))
end
it "returns course start if available" do
@course.update start_at: "2022-01-28"
expect(@pace_plan.start_date.to_date).to eq(Date.parse("2022-01-28"))
expect(@course_pace.start_date.to_date).to eq(Date.parse("2022-01-28"))
end
it "returns course's term start if available" do
@course.enrollment_term.update start_at: "2022-01-27"
expect(@pace_plan.start_date.to_date).to eq(Date.parse("2022-01-27"))
expect(@course_pace.start_date.to_date).to eq(Date.parse("2022-01-27"))
end
it "returns course created_at date as a last resort" do
expect(@pace_plan.start_date.to_date).to eq(@course.created_at.to_date)
expect(@course_pace.start_date.to_date).to eq(@course.created_at.to_date)
end
end
end

View File

@ -3112,21 +3112,21 @@ describe Course do
end
end
context "pace plans" do
context "course paces" do
before :once do
@course.account.enable_feature!(:pace_plans)
@course.enable_pace_plans = true
@course.account.enable_feature!(:course_paces)
@course.enable_course_paces = true
@course.save!
end
it "is included when pace plans is enabled" do
it "is included when course paces is enabled" do
tabs = @course.tabs_available(@teacher).pluck(:id)
expect(tabs).to include(Course::TAB_PACE_PLANS)
expect(tabs).to include(Course::TAB_COURSE_PACES)
end
it "is not included for students" do
tabs = @course.tabs_available(@student).pluck(:id)
expect(tabs).not_to include(Course::TAB_PACE_PLANS)
expect(tabs).not_to include(Course::TAB_COURSE_PACES)
end
end
end

View File

@ -153,58 +153,58 @@ describe StudentEnrollment do
end
end
describe "pace plan republishing" do
describe "course pace republishing" do
before :once do
@enrollment = course_with_student active_all: true
@pace_plan = @course.pace_plans.create!
@pace_plan.publish
@course_pace = @course.course_paces.create!
@course_pace.publish
end
it "does nothing if pace plans aren't turned on" do
it "does nothing if course paces aren't turned on" do
@enrollment.update start_at: 1.day.from_now
expect(Delayed::Job.where(singleton: "pace_plan_republish:#{@course.global_id}:")).not_to exist
expect(Delayed::Job.where(singleton: "course_pace_republish:#{@course.global_id}:")).not_to exist
end
context "with pace plans enabled" do
context "with course paces enabled" do
before :once do
@course.enable_pace_plans = true
@course.enable_course_paces = true
@course.save!
end
it "queues an update for a new student enrollment" do
student_in_course(active_all: true, user: user_with_pseudonym)
expect(Delayed::Job.where(singleton: "pace_plan_republish:#{@course.global_id}:")).to exist
expect(Delayed::Job.where(singleton: "course_pace_republish:#{@course.global_id}:")).to exist
end
it "doesn't queue an update if the pace plan isn't published" do
@pace_plan.update workflow_state: "unpublished"
it "doesn't queue an update if the course pace isn't published" do
@course_pace.update workflow_state: "unpublished"
student_in_course(active_all: true, user: user_with_pseudonym)
expect(Delayed::Job.where(singleton: "pace_plan_republish:#{@course.global_id}:")).not_to exist
expect(Delayed::Job.where(singleton: "course_pace_republish:#{@course.global_id}:")).not_to exist
end
it "publishes a student pace plan (alone) if it exists" do
student_pace_plan = @course.pace_plans.create!(user_id: @enrollment.user_id)
student_pace_plan.publish
it "publishes a student course pace (alone) if it exists" do
student_course_pace = @course.course_paces.create!(user_id: @enrollment.user_id)
student_course_pace.publish
@enrollment.start_at = 2.days.from_now
@enrollment.save!
expect(Delayed::Job.where(singleton: "pace_plan_republish:#{@course.global_id}:")).not_to exist
expect(Delayed::Job.where(singleton: "pace_plan_republish:#{@course.global_id}:#{@enrollment.global_user_id}")).to exist
expect(Delayed::Job.where(singleton: "course_pace_republish:#{@course.global_id}:")).not_to exist
expect(Delayed::Job.where(singleton: "course_pace_republish:#{@course.global_id}:#{@enrollment.global_user_id}")).to exist
end
it "doesn't queue an update for irrelevant changes" do
@enrollment.last_attended_at = 1.day.ago
@enrollment.save!
expect(Delayed::Job.where(singleton: "pace_plan_republish:#{@course.global_id}:")).not_to exist
expect(Delayed::Job.where(singleton: "course_pace_republish:#{@course.global_id}:")).not_to exist
end
it "queues only one update when multiple enrollments are created" do
3.times { student_in_course(active_all: true, user: user_with_pseudonym) }
expect(Delayed::Job.where("singleton LIKE 'pace_plan_republish:%'").count).to eq 1
expect(Delayed::Job.where("singleton LIKE 'course_pace_republish:%'").count).to eq 1
end
it "doesn't queue an update for non-student-enrollment creation" do
ta_in_course(active_all: true, user: user_with_pseudonym)
expect(Delayed::Job.where(singleton: "pace_plan_republish:#{@course.global_id}:")).not_to exist
expect(Delayed::Job.where(singleton: "course_pace_republish:#{@course.global_id}:")).not_to exist
end
end
end

View File

@ -17,12 +17,12 @@
# You should have received a copy of the GNU Affero General Public License along
# with this program. If not, see <http://www.gnu.org/licenses/>.
describe PacePlanPresenter do
describe CoursePacePresenter do
describe "#as_json" do
before :once do
course_with_teacher(active_all: true)
student_in_course(active_all: true)
pace_plan_model(course: @course)
course_pace_model(course: @course)
@mod1 = @course.context_modules.create! name: "M1"
@a1 = @course.assignments.create! name: "A1", points_possible: 100, workflow_state: "active"
@ -35,23 +35,23 @@ describe PacePlanPresenter do
@ct3 = @mod2.add_item id: @a3.id, type: "assignment"
end
it "returns all necessary data for the pace plan" do
formatted_plan = PacePlanPresenter.new(@pace_plan).as_json
it "returns all necessary data for the course pace" do
formatted_plan = CoursePacePresenter.new(@course_pace).as_json
expect(formatted_plan[:id]).to eq(@pace_plan.id)
expect(formatted_plan[:context_id]).to eq(@pace_plan.course_id)
expect(formatted_plan[:id]).to eq(@course_pace.id)
expect(formatted_plan[:context_id]).to eq(@course_pace.course_id)
expect(formatted_plan[:context_type]).to eq("Course")
expect(formatted_plan[:course_id]).to eq(@pace_plan.course_id)
expect(formatted_plan[:course_section_id]).to eq(@pace_plan.course_section_id)
expect(formatted_plan[:user_id]).to eq(@pace_plan.user_id)
expect(formatted_plan[:workflow_state]).to eq(@pace_plan.workflow_state)
expect(formatted_plan[:end_date]).to eq(@pace_plan.end_date)
expect(formatted_plan[:exclude_weekends]).to eq(@pace_plan.exclude_weekends)
expect(formatted_plan[:hard_end_dates]).to eq(@pace_plan.hard_end_dates)
expect(formatted_plan[:created_at]).to eq(@pace_plan.created_at)
expect(formatted_plan[:updated_at]).to eq(@pace_plan.updated_at)
expect(formatted_plan[:published_at]).to eq(@pace_plan.published_at)
expect(formatted_plan[:root_account_id]).to eq(@pace_plan.root_account_id)
expect(formatted_plan[:course_id]).to eq(@course_pace.course_id)
expect(formatted_plan[:course_section_id]).to eq(@course_pace.course_section_id)
expect(formatted_plan[:user_id]).to eq(@course_pace.user_id)
expect(formatted_plan[:workflow_state]).to eq(@course_pace.workflow_state)
expect(formatted_plan[:end_date]).to eq(@course_pace.end_date)
expect(formatted_plan[:exclude_weekends]).to eq(@course_pace.exclude_weekends)
expect(formatted_plan[:hard_end_dates]).to eq(@course_pace.hard_end_dates)
expect(formatted_plan[:created_at]).to eq(@course_pace.created_at)
expect(formatted_plan[:updated_at]).to eq(@course_pace.updated_at)
expect(formatted_plan[:published_at]).to eq(@course_pace.published_at)
expect(formatted_plan[:root_account_id]).to eq(@course_pace.root_account_id)
expect(formatted_plan[:modules].size).to eq(2)
first_module = formatted_plan[:modules].first
@ -86,27 +86,27 @@ describe PacePlanPresenter do
expect(second_module_item[:published]).to eq(true)
end
it "returns necessary data if the pace plan is only instantiated" do
pace_plan = @course.pace_plans.new
it "returns necessary data if the course pace is only instantiated" do
course_pace = @course.course_paces.new
@course.context_module_tags.each do |module_item|
pace_plan.pace_plan_module_items.new module_item: module_item, duration: 0
course_pace.course_pace_module_items.new module_item: module_item, duration: 0
end
formatted_plan = PacePlanPresenter.new(pace_plan).as_json
formatted_plan = CoursePacePresenter.new(course_pace).as_json
expect(formatted_plan[:id]).to eq(pace_plan.id)
expect(formatted_plan[:context_id]).to eq(pace_plan.course_id)
expect(formatted_plan[:id]).to eq(course_pace.id)
expect(formatted_plan[:context_id]).to eq(course_pace.course_id)
expect(formatted_plan[:context_type]).to eq("Course")
expect(formatted_plan[:course_id]).to eq(pace_plan.course_id)
expect(formatted_plan[:course_section_id]).to eq(pace_plan.course_section_id)
expect(formatted_plan[:user_id]).to eq(pace_plan.user_id)
expect(formatted_plan[:workflow_state]).to eq(pace_plan.workflow_state)
expect(formatted_plan[:end_date]).to eq(pace_plan.end_date)
expect(formatted_plan[:exclude_weekends]).to eq(pace_plan.exclude_weekends)
expect(formatted_plan[:hard_end_dates]).to eq(pace_plan.hard_end_dates)
expect(formatted_plan[:created_at]).to eq(pace_plan.created_at)
expect(formatted_plan[:updated_at]).to eq(pace_plan.updated_at)
expect(formatted_plan[:published_at]).to eq(pace_plan.published_at)
expect(formatted_plan[:root_account_id]).to eq(pace_plan.root_account_id)
expect(formatted_plan[:course_id]).to eq(course_pace.course_id)
expect(formatted_plan[:course_section_id]).to eq(course_pace.course_section_id)
expect(formatted_plan[:user_id]).to eq(course_pace.user_id)
expect(formatted_plan[:workflow_state]).to eq(course_pace.workflow_state)
expect(formatted_plan[:end_date]).to eq(course_pace.end_date)
expect(formatted_plan[:exclude_weekends]).to eq(course_pace.exclude_weekends)
expect(formatted_plan[:hard_end_dates]).to eq(course_pace.hard_end_dates)
expect(formatted_plan[:created_at]).to eq(course_pace.created_at)
expect(formatted_plan[:updated_at]).to eq(course_pace.updated_at)
expect(formatted_plan[:published_at]).to eq(course_pace.published_at)
expect(formatted_plan[:root_account_id]).to eq(course_pace.root_account_id)
expect(formatted_plan[:modules].size).to eq(2)
first_module = formatted_plan[:modules].first

View File

@ -18,14 +18,14 @@
# with this program. If not, see <http://www.gnu.org/licenses/>.
require_relative "../common"
require_relative "pages/paceplans_common_page"
require_relative "pages/paceplans_page"
require_relative "pages/coursepaces_common_page"
require_relative "pages/coursepaces_page"
require_relative "../courses/pages/courses_home_page"
describe "pace plans edit tray" do
describe "course paces edit tray" do
include_context "in-process server selenium tests"
include PacePlansCommonPageObject
include PacePlansPageObject
include CoursePacesCommonPageObject
include CoursePacesPageObject
include CoursesHomePage
before :once do
@ -35,7 +35,7 @@ describe "pace plans edit tray" do
name: "Jessi Jenkins",
course: @course
)
enable_pace_plans_in_course
enable_course_paces_in_course
end
before do
@ -47,11 +47,11 @@ describe "pace plans edit tray" do
let(:module_assignment_title) { "Module Assignment 1" }
before :once do
create_published_pace_plan(pace_module_title, module_assignment_title)
create_published_course_pace(pace_module_title, module_assignment_title)
end
it "shows tray link not available when updates have not been made" do
visit_pace_plans_page
visit_course_paces_page
expect(publish_status).to be_displayed
expect(publish_status.text).to eq("All changes published")
@ -59,7 +59,7 @@ describe "pace plans edit tray" do
end
it "provides tray link button when updates have been made" do
visit_pace_plans_page
visit_course_paces_page
expect(publish_status_button_exists?).to be_falsey
@ -73,7 +73,7 @@ describe "pace plans edit tray" do
end
it "brings up the edit tray when unpublished changes button is clicked" do
visit_pace_plans_page
visit_course_paces_page
update_module_item_duration(0, 3)
click_unpublished_changes_button
@ -82,7 +82,7 @@ describe "pace plans edit tray" do
end
it "shows the unpublished change in the tray" do
visit_pace_plans_page
visit_course_paces_page
update_module_item_duration(0, 3)
click_unpublished_changes_button
@ -91,7 +91,7 @@ describe "pace plans edit tray" do
end
it "closes the tray when close button clicked" do
visit_pace_plans_page
visit_course_paces_page
update_module_item_duration(0, 3)
click_unpublished_changes_button

View File

@ -18,14 +18,14 @@
# with this program. If not, see <http://www.gnu.org/licenses/>.
require_relative "../common"
require_relative "pages/paceplans_common_page"
require_relative "pages/paceplans_page"
require_relative "pages/coursepaces_common_page"
require_relative "pages/coursepaces_page"
require_relative "../courses/pages/courses_home_page"
describe "pace plan page" do
describe "course pace page" do
include_context "in-process server selenium tests"
include PacePlansCommonPageObject
include PacePlansPageObject
include CoursePacesCommonPageObject
include CoursePacesPageObject
include CoursesHomePage
before :once do
@ -35,45 +35,45 @@ describe "pace plan page" do
name: "Jessi Jenkins",
course: @course
)
enable_pace_plans_in_course
enable_course_paces_in_course
end
before do
user_session @teacher
end
context "pace plans show/hide projections" do
context "course paces show/hide projections" do
it "have a projections button that changes text from hide to show when pressed" do
visit_pace_plans_page
visit_course_paces_page
expect(show_hide_pace_plans_button_text).to eq("Show Projections")
expect(show_hide_course_paces_button_text).to eq("Show Projections")
click_show_hide_projections_button
expect(show_hide_pace_plans_button_text).to eq("Hide Projections")
expect(show_hide_course_paces_button_text).to eq("Hide Projections")
end
it "shows start and end date fields when Show Projections button is clicked" do
visit_pace_plans_page
visit_course_paces_page
click_show_hide_projections_button
expect(pace_plan_start_date).to be_displayed
expect(pace_plan_end_date).to be_displayed
expect(course_pace_start_date).to be_displayed
expect(course_pace_end_date).to be_displayed
end
it "does not show date fields when Hide Projections button is clicked" do
visit_pace_plans_page
visit_course_paces_page
click_show_hide_projections_button
click_show_hide_projections_button
expect(pace_plan_start_date_exists?).to be_falsey
expect(pace_plan_end_date_exists?).to be_falsey
expect(course_pace_start_date_exists?).to be_falsey
expect(course_pace_end_date_exists?).to be_falsey
end
it "shows only a projection icon when window size is narrowed" do
visit_pace_plans_page
visit_course_paces_page
window_size_width = driver.manage.window.size.width
window_size_height = driver.manage.window.size.height
@ -81,15 +81,15 @@ describe "pace plan page" do
scroll_to_element(show_hide_button_with_icon)
expect(show_hide_icon_button_exists?).to be_truthy
expect(show_hide_pace_plans_exists?).to be_falsey
expect(show_hide_course_paces_exists?).to be_falsey
end
it "shows an error message when weekend date is input and skip weekends is toggled on" do
visit_pace_plans_page
visit_course_paces_page
click_show_hide_projections_button
add_start_date(calculate_saturday_date)
expect { pace_plans_page_text.include?("The selected date is on a weekend and this pace plan skips weekends.") }.to become(true)
expect { course_paces_page_text.include?("The selected date is on a weekend and this course pace skips weekends.") }.to become(true)
end
it "shows a due date tooltip when plan is compressed" do
@ -97,7 +97,7 @@ describe "pace plan page" do
@assignment = create_assignment(@course, "Module Assignment", "Module Assignment Description", 10, "published")
@module_item = @course_module.add_item(id: @assignment.id, type: "assignment")
visit_pace_plans_page
visit_course_paces_page
click_show_hide_projections_button
click_require_end_date_checkbox
@ -116,7 +116,7 @@ describe "pace plan page" do
discussion_assignment = create_graded_discussion(@course, "Module Discussion", "published")
@course_module.add_item(id: discussion_assignment.id, type: "discussion_topic")
visit_pace_plans_page
visit_course_paces_page
click_show_hide_projections_button
expect(number_of_assignments.text).to eq("2 assignments")
@ -132,7 +132,7 @@ describe "pace plan page" do
@assignment = create_assignment(@course, "Module Assignment", "Module Assignment Description", 10, "published")
@module_item = @course_module.add_item(id: @assignment.id, type: "assignment")
visit_pace_plans_page
visit_course_paces_page
click_show_hide_projections_button
expect(dates_shown).to be_displayed
@ -141,7 +141,7 @@ describe "pace plan page" do
context "Projected Dates" do
it "toggles provides input field for required end date when clicked" do
visit_pace_plans_page
visit_course_paces_page
click_show_hide_projections_button
click_require_end_date_checkbox
@ -156,7 +156,7 @@ describe "pace plan page" do
it "allows inputting a date in the required date field" do
later_date = Time.zone.now + 2.weeks
visit_pace_plans_page
visit_course_paces_page
click_show_hide_projections_button
click_require_end_date_checkbox
@ -174,7 +174,7 @@ describe "pace plan page" do
end
it "shows dates with weekends included in calculation" do
visit_pace_plans_page
visit_course_paces_page
click_settings_button
click_weekends_checkbox
click_show_hide_projections_button
@ -186,7 +186,7 @@ describe "pace plan page" do
end
it "shows dates with weekends not included in calculation" do
visit_pace_plans_page
visit_course_paces_page
click_settings_button
click_show_hide_projections_button
today = Date.today

View File

@ -18,14 +18,14 @@
# with this program. If not, see <http://www.gnu.org/licenses/>.
require_relative "../common"
require_relative "pages/paceplans_common_page"
require_relative "pages/paceplans_page"
require_relative "pages/coursepaces_common_page"
require_relative "pages/coursepaces_page"
require_relative "../courses/pages/courses_home_page"
describe "pace plan page" do
describe "course pace page" do
include_context "in-process server selenium tests"
include PacePlansCommonPageObject
include PacePlansPageObject
include CoursePacesCommonPageObject
include CoursePacesPageObject
include CoursesHomePage
before :once do
@ -35,33 +35,33 @@ describe "pace plan page" do
name: "Jessi Jenkins",
course: @course
)
enable_pace_plans_in_course
enable_course_paces_in_course
end
before do
user_session @teacher
end
context "pace plan not enabled" do
it "does not include Pace Plans navigation element when disabled" do
disable_pace_plans_in_course
context "course pace not enabled" do
it "does not include Course Pacing navigation element when disabled" do
disable_course_paces_in_course
visit_course(@course)
expect(pace_plans_nav_exists?).to be_falsey
expect(course_paces_nav_exists?).to be_falsey
end
end
context "pace plans in course navigation" do
it "navigates to the pace plans page when Pace Plans is clicked" do
context "course paces in course navigation" do
it "navigates to the course paces page when Course Pacing is clicked" do
visit_course(@course)
click_pace_plans
click_course_paces
expect(driver.current_url).to include("/courses/#{@course.id}/pace_plans")
expect(driver.current_url).to include("/courses/#{@course.id}/course_paces")
end
end
context "pace plans modules" do
context "course paces modules" do
let(:module_title) { "First Module" }
let(:module_assignment_title) { "Module Assignment" }
@ -71,7 +71,7 @@ describe "pace plan page" do
@module_item = @course_module.add_item(id: @assignment.id, type: "assignment")
end
it "shows the module and module items in the pace plan" do
it "shows the module and module items in the course pace" do
discussion_title = "Module Discussion"
discussion_assignment = create_graded_discussion(@course, discussion_title, "published")
@course_module.add_item(id: discussion_assignment.id, type: "discussion_topic")
@ -79,7 +79,7 @@ describe "pace plan page" do
quiz = create_quiz(@course, quiz_title)
@course_module.add_item(id: quiz.id, type: "quiz")
visit_pace_plans_page
visit_course_paces_page
expect(module_title_text(0)).to include(module_title)
expect(module_item_title_text(0)).to start_with(module_assignment_title)
@ -91,14 +91,14 @@ describe "pace plan page" do
unpublished_assignment = create_assignment(@course, "unpub assignment", "unpub description", 10, "unpublished")
@course_module.add_item(id: unpublished_assignment.id, type: "assignment")
visit_pace_plans_page
visit_course_paces_page
expect(module_item_publish_status[0]).to be_displayed
expect(module_item_unpublish_status[0]).to be_displayed
end
it "has a link to the assignment for the title" do
visit_pace_plans_page
visit_course_paces_page
title_element = module_item_title(@assignment.title)
expect(
@ -107,7 +107,7 @@ describe "pace plan page" do
end
it "shows the points possible for a module item" do
visit_pace_plans_page
visit_course_paces_page
expect(module_item_points_possible[0].text).to eq("10 pts")
end
@ -120,20 +120,20 @@ describe "pace plan page" do
title: "pls view")
@course_module.add_item(type: "sub_header", title: "silly tag")
visit_pace_plans_page
visit_course_paces_page
expect(module_items.count).to eq(1)
expect(module_item_title_text(0)).to start_with(module_assignment_title)
end
it "does not show any publish status when no pace plan created yet" do
visit_pace_plans_page
it "does not show any publish status when no course pace created yet" do
visit_course_paces_page
expect(publish_status_exists?).to be_falsey
end
it "updates duration to make Publish and Cancel buttons enabled" do
visit_pace_plans_page
visit_course_paces_page
update_module_item_duration(0, 2)
@ -142,7 +142,7 @@ describe "pace plan page" do
end
it "does not allow duration to be set to negative number" do
visit_pace_plans_page
visit_course_paces_page
update_module_item_duration(0, "-1")
@ -150,44 +150,44 @@ describe "pace plan page" do
end
end
context "Pace Plan Menu" do
context "Course Pace Menu" do
let(:pace_module_title) { "Pace Module" }
let(:module_assignment_title) { "Module Assignment 1" }
before :once do
create_published_pace_plan(pace_module_title, module_assignment_title)
create_published_course_pace(pace_module_title, module_assignment_title)
end
it "initially shows the Course Pace Plan in pace plan menu" do
visit_pace_plans_page
it "initially shows the Course Pace in course pace menu" do
visit_course_paces_page
expect(pace_plan_menu_value).to eq("Course Pace Plan")
expect(course_pace_menu_value).to eq("Course Pace")
end
it "opens the pace plan menu and selects the student view when clicked" do
visit_pace_plans_page
click_main_pace_plan_menu
it "opens the course pace menu and selects the student view when clicked" do
visit_course_paces_page
click_main_course_pace_menu
click_students_menu_item
click_student_pace_plan(@student.name)
click_student_course_pace(@student.name)
expect(pace_plan_menu_value).to eq(@student.name)
expect(course_pace_menu_value).to eq(@student.name)
end
it "shows actual student assignment day and due dates" do
visit_pace_plans_page
click_main_pace_plan_menu
visit_course_paces_page
click_main_course_pace_menu
click_students_menu_item
click_student_pace_plan(@student.name)
click_student_course_pace(@student.name)
expect(duration_readonly.text).to eq("2")
end
it "displays modal regarding unpublished changes when going to student view" do
visit_pace_plans_page
visit_course_paces_page
update_module_item_duration(0, 3)
click_main_pace_plan_menu
click_main_course_pace_menu
click_students_menu_item
click_student_pace_plan(@student.name)
click_student_course_pace(@student.name)
expect(unpublished_warning_modal).to be_displayed
end
@ -195,14 +195,14 @@ describe "pace plan page" do
context "settings button" do
it "opens the settings menu when button is clicked" do
visit_pace_plans_page
visit_course_paces_page
click_settings_button
expect(skip_weekends_exists?).to be_truthy
end
it "toggles skip weekends when clicked" do
visit_pace_plans_page
visit_course_paces_page
click_settings_button
click_weekends_checkbox
@ -213,16 +213,16 @@ describe "pace plan page" do
end
end
context "Published Pace Plan Status" do
context "Published Course Pace Status" do
let(:pace_module_title) { "Pace Module" }
let(:module_assignment_title) { "Module Assignment 1" }
before :once do
create_published_pace_plan(pace_module_title, module_assignment_title)
create_published_course_pace(pace_module_title, module_assignment_title)
end
it "shows publish status on when PP is published" do
visit_pace_plans_page
visit_course_paces_page
expect { publish_status.text }.to become("All changes published")
end

View File

@ -19,7 +19,7 @@
require_relative "../../common"
module PacePlansCommonPageObject
module CoursePacesCommonPageObject
def admin_setup
feature_setup
teacher_setup
@ -88,30 +88,30 @@ module PacePlansCommonPageObject
quiz
end
def create_published_pace_plan(module_title, assignment_title)
def create_published_course_pace(module_title, assignment_title)
# We want the module item autopublish to happen immediately in test
Setting.set("pace_plan_publish_interval", "0")
Setting.set("course_pace_publish_interval", "0")
pace_plan_model(course: @course, end_date: Time.zone.now.advance(days: 30))
pace_plan_module = create_course_module(module_title)
pace_plan_assignment = create_assignment(@course, assignment_title, "Assignment 1", 10, "published")
pace_plan_module.add_item(id: pace_plan_assignment.id, type: "assignment")
@pace_plan.pace_plan_module_items.last.update! duration: 2
course_pace_model(course: @course, end_date: Time.zone.now.advance(days: 30))
course_pace_module = create_course_module(module_title)
course_pace_assignment = create_assignment(@course, assignment_title, "Assignment 1", 10, "published")
course_pace_module.add_item(id: course_pace_assignment.id, type: "assignment")
@course_pace.course_pace_module_items.last.update! duration: 2
run_jobs # Run the autopublish job
@pace_plan
@course_pace
end
def disable_pace_plans_in_course
@course.update(enable_pace_plans: false)
def disable_course_paces_in_course
@course.update(enable_course_paces: false)
end
def enable_pace_plans_in_course
@course.update(enable_pace_plans: true)
def enable_course_paces_in_course
@course.update(enable_course_paces: true)
end
def feature_setup
@account = Account.default
@account.enable_feature!(:pace_plans)
@account.enable_feature!(:course_paces)
end
def skip_weekends(date, duration = 1)
@ -127,13 +127,13 @@ module PacePlansCommonPageObject
def teacher_setup
feature_setup
@course_name = "Pace Plans Course"
@course_name = "Course Paces Course"
course_with_teacher(
account: @account,
active_course: 1,
active_enrollment: 1,
course_name: @course_name,
name: "PacePlan Teacher"
name: "CoursePace Teacher"
)
end
end

View File

@ -19,7 +19,7 @@
require_relative "../../common"
module PacePlansPageObject
module CoursePacesPageObject
#------------------------- Selectors -------------------------------
def assignment_due_date_selector
"[data-testid='assignment-due-date']"
@ -50,11 +50,11 @@ module PacePlansPageObject
end
def hypothetical_end_date_selector
"[data-testid='pace-plans-collapse']:contains('Hypothetical end date')"
"[data-testid='course-paces-collapse']:contains('Hypothetical end date')"
end
def module_item_points_possible_selector
".pace-plans-assignment-row-points-possible"
".course-paces-assignment-row-points-possible"
end
def module_item_publish_status_selector
@ -77,31 +77,31 @@ module PacePlansPageObject
"[data-testid='number-of-weeks'] i"
end
def pace_plan_end_date_selector
"[data-testid='paceplan-date-text']"
def course_pace_end_date_selector
"[data-testid='coursepace-date-text']"
end
def pace_plan_menu_selector
"[data-position-target='pace-plan-menu']"
def course_pace_menu_selector
"[data-position-target='course-pace-menu']"
end
def pace_plan_picker_selector
"[data-testid='pace-plan-picker']"
def course_pace_picker_selector
"[data-testid='course-pace-picker']"
end
def pace_plan_student_option_selector
"[data-position-target='pace-plan-student-menu']"
def course_pace_student_option_selector
"[data-position-target='course-pace-student-menu']"
end
def pace_plans_page_selector
"#pace_plans"
def course_paces_page_selector
"#course_paces"
end
def pace_plan_start_date_selector
"[data-testid='pace-plan-date']"
def course_pace_start_date_selector
"[data-testid='course-pace-date']"
end
def pace_plan_table_module_selector
def course_pace_table_module_selector
"h2"
end
@ -126,11 +126,11 @@ module PacePlansPageObject
end
def required_end_date_input_selector
"#pace-plans-required-end-date-input [data-testid='pace-plan-date']"
"#course-paces-required-end-date-input [data-testid='course-pace-date']"
end
def required_end_date_message_selector
"#pace-plans-required-end-date-input:contains('Required by specified end date')"
"#course-paces-required-end-date-input:contains('Required by specified end date')"
end
def settings_button_selector
@ -141,7 +141,7 @@ module PacePlansPageObject
"[data-testid='projections-icon-button']"
end
def show_hide_pace_plans_selector
def show_hide_course_paces_selector
"[data-test-id='projections-text-button']"
end
@ -157,7 +157,7 @@ module PacePlansPageObject
"ul[aria-label='Students']"
end
def student_pace_plan_selector(student_name)
def student_course_pace_selector(student_name)
"span[role=menuitem]:contains(#{student_name})"
end
@ -243,32 +243,32 @@ module PacePlansPageObject
f(number_of_weeks_selector)
end
def pace_plan_end_date
f(pace_plan_end_date_selector)
def course_pace_end_date
f(course_pace_end_date_selector)
end
def pace_plan_menu
ff(pace_plan_menu_selector)
def course_pace_menu
ff(course_pace_menu_selector)
end
def pace_plan_picker
f(pace_plan_picker_selector)
def course_pace_picker
f(course_pace_picker_selector)
end
def pace_plan_student_option
f(pace_plan_student_option_selector)
def course_pace_student_option
f(course_pace_student_option_selector)
end
def pace_plans_page
f(pace_plans_page_selector)
def course_paces_page
f(course_paces_page_selector)
end
def pace_plan_start_date
f(pace_plan_start_date_selector)
def course_pace_start_date
f(course_pace_start_date_selector)
end
def pace_plan_table_module_elements
ff(pace_plan_table_module_selector)
def course_pace_table_module_elements
ff(course_pace_table_module_selector)
end
def publish_button
@ -303,16 +303,16 @@ module PacePlansPageObject
f(show_hide_button_with_icon_selector)
end
def show_hide_pace_plans
f(show_hide_pace_plans_selector)
def show_hide_course_paces
f(show_hide_course_paces_selector)
end
def skip_weekends_checkbox
fxpath(skip_weekends_checkbox_xpath_selector)
end
def student_pace_plan(student_name)
fj(student_pace_plan_selector(student_name))
def student_course_pace(student_name)
fj(student_course_pace_selector(student_name))
end
def students_menu_item
@ -332,8 +332,8 @@ module PacePlansPageObject
end
#----------------------- Actions & Methods -------------------------
def visit_pace_plans_page
get "/courses/#{@course.id}/pace_plans"
def visit_course_paces_page
get "/courses/#{@course.id}/course_paces"
end
#----------------------- Click Items -------------------------------
@ -346,8 +346,8 @@ module PacePlansPageObject
edit_tray_close_button.click
end
def click_main_pace_plan_menu
pace_plan_picker.click
def click_main_course_pace_menu
course_pace_picker.click
end
def click_require_end_date_checkbox
@ -359,33 +359,33 @@ module PacePlansPageObject
end
def click_show_hide_projections_button
show_hide_pace_plans.click
show_hide_course_paces.click
end
def click_skip_weekends_toggle
skip_weekends_checkbox.click
end
def click_student_pace_plan(student_name)
def click_student_course_pace(student_name)
# This check reduces the flakiness of the clicking in this menu. Keeping
# the puts line for verification in the logs
unless element_exists?(student_pp_xpath_selector(student_name), true)
puts "Student pace plan selector didn't exist so retrying click"
puts "Student course pace selector didn't exist so retrying click"
click_students_menu_item
end
student_pace_plan(student_name).click
student_course_pace(student_name).click
end
def click_students_menu_item
unless element_exists?(pace_plan_student_option_selector)
unless element_exists?(course_pace_student_option_selector)
puts "retrying the main menu click"
click_main_pace_plan_menu
click_main_course_pace_menu
end
pace_plan_student_option.click
course_pace_student_option.click
# Reducing the flakiness of this menu
unless element_exists?(student_menu_selector)
pace_plan_student_option.click
course_pace_student_option.click
end
end
@ -404,10 +404,10 @@ module PacePlansPageObject
end
def module_title_text(element_number)
pace_plan_table_module_elements[element_number].text
course_pace_table_module_elements[element_number].text
end
delegate :text, to: :pace_plans_page, prefix: true
delegate :text, to: :course_paces_page, prefix: true
#----------------------------Element Management---------------------
@ -416,7 +416,7 @@ module PacePlansPageObject
end
def add_start_date(start_date)
pace_plan_start_date.send_keys([:control, "a"], :backspace, format_date_for_view(start_date), :enter)
course_pace_start_date.send_keys([:control, "a"], :backspace, format_date_for_view(start_date), :enter)
end
delegate :text, to: :assignment_due_date, prefix: true
@ -430,16 +430,16 @@ module PacePlansPageObject
element_exists?(module_items_selector)
end
def pace_plan_end_date_exists?
element_exists?(pace_plan_end_date_selector)
def course_pace_end_date_exists?
element_exists?(course_pace_end_date_selector)
end
def pace_plan_menu_value
element_value_for_attr(pace_plan_menu[1], "value")
def course_pace_menu_value
element_value_for_attr(course_pace_menu[1], "value")
end
def pace_plan_start_date_exists?
element_exists?(pace_plan_start_date_selector)
def course_pace_start_date_exists?
element_exists?(course_pace_start_date_selector)
end
def publish_status_exists?
@ -458,12 +458,12 @@ module PacePlansPageObject
element_value_for_attr(required_end_date_input, "value")
end
def show_hide_pace_plans_button_text
show_hide_pace_plans.text
def show_hide_course_paces_button_text
show_hide_course_paces.text
end
def show_hide_pace_plans_exists?
element_exists?(show_hide_pace_plans_selector)
def show_hide_course_paces_exists?
element_exists?(show_hide_course_paces_selector)
end
def show_hide_icon_button_exists?

View File

@ -256,39 +256,39 @@ describe "course settings" do
expect(home_page_announcement_limit).not_to be_disabled
end
describe "pace plans setting" do
describe "when the pace plans feature flag is enabled" do
describe "course paces setting" do
describe "when the course paces feature flag is enabled" do
before do
@account.enable_feature!(:pace_plans)
@account.enable_feature!(:course_paces)
end
it "displays the pace plans setting (and if checked, the caution text)" do
it "displays the course paces setting (and if checked, the caution text)" do
get "/courses/#{@course.id}/settings"
expect(element_exists?(".pace-plans-row")).to be_truthy
expect(element_exists?(".course-paces-row")).to be_truthy
caution_text = "Pace Plans is in active development."
pace_plans_checkbox = f("#course_enable_pace_plans")
caution_text = "Course Pacing is in active development."
course_paces_checkbox = f("#course_enable_course_paces")
pace_plans_checkbox.click
course_paces_checkbox.click
wait_for_ajaximations
expect(f(".pace-plans-row")).to include_text caution_text
expect(f(".course-paces-row")).to include_text caution_text
pace_plans_checkbox.click
course_paces_checkbox.click
wait_for_ajaximations
expect(f(".pace-plans-row")).not_to include_text caution_text
expect(f(".course-paces-row")).not_to include_text caution_text
end
end
describe "when the pace plans feature flag is disabled" do
describe "when the course paces feature flag is disabled" do
before do
@account.disable_feature!(:pace_plans)
@account.disable_feature!(:course_paces)
end
it "does not display the pace plans setting" do
it "does not display the course paces setting" do
get "/courses/#{@course.id}/settings"
expect(element_exists?(".pace-plans-row")).to be_falsey
expect(element_exists?(".course-paces-row")).to be_falsey
end
end
end

View File

@ -47,8 +47,8 @@ module CoursesHomePage
".ic-notification button[name='accept']"
end
def course_pace_plan_selector
".pace_plans"
def course_course_pace_selector
".course_paces"
end
def course_menu_toggle_selector
@ -96,8 +96,8 @@ module CoursesHomePage
f("#content")
end
def course_pace_plan_link
f(course_pace_plan_selector)
def course_course_pace_link
f(course_course_pace_selector)
end
def course_menu_toggle
@ -127,12 +127,12 @@ module CoursesHomePage
wait_for(method: nil, timeout: 2) { wizard_box.displayed? }
end
def click_pace_plans
course_pace_plan_link.click
def click_course_paces
course_course_pace_link.click
end
def pace_plans_nav_exists?
element_exists?(course_pace_plan_selector)
def course_paces_nav_exists?
element_exists?(course_course_pace_selector)
end
def click_course_menu_toggle

View File

@ -31,5 +31,5 @@ const CoursePage: React.FC = () => (
)
ready(() => {
ReactDOM.render(<CoursePage />, document.getElementById('pace_plans'))
ReactDOM.render(<CoursePage />, document.getElementById('course_paces'))
})

View File

@ -24,9 +24,9 @@ import {
Enrollments,
EnrollmentsState,
Module,
PacePlan,
PacePlanItem,
PacePlansState,
CoursePace,
CoursePaceItem,
CoursePacesState,
Section,
Sections,
SectionsState,
@ -54,7 +54,7 @@ export const ENROLLMENT_1: Enrollment = {
full_name: 'Henry Dorsett Case',
sortable_name: 'Case, Henry Dorsett',
start_at: undefined,
completed_pace_plan_at: undefined
completed_course_pace_at: undefined
}
export const ENROLLMENT_2: Enrollment = {
@ -64,7 +64,7 @@ export const ENROLLMENT_2: Enrollment = {
full_name: 'Molly Millions',
sortable_name: 'Millions, Molly',
start_at: undefined,
completed_pace_plan_at: undefined
completed_course_pace_at: undefined
}
export const ENROLLMENTS: Enrollments = keyBy([ENROLLMENT_1, ENROLLMENT_2], 'id')
@ -91,7 +91,7 @@ export const SECTIONS: Sections = keyBy([SECTION_1, SECTION_2], 'id')
export const SORTED_SECTIONS: Section[] = [SECTION_1, SECTION_2]
export const PLAN_ITEM_1: PacePlanItem = {
export const PACE_ITEM_1: CoursePaceItem = {
id: '50',
duration: 2,
assignment_title: 'Basic encryption/decryption',
@ -103,7 +103,7 @@ export const PLAN_ITEM_1: PacePlanItem = {
published: true
}
export const PLAN_ITEM_2: PacePlanItem = {
export const PACE_ITEM_2: CoursePaceItem = {
id: '51',
duration: 5,
assignment_title: 'Being 1337',
@ -115,7 +115,7 @@ export const PLAN_ITEM_2: PacePlanItem = {
published: false
}
export const PLAN_ITEM_3: PacePlanItem = {
export const PACE_ITEM_3: CoursePaceItem = {
id: '52',
duration: 3,
assignment_title: 'What are laws, anyway?',
@ -127,21 +127,21 @@ export const PLAN_ITEM_3: PacePlanItem = {
published: true
}
export const PLAN_MODULE_1: Module = {
export const PACE_MODULE_1: Module = {
id: '40',
name: 'How 2 B A H4CK32',
position: 1,
items: [PLAN_ITEM_1, PLAN_ITEM_2]
items: [PACE_ITEM_1, PACE_ITEM_2]
}
export const PLAN_MODULE_2: Module = {
export const PACE_MODULE_2: Module = {
id: '45',
name: 'Intro to Corporate Espionage',
position: 2,
items: [PLAN_ITEM_3]
items: [PACE_ITEM_3]
}
export const PRIMARY_PLAN: PacePlan = {
export const PRIMARY_PACE: CoursePace = {
id: '1',
course_id: COURSE.id,
course_section_id: undefined,
@ -153,10 +153,10 @@ export const PRIMARY_PLAN: PacePlan = {
workflow_state: 'active',
exclude_weekends: true,
hard_end_dates: true,
modules: [PLAN_MODULE_1, PLAN_MODULE_2]
modules: [PACE_MODULE_1, PACE_MODULE_2]
}
export const SECTION_PLAN: PacePlan = {
export const SECTION_PACE: CoursePace = {
id: '2',
course_id: COURSE.id,
course_section_id: SECTION_1.id,
@ -168,10 +168,10 @@ export const SECTION_PLAN: PacePlan = {
workflow_state: 'active',
exclude_weekends: false,
hard_end_dates: true,
modules: [PLAN_MODULE_1, PLAN_MODULE_2]
modules: [PACE_MODULE_1, PACE_MODULE_2]
}
export const STUDENT_PLAN: PacePlan = {
export const STUDENT_PACE: CoursePace = {
id: '3',
course_id: COURSE.id,
course_section_id: undefined,
@ -183,7 +183,7 @@ export const STUDENT_PLAN: PacePlan = {
workflow_state: 'active',
exclude_weekends: true,
hard_end_dates: true,
modules: [PLAN_MODULE_1, PLAN_MODULE_2]
modules: [PACE_MODULE_1, PACE_MODULE_2]
}
export const PROGRESS_RUNNING = {
@ -203,7 +203,7 @@ export const PROGRESS_FAILED = {
}
export interface DefaultStoreState {
readonly pacePlan?: PacePlansState
readonly coursePace?: CoursePacesState
readonly enrollments?: EnrollmentsState
readonly sections?: SectionsState
readonly ui?: UIState
@ -215,6 +215,6 @@ export const DEFAULT_STORE_STATE: DefaultStoreState = {
blackoutDates: BLACKOUT_DATES,
course: COURSE,
enrollments: ENROLLMENTS,
pacePlan: {...PRIMARY_PLAN, originalPlan: PRIMARY_PLAN},
coursePace: {...PRIMARY_PACE, originalPace: PRIMARY_PACE},
sections: SECTIONS
}

View File

@ -20,26 +20,26 @@ import fetchMock from 'fetch-mock'
import {screen, waitFor} from '@testing-library/react'
import {actions as uiActions} from '../ui'
import {pacePlanActions, PUBLISH_STATUS_POLLING_MS} from '../pace_plans'
import {coursePaceActions, PUBLISH_STATUS_POLLING_MS} from '../course_paces'
import {
COURSE,
DEFAULT_STORE_STATE,
PRIMARY_PLAN,
PRIMARY_PACE,
PROGRESS_FAILED,
PROGRESS_RUNNING
} from '../../__tests__/fixtures'
const CREATE_API = `/api/v1/courses/${COURSE.id}/pace_plans`
const UPDATE_API = `/api/v1/courses/${COURSE.id}/pace_plans/${PRIMARY_PLAN.id}`
const CREATE_API = `/api/v1/courses/${COURSE.id}/course_paces`
const UPDATE_API = `/api/v1/courses/${COURSE.id}/course_paces/${PRIMARY_PACE.id}`
const PROGRESS_API = `/api/v1/progress/${PROGRESS_RUNNING.id}`
const COMPRESS_API = `/api/v1/courses/${COURSE.id}/pace_plans/compress_dates`
const COMPRESS_API = `/api/v1/courses/${COURSE.id}/course_paces/compress_dates`
const dispatch = jest.fn()
const mockGetState = (plan, originalPlan) => () => ({
const mockGetState = (pace, originalPace) => () => ({
...DEFAULT_STORE_STATE,
pacePlan: {...plan},
originalPlan: {...originalPlan}
coursePace: {...pace},
originalPace: {...originalPace}
})
beforeEach(() => {
@ -53,51 +53,51 @@ afterEach(() => {
fetchMock.restore()
})
describe('Pace plans actions', () => {
describe('publishPlan', () => {
it('Updates plan, manages loading state, and starts polling for publish status', async () => {
const updatedPlan = {...PRIMARY_PLAN, excludeWeekends: false}
const getState = mockGetState(updatedPlan, PRIMARY_PLAN)
describe('Course paces actions', () => {
describe('publishPace', () => {
it('Updates pace, manages loading state, and starts polling for publish status', async () => {
const updatedPace = {...PRIMARY_PACE, excludeWeekends: false}
const getState = mockGetState(updatedPace, PRIMARY_PACE)
fetchMock.put(UPDATE_API, {
pace_plan: updatedPlan,
course_pace: updatedPace,
progress: PROGRESS_RUNNING
})
const thunkedAction = pacePlanActions.publishPlan()
const thunkedAction = coursePaceActions.publishPace()
await thunkedAction(dispatch, getState)
expect(dispatch.mock.calls[0]).toEqual([uiActions.showLoadingOverlay('Starting publish...')])
expect(dispatch.mock.calls[1]).toEqual([uiActions.clearCategoryError('publish')])
expect(dispatch.mock.calls[2]).toEqual([pacePlanActions.setPacePlan(updatedPlan)])
expect(dispatch.mock.calls[3]).toEqual([pacePlanActions.setProgress(PROGRESS_RUNNING)])
expect(dispatch.mock.calls[2]).toEqual([coursePaceActions.setCoursePace(updatedPace)])
expect(dispatch.mock.calls[3]).toEqual([coursePaceActions.setProgress(PROGRESS_RUNNING)])
// Compare dispatched functions by name since they won't be directly equal
expect(JSON.stringify(dispatch.mock.calls[4])).toEqual(
JSON.stringify([pacePlanActions.pollForPublishStatus()])
JSON.stringify([coursePaceActions.pollForPublishStatus()])
)
expect(dispatch.mock.calls[5]).toEqual([uiActions.hideLoadingOverlay()])
expect(fetchMock.called(UPDATE_API, 'PUT')).toBe(true)
})
it('Calls create API when an ID is not present', async () => {
fetchMock.post(CREATE_API, {pace_plan: {...PRIMARY_PLAN}, progress: PROGRESS_RUNNING})
const planToCreate = {...PRIMARY_PLAN, id: undefined}
const getState = mockGetState(planToCreate, planToCreate)
fetchMock.post(CREATE_API, {course_pace: {...PRIMARY_PACE}, progress: PROGRESS_RUNNING})
const paceToCreate = {...PRIMARY_PACE, id: undefined}
const getState = mockGetState(paceToCreate, paceToCreate)
const thunkedAction = pacePlanActions.publishPlan()
const thunkedAction = coursePaceActions.publishPace()
await thunkedAction(dispatch, getState)
expect(fetchMock.called(CREATE_API, 'POST')).toBe(true)
})
it('Sets an error message if the plan update fails', async () => {
const updatedPlan = {...PRIMARY_PLAN, excludeWeekends: false}
it('Sets an error message if the pace update fails', async () => {
const updatedPace = {...PRIMARY_PACE, excludeWeekends: false}
const error = new Error("You don't actually want to publish this")
const getState = mockGetState(updatedPlan, PRIMARY_PLAN)
const getState = mockGetState(updatedPace, PRIMARY_PACE)
fetchMock.put(UPDATE_API, {
throws: error
})
const thunkedAction = pacePlanActions.publishPlan()
const thunkedAction = coursePaceActions.publishPace()
await thunkedAction(dispatch, getState)
expect(dispatch.mock.calls).toEqual([
@ -112,19 +112,19 @@ describe('Pace plans actions', () => {
describe('pollForPublishState', () => {
it('does nothing without a progress or for progresses in terminal statuses', () => {
const getStateNoProgress = () => ({...DEFAULT_STORE_STATE})
pacePlanActions.pollForPublishStatus()(dispatch, getStateNoProgress)
coursePaceActions.pollForPublishStatus()(dispatch, getStateNoProgress)
const getStateFailed = () => ({
...DEFAULT_STORE_STATE,
pacePlan: {publishingProgress: PROGRESS_FAILED}
coursePace: {publishingProgress: PROGRESS_FAILED}
})
pacePlanActions.pollForPublishStatus()(dispatch, getStateFailed)
coursePaceActions.pollForPublishStatus()(dispatch, getStateFailed)
const getStateCompleted = () => ({
...DEFAULT_STORE_STATE,
pacePlan: {publishingProgress: {...PROGRESS_FAILED, workflow_state: 'completed'}}
coursePace: {publishingProgress: {...PROGRESS_FAILED, workflow_state: 'completed'}}
})
pacePlanActions.pollForPublishStatus()(dispatch, getStateCompleted)
coursePaceActions.pollForPublishStatus()(dispatch, getStateCompleted)
expect(dispatch).not.toHaveBeenCalled()
})
@ -132,14 +132,14 @@ describe('Pace plans actions', () => {
it('sets a timeout that updates progress status and clears when a terminal status is reached', async () => {
const getState = () => ({
...DEFAULT_STORE_STATE,
pacePlan: {publishingProgress: {...PROGRESS_RUNNING}}
coursePace: {publishingProgress: {...PROGRESS_RUNNING}}
})
const progressUpdated = {...PROGRESS_RUNNING, completion: 60}
fetchMock.get(PROGRESS_API, progressUpdated)
await pacePlanActions.pollForPublishStatus()(dispatch, getState)
await coursePaceActions.pollForPublishStatus()(dispatch, getState)
expect(dispatch.mock.calls[0]).toEqual([pacePlanActions.setProgress(progressUpdated)])
expect(dispatch.mock.calls[0]).toEqual([coursePaceActions.setProgress(progressUpdated)])
expect(dispatch.mock.calls[1]).toEqual([uiActions.clearCategoryError('checkPublishStatus')])
expect(setTimeout).toHaveBeenCalledTimes(1)
@ -151,20 +151,20 @@ describe('Pace plans actions', () => {
await waitFor(() => {
expect(dispatch.mock.calls.length).toBe(4)
expect(dispatch.mock.calls[1]).toEqual([uiActions.clearCategoryError('checkPublishStatus')])
expect(dispatch.mock.calls[2]).toEqual([pacePlanActions.setProgress(undefined)])
expect(screen.getByText('Finished publishing plan')).toBeInTheDocument()
expect(dispatch.mock.calls[2]).toEqual([coursePaceActions.setProgress(undefined)])
expect(screen.getByText('Finished publishing pace')).toBeInTheDocument()
})
})
it('stops polling and displays an error message if checking the progress API fails', async () => {
const getState = () => ({
...DEFAULT_STORE_STATE,
pacePlan: {publishingProgress: {...PROGRESS_RUNNING}}
coursePace: {publishingProgress: {...PROGRESS_RUNNING}}
})
const error = new Error('Progress? What progress?')
fetchMock.get(PROGRESS_API, {throws: error})
await pacePlanActions.pollForPublishStatus()(dispatch, getState)
await coursePaceActions.pollForPublishStatus()(dispatch, getState)
expect(dispatch.mock.calls).toEqual([
[uiActions.setCategoryError('checkPublishStatus', error?.toString())]
@ -174,33 +174,33 @@ describe('Pace plans actions', () => {
})
describe('compressDates', () => {
it('Updates plan and manages loading state', async () => {
const updatedPlan = {...PRIMARY_PLAN}
const getState = mockGetState(updatedPlan, PRIMARY_PLAN)
it('Updates pace and manages loading state', async () => {
const updatedPace = {...PRIMARY_PACE}
const getState = mockGetState(updatedPace, PRIMARY_PACE)
const compressResponse = {
1: 'a date',
2: 'another date'
}
fetchMock.post(COMPRESS_API, compressResponse)
const thunkedAction = pacePlanActions.compressDates()
const thunkedAction = coursePaceActions.compressDates()
await thunkedAction(dispatch, getState)
expect(dispatch.mock.calls[0]).toEqual([uiActions.showLoadingOverlay('Compressing...')])
expect(dispatch.mock.calls[1]).toEqual([uiActions.clearCategoryError('compress')])
expect(dispatch.mock.calls[2]).toEqual([
pacePlanActions.setCompressedItemDates(compressResponse)
coursePaceActions.setCompressedItemDates(compressResponse)
])
// Compare dispatched functions by name since they won't be directly equal
expect(dispatch.mock.calls[3]).toEqual([uiActions.hideLoadingOverlay()])
// compress() POSTs a flattened and stripped-down version of the pace plan
// compress() POSTs a flattened and stripped-down version of the course pace
expect(fetchMock.calls()[0][1].body).toEqual(
JSON.stringify({
pace_plan: {
start_date: updatedPlan.start_date,
end_date: updatedPlan.end_date,
exclude_weekends: updatedPlan.exclude_weekends,
pace_plan_module_items_attributes: updatedPlan.modules.reduce(
course_pace: {
start_date: updatedPace.start_date,
end_date: updatedPace.end_date,
exclude_weekends: updatedPace.exclude_weekends,
course_pace_module_items_attributes: updatedPace.modules.reduce(
(runningValue: Array<any>, module) => {
return runningValue.concat(
module.items.map(item => ({
@ -219,14 +219,14 @@ describe('Pace plans actions', () => {
})
it('Sets an error message if compression fails', async () => {
const updatedPlan = {...PRIMARY_PLAN}
const updatedPace = {...PRIMARY_PACE}
const error = new Error('Whoops!')
const getState = mockGetState(updatedPlan, PRIMARY_PLAN)
const getState = mockGetState(updatedPace, PRIMARY_PACE)
fetchMock.post(COMPRESS_API, {
throws: error
})
const thunkedAction = pacePlanActions.compressDates()
const thunkedAction = coursePaceActions.compressDates()
await thunkedAction(dispatch, getState)
expect(dispatch.mock.calls).toEqual([

View File

@ -21,39 +21,39 @@ import {Action, Dispatch} from 'redux'
import {ThunkAction} from 'redux-thunk'
import {deepEqual} from '@instructure/ui-utils'
import {getPacePlan} from '../reducers/pace_plans'
import {StoreState, PacePlan} from '../types'
import * as pacePlanAPI from '../api/pace_plan_api'
import {getCoursePace} from '../reducers/course_paces'
import {StoreState, CoursePace} from '../types'
import * as coursePaceAPI from '../api/course_pace_api'
import {actions as uiActions} from './ui'
import {pacePlanActions} from './pace_plans'
import {coursePaceActions} from './course_paces'
const updatePacePlan = (
const updateCoursePace = (
dispatch: Dispatch<Action>,
getState: () => StoreState,
planBefore: PacePlan,
paceBefore: CoursePace,
shouldBlock: boolean,
extraSaveParams = {}
) => {
const plan = getPacePlan(getState())
const pace = getCoursePace(getState())
if (planBefore.id && plan.id !== planBefore.id) {
if (paceBefore.id && pace.id !== paceBefore.id) {
dispatch(uiActions.autoSaveCompleted())
return
}
const persisted = !!plan.id
const method = persisted ? pacePlanAPI.update : pacePlanAPI.create
const persisted = !!pace.id
const method = persisted ? coursePaceAPI.update : coursePaceAPI.create
// Whether we should update the plan to match the state presented by the backend.
// Whether we should update the pace to match the state presented by the backend.
// We don't do this all the time, because it results in race conditions that cause
// the ui to get out of sync.
const updateAfterRequest =
!persisted || shouldBlock || (plan.hard_end_dates && plan.context_type === 'Enrollment')
!persisted || shouldBlock || (pace.hard_end_dates && pace.context_type === 'Enrollment')
return method(plan, extraSaveParams) // Hit the API to update
.then(updatedPlan => {
return method(pace, extraSaveParams) // Hit the API to update
.then(updatedPace => {
if (updateAfterRequest) {
dispatch(pacePlanActions.planCreated(updatedPlan))
dispatch(coursePaceActions.paceCreated(updatedPace))
}
if (shouldBlock) {
@ -72,7 +72,10 @@ const updatePacePlan = (
})
}
const debouncedUpdatePacePlan = _.debounce(updatePacePlan, 1000, {trailing: true, maxWait: 2000})
const debouncedUpdateCoursePace = _.debounce(updateCoursePace, 1000, {
trailing: true,
maxWait: 2000
})
/*
Given any action, returns a new thunked action that applies the action and
@ -80,7 +83,7 @@ const debouncedUpdatePacePlan = _.debounce(updatePacePlan, 1000, {trailing: true
action - pass any redux action that should initiate an auto save
debounce - whether the action should be immediately autosaved, or debounced
shouldBlock - whether you want the plan updated after the autosave and for a loading icon to block user interaction
shouldBlock - whether you want the pace updated after the autosave and for a loading icon to block user interaction
until that is complete
extraSaveParams - params that should be passed to the backend during the API call
*/
@ -95,20 +98,20 @@ export const createAutoSavingAction = (
dispatch(uiActions.showLoadingOverlay('Updating...'))
}
const planBefore = getPacePlan(getState())
const paceBefore = getCoursePace(getState())
dispatch(action) // Dispatch the original action
// Don't autosave if no changes have occured
if (deepEqual(planBefore, getPacePlan(getState()))) {
if (deepEqual(paceBefore, getCoursePace(getState()))) {
return
}
dispatch(uiActions.startAutoSave())
if (debounce) {
return debouncedUpdatePacePlan(dispatch, getState, planBefore, shouldBlock, extraSaveParams)
return debouncedUpdateCoursePace(dispatch, getState, paceBefore, shouldBlock, extraSaveParams)
} else {
return updatePacePlan(dispatch, getState, planBefore, shouldBlock, extraSaveParams)
return updateCoursePace(dispatch, getState, paceBefore, shouldBlock, extraSaveParams)
}
}
}

View File

@ -19,14 +19,14 @@
import {createAction, ActionsUnion} from '../shared/types'
export enum Constants {
SET_PLAN_ITEM_DURATION = 'PACE_PLAN_ITEMS/SET_PLAN_ITEM_DURATION'
SET_PACE_ITEM_DURATION = 'COURSE_PACE_ITEMS/SET_PACE_ITEM_DURATION'
}
/* Action creators */
export const actions = {
setPlanItemDuration: (planItemId: string, duration: number) =>
createAction(Constants.SET_PLAN_ITEM_DURATION, {planItemId, duration})
setPaceItemDuration: (paceItemId: string, duration: number) =>
createAction(Constants.SET_PACE_ITEM_DURATION, {paceItemId, duration})
}
export type PacePlanItemAction = ActionsUnion<typeof actions>
export type CoursePaceItemAction = ActionsUnion<typeof actions>

View File

@ -20,66 +20,66 @@ import {Action} from 'redux'
import {ThunkAction} from 'redux-thunk'
import {showFlashAlert} from '@canvas/alerts/react/FlashAlert'
// @ts-ignore: TS doesn't understand i18n scoped imports
import { useScope as useI18nScope } from '@canvas/i18n';
import {useScope as useI18nScope} from '@canvas/i18n'
import {PacePlanItemDueDates, PacePlan, PlanContextTypes, Progress, StoreState} from '../types'
import {CoursePaceItemDueDates, CoursePace, PaceContextTypes, Progress, StoreState} from '../types'
import {createAction, ActionsUnion} from '../shared/types'
import {actions as uiActions} from './ui'
import * as Api from '../api/pace_plan_api'
import * as Api from '../api/course_pace_api'
const I18n = useI18nScope('pace_plans_actions');
const I18n = useI18nScope('course_paces_actions')
export const PUBLISH_STATUS_POLLING_MS = 3000
const TERMINAL_PROGRESS_STATUSES = ['completed', 'failed']
export enum Constants {
SET_END_DATE = 'PACE_PLAN/SET_END_DATE',
SET_START_DATE = 'PACE_PLAN/SET_START_DATE',
PUBLISH_PLAN = 'PACE_PLAN/PUBLISH_PLAN',
TOGGLE_EXCLUDE_WEEKENDS = 'PACE_PLAN/TOGGLE_EXCLUDE_WEEKENDS',
SET_PACE_PLAN = 'PACE_PLAN/SET_PACE_PLAN',
PLAN_CREATED = 'PACE_PLAN/PLAN_CREATED',
TOGGLE_HARD_END_DATES = 'PACE_PLAN/TOGGLE_HARD_END_DATES',
RESET_PLAN = 'PACE_PLAN/RESET_PLAN',
SET_PROGRESS = 'PACE_PLAN/SET_PROGRESS',
SET_COMPRESSED_ITEM_DATES = 'PACE_PLAN/SET_COMPRESSED_ITEM_DATES',
UNCOMPRESS_DATES = 'PACE_PLAN/UNCOMPRESS_ITEM_DATES'
SET_END_DATE = 'COURSE_PACE/SET_END_DATE',
SET_START_DATE = 'COURSE_PACE/SET_START_DATE',
PUBLISH_PACE = 'COURSE_PACE/PUBLISH_PACE',
TOGGLE_EXCLUDE_WEEKENDS = 'COURSE_PACE/TOGGLE_EXCLUDE_WEEKENDS',
SET_COURSE_PACE = 'COURSE_PACE/SET_COURSE_PACE',
PACE_CREATED = 'COURSE_PACE/PACE_CREATED',
TOGGLE_HARD_END_DATES = 'COURSE_PACE/TOGGLE_HARD_END_DATES',
RESET_PACE = 'COURSE_PACE/RESET_PACE',
SET_PROGRESS = 'COURSE_PACE/SET_PROGRESS',
SET_COMPRESSED_ITEM_DATES = 'COURSE_PACE/SET_COMPRESSED_ITEM_DATES',
UNCOMPRESS_DATES = 'COURSE_PACE/UNCOMPRESS_ITEM_DATES'
}
/* Action creators */
type LoadingAfterAction = (plan: PacePlan) => any
// Without this, we lose the ReturnType through our mapped ActionsUnion (because of setPlanDays), and the type just becomes any.
type LoadingAfterAction = (pace: CoursePace) => any
// Without this, we lose the ReturnType through our mapped ActionsUnion (because of setPaceDays), and the type just becomes any.
type SetEndDate = {type: Constants.SET_END_DATE; payload: string}
const regularActions = {
setPacePlan: (plan: PacePlan) =>
createAction(Constants.SET_PACE_PLAN, {...plan, originalPlan: plan}),
setCoursePace: (pace: CoursePace) =>
createAction(Constants.SET_COURSE_PACE, {...pace, originalPace: pace}),
setStartDate: (date: string) => createAction(Constants.SET_START_DATE, date),
setEndDate: (date: string): SetEndDate => createAction(Constants.SET_END_DATE, date),
setCompressedItemDates: (compressedItemDates: PacePlanItemDueDates) =>
setCompressedItemDates: (compressedItemDates: CoursePaceItemDueDates) =>
createAction(Constants.SET_COMPRESSED_ITEM_DATES, compressedItemDates),
uncompressDates: () => createAction(Constants.UNCOMPRESS_DATES),
planCreated: (plan: PacePlan) => createAction(Constants.PLAN_CREATED, plan),
paceCreated: (pace: CoursePace) => createAction(Constants.PACE_CREATED, pace),
toggleExcludeWeekends: () => createAction(Constants.TOGGLE_EXCLUDE_WEEKENDS),
toggleHardEndDates: () => createAction(Constants.TOGGLE_HARD_END_DATES),
resetPlan: () => createAction(Constants.RESET_PLAN),
resetPace: () => createAction(Constants.RESET_PACE),
setProgress: (progress?: Progress) => createAction(Constants.SET_PROGRESS, progress)
}
const thunkActions = {
publishPlan: (): ThunkAction<Promise<void>, StoreState, void, Action> => {
publishPace: (): ThunkAction<Promise<void>, StoreState, void, Action> => {
return (dispatch, getState) => {
dispatch(uiActions.showLoadingOverlay(I18n.t('Starting publish...')))
dispatch(uiActions.clearCategoryError('publish'))
return Api.publish(getState().pacePlan)
return Api.publish(getState().coursePace)
.then(responseBody => {
if (!responseBody) throw new Error(I18n.t('Response body was empty'))
const {pace_plan: updatedPlan, progress} = responseBody
dispatch(pacePlanActions.setPacePlan(updatedPlan))
dispatch(pacePlanActions.setProgress(progress))
dispatch(pacePlanActions.pollForPublishStatus())
const {course_pace: updatedPace, progress} = responseBody
dispatch(coursePaceActions.setCoursePace(updatedPace))
dispatch(coursePaceActions.setProgress(progress))
dispatch(coursePaceActions.pollForPublishStatus())
dispatch(uiActions.hideLoadingOverlay())
})
.catch(error => {
@ -91,7 +91,7 @@ const thunkActions = {
pollForPublishStatus: (): ThunkAction<void, StoreState, void, Action> => {
// Give the thunk function a name so that we can assert on it in tests
return function pollingThunk(dispatch, getState) {
const progress = getState().pacePlan.publishingProgress
const progress = getState().coursePace.publishingProgress
if (!progress || TERMINAL_PROGRESS_STATUSES.includes(progress.workflow_state)) return
const pollingLoop = () =>
@ -99,14 +99,14 @@ const thunkActions = {
.then(updatedProgress => {
if (!updatedProgress) throw new Error(I18n.t('Response body was empty'))
dispatch(
pacePlanActions.setProgress(
coursePaceActions.setProgress(
updatedProgress.workflow_state !== 'completed' ? updatedProgress : undefined
)
)
dispatch(uiActions.clearCategoryError('checkPublishStatus'))
if (TERMINAL_PROGRESS_STATUSES.includes(updatedProgress.workflow_state)) {
showFlashAlert({
message: I18n.t('Finished publishing plan'),
message: I18n.t('Finished publishing pace'),
err: null,
type: 'success',
srOnly: true
@ -123,7 +123,7 @@ const thunkActions = {
}
},
resetToLastPublished: (
contextType: PlanContextTypes,
contextType: PaceContextTypes,
contextId: string
): ThunkAction<void, StoreState, void, Action> => {
return async (dispatch, getState) => {
@ -133,9 +133,9 @@ const thunkActions = {
await Api.waitForActionCompletion(() => getState().ui.autoSaving)
return Api.resetToLastPublished(contextType, contextId)
.then(pacePlan => {
if (!pacePlan) throw new Error(I18n.t('Response body was empty'))
dispatch(pacePlanActions.setPacePlan(pacePlan))
.then(coursePace => {
if (!coursePace) throw new Error(I18n.t('Response body was empty'))
dispatch(coursePaceActions.setCoursePace(coursePace))
dispatch(uiActions.hideLoadingOverlay())
})
.catch(error => {
@ -145,10 +145,10 @@ const thunkActions = {
})
}
},
loadLatestPlanByContext: (
contextType: PlanContextTypes,
loadLatestPaceByContext: (
contextType: PaceContextTypes,
contextId: string,
afterAction: LoadingAfterAction = pacePlanActions.setPacePlan
afterAction: LoadingAfterAction = coursePaceActions.setCoursePace
): ThunkAction<void, StoreState, void, Action> => {
return async (dispatch, getState) => {
dispatch(uiActions.showLoadingOverlay(I18n.t('Loading...')))
@ -156,10 +156,10 @@ const thunkActions = {
await Api.waitForActionCompletion(() => getState().ui.autoSaving)
return Api.getNewPacePlanFor(getState().course.id, contextType, contextId)
.then(pacePlan => {
if (!pacePlan) throw new Error(I18n.t('Response body was empty'))
dispatch(afterAction(pacePlan))
return Api.getNewCoursePaceFor(getState().course.id, contextType, contextId)
.then(coursePace => {
if (!coursePace) throw new Error(I18n.t('Response body was empty'))
dispatch(afterAction(coursePace))
dispatch(uiActions.hideLoadingOverlay())
})
.catch(error => {
@ -169,20 +169,20 @@ const thunkActions = {
})
}
},
relinkToParentPlan: (): ThunkAction<void, StoreState, void, Action> => {
relinkToParentPace: (): ThunkAction<void, StoreState, void, Action> => {
return async (dispatch, getState) => {
const pacePlanId = getState().pacePlan.id
if (!pacePlanId) return Promise.reject(new Error(I18n.t('Cannot relink unsaved plans')))
const coursePaceId = getState().coursePace.id
if (!coursePaceId) return Promise.reject(new Error(I18n.t('Cannot relink unsaved paces')))
dispatch(uiActions.showLoadingOverlay(I18n.t('Relinking plans...')))
dispatch(uiActions.showLoadingOverlay(I18n.t('Relinking paces...')))
dispatch(uiActions.clearCategoryError('relinkToParent'))
await Api.waitForActionCompletion(() => getState().ui.autoSaving)
return Api.relinkToParentPlan(pacePlanId)
.then(pacePlan => {
if (!pacePlan) throw new Error(I18n.t('Response body was empty'))
dispatch(pacePlanActions.setPacePlan(pacePlan))
return Api.relinkToParentPace(coursePaceId)
.then(coursePace => {
if (!coursePace) throw new Error(I18n.t('Response body was empty'))
dispatch(coursePaceActions.setCoursePace(coursePace))
dispatch(uiActions.hideLoadingOverlay())
})
.catch(error => {
@ -197,11 +197,11 @@ const thunkActions = {
dispatch(uiActions.showLoadingOverlay(I18n.t('Compressing...')))
dispatch(uiActions.clearCategoryError('compress'))
return Api.compress(getState().pacePlan)
return Api.compress(getState().coursePace)
.then(responseBody => {
if (!responseBody) throw new Error(I18n.t('Response body was empty'))
const compressedItemDates = responseBody
dispatch(pacePlanActions.setCompressedItemDates(compressedItemDates))
dispatch(coursePaceActions.setCompressedItemDates(compressedItemDates))
dispatch(uiActions.hideLoadingOverlay())
})
.catch(error => {
@ -213,5 +213,5 @@ const thunkActions = {
}
}
export const pacePlanActions = {...regularActions, ...thunkActions}
export type PacePlanAction = ActionsUnion<typeof regularActions>
export const coursePaceActions = {...regularActions, ...thunkActions}
export type CoursePaceAction = ActionsUnion<typeof regularActions>

View File

@ -23,9 +23,9 @@
import {Action} from 'redux'
import {ThunkAction} from 'redux-thunk'
import {PacePlan, PlanContextTypes, ResponsiveSizes, StoreState} from '../types'
import {CoursePace, PaceContextTypes, ResponsiveSizes, StoreState} from '../types'
import {createAction, ActionsUnion} from '../shared/types'
import {pacePlanActions} from './pace_plans'
import {coursePaceActions} from './course_paces'
export enum Constants {
START_AUTO_SAVING = 'UI/START_AUTO_SAVING',
@ -34,7 +34,7 @@ export enum Constants {
CLEAR_CATEGORY_ERROR = 'UI/CLEAR_CATEGORY_ERROR',
TOGGLE_DIVIDE_INTO_WEEKS = 'UI/TOGGLE_DIVIDE_INTO_WEEKS',
TOGGLE_SHOW_PROJECTIONS = 'UI/TOGGLE_SHOW_PROJECTIONS',
SET_SELECTED_PLAN_CONTEXT = 'UI/SET_SELECTED_PLAN_CONTEXT',
SET_SELECTED_PACE_CONTEXT = 'UI/SET_SELECTED_PACE_CONTEXT',
SET_RESPONSIVE_SIZE = 'UI/SET_RESPONSIVE_SIZE',
SHOW_LOADING_OVERLAY = 'UI/SHOW_LOADING_OVERLAY',
HIDE_LOADING_OVERLAY = 'UI/HIDE_LOADING_OVERLAY',
@ -55,30 +55,30 @@ export const regularActions = {
hideLoadingOverlay: () => createAction(Constants.HIDE_LOADING_OVERLAY),
setEditingBlackoutDates: (editing: boolean) =>
createAction(Constants.SET_EDITING_BLACKOUT_DATES, editing),
setSelectedPlanContext: (
contextType: PlanContextTypes,
setSelectedPaceContext: (
contextType: PaceContextTypes,
contextId: string,
newSelectedPlan: PacePlan
) => createAction(Constants.SET_SELECTED_PLAN_CONTEXT, {contextType, contextId, newSelectedPlan}),
newSelectedPace: CoursePace
) => createAction(Constants.SET_SELECTED_PACE_CONTEXT, {contextType, contextId, newSelectedPace}),
setResponsiveSize: (responsiveSize: ResponsiveSizes) =>
createAction(Constants.SET_RESPONSIVE_SIZE, responsiveSize)
}
export const thunkActions = {
setSelectedPlanContext: (
contextType: PlanContextTypes,
setSelectedPaceContext: (
contextType: PaceContextTypes,
contextId: string
): ThunkAction<void, StoreState, void, Action> => {
// Switch to the other plan type, and load the exact plan we should switch to
// Switch to the other pace type, and load the exact pace we should switch to
return dispatch => {
const afterLoadActionCreator = (newSelectedPlan: PacePlan): SetSelectedPlanType => {
const afterLoadActionCreator = (newSelectedPace: CoursePace): SetSelectedPaceType => {
return {
type: Constants.SET_SELECTED_PLAN_CONTEXT,
payload: {contextType, contextId, newSelectedPlan}
type: Constants.SET_SELECTED_PACE_CONTEXT,
payload: {contextType, contextId, newSelectedPace}
}
}
dispatch(
pacePlanActions.loadLatestPlanByContext(contextType, contextId, afterLoadActionCreator)
coursePaceActions.loadLatestPaceByContext(contextType, contextId, afterLoadActionCreator)
)
}
}
@ -87,4 +87,4 @@ export const thunkActions = {
export const actions = {...regularActions, ...thunkActions}
export type UIAction = ActionsUnion<typeof regularActions>
export type SetSelectedPlanType = ReturnType<typeof regularActions.setSelectedPlanContext>
export type SetSelectedPaceType = ReturnType<typeof regularActions.setSelectedPaceContext>

View File

@ -16,7 +16,7 @@
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import {PacePlan, PlanContextTypes, Progress, WorkflowStates} from '../types'
import {CoursePace, PaceContextTypes, Progress, WorkflowStates} from '../types'
import doFetchApi from '@canvas/do-fetch-api-effect'
enum ApiMode {
@ -55,88 +55,88 @@ export const waitForActionCompletion = (actionInProgress: () => boolean, waitTim
/* API methods */
export const update = (pacePlan: PacePlan, extraSaveParams = {}) =>
doFetchApi<{pace_plan: PacePlan; progress: Progress}>({
path: `/api/v1/courses/${pacePlan.course_id}/pace_plans/${pacePlan.id}`,
export const update = (coursePace: CoursePace, extraSaveParams = {}) =>
doFetchApi<{course_pace: CoursePace; progress: Progress}>({
path: `/api/v1/courses/${coursePace.course_id}/course_paces/${coursePace.id}`,
method: 'PUT',
body: {
...extraSaveParams,
pace_plan: transformPacePlanForApi(pacePlan)
course_pace: transformCoursePaceForApi(coursePace)
}
}).then(({json}) => json)
export const create = (pacePlan: PacePlan, extraSaveParams = {}) =>
doFetchApi<{pace_plan: PacePlan; progress: Progress}>({
path: `/api/v1/courses/${pacePlan.course_id}/pace_plans`,
export const create = (coursePace: CoursePace, extraSaveParams = {}) =>
doFetchApi<{course_pace: CoursePace; progress: Progress}>({
path: `/api/v1/courses/${coursePace.course_id}/course_paces`,
method: 'POST',
body: {
...extraSaveParams,
pace_plan: transformPacePlanForApi(pacePlan)
course_pace: transformCoursePaceForApi(coursePace)
}
}).then(({json}) => json)
// This is now just a convenience function for creating/update depending on the
// state of the plan
export const publish = (plan: PacePlan) => (plan?.id ? update(plan) : create(plan))
// state of the pace
export const publish = (pace: CoursePace) => (pace?.id ? update(pace) : create(pace))
export const getPublishProgress = (progressId: string) =>
doFetchApi<Progress>({
path: `/api/v1/progress/${progressId}`
}).then(({json}) => json)
export const resetToLastPublished = (contextType: PlanContextTypes, contextId: string) =>
doFetchApi<{pace_plan: PacePlan}>({
path: `/api/v1/pace_plans/reset_to_last_published`,
export const resetToLastPublished = (contextType: PaceContextTypes, contextId: string) =>
doFetchApi<{course_pace: CoursePace}>({
path: `/api/v1/course_paces/reset_to_last_published`,
method: 'POST',
body: {
context_type: contextType,
context_id: contextId
}
}).then(({json}) => json?.pace_plan)
}).then(({json}) => json?.course_pace)
export const load = (pacePlanId: string) =>
doFetchApi<PacePlan>({path: `/api/v1/pace_plans/${pacePlanId}`}).then(({json}) => json)
export const load = (coursePaceId: string) =>
doFetchApi<CoursePace>({path: `/api/v1/course_paces/${coursePaceId}`}).then(({json}) => json)
export const getNewPacePlanFor = (
export const getNewCoursePaceFor = (
courseId: string,
context: PlanContextTypes,
context: PaceContextTypes,
contextId: string
) => {
let url = `/api/v1/courses/${courseId}/pace_plans/new`
let url = `/api/v1/courses/${courseId}/course_paces/new`
if (context === 'Section') {
url = `/api/v1/courses/${courseId}/pace_plans/new?course_section_id=${contextId}`
url = `/api/v1/courses/${courseId}/course_paces/new?course_section_id=${contextId}`
} else if (context === 'Enrollment') {
url = `/api/v1/courses/${courseId}/pace_plans/new?enrollment_id=${contextId}`
url = `/api/v1/courses/${courseId}/course_paces/new?enrollment_id=${contextId}`
}
return doFetchApi<{pace_plan: PacePlan}>({path: url}).then(({json}) => json?.pace_plan)
return doFetchApi<{course_pace: CoursePace}>({path: url}).then(({json}) => json?.course_pace)
}
export const republishAllPlansForCourse = (courseId: string) =>
export const republishAllPacesForCourse = (courseId: string) =>
doFetchApi({
path: `/api/v1/pace_plans/republish_all_plans`,
path: `/api/v1/course_paces/republish_all_paces`,
method: 'POST',
body: {course_id: courseId}
}).then(({json}) => json)
export const republishAllPlans = () =>
export const republishAllPaces = () =>
doFetchApi({
path: `/api/v1/pace_plans/republish_all_plans`,
path: `/api/v1/course_paces/republish_all_paces`,
method: 'POST'
}).then(({json}) => json)
export const relinkToParentPlan = (planId: string) =>
doFetchApi<{pace_plan: PacePlan}>({
path: `/api/v1/pace_plans/${planId}/relink_to_parent_plan`,
export const relinkToParentPace = (paceId: string) =>
doFetchApi<{course_pace: CoursePace}>({
path: `/api/v1/course_paces/${paceId}/relink_to_parent_pace`,
method: 'POST'
}).then(({json}) => json?.pace_plan)
}).then(({json}) => json?.course_pace)
export const compress = (pacePlan: PacePlan, extraSaveParams = {}) =>
doFetchApi<{pace_plan: PacePlan; progress: Progress}>({
path: `/api/v1/courses/${pacePlan.course_id}/pace_plans/compress_dates`,
export const compress = (coursePace: CoursePace, extraSaveParams = {}) =>
doFetchApi<{course_pace: CoursePace; progress: Progress}>({
path: `/api/v1/courses/${coursePace.course_id}/course_paces/compress_dates`,
method: 'POST',
body: {
...extraSaveParams,
pace_plan: transformPacePlanForApi(pacePlan, ApiMode.COMPRESS)
course_pace: transformCoursePaceForApi(coursePace, ApiMode.COMPRESS)
}
}).then(({json}) => json)
@ -149,33 +149,33 @@ export const compress = (pacePlan: PacePlan, extraSaveParams = {}) =>
* if more models are saved using the same pattern.
*/
interface ApiPacePlanModuleItemsAttributes {
interface ApiCoursePaceModuleItemsAttributes {
readonly id: string
readonly duration: number
readonly module_item_id: string
}
interface CompressApiFormattedPacePlan {
interface CompressApiFormattedCoursePace {
readonly start_date?: string
readonly end_date?: string
readonly exclude_weekends: boolean
readonly pace_plan_module_items_attributes: ApiPacePlanModuleItemsAttributes[]
readonly course_pace_module_items_attributes: ApiCoursePaceModuleItemsAttributes[]
}
interface PublishApiFormattedPacePlan extends CompressApiFormattedPacePlan {
interface PublishApiFormattedCoursePace extends CompressApiFormattedCoursePace {
readonly workflow_state: WorkflowStates
readonly context_type: PlanContextTypes
readonly context_type: PaceContextTypes
readonly context_id: string
readonly hard_end_dates: boolean
}
const transformPacePlanForApi = (
pacePlan: PacePlan,
const transformCoursePaceForApi = (
coursePace: CoursePace,
mode: ApiMode = ApiMode.PUBLISH
): PublishApiFormattedPacePlan | CompressApiFormattedPacePlan => {
const pacePlanItems: ApiPacePlanModuleItemsAttributes[] = []
pacePlan.modules.forEach(module => {
): PublishApiFormattedCoursePace | CompressApiFormattedCoursePace => {
const coursePaceItems: ApiCoursePaceModuleItemsAttributes[] = []
coursePace.modules.forEach(module => {
module.items.forEach(item => {
pacePlanItems.push({
coursePaceItems.push({
id: item.id,
duration: item.duration,
module_item_id: item.module_item_id
@ -185,19 +185,19 @@ const transformPacePlanForApi = (
return mode === ApiMode.COMPRESS
? {
start_date: pacePlan.start_date,
end_date: pacePlan.end_date,
exclude_weekends: pacePlan.exclude_weekends,
pace_plan_module_items_attributes: pacePlanItems
start_date: coursePace.start_date,
end_date: coursePace.end_date,
exclude_weekends: coursePace.exclude_weekends,
course_pace_module_items_attributes: coursePaceItems
}
: {
start_date: pacePlan.start_date,
end_date: pacePlan.end_date,
workflow_state: pacePlan.workflow_state,
exclude_weekends: pacePlan.exclude_weekends,
context_type: pacePlan.context_type,
context_id: pacePlan.context_id,
hard_end_dates: !!pacePlan.hard_end_dates,
pace_plan_module_items_attributes: pacePlanItems
start_date: coursePace.start_date,
end_date: coursePace.end_date,
workflow_state: coursePace.workflow_state,
exclude_weekends: coursePace.exclude_weekends,
context_type: coursePace.context_type,
context_id: coursePace.context_id,
hard_end_dates: !!coursePace.hard_end_dates,
course_pace_module_items_attributes: coursePaceItems
}
}

View File

@ -33,14 +33,14 @@ import {ResponsiveSizes, StoreState} from './types'
import {getLoadingMessage, getShowLoadingOverlay} from './reducers/ui'
import UnpublishedChangesTrayContents from './components/unpublished_changes_tray_contents'
// @ts-ignore: TS doesn't understand i18n scoped imports
import { useScope as useI18nScope } from '@canvas/i18n';
import {getSummarizedChanges} from './reducers/pace_plans'
import {pacePlanActions} from './actions/pace_plans'
import {useScope as useI18nScope} from '@canvas/i18n'
import {getSummarizedChanges} from './reducers/course_paces'
import {coursePaceActions} from './actions/course_paces'
import {SummarizedChange} from './utils/change_tracking'
import {Tray} from '@instructure/ui-tray'
import Errors from './components/errors'
const I18n = useI18nScope('pace_plans_app');
const I18n = useI18nScope('course_paces_app')
interface StoreProps {
readonly loadingMessage: string
@ -49,7 +49,7 @@ interface StoreProps {
}
interface DispatchProps {
readonly pollForPublishStatus: typeof pacePlanActions.pollForPublishStatus
readonly pollForPublishStatus: typeof coursePaceActions.pollForPublishStatus
readonly setResponsiveSize: typeof actions.setResponsiveSize
}
@ -135,6 +135,6 @@ const mapStateToProps = (state: StoreState): StoreProps => {
}
export default connect(mapStateToProps, {
pollForPublishStatus: pacePlanActions.pollForPublishStatus,
pollForPublishStatus: coursePaceActions.pollForPublishStatus,
setResponsiveSize: actions.setResponsiveSize
})(ResponsiveApp)

View File

@ -21,7 +21,7 @@ import {act, render} from '@testing-library/react'
import React from 'react'
import userEvent from '@testing-library/user-event'
const publishPlan = jest.fn()
const publishPace = jest.fn()
afterEach(jest.clearAllMocks)
describe('Errors', () => {
@ -32,7 +32,7 @@ describe('Errors', () => {
darkMode: 'E_THEME_TOO_DARK: Theme too dark, user could trip and fall'
},
responsiveSize: 'large',
publishPlan
publishPace
}
it('renders nothing when there are no errors', () => {
@ -45,8 +45,8 @@ describe('Errors', () => {
const {getAllByText} = render(<Errors {...defaultProps} />)
for (const error of [
...getAllByText('There was an error publishing your pace plan.'),
...getAllByText('There was an error loading the plan.'),
...getAllByText('There was an error publishing your course pace.'),
...getAllByText('There was an error loading the pace.'),
...getAllByText('An error has occurred.')
]) {
expect(error).toBeInTheDocument()
@ -57,6 +57,6 @@ describe('Errors', () => {
const {getByRole} = render(<Errors {...defaultProps} />)
act(() => userEvent.click(getByRole('button', {name: 'Retry'})))
expect(publishPlan).toHaveBeenCalled()
expect(publishPace).toHaveBeenCalled()
})
})

View File

@ -21,16 +21,16 @@ import {act, render, within} from '@testing-library/react'
import {Footer} from '../footer'
const publishPlan = jest.fn()
const resetPlan = jest.fn()
const publishPace = jest.fn()
const resetPace = jest.fn()
const defaultProps = {
autoSaving: false,
planPublishing: false,
publishPlan,
resetPlan,
pacePublishing: false,
publishPace,
resetPace,
showLoadingOverlay: false,
studentPlan: false,
studentPace: false,
unpublishedChanges: true
}
@ -45,12 +45,12 @@ describe('Footer', () => {
const cancelButton = getByRole('button', {name: 'Cancel'})
expect(cancelButton).toBeInTheDocument()
act(() => cancelButton.click())
expect(resetPlan).toHaveBeenCalled()
expect(resetPace).toHaveBeenCalled()
const publishButton = getByRole('button', {name: 'Publish'})
expect(publishButton).toBeInTheDocument()
act(() => publishButton.click())
expect(publishPlan).toHaveBeenCalled()
expect(publishPace).toHaveBeenCalled()
})
it('shows cannot cancel and publish tooltip when there are no unpublished changes', () => {
@ -60,7 +60,7 @@ describe('Footer', () => {
})
it('shows cannot cancel and publish tooltip while publishing', () => {
const {getByText} = render(<Footer {...defaultProps} planPublishing />)
const {getByText} = render(<Footer {...defaultProps} pacePublishing />)
expect(getByText('You cannot cancel while publishing')).toBeInTheDocument()
expect(getByText('You cannot publish while publishing')).toBeInTheDocument()
})
@ -73,22 +73,22 @@ describe('Footer', () => {
it('shows cannot cancel and publish tooltip while loading', () => {
const {getByText} = render(<Footer {...defaultProps} showLoadingOverlay />)
expect(getByText('You cannot cancel while loading the plan')).toBeInTheDocument()
expect(getByText('You cannot publish while loading the plan')).toBeInTheDocument()
expect(getByText('You cannot cancel while loading the pace')).toBeInTheDocument()
expect(getByText('You cannot publish while loading the pace')).toBeInTheDocument()
})
it('renders a loading spinner inside the publish button when publishing is ongoing', () => {
const {getByRole} = render(<Footer {...defaultProps} planPublishing />)
const {getByRole} = render(<Footer {...defaultProps} pacePublishing />)
const publishButton = getByRole('button', {name: 'Publishing plan...'})
const publishButton = getByRole('button', {name: 'Publishing pace...'})
expect(publishButton).toBeInTheDocument()
const spinner = within(publishButton).getByRole('img', {name: 'Publishing plan...'})
const spinner = within(publishButton).getByRole('img', {name: 'Publishing pace...'})
expect(spinner).toBeInTheDocument()
})
it('renders nothing for student plans', () => {
const {queryByRole} = render(<Footer {...defaultProps} studentPlan />)
it('renders nothing for student paces', () => {
const {queryByRole} = render(<Footer {...defaultProps} studentPace />)
expect(queryByRole('button')).not.toBeInTheDocument()
})

View File

@ -26,8 +26,8 @@ const onUnpublishedNavigation = jest.fn()
const defaultProps = {
changeCount: 2,
onUnpublishedNavigation,
planPublishing: false,
newPlan: false
pacePublishing: false,
newPace: false
}
afterEach(() => {
@ -98,13 +98,13 @@ describe('UnpublishedChangesIndicator', () => {
).toThrow()
})
it('displays a spinner indicating ongoing publishing when planPublishing is true', () => {
const {getAllByText} = render(<UnpublishedChangesIndicator {...defaultProps} planPublishing />)
expect(getAllByText('Publishing plan...')[0]).toBeInTheDocument()
it('displays a spinner indicating ongoing publishing when pacePublishing is true', () => {
const {getAllByText} = render(<UnpublishedChangesIndicator {...defaultProps} pacePublishing />)
expect(getAllByText('Publishing pace...')[0]).toBeInTheDocument()
})
it('renders nothing if the plan has not yet been published', () => {
const {queryByText} = render(<UnpublishedChangesIndicator {...defaultProps} newPlan />)
it('renders nothing if the pace has not yet been published', () => {
const {queryByText} = render(<UnpublishedChangesIndicator {...defaultProps} newPace />)
expect(queryByText('All changes published')).not.toBeInTheDocument()
})
})

View File

@ -17,8 +17,8 @@
*/
import React from 'react'
import PacePlanTable from './pace_plan_table/pace_plan_table'
import CoursePaceTable from './course_pace_table/course_pace_table'
const Body: React.FC = () => <PacePlanTable />
const Body: React.FC = () => <CoursePaceTable />
export default Body

View File

@ -22,33 +22,33 @@ import userEvent from '@testing-library/user-event'
import {
BLACKOUT_DATES,
PLAN_ITEM_1,
PLAN_ITEM_3,
PRIMARY_PLAN,
STUDENT_PLAN
PACE_ITEM_1,
PACE_ITEM_3,
PRIMARY_PACE,
STUDENT_PACE
} from '../../../__tests__/fixtures'
import {renderConnected} from '../../../__tests__/utils'
import {AssignmentRow} from '../assignment_row'
const setPlanItemDuration = jest.fn()
const setPaceItemDuration = jest.fn()
const defaultProps = {
pacePlan: PRIMARY_PLAN,
coursePace: PRIMARY_PACE,
dueDate: '2020-01-01T02:00:00-05:00',
excludeWeekends: false,
pacePlanItem: PRIMARY_PLAN.modules[0].items[0],
pacePlanItemPosition: 0,
planPublishing: false,
coursePaceItem: PRIMARY_PACE.modules[0].items[0],
coursePaceItemPosition: 0,
pacePublishing: false,
blackoutDates: BLACKOUT_DATES,
autosaving: false,
disabledDaysOfWeek: [],
showProjections: true,
setPlanItemDuration,
setPaceItemDuration,
datesVisible: true,
hover: false,
isStacked: false,
isStudentPlan: false
isStudentPace: false
}
beforeAll(() => {
@ -62,7 +62,7 @@ afterEach(() => {
describe('AssignmentRow', () => {
it('renders the assignment title and icon of the module item', () => {
const {getByText} = renderConnected(<AssignmentRow {...defaultProps} />)
expect(getByText(defaultProps.pacePlanItem.assignment_title)).toBeInTheDocument()
expect(getByText(defaultProps.coursePaceItem.assignment_title)).toBeInTheDocument()
})
it('renders the assignment title as a link to the assignment', () => {
@ -70,8 +70,8 @@ describe('AssignmentRow', () => {
// Implementation detail bleeds in here; but the `TruncateText` means that the title isn't directly in the `a`
expect(
getByText(defaultProps.pacePlanItem.assignment_title)?.parentNode?.parentNode
).toHaveAttribute('href', defaultProps.pacePlanItem.assignment_link)
getByText(defaultProps.coursePaceItem.assignment_title)?.parentNode?.parentNode
).toHaveAttribute('href', defaultProps.coursePaceItem.assignment_link)
})
it('renders an input that updates the duration for that module item', () => {
@ -85,8 +85,8 @@ describe('AssignmentRow', () => {
userEvent.type(daysInput, '{selectall}{backspace}4')
act(() => daysInput.blur())
expect(setPlanItemDuration).toHaveBeenCalled()
expect(setPlanItemDuration).toHaveBeenCalledWith('60', 4)
expect(setPaceItemDuration).toHaveBeenCalled()
expect(setPaceItemDuration).toHaveBeenCalledWith('60', 4)
})
it('renders the projected due date if projections are being shown', () => {
@ -109,7 +109,7 @@ describe('AssignmentRow', () => {
const unpublishedProps = {
...defaultProps,
pacePlanItem: {...defaultProps.pacePlanItem, published: false}
coursePaceItem: {...defaultProps.coursePaceItem, published: false}
}
const unpublishedIcon = renderConnected(<AssignmentRow {...unpublishedProps} />).getByText(
'Unpublished'
@ -118,7 +118,7 @@ describe('AssignmentRow', () => {
})
it('disables duration inputs while publishing', () => {
const {getByRole} = renderConnected(<AssignmentRow {...defaultProps} planPublishing />)
const {getByRole} = renderConnected(<AssignmentRow {...defaultProps} pacePublishing />)
const daysInput = getByRole('textbox', {
name: 'Duration for module Basic encryption/decryption'
})
@ -130,7 +130,7 @@ describe('AssignmentRow', () => {
expect(getByText('100 pts')).toBeInTheDocument()
rerender(<AssignmentRow {...defaultProps} pacePlanItem={PLAN_ITEM_3} />)
rerender(<AssignmentRow {...defaultProps} coursePaceItem={PACE_ITEM_3} />)
expect(getByText('1 pt')).toBeInTheDocument()
})
@ -138,22 +138,22 @@ describe('AssignmentRow', () => {
const {getByText, rerender} = renderConnected(
<AssignmentRow
{...defaultProps}
pacePlanItem={{...PLAN_ITEM_1, points_possible: undefined}}
coursePaceItem={{...PACE_ITEM_1, points_possible: undefined}}
/>
)
expect(getByText(PLAN_ITEM_1.assignment_title)).toBeInTheDocument()
expect(getByText(PACE_ITEM_1.assignment_title)).toBeInTheDocument()
rerender(
<AssignmentRow {...defaultProps} pacePlanItem={{...PLAN_ITEM_1, points_possible: null}} />
<AssignmentRow {...defaultProps} coursePaceItem={{...PACE_ITEM_1, points_possible: null}} />
)
expect(getByText(PLAN_ITEM_1.assignment_title)).toBeInTheDocument()
expect(getByText(PACE_ITEM_1.assignment_title)).toBeInTheDocument()
})
it('shows durations as read-only text when on student plans', () => {
it('shows durations as read-only text when on student paces', () => {
const {queryByRole, getByText} = renderConnected(
<AssignmentRow {...defaultProps} pacePlan={STUDENT_PLAN} isStudentPlan />
<AssignmentRow {...defaultProps} coursePace={STUDENT_PACE} isStudentPace />
)
expect(
queryByRole('textbox', {

View File

@ -19,15 +19,15 @@
import React from 'react'
import {act} from '@testing-library/react'
import {PLAN_MODULE_1, PRIMARY_PLAN} from '../../../__tests__/fixtures'
import {PACE_MODULE_1, PRIMARY_PACE} from '../../../__tests__/fixtures'
import {renderConnected} from '../../../__tests__/utils'
import {Module} from '../module'
const defaultProps = {
index: 1,
module: PLAN_MODULE_1,
pacePlan: PRIMARY_PLAN,
module: PACE_MODULE_1,
coursePace: PRIMARY_PACE,
responsiveSize: 'large' as const,
showProjections: true,
isCompressing: false
@ -38,14 +38,14 @@ describe('Module', () => {
const {getByRole, queryByRole, queryByText} = renderConnected(<Module {...defaultProps} />)
const moduleHeader = getByRole('button', {name: '1. How 2 B A H4CK32'})
expect(moduleHeader).toBeInTheDocument()
expect(queryByText(PLAN_MODULE_1.items[0].assignment_title)).toBeInTheDocument()
expect(queryByText(PACE_MODULE_1.items[0].assignment_title)).toBeInTheDocument()
expect(queryByRole('columnheader', {name: 'Days'})).toBeInTheDocument()
expect(queryByRole('columnheader', {name: 'Due Date'})).toBeInTheDocument()
expect(queryByRole('columnheader', {name: 'Status'})).toBeInTheDocument()
act(() => moduleHeader.click())
expect(getByRole('button', {name: '1. How 2 B A H4CK32'})).toBeInTheDocument()
expect(queryByText(PLAN_MODULE_1.items[0].assignment_title)).not.toBeInTheDocument()
expect(queryByText(PACE_MODULE_1.items[0].assignment_title)).not.toBeInTheDocument()
expect(queryByRole('columnheader', {name: 'Days'})).not.toBeInTheDocument()
expect(queryByRole('columnheader', {name: 'Due Date'})).not.toBeInTheDocument()
expect(queryByRole('columnheader', {name: 'Status'})).not.toBeInTheDocument()

View File

@ -19,7 +19,7 @@
import React from 'react'
import {connect} from 'react-redux'
// @ts-ignore: TS doesn't understand i18n scoped imports
import { useScope as useI18nScope } from '@canvas/i18n';
import {useScope as useI18nScope} from '@canvas/i18n'
import {debounce, pick} from 'lodash'
import moment from 'moment-timezone'
@ -39,23 +39,23 @@ import {Text} from '@instructure/ui-text'
import {TruncateText} from '@instructure/ui-truncate-text'
import {View} from '@instructure/ui-view'
import {pacePlanTimezone} from '../../shared/api/backend_serializer'
import {PacePlanItem, PacePlan, StoreState} from '../../types'
import {coursePaceTimezone} from '../../shared/api/backend_serializer'
import {CoursePaceItem, CoursePace, StoreState} from '../../types'
import {BlackoutDate} from '../../shared/types'
import {
getPacePlan,
getCoursePace,
getDueDate,
getExcludeWeekends,
getPacePlanItemPosition,
getPlanPublishing,
isStudentPlan
} from '../../reducers/pace_plans'
import {actions} from '../../actions/pace_plan_items'
getCoursePaceItemPosition,
getPacePublishing,
isStudentPace
} from '../../reducers/course_paces'
import {actions} from '../../actions/course_pace_items'
import * as DateHelpers from '../../utils/date_stuff/date_helpers'
import {getShowProjections} from '../../reducers/ui'
import {getBlackoutDates} from '../../shared/reducers/blackout_dates'
const I18n = useI18nScope('pace_plans_assignment_row');
const I18n = useI18nScope('course_paces_assignment_row')
// Doing this to avoid TS2339 errors-- remove once we're on InstUI 8
const {Cell, Row} = Table as any
@ -65,22 +65,22 @@ interface PassedProps {
readonly headers?: object[]
readonly hover: boolean
readonly isStacked: boolean
readonly pacePlanItem: PacePlanItem
readonly coursePaceItem: CoursePaceItem
}
interface StoreProps {
readonly pacePlan: PacePlan
readonly coursePace: CoursePace
readonly dueDate: string
readonly excludeWeekends: boolean
readonly pacePlanItemPosition: number
readonly coursePaceItemPosition: number
readonly blackoutDates: BlackoutDate[]
readonly planPublishing: boolean
readonly pacePublishing: boolean
readonly showProjections: boolean
readonly isStudentPlan: boolean
readonly isStudentPace: boolean
}
interface DispatchProps {
readonly setPlanItemDuration: typeof actions.setPlanItemDuration
readonly setPaceItemDuration: typeof actions.setPaceItemDuration
}
interface LocalState {
@ -92,7 +92,7 @@ type ComponentProps = PassedProps & StoreProps & DispatchProps
export class AssignmentRow extends React.Component<ComponentProps, LocalState> {
state: LocalState = {
duration: String(this.props.pacePlanItem.duration),
duration: String(this.props.coursePaceItem.duration),
hovering: false
}
@ -113,7 +113,7 @@ export class AssignmentRow extends React.Component<ComponentProps, LocalState> {
month: 'short',
day: 'numeric',
year: 'numeric',
timeZone: pacePlanTimezone
timeZone: coursePaceTimezone
})
}
@ -122,11 +122,11 @@ export class AssignmentRow extends React.Component<ComponentProps, LocalState> {
nextProps.dueDate !== this.props.dueDate ||
nextState.duration !== this.state.duration ||
nextState.hovering !== this.state.hovering ||
nextProps.pacePlan.exclude_weekends !== this.props.pacePlan.exclude_weekends ||
nextProps.pacePlan.context_type !== this.props.pacePlan.context_type ||
(nextProps.pacePlan.context_type === this.props.pacePlan.context_type &&
nextProps.pacePlan.context_id !== this.props.pacePlan.context_id) ||
nextProps.planPublishing !== this.props.planPublishing ||
nextProps.coursePace.exclude_weekends !== this.props.coursePace.exclude_weekends ||
nextProps.coursePace.context_type !== this.props.coursePace.context_type ||
(nextProps.coursePace.context_type === this.props.coursePace.context_type &&
nextProps.coursePace.context_id !== this.props.coursePace.context_id) ||
nextProps.pacePublishing !== this.props.pacePublishing ||
nextProps.showProjections !== this.props.showProjections ||
nextProps.datesVisible !== this.props.datesVisible
)
@ -208,7 +208,7 @@ export class AssignmentRow extends React.Component<ComponentProps, LocalState> {
const duration = parseInt(this.state.duration, 10)
if (!Number.isNaN(duration)) {
this.props.setPlanItemDuration(this.props.pacePlanItem.module_item_id, duration)
this.props.setPaceItemDuration(this.props.coursePaceItem.module_item_id, duration)
}
}
@ -220,9 +220,9 @@ export class AssignmentRow extends React.Component<ComponentProps, LocalState> {
renderAssignmentIcon = () => {
const size = '20px'
const color = this.props.pacePlanItem.published ? '#4AA937' : '#75808B'
const color = this.props.coursePaceItem.published ? '#4AA937' : '#75808B'
switch (this.props.pacePlanItem.module_item_type) {
switch (this.props.coursePaceItem.module_item_type) {
case 'Assignment':
return <IconAssignmentLine width={size} height={size} style={{color}} />
case 'Quizzes::Quiz':
@ -239,7 +239,7 @@ export class AssignmentRow extends React.Component<ComponentProps, LocalState> {
}
renderPublishStatusBadge = () => {
return this.props.pacePlanItem.published ? (
return this.props.coursePaceItem.published ? (
<IconPublishSolid color="success" size="x-small" title={I18n.t('Published')} />
) : (
<IconUnpublishedLine size="x-small" title={I18n.t('Unpublished')} />
@ -247,7 +247,7 @@ export class AssignmentRow extends React.Component<ComponentProps, LocalState> {
}
renderDurationInput = () => {
if (this.props.isStudentPlan) {
if (this.props.isStudentPace) {
return (
<Flex height="2.375rem" alignItems="center" justifyItems="center">
{this.state.duration}
@ -257,10 +257,10 @@ export class AssignmentRow extends React.Component<ComponentProps, LocalState> {
return (
<NumberInput
interaction={this.props.planPublishing ? 'disabled' : 'enabled'}
interaction={this.props.pacePublishing ? 'disabled' : 'enabled'}
renderLabel={
<ScreenReaderContent>
Duration for module {this.props.pacePlanItem.assignment_title}
Duration for module {this.props.coursePaceItem.assignment_title}
</ScreenReaderContent>
}
data-testid="duration-number-input"
@ -287,16 +287,16 @@ export class AssignmentRow extends React.Component<ComponentProps, LocalState> {
<View margin="0 x-small 0 0">{this.renderAssignmentIcon()}</View>
<div>
<Text weight="bold">
<a href={this.props.pacePlanItem.assignment_link} style={{color: 'inherit'}}>
<TruncateText>{this.props.pacePlanItem.assignment_title}</TruncateText>
<a href={this.props.coursePaceItem.assignment_link} style={{color: 'inherit'}}>
<TruncateText>{this.props.coursePaceItem.assignment_title}</TruncateText>
</a>
</Text>
{typeof this.props.pacePlanItem.points_possible === 'number' && (
<span className="pace-plans-assignment-row-points-possible">
{typeof this.props.coursePaceItem.points_possible === 'number' && (
<span className="course-paces-assignment-row-points-possible">
<Text size="x-small">
{I18n.t(
{one: '1 pt', other: '%{count} pts'},
{count: this.props.pacePlanItem.points_possible}
{count: this.props.coursePaceItem.points_possible}
)}
</Text>
</span>
@ -343,22 +343,22 @@ export class AssignmentRow extends React.Component<ComponentProps, LocalState> {
}
const mapStateToProps = (state: StoreState, props: PassedProps): StoreProps => {
const pacePlan = getPacePlan(state)
const coursePace = getCoursePace(state)
return {
pacePlan,
coursePace,
dueDate: getDueDate(state, props),
excludeWeekends: getExcludeWeekends(state),
pacePlanItemPosition: getPacePlanItemPosition(state, props),
coursePaceItemPosition: getCoursePaceItemPosition(state, props),
blackoutDates: getBlackoutDates(state),
planPublishing: getPlanPublishing(state),
pacePublishing: getPacePublishing(state),
showProjections: getShowProjections(state),
isStudentPlan: isStudentPlan(state)
isStudentPace: isStudentPace(state)
}
}
const ConnectedAssignmentRow = connect(mapStateToProps, {
setPlanItemDuration: actions.setPlanItemDuration
setPaceItemDuration: actions.setPaceItemDuration
})(AssignmentRow)
// This hack allows AssignmentRow to be rendered inside an InstUI Table.Body

View File

@ -19,31 +19,31 @@
import React from 'react'
import Module from './module'
import {PacePlan, ResponsiveSizes, StoreState} from '../../types'
import {CoursePace, ResponsiveSizes, StoreState} from '../../types'
import {connect} from 'react-redux'
import {getPacePlan, getIsCompressing} from '../../reducers/pace_plans'
import {getCoursePace, getIsCompressing} from '../../reducers/course_paces'
import {getResponsiveSize, getShowProjections} from '../../reducers/ui'
interface StoreProps {
readonly pacePlan: PacePlan
readonly coursePace: CoursePace
readonly responsiveSize: ResponsiveSizes
readonly showProjections: boolean
readonly isCompressing: boolean
}
export const PacePlanTable: React.FC<StoreProps> = ({
pacePlan,
export const CoursePaceTable: React.FC<StoreProps> = ({
coursePace,
responsiveSize,
showProjections,
isCompressing
}) => (
<>
{pacePlan.modules.map((module, index) => (
{coursePace.modules.map((module, index) => (
<Module
key={`module-${module.id}`}
index={index + 1}
module={module}
pacePlan={pacePlan}
coursePace={coursePace}
responsiveSize={responsiveSize}
showProjections={showProjections}
isCompressing={isCompressing}
@ -54,11 +54,11 @@ export const PacePlanTable: React.FC<StoreProps> = ({
const mapStateToProps = (state: StoreState) => {
return {
pacePlan: getPacePlan(state),
coursePace: getCoursePace(state),
responsiveSize: getResponsiveSize(state),
showProjections: getShowProjections(state),
isCompressing: getIsCompressing(state)
}
}
export default connect(mapStateToProps)(PacePlanTable)
export default connect(mapStateToProps)(CoursePaceTable)

View File

@ -18,7 +18,7 @@
import React, {useEffect, useRef, useState} from 'react'
// @ts-ignore: TS doesn't understand i18n scoped imports
import { useScope as useI18nScope } from '@canvas/i18n';
import {useScope as useI18nScope} from '@canvas/i18n'
import {ApplyTheme} from '@instructure/ui-themeable'
import {Button} from '@instructure/ui-buttons'
@ -31,9 +31,9 @@ import {Tooltip} from '@instructure/ui-tooltip'
import {View} from '@instructure/ui-view'
import AssignmentRow from './assignment_row'
import {Module as IModule, PacePlan, ResponsiveSizes} from '../../types'
import {Module as IModule, CoursePace, ResponsiveSizes} from '../../types'
const I18n = useI18nScope('pace_plans_module');
const I18n = useI18nScope('course_paces_module')
// Doing this to avoid TS2339 errors-- remove once we're on InstUI 8
const {Body, ColHeader, Head, Row} = Table as any
@ -41,7 +41,7 @@ const {Body, ColHeader, Head, Row} = Table as any
interface PassedProps {
readonly index: number
readonly module: IModule
readonly pacePlan: PacePlan
readonly coursePace: CoursePace
readonly responsiveSize: ResponsiveSizes
readonly showProjections: boolean
readonly isCompressing: boolean
@ -51,7 +51,7 @@ export const Module: React.FC<PassedProps> = props => {
const [actuallyExpanded, setActuallyExpanded] = useState(props.showProjections)
const [datesVisible, setDatesVisible] = useState(props.showProjections)
const wasExpanded = useRef(props.showProjections)
const isStudentPlan = props.pacePlan.context_type === 'Enrollment'
const isStudentPace = props.coursePace.context_type === 'Enrollment'
useEffect(() => {
if (!wasExpanded.current && props.showProjections) {
@ -100,17 +100,17 @@ export const Module: React.FC<PassedProps> = props => {
}
const assignmentRows: JSX.Element[] = props.module.items.map(item => {
// Scoping the key to the state of hard_end_dates and the pacePlan id ensures a full re-render of the row if either the hard_end_date
// status changes or the pace plan changes. This is necessary because the AssignmentRow maintains the duration in local state,
// Scoping the key to the state of hard_end_dates and the coursePace id ensures a full re-render of the row if either the hard_end_date
// status changes or the course pace changes. This is necessary because the AssignmentRow maintains the duration in local state,
// and applying updates with componentWillReceiveProps makes it buggy (because the Redux updates can be slow, causing changes to
// get reverted as you type).
const key = `${item.id}|${item.module_item_id}|${props.pacePlan.hard_end_dates}|${props.pacePlan.updated_at}`
const key = `${item.id}|${item.module_item_id}|${props.coursePace.hard_end_dates}|${props.coursePace.updated_at}`
return (
<AssignmentRow
key={key}
actuallyExpanded={actuallyExpanded}
datesVisible={datesVisible}
pacePlanItem={item}
coursePaceItem={item}
/>
)
})
@ -120,7 +120,7 @@ export const Module: React.FC<PassedProps> = props => {
return (
<View
as="div"
className={`pace-plans-module-table ${actuallyExpanded ? 'actually-expanded' : ''}`}
className={`course-paces-module-table ${actuallyExpanded ? 'actually-expanded' : ''}`}
margin="0 0 medium"
>
<ApplyTheme
@ -163,14 +163,14 @@ export const Module: React.FC<PassedProps> = props => {
</ColHeader>
<ColHeader
id={`module-${props.module.id}-days`}
width={isStudentPlan ? '5rem' : '7.5rem'}
width={isStudentPace ? '5rem' : '7.5rem'}
>
<Flex
as="div"
alignItems="end"
justifyItems={isStudentPlan ? 'center' : 'start'}
justifyItems={isStudentPace ? 'center' : 'start'}
padding={headerPadding}
margin={`0 0 0 ${isStudentPlan ? '0' : 'xx-small'}`}
margin={`0 0 0 ${isStudentPace ? '0' : 'xx-small'}`}
>
{I18n.t('Days')}
</Flex>

View File

@ -18,16 +18,16 @@
import {CategoryErrors, ResponsiveSizes, StoreState} from '../types'
import {getErrors, getResponsiveSize} from '../reducers/ui'
import {pacePlanActions} from '../actions/pace_plans'
import {coursePaceActions} from '../actions/course_paces'
import {connect} from 'react-redux'
import React, {createRef, ReactNode, RefObject} from 'react'
import {ExpandableErrorAlert} from '@canvas/alerts/react/ExpandableErrorAlert'
// @ts-ignore: TS doesn't understand i18n scoped imports
import { useScope as useI18nScope } from '@canvas/i18n';
import {useScope as useI18nScope} from '@canvas/i18n'
import {View} from '@instructure/ui-view'
import {Button} from '@instructure/ui-buttons'
const I18n = useI18nScope('pace_plans_errors');
const I18n = useI18nScope('course_paces_errors')
type StoreProps = {
errors: CategoryErrors
@ -35,12 +35,12 @@ type StoreProps = {
}
type DispatchProps = {
publishPlan: typeof pacePlanActions.publishPlan
publishPace: typeof coursePaceActions.publishPace
}
export type ErrorsProps = StoreProps & DispatchProps
export const Errors = ({errors, responsiveSize, publishPlan}: ErrorsProps) => {
export const Errors = ({errors, responsiveSize, publishPace}: ErrorsProps) => {
const alerts = Object.entries(errors).map(([category, error]) => {
const result: {
category: string
@ -58,9 +58,9 @@ export const Errors = ({errors, responsiveSize, publishPlan}: ErrorsProps) => {
result.contents = (
<>
<div ref={result.focusRef} tabIndex={-1}>
{I18n.t('There was an error publishing your pace plan.')}
{I18n.t('There was an error publishing your course pace.')}
</div>
<Button variant="primary" display="block" margin="x-small 0 0" onClick={publishPlan}>
<Button variant="primary" display="block" margin="x-small 0 0" onClick={publishPace}>
{I18n.t('Retry')}
</Button>
</>
@ -69,21 +69,21 @@ export const Errors = ({errors, responsiveSize, publishPlan}: ErrorsProps) => {
break
case 'resetToLastPublished':
result.contents = result.summary = I18n.t(
'There was an error resetting to the previous plan.'
'There was an error resetting to the previous pace.'
)
break
case 'loading':
result.contents = result.summary = I18n.t('There was an error loading the plan.')
result.contents = result.summary = I18n.t('There was an error loading the pace.')
break
case 'autosaving':
result.contents = result.summary = I18n.t('There was an error saving your changes.')
break
case 'relinkToParent':
result.contents = result.summary = I18n.t('There was an error linking plan.')
result.contents = result.summary = I18n.t('There was an error linking pace.')
break
case 'checkPublishStatus':
result.contents = result.summary = I18n.t(
'There was an error checking plan publishing status.'
'There was an error checking pace publishing status.'
)
break
default:
@ -123,5 +123,5 @@ const mapStateToProps = (state: StoreState): StoreProps => {
}
export default connect(mapStateToProps, {
publishPlan: pacePlanActions.publishPlan
publishPace: coursePaceActions.publishPace
})(Errors)

View File

@ -19,7 +19,7 @@
import React from 'react'
import {connect} from 'react-redux'
// @ts-ignore: TS doesn't understand i18n scoped imports
import { useScope as useI18nScope } from '@canvas/i18n';
import {useScope as useI18nScope} from '@canvas/i18n'
import {Button} from '@instructure/ui-buttons'
import {Flex} from '@instructure/ui-flex'
@ -28,53 +28,53 @@ import {Tooltip} from '@instructure/ui-tooltip'
import {StoreState} from '../types'
import {getAutoSaving, getShowLoadingOverlay} from '../reducers/ui'
import {pacePlanActions} from '../actions/pace_plans'
import {getPlanPublishing, getUnpublishedChangeCount, isStudentPlan} from '../reducers/pace_plans'
import {coursePaceActions} from '../actions/course_paces'
import {getPacePublishing, getUnpublishedChangeCount, isStudentPace} from '../reducers/course_paces'
const I18n = useI18nScope('pace_plans_footer');
const I18n = useI18nScope('course_paces_footer')
interface StoreProps {
readonly autoSaving: boolean
readonly planPublishing: boolean
readonly pacePublishing: boolean
readonly showLoadingOverlay: boolean
readonly studentPlan: boolean
readonly studentPace: boolean
readonly unpublishedChanges: boolean
}
interface DispatchProps {
publishPlan: typeof pacePlanActions.publishPlan
resetPlan: typeof pacePlanActions.resetPlan
publishPace: typeof coursePaceActions.publishPace
resetPace: typeof coursePaceActions.resetPace
}
type ComponentProps = StoreProps & DispatchProps
export const Footer: React.FC<ComponentProps> = ({
autoSaving,
planPublishing,
publishPlan,
resetPlan,
pacePublishing,
publishPace,
resetPace,
showLoadingOverlay,
studentPlan,
studentPace,
unpublishedChanges
}) => {
if (studentPlan) return null
if (studentPace) return null
const disabled = autoSaving || planPublishing || showLoadingOverlay || !unpublishedChanges
const disabled = autoSaving || pacePublishing || showLoadingOverlay || !unpublishedChanges
// This wrapper div attempts to roughly match the dimensions of the publish button
const publishLabel = planPublishing ? (
const publishLabel = pacePublishing ? (
<div style={{display: 'inline-block', margin: '-0.5rem 0.9rem'}}>
<Spinner size="x-small" renderTitle={I18n.t('Publishing plan...')} />
<Spinner size="x-small" renderTitle={I18n.t('Publishing pace...')} />
</div>
) : (
I18n.t('Publish')
)
let cancelTip, pubTip
if (autoSaving || planPublishing) {
if (autoSaving || pacePublishing) {
cancelTip = I18n.t('You cannot cancel while publishing')
pubTip = I18n.t('You cannot publish while publishing')
} else if (showLoadingOverlay) {
cancelTip = I18n.t('You cannot cancel while loading the plan')
pubTip = I18n.t('You cannot publish while loading the plan')
cancelTip = I18n.t('You cannot cancel while loading the pace')
pubTip = I18n.t('You cannot publish while loading the pace')
} else {
cancelTip = I18n.t('There are no pending changes to cancel')
pubTip = I18n.t('There are no pending changes to publish')
@ -82,12 +82,12 @@ export const Footer: React.FC<ComponentProps> = ({
return (
<Flex as="section" justifyItems="end">
<Tooltip renderTip={disabled && cancelTip} on={disabled ? ['hover', 'focus'] : []}>
<Button color="secondary" margin="0 small 0" onClick={() => disabled || resetPlan()}>
<Button color="secondary" margin="0 small 0" onClick={() => disabled || resetPace()}>
{I18n.t('Cancel')}
</Button>
</Tooltip>
<Tooltip renderTip={disabled && pubTip} on={disabled ? ['hover', 'focus'] : []}>
<Button color="primary" onClick={() => disabled || publishPlan()}>
<Button color="primary" onClick={() => disabled || publishPace()}>
{publishLabel}
</Button>
</Tooltip>
@ -98,14 +98,14 @@ export const Footer: React.FC<ComponentProps> = ({
const mapStateToProps = (state: StoreState): StoreProps => {
return {
autoSaving: getAutoSaving(state),
planPublishing: getPlanPublishing(state),
pacePublishing: getPacePublishing(state),
showLoadingOverlay: getShowLoadingOverlay(state),
studentPlan: isStudentPlan(state),
studentPace: isStudentPace(state),
unpublishedChanges: getUnpublishedChangeCount(state) !== 0
}
}
export default connect(mapStateToProps, {
publishPlan: pacePlanActions.publishPlan,
resetPlan: pacePlanActions.resetPlan
publishPace: coursePaceActions.publishPace,
resetPace: coursePaceActions.resetPace
})(Footer)

View File

@ -19,7 +19,7 @@
import React from 'react'
import {act, render, screen} from '@testing-library/react'
import {PlanPicker} from '../plan_picker'
import {PacePicker} from '../pace_picker'
import {
COURSE,
ENROLLMENT_1,
@ -28,7 +28,7 @@ import {
SORTED_SECTIONS
} from '../../../__tests__/fixtures'
const selectPlanContextFn = jest.fn()
const selectPaceContextFn = jest.fn()
const defaultProps = {
course: COURSE,
@ -36,7 +36,7 @@ const defaultProps = {
sections: SORTED_SECTIONS,
selectedContextId: COURSE.id,
selectedContextType: 'Course' as const,
setSelectedPlanContext: selectPlanContextFn,
setSelectedPaceContext: selectPaceContextFn,
changeCount: 0
}
@ -44,16 +44,16 @@ afterEach(() => {
jest.clearAllMocks()
})
describe('PlanPicker', () => {
it('renders a drop-down with all plan types represented', () => {
const {getByLabelText} = render(<PlanPicker {...defaultProps} />)
describe('PacePicker', () => {
it('renders a drop-down with all pace types represented', () => {
const {getByLabelText} = render(<PacePicker {...defaultProps} />)
const picker = getByLabelText('Pace Plans') as HTMLInputElement
const picker = getByLabelText('Course Paces') as HTMLInputElement
expect(picker).toBeInTheDocument()
expect(picker.value).toBe('Course Pace Plan')
expect(picker.value).toBe('Course Pace')
act(() => picker.click())
expect(screen.getByRole('menuitem', {name: 'Course Pace Plan'})).toBeInTheDocument()
expect(screen.getByRole('menuitem', {name: 'Course Pace'})).toBeInTheDocument()
// Commented out since we're not implementing this feature yet
// const sections = screen.getByRole('button', {name: 'Sections'})
@ -70,86 +70,84 @@ describe('PlanPicker', () => {
})
it('sets the selected context when an option is clicked', () => {
const {getByLabelText} = render(<PlanPicker {...defaultProps} />)
const picker = getByLabelText('Pace Plans') as HTMLInputElement
const {getByLabelText} = render(<PacePicker {...defaultProps} />)
const picker = getByLabelText('Course Paces') as HTMLInputElement
act(() => picker.click())
act(() => screen.getByRole('menuitem', {name: 'Course Pace Plan'}).click())
expect(selectPlanContextFn).toHaveBeenCalledWith('Course', COURSE.id)
act(() => screen.getByRole('menuitem', {name: 'Course Pace'}).click())
expect(selectPaceContextFn).toHaveBeenCalledWith('Course', COURSE.id)
// Commented out since we're not implementing this feature yet
// act(() => picker.click())
// act(() => screen.getByRole('button', {name: 'Sections'}).click())
// act(() => screen.getByRole('menuitem', {name: 'Hackers'}).click())
// expect(selectPlanContextFn).toHaveBeenCalledWith('Section', SECTION_1.id)
// expect(selectPaceContextFn).toHaveBeenCalledWith('Section', SECTION_1.id)
act(() => picker.click())
act(() => screen.getByRole('button', {name: 'Students'}).click())
act(() => screen.getByRole('menuitem', {name: 'Molly Millions'}).click())
expect(selectPlanContextFn).toHaveBeenCalledWith('Enrollment', ENROLLMENT_2.id)
expect(selectPaceContextFn).toHaveBeenCalledWith('Enrollment', ENROLLMENT_2.id)
})
it('displays the name of the currently selected context', () => {
const {getByLabelText} = render(
<PlanPicker
<PacePicker
{...defaultProps}
selectedContextType="Enrollment"
selectedContextId={ENROLLMENT_1.id}
/>
)
const picker = getByLabelText('Pace Plans') as HTMLInputElement
const picker = getByLabelText('Course Paces') as HTMLInputElement
expect(picker.value).toBe('Henry Dorsett Case')
})
describe('warning modal', () => {
it('is displayed if context changes with unpublished changes', () => {
const {getByText, getByLabelText} = render(<PlanPicker {...defaultProps} changeCount={1} />)
const picker = getByLabelText('Pace Plans') as HTMLInputElement
const {getByText, getByLabelText} = render(<PacePicker {...defaultProps} changeCount={1} />)
const picker = getByLabelText('Course Paces') as HTMLInputElement
act(() => picker.click())
act(() => screen.getByRole('button', {name: 'Students'}).click())
act(() => screen.getByRole('menuitem', {name: 'Molly Millions'}).click())
expect(
getByText(/You have unpublished changes to your Course Pace Plan./)
).toBeInTheDocument()
expect(getByText(/You have unpublished changes to your Course Pace./)).toBeInTheDocument()
})
it('aborts context change on cancel', () => {
const {getByDisplayValue, getByText, getByLabelText} = render(
<PlanPicker {...defaultProps} changeCount={1} />
<PacePicker {...defaultProps} changeCount={1} />
)
const picker = getByLabelText('Pace Plans') as HTMLInputElement
const picker = getByLabelText('Course Paces') as HTMLInputElement
act(() => picker.click())
act(() => screen.getByRole('button', {name: 'Students'}).click())
act(() => screen.getByRole('menuitem', {name: 'Molly Millions'}).click())
const cancelBtn = getByText('Keep Editing').closest('button')
act(() => cancelBtn?.click())
expect(getByDisplayValue('Course Pace Plan')).toBeInTheDocument()
expect(getByDisplayValue('Course Pace')).toBeInTheDocument()
})
it('cancels context change on "Keep Editing"', () => {
const {getByText, getByLabelText} = render(<PlanPicker {...defaultProps} changeCount={1} />)
const picker = getByLabelText('Pace Plans') as HTMLInputElement
const {getByText, getByLabelText} = render(<PacePicker {...defaultProps} changeCount={1} />)
const picker = getByLabelText('Course Paces') as HTMLInputElement
act(() => picker.click())
act(() => screen.getByRole('button', {name: 'Students'}).click())
act(() => screen.getByRole('menuitem', {name: 'Molly Millions'}).click())
const cancelBtn = getByText('Keep Editing').closest('button')
act(() => cancelBtn?.click())
expect(selectPlanContextFn).not.toHaveBeenCalledWith('Molly Millions', '98')
expect(selectPaceContextFn).not.toHaveBeenCalledWith('Molly Millions', '98')
})
it('changes context change on "Discard Changes"', () => {
const {getByText, getByLabelText} = render(<PlanPicker {...defaultProps} changeCount={1} />)
const picker = getByLabelText('Pace Plans') as HTMLInputElement
const {getByText, getByLabelText} = render(<PacePicker {...defaultProps} changeCount={1} />)
const picker = getByLabelText('Course Paces') as HTMLInputElement
act(() => picker.click())
act(() => screen.getByRole('button', {name: 'Students'}).click())
act(() => screen.getByRole('menuitem', {name: 'Molly Millions'}).click())
const confirmBtn = getByText('Discard Changes').closest('button')
act(() => confirmBtn?.click())
expect(selectPlanContextFn).toHaveBeenCalledWith('Enrollment', '25')
expect(selectPaceContextFn).toHaveBeenCalledWith('Enrollment', '25')
})
})
})

View File

@ -26,7 +26,7 @@ const toggleShowProjections = jest.fn()
const defaultProps = {
responsiveSize: 'large' as const,
showProjections: false,
studentPlan: false,
studentPace: false,
toggleShowProjections
}
@ -69,8 +69,8 @@ describe('ShowProjectionsButton', () => {
expect(queryByTestId('projections-text-button')).not.toBeInTheDocument()
})
it('does not render anything for student plans', () => {
const {queryByRole} = render(<ShowProjectionsButton {...defaultProps} studentPlan />)
it('does not render anything for student paces', () => {
const {queryByRole} = render(<ShowProjectionsButton {...defaultProps} studentPace />)
expect(queryByRole('button')).not.toBeInTheDocument()
})
})

View File

@ -20,7 +20,7 @@ import React from 'react'
import {Flex} from '@instructure/ui-flex'
import {View} from '@instructure/ui-view'
import PlanPicker from './plan_picker'
import PacePicker from './pace_picker'
import ProjectedDates from './projected_dates/projected_dates'
import Settings from './settings/settings'
import ShowProjectionsButton from './show_projections_button'
@ -35,7 +35,7 @@ const Header = (props: HeaderProps) => (
<View as="div" borderWidth="0 0 small 0" margin="0 0 medium" padding="0 0 small">
<Flex as="section" alignItems="end" wrapItems>
<Flex.Item margin="0 0 small">
<PlanPicker />
<PacePicker />
</Flex.Item>
<Flex.Item margin="0 0 small" shouldGrow>
<Settings margin="0 0 0 small" />

View File

@ -20,7 +20,7 @@ import React, {useState} from 'react'
import keycode from 'keycode'
import {connect} from 'react-redux'
// @ts-ignore: TS doesn't understand i18n scoped imports
import { useScope as useI18nScope } from '@canvas/i18n';
import {useScope as useI18nScope} from '@canvas/i18n'
import {ApplyTheme} from '@instructure/ui-themeable'
import {IconArrowOpenDownSolid, IconArrowOpenUpSolid} from '@instructure/ui-icons'
@ -31,16 +31,16 @@ import {View} from '@instructure/ui-view'
import UnpublishedWarningModal from './unpublished_warning_modal'
import {StoreState, Enrollment, Section, PlanContextTypes} from '../../types'
import {StoreState, Enrollment, Section, PaceContextTypes} from '../../types'
import {Course} from '../../shared/types'
import {getUnpublishedChangeCount} from '../../reducers/pace_plans'
import {getUnpublishedChangeCount} from '../../reducers/course_paces'
import {getSortedEnrollments} from '../../reducers/enrollments'
import {getSortedSections} from '../../reducers/sections'
import {getCourse} from '../../reducers/course'
import {actions} from '../../actions/ui'
import {getSelectedContextId, getSelectedContextType} from '../../reducers/ui'
const I18n = useI18nScope('pace_plans_plan_picker');
const I18n = useI18nScope('course_paces_pace_picker')
const PICKER_WIDTH = '20rem'
@ -52,37 +52,37 @@ interface StoreProps {
readonly enrollments: Enrollment[]
readonly sections: Section[]
readonly selectedContextId: string
readonly selectedContextType: PlanContextTypes
readonly selectedContextType: PaceContextTypes
readonly changeCount: number
}
interface DispatchProps {
readonly setSelectedPlanContext: typeof actions.setSelectedPlanContext
readonly setSelectedPaceContext: typeof actions.setSelectedPaceContext
}
type ComponentProps = StoreProps & DispatchProps
type ContextArgs = [PlanContextTypes, string]
type ContextArgs = [PaceContextTypes, string]
const createContextKey = (contextType: PlanContextTypes, contextId: string): string =>
const createContextKey = (contextType: PaceContextTypes, contextId: string): string =>
`${contextType}:${contextId}`
const parseContextKey = (key: string): ContextArgs => key.split(':') as ContextArgs
export const PlanPicker: React.FC<ComponentProps> = ({
export const PacePicker: React.FC<ComponentProps> = ({
changeCount,
course,
enrollments,
sections,
selectedContextType,
selectedContextId,
setSelectedPlanContext
setSelectedPaceContext
}) => {
const [open, setOpen] = useState(false)
const [pendingContext, setPendingContext] = useState('')
const hasChanges = changeCount > 0
let selectedContextName = I18n.t('Course Pace Plan')
let selectedContextName = I18n.t('Course Pace')
if (selectedContextType === 'Section') {
selectedContextName = sections.find(({id}) => id === selectedContextId)?.name
}
@ -105,7 +105,7 @@ export const PlanPicker: React.FC<ComponentProps> = ({
if (hasChanges) {
setPendingContext(value)
} else {
setSelectedPlanContext(...parseContextKey(value))
setSelectedPaceContext(...parseContextKey(value))
}
}
@ -119,12 +119,12 @@ export const PlanPicker: React.FC<ComponentProps> = ({
const trigger = (
<TextInput
renderLabel={I18n.t('Pace Plans')}
renderLabel={I18n.t('Course Paces')}
renderAfterInput={
open ? <IconArrowOpenUpSolid inline={false} /> : <IconArrowOpenDownSolid inline={false} />
}
value={selectedContextName}
data-testid="pace-plan-picker"
data-testid="course-pace-picker"
interaction="readonly"
role="button"
onKeyDown={handleKeyDown}
@ -141,7 +141,7 @@ export const PlanPicker: React.FC<ComponentProps> = ({
}}
>
<Menu
id="pace-plan-menu"
id="course-pace-menu"
placement="bottom"
withArrow={false}
trigger={trigger}
@ -149,14 +149,14 @@ export const PlanPicker: React.FC<ComponentProps> = ({
onToggle={setOpen}
onSelect={handleSelect}
>
{renderOption(createContextKey('Course', course.id), I18n.t('Course Pace Plan'))}
{renderOption(createContextKey('Course', course.id), I18n.t('Course Pace'))}
{/* Commenting out since we're not implementing sections yet */}
{/* <Menu id="pace-plan-menu" label={I18n.t('Sections')}> */}
{/* <Menu id="course-pace-menu" label={I18n.t('Sections')}> */}
{/* {sections.map(s => */}
{/* renderOption(createContextKey('Section', s.id), s.name, `section-${s.id}`) */}
{/* )} */}
{/* </Menu> */}
<Menu id="pace-plan-student-menu" label={I18n.t('Students')}>
<Menu id="course-pace-student-menu" label={I18n.t('Students')}>
{enrollments.map(e =>
renderOption(createContextKey('Enrollment', e.id), e.full_name, `student-${e.id}`)
)}
@ -168,7 +168,7 @@ export const PlanPicker: React.FC<ComponentProps> = ({
setPendingContext('')
}}
onConfirm={() => {
setSelectedPlanContext(...parseContextKey(pendingContext))
setSelectedPaceContext(...parseContextKey(pendingContext))
setPendingContext('')
}}
/>
@ -186,5 +186,5 @@ const mapStateToProps = (state: StoreState) => ({
})
export default connect(mapStateToProps, {
setSelectedPlanContext: actions.setSelectedPlanContext
})(PlanPicker)
setSelectedPaceContext: actions.setSelectedPaceContext
})(PacePicker)

View File

@ -19,15 +19,15 @@
import React from 'react'
import {act} from '@testing-library/react'
import {renderConnected} from '../../../../__tests__/utils'
import {COURSE, PRIMARY_PLAN} from '../../../../__tests__/fixtures'
import {COURSE, PRIMARY_PACE} from '../../../../__tests__/fixtures'
import {ProjectedDates} from '../projected_dates'
const defaultProps = {
pacePlan: PRIMARY_PLAN,
coursePace: PRIMARY_PACE,
assignments: 5,
planPublishing: false,
planWeeks: 8,
pacePublishing: false,
paceWeeks: 8,
projectedEndDate: '2021-12-01',
blackoutDates: [],
weekendsDisabled: false,
@ -76,7 +76,7 @@ describe('ProjectedDates', () => {
expect(specifiedEndDateCheckbox.checked).toBeTruthy()
})
it('shows the number of assignments and weeks in the plan when projections are shown', () => {
it('shows the number of assignments and weeks in the pace when projections are shown', () => {
const {getByText} = renderConnected(<ProjectedDates {...defaultProps} />)
expect(getByText('5 assignments')).toBeInTheDocument()
expect(getByText('8 weeks')).toBeInTheDocument()
@ -89,14 +89,14 @@ describe('ProjectedDates', () => {
})
it('shows error if start date is before course start date', () => {
const plan = {...defaultProps.pacePlan, start_date: '2021-08-01'}
const {getByText} = renderConnected(<ProjectedDates {...defaultProps} pacePlan={plan} />)
const pace = {...defaultProps.coursePace, start_date: '2021-08-01'}
const {getByText} = renderConnected(<ProjectedDates {...defaultProps} coursePace={pace} />)
expect(getByText('Date is before the course start date')).toBeInTheDocument()
})
it('shows error if start date is after specified end date', () => {
const plan = {...defaultProps.pacePlan, start_date: '2021-12-16'}
const {getByText} = renderConnected(<ProjectedDates {...defaultProps} pacePlan={plan} />)
const pace = {...defaultProps.coursePace, start_date: '2021-12-16'}
const {getByText} = renderConnected(<ProjectedDates {...defaultProps} coursePace={pace} />)
expect(getByText('Date is after the specified end date')).toBeInTheDocument()
})
@ -105,9 +105,9 @@ describe('ProjectedDates', () => {
end_at: {date: null, date_context: 'course'},
start_at: {date: null, date_context: 'course'}
}
const plan = {...defaultProps.pacePlan, hard_end_dates: false}
const pace = {...defaultProps.coursePace, hard_end_dates: false}
const {getAllByText} = renderConnected(
<ProjectedDates {...defaultProps} pacePlan={plan} projectedEndDate="2022-01-02" />
<ProjectedDates {...defaultProps} coursePace={pace} projectedEndDate="2022-01-02" />
)
expect(getAllByText('Hypothetical student enrollment date').length).toBeTruthy()
})
@ -115,18 +115,18 @@ describe('ProjectedDates', () => {
describe('end date messages', () => {
it('shows course end date text', () => {
const plan = {
...defaultProps.pacePlan,
const pace = {
...defaultProps.coursePace,
hard_end_dates: false,
start_sate: '2022-01-03',
end_date: undefined
}
const {getAllByText, getByTestId} = renderConnected(
<ProjectedDates {...defaultProps} pacePlan={plan} />
<ProjectedDates {...defaultProps} coursePace={pace} />
)
expect(getAllByText('Required by course end date').length).toBeTruthy()
// expect the course end date
expect(getByTestId('paceplan-date-text').textContent).toStrictEqual('December 31, 2021')
expect(getByTestId('coursepace-date-text').textContent).toStrictEqual('December 31, 2021')
})
it('shows term end date text', () => {
@ -134,18 +134,18 @@ describe('ProjectedDates', () => {
end_at: {date: COURSE.end_at, date_context: 'term'},
start_at: {date: COURSE.start_at, date_context: 'term'}
}
const plan = {
...defaultProps.pacePlan,
const pace = {
...defaultProps.coursePace,
hard_end_dates: false,
start_sate: '2022-01-03',
end_date: undefined
}
const {getAllByText, getByTestId} = renderConnected(
<ProjectedDates {...defaultProps} pacePlan={plan} />
<ProjectedDates {...defaultProps} coursePace={pace} />
)
expect(getAllByText('Required by term end date').length).toBeTruthy()
// expect the term end date
expect(getByTestId('paceplan-date-text').textContent).toStrictEqual('December 31, 2021')
expect(getByTestId('coursepace-date-text').textContent).toStrictEqual('December 31, 2021')
})
it('shows specified end date input', () => {
@ -153,41 +153,41 @@ describe('ProjectedDates', () => {
<ProjectedDates {...defaultProps} />
)
expect(getAllByText('Required by specified end date').length).toBeTruthy()
// expect the specified plan end date
// expect the specified pace end date
expect(getByDisplayValue('December 15, 2021')).toBeInTheDocument()
})
it('shows error if end date is before start date', () => {
const plan = {...defaultProps.pacePlan, start_date: '2021-08-01', end_date: '2021-07-31'}
const {getByText} = renderConnected(<ProjectedDates {...defaultProps} pacePlan={plan} />)
const pace = {...defaultProps.coursePace, start_date: '2021-08-01', end_date: '2021-07-31'}
const {getByText} = renderConnected(<ProjectedDates {...defaultProps} coursePace={pace} />)
expect(getByText('Date is before student enrollment date')).toBeInTheDocument()
})
it('shows error if end date is after course end date', () => {
const d = new Date(ENV.VALID_DATE_RANGE.end_at.date)
d.setDate(d.getDate() + 1)
const plan = {...defaultProps.pacePlan, end_date: d.toISOString()}
const {getByText} = renderConnected(<ProjectedDates {...defaultProps} pacePlan={plan} />)
const pace = {...defaultProps.coursePace, end_date: d.toISOString()}
const {getByText} = renderConnected(<ProjectedDates {...defaultProps} coursePace={pace} />)
expect(getByText('Date is after the course end date')).toBeInTheDocument()
})
it('shows open-ended plan text', () => {
it('shows open-ended pace text', () => {
window.ENV.VALID_DATE_RANGE = {
end_at: {date: null, date_context: 'course'},
start_at: {date: null, date_context: 'course'}
}
const plan = {
...defaultProps.pacePlan,
const pace = {
...defaultProps.coursePace,
hard_end_dates: false,
start_date: '2022-01-03',
end_date: undefined
}
const {getAllByText, getByTestId} = renderConnected(
<ProjectedDates {...defaultProps} pacePlan={plan} />
<ProjectedDates {...defaultProps} coursePace={pace} />
)
expect(getAllByText('Hypothetical end date').length).toBeTruthy()
// expect projectedEndDate in this case
expect(getByTestId('paceplan-date-text').textContent).toStrictEqual('December 1, 2021')
expect(getByTestId('coursepace-date-text').textContent).toStrictEqual('December 1, 2021')
})
})
@ -200,9 +200,9 @@ describe('ProjectedDates', () => {
})
it('calls compressDates if start date does not allow enough days before the specified end date', () => {
const pp = {...defaultProps.pacePlan}
const pp = {...defaultProps.coursePace}
pp.end_date = '2021-09-05'
renderConnected(<ProjectedDates {...defaultProps} pacePlan={pp} />)
renderConnected(<ProjectedDates {...defaultProps} coursePace={pp} />)
expect(defaultProps.uncompressDates).not.toHaveBeenCalled()
expect(defaultProps.compressDates).toHaveBeenCalled()
@ -210,31 +210,31 @@ describe('ProjectedDates', () => {
it('calls compressDates if start date does not allow enough days before the course end date', () => {
// the course ends on 2021-12-31
const pp = {...defaultProps.pacePlan}
const pp = {...defaultProps.coursePace}
pp.hard_end_dates = false
pp.end_date = undefined
const ped = '2022-01-01'
renderConnected(<ProjectedDates {...defaultProps} pacePlan={pp} projectedEndDate={ped} />)
renderConnected(<ProjectedDates {...defaultProps} coursePace={pp} projectedEndDate={ped} />)
expect(defaultProps.uncompressDates).not.toHaveBeenCalled()
expect(defaultProps.compressDates).toHaveBeenCalled()
})
it('calls compressDates when switching from course to student plan and squshing is necessary', () => {
it('calls compressDates when switching from course to student pace and squshing is necessary', () => {
// the course ends on 2021-12-31
const pp = {...defaultProps.pacePlan}
const pp = {...defaultProps.pacePace}
pp.hard_end_dates = false
pp.end_date = undefined
const ped = '2022-01-01'
const {rerender} = renderConnected(
<ProjectedDates {...defaultProps} pacePlan={pp} projectedEndDate={ped} />
<ProjectedDates {...defaultProps} pacePace={pp} projectedEndDate={ped} />
)
expect(defaultProps.compressDates).toHaveBeenCalledTimes(1)
pp.context_id = '1'
pp.context_type = 'Enrollment'
rerender(<ProjectedDates {...defaultProps} pacePlan={pp} projectedEndDate={ped} />)
rerender(<ProjectedDates {...defaultProps} pacePace={pp} projectedEndDate={ped} />)
expect(defaultProps.compressDates).toHaveBeenCalledTimes(2)
})
@ -251,8 +251,8 @@ describe('ProjectedDates', () => {
expect(defaultProps.toggleHardEndDates).toHaveBeenCalled()
})
it('is disabled while the plan is publishing', () => {
const {getByRole} = renderConnected(<ProjectedDates {...defaultProps} planPublishing />)
it('is disabled while the pace is publishing', () => {
const {getByRole} = renderConnected(<ProjectedDates {...defaultProps} pacePublishing />)
const hardEndDatesToggle = getByRole('checkbox', {
name: 'Require Completion by Specified End Date'
})

View File

@ -20,7 +20,7 @@ import React, {useCallback, useEffect} from 'react'
import {connect} from 'react-redux'
import moment from 'moment-timezone'
// @ts-ignore: TS doesn't understand i18n scoped imports
import { useScope as useI18nScope } from '@canvas/i18n';
import {useScope as useI18nScope} from '@canvas/i18n'
import {Checkbox} from '@instructure/ui-checkbox'
import {Flex} from '@instructure/ui-flex'
@ -29,31 +29,29 @@ import {Text} from '@instructure/ui-text'
import {View} from '@instructure/ui-view'
import {BlackoutDate, InputInteraction} from '../../../shared/types'
import {StoreState, PacePlan} from '../../../types'
import {pacePlanActions as actions} from '../../../actions/pace_plans'
import {StoreState, CoursePace} from '../../../types'
import {coursePaceActions as actions} from '../../../actions/course_paces'
import {
getPacePlan,
getPacePlanItems,
getPlanWeeks,
getCoursePace,
getCoursePaceItems,
getPaceWeeks,
getProjectedEndDate,
getExcludeWeekends,
getPlanPublishing
} from '../../../reducers/pace_plans'
getPacePublishing
} from '../../../reducers/course_paces'
import {getBlackoutDates} from '../../../shared/reducers/blackout_dates'
import {getShowProjections} from '../../../reducers/ui'
import PacePlanDateInput, {
PacePlansDateInputProps
} from '../../../shared/components/pace_plan_date_input'
import CoursePaceDateInput from '../../../shared/components/course_pace_date_input'
import SlideTransition from '../../../utils/slide_transition'
const I18n = useI18nScope('pace_plans_projected_dates');
const I18n = useI18nScope('course_paces_projected_dates')
interface StoreProps {
readonly pacePlan: PacePlan
readonly planPublishing: boolean
readonly coursePace: CoursePace
readonly pacePublishing: boolean
readonly projectedEndDate: string
readonly assignments: number
readonly planWeeks: number
readonly paceWeeks: number
readonly showProjections: boolean
readonly weekendsDisabled: boolean
readonly blackoutDates: BlackoutDate[]
@ -75,10 +73,10 @@ const enum WHICH_DATE {
}
export const ProjectedDates: React.FC<ComponentProps> = ({
pacePlan,
coursePace,
assignments,
planPublishing,
planWeeks,
pacePublishing,
paceWeeks,
projectedEndDate,
setStartDate,
setEndDate,
@ -89,7 +87,7 @@ export const ProjectedDates: React.FC<ComponentProps> = ({
blackoutDates,
weekendsDisabled
}) => {
// PacePlanDateInput.validateDay plays 2 roles
// CoursePaceDateInput.validateDay plays 2 roles
// 1. validate the new date in response to a date change
// 2. to determine valid dates in the INSTUI DateInput's popup calendar
// so we can only return an error message when the input date
@ -97,7 +95,7 @@ export const ProjectedDates: React.FC<ComponentProps> = ({
// See useEffect for that
const validateDate = useCallback(
(date: moment.Moment, which = WHICH_DATE.START) => {
if (which === WHICH_DATE.END && date.isBefore(pacePlan.start_date)) {
if (which === WHICH_DATE.END && date.isBefore(coursePace.start_date)) {
return I18n.t('Date is before student enrollment date')
}
@ -112,14 +110,14 @@ export const ProjectedDates: React.FC<ComponentProps> = ({
if (
which === WHICH_DATE.START &&
pacePlan.hard_end_dates &&
date.isAfter(moment(pacePlan.end_date), 'day')
coursePace.hard_end_dates &&
date.isAfter(moment(coursePace.end_date), 'day')
) {
return I18n.t('Date is after the specified end date')
}
if (
(which === WHICH_DATE.END || !pacePlan.hard_end_dates) &&
(which === WHICH_DATE.END || !coursePace.hard_end_dates) &&
ENV.VALID_DATE_RANGE.end_at.date &&
date.isAfter(moment(ENV.VALID_DATE_RANGE.end_at.date), 'day')
) {
@ -128,11 +126,11 @@ export const ProjectedDates: React.FC<ComponentProps> = ({
: I18n.t('Date is after the term end date')
}
},
[pacePlan.end_date, pacePlan.hard_end_dates, pacePlan.start_date]
[coursePace.end_date, coursePace.hard_end_dates, coursePace.start_date]
)
useEffect(() => {
if (moment(pacePlan.end_date).isBefore(moment(pacePlan.start_date), 'day')) {
if (moment(coursePace.end_date).isBefore(moment(coursePace.start_date), 'day')) {
// an invalid state
return
}
@ -142,7 +140,9 @@ export const ProjectedDates: React.FC<ComponentProps> = ({
// Since we may not have a new projected end date yet when validateDate
// is called, do it here.
if (
(pacePlan.hard_end_dates && pacePlan.end_date && projectedEndDate > pacePlan.end_date) ||
(coursePace.hard_end_dates &&
coursePace.end_date &&
projectedEndDate > coursePace.end_date) ||
moment(projectedEndDate) > moment(ENV.VALID_DATE_RANGE.end_at.date)
) {
compressDates()
@ -151,27 +151,27 @@ export const ProjectedDates: React.FC<ComponentProps> = ({
}
}, [
compressDates,
pacePlan.end_date,
pacePlan.hard_end_dates,
pacePlan.start_date,
pacePlan.context_id,
pacePlan.context_type,
coursePace.end_date,
coursePace.hard_end_dates,
coursePace.start_date,
coursePace.context_id,
coursePace.context_type,
projectedEndDate,
uncompressDates
])
const enrollmentType = pacePlan.context_type === 'Enrollment'
const enrollmentType = coursePace.context_type === 'Enrollment'
const startDateValue = pacePlan.start_date
const startDateValue = coursePace.start_date
const startHelpText = enrollmentType
? I18n.t('Student enrollment date')
: I18n.t('Hypothetical student enrollment date')
let endDateValue, endHelpText, endDateInteraction
if (pacePlan.hard_end_dates) {
endDateValue = pacePlan.end_date
if (coursePace.hard_end_dates) {
endDateValue = coursePace.end_date
endHelpText = I18n.t('Required by specified end date')
endDateInteraction = planPublishing ? 'disabled' : 'enabled'
endDateInteraction = pacePublishing ? 'disabled' : 'enabled'
} else if (ENV.VALID_DATE_RANGE.end_at.date) {
endDateValue = ENV.VALID_DATE_RANGE.end_at.date
if (ENV.VALID_DATE_RANGE.end_at.date_context === 'course') {
@ -189,7 +189,7 @@ export const ProjectedDates: React.FC<ComponentProps> = ({
let startInteraction: InputInteraction = 'enabled'
if (enrollmentType) {
startInteraction = 'readonly'
} else if (planPublishing) {
} else if (pacePublishing) {
startInteraction = 'disabled'
}
@ -198,7 +198,7 @@ export const ProjectedDates: React.FC<ComponentProps> = ({
<View as="div">
<Flex as="section" alignItems="center" margin="0" wrap="wrap">
<Flex.Item margin="0 medium medium 0" shouldGrow>
<PacePlanDateInput
<CoursePaceDateInput
label={I18n.t('Start Date')}
helpText={startHelpText}
interaction={startInteraction}
@ -211,8 +211,8 @@ export const ProjectedDates: React.FC<ComponentProps> = ({
/>
</Flex.Item>
<Flex.Item margin="0 medium medium 0" shouldGrow>
<PacePlanDateInput
id="pace-plans-required-end-date-input"
<CoursePaceDateInput
id="course-paces-required-end-date-input"
label={I18n.t('End Date')}
helpText={endHelpText}
interaction={endDateInteraction}
@ -228,8 +228,8 @@ export const ProjectedDates: React.FC<ComponentProps> = ({
<Checkbox
data-testid="require-end-date-toggle"
label={I18n.t('Require Completion by Specified End Date')}
checked={pacePlan.hard_end_dates}
disabled={planPublishing}
checked={coursePace.hard_end_dates}
disabled={pacePublishing}
onChange={() => {
toggleHardEndDates()
}}
@ -262,7 +262,7 @@ export const ProjectedDates: React.FC<ComponentProps> = ({
one: '1 week',
other: '%{count} weeks'
},
{count: planWeeks}
{count: paceWeeks}
)}
</i>
</Text>
@ -281,14 +281,14 @@ export const ProjectedDates: React.FC<ComponentProps> = ({
const mapStateToProps = (state: StoreState) => {
return {
pacePlan: getPacePlan(state),
assignments: getPacePlanItems(state).length,
planWeeks: getPlanWeeks(state),
coursePace: getCoursePace(state),
assignments: getCoursePaceItems(state).length,
paceWeeks: getPaceWeeks(state),
showProjections: getShowProjections(state),
projectedEndDate: getProjectedEndDate(state),
weekendsDisabled: getExcludeWeekends(state),
blackoutDates: getBlackoutDates(state),
planPublishing: getPlanPublishing(state)
pacePublishing: getPacePublishing(state)
}
}

View File

@ -17,15 +17,14 @@
*/
import React from 'react'
import moment from 'moment-timezone'
import {act, screen} from '@testing-library/react'
import {COURSE, PRIMARY_PLAN} from '../../../../__tests__/fixtures'
import {COURSE, PRIMARY_PACE} from '../../../../__tests__/fixtures'
import {renderConnected} from '../../../../__tests__/utils'
import {Settings} from '../settings'
const loadLatestPlanByContext = jest.fn()
const loadLatestPaceByContext = jest.fn()
const setEditingBlackoutDates = jest.fn()
const showLoadingOverlay = jest.fn()
const toggleExcludeWeekends = jest.fn()
@ -35,10 +34,10 @@ const setEndDate = jest.fn()
const defaultProps = {
course: COURSE,
courseId: COURSE.id,
excludeWeekends: PRIMARY_PLAN.exclude_weekends,
pacePlan: PRIMARY_PLAN,
planPublishing: false,
loadLatestPlanByContext,
excludeWeekends: PRIMARY_PACE.exclude_weekends,
coursePace: PRIMARY_PACE,
pacePublishing: false,
loadLatestPaceByContext,
setEditingBlackoutDates,
showLoadingOverlay,
toggleExcludeWeekends,
@ -81,7 +80,7 @@ describe('Settings', () => {
})
it('disables all settings while publishing', () => {
const {getByRole} = renderConnected(<Settings {...defaultProps} planPublishing />)
const {getByRole} = renderConnected(<Settings {...defaultProps} pacePublishing />)
const settingsButton = getByRole('button', {name: 'Modify Settings'})
act(() => settingsButton.click())

View File

@ -18,7 +18,7 @@
import React from 'react'
// @ts-ignore: TS doesn't understand i18n scoped imports
import { useScope as useI18nScope } from '@canvas/i18n';
import {useScope as useI18nScope} from '@canvas/i18n'
import moment from 'moment-timezone'
import {connect} from 'react-redux'
@ -32,31 +32,31 @@ import {View} from '@instructure/ui-view'
import {showFlashAlert} from '@canvas/alerts/react/FlashAlert'
import BlackoutDates from './blackout_dates'
import * as PacePlanApi from '../../../api/pace_plan_api'
import {StoreState, PacePlan} from '../../../types'
import * as CoursePaceApi from '../../../api/course_pace_api'
import {StoreState, CoursePace} from '../../../types'
import {Course} from '../../../shared/types'
import {getCourse} from '../../../reducers/course'
import {getExcludeWeekends, getPacePlan, getPlanPublishing} from '../../../reducers/pace_plans'
import {pacePlanActions} from '../../../actions/pace_plans'
import {getExcludeWeekends, getCoursePace, getPacePublishing} from '../../../reducers/course_paces'
import {coursePaceActions} from '../../../actions/course_paces'
import {actions as uiActions} from '../../../actions/ui'
import UpdateExistingPlansModal from '../../../shared/components/update_existing_plans_modal'
import UpdateExistingPacesModal from '../../../shared/components/update_existing_paces_modal'
const I18n = useI18nScope('pace_plans_settings');
const I18n = useI18nScope('course_paces_settings')
interface StoreProps {
readonly course: Course
readonly courseId: string
readonly excludeWeekends: boolean
readonly pacePlan: PacePlan
readonly planPublishing: boolean
readonly coursePace: CoursePace
readonly pacePublishing: boolean
}
interface DispatchProps {
readonly loadLatestPlanByContext: typeof pacePlanActions.loadLatestPlanByContext
readonly loadLatestPaceByContext: typeof coursePaceActions.loadLatestPaceByContext
readonly setEditingBlackoutDates: typeof uiActions.setEditingBlackoutDates
readonly setEndDate: typeof pacePlanActions.setEndDate
readonly setEndDate: typeof coursePaceActions.setEndDate
readonly showLoadingOverlay: typeof uiActions.showLoadingOverlay
readonly toggleExcludeWeekends: typeof pacePlanActions.toggleExcludeWeekends
readonly toggleExcludeWeekends: typeof coursePaceActions.toggleExcludeWeekends
}
interface PassedProps {
@ -69,7 +69,7 @@ interface LocalState {
readonly changeMadeToBlackoutDates: boolean
readonly showBlackoutDatesModal: boolean
readonly showSettingsPopover: boolean
readonly showUpdateExistingPlansModal: boolean
readonly showUpdateExistingPacesModal: boolean
}
export class Settings extends React.Component<ComponentProps, LocalState> {
@ -79,7 +79,7 @@ export class Settings extends React.Component<ComponentProps, LocalState> {
changeMadeToBlackoutDates: false,
showBlackoutDatesModal: false,
showSettingsPopover: false,
showUpdateExistingPlansModal: false
showUpdateExistingPacesModal: false
}
}
@ -90,13 +90,13 @@ export class Settings extends React.Component<ComponentProps, LocalState> {
this.props.setEditingBlackoutDates(true)
}
republishAllPlans = () => {
republishAllPaces = () => {
this.props.showLoadingOverlay('Publishing...')
PacePlanApi.republishAllPlansForCourse(this.props.courseId)
.then(this.onCloseUpdateExistingPlansModal)
CoursePaceApi.republishAllPacesForCourse(this.props.courseId)
.then(this.onCloseUpdateExistingPacesModal)
.catch(err => {
showFlashAlert({
message: I18n.t('Failed publishing plan'),
message: I18n.t('Failed publishing pace'),
err,
type: 'error'
})
@ -106,7 +106,7 @@ export class Settings extends React.Component<ComponentProps, LocalState> {
onCloseBlackoutDatesModal = () => {
this.setState(({changeMadeToBlackoutDates}) => ({
showBlackoutDatesModal: false,
showUpdateExistingPlansModal: changeMadeToBlackoutDates,
showUpdateExistingPacesModal: changeMadeToBlackoutDates,
changeMadeToBlackoutDates: false
}))
if (!this.state.changeMadeToBlackoutDates) {
@ -114,11 +114,11 @@ export class Settings extends React.Component<ComponentProps, LocalState> {
}
}
onCloseUpdateExistingPlansModal = async () => {
this.setState({showUpdateExistingPlansModal: false})
await this.props.loadLatestPlanByContext(
this.props.pacePlan.context_type,
this.props.pacePlan.context_id
onCloseUpdateExistingPacesModal = async () => {
this.setState({showUpdateExistingPacesModal: false})
await this.props.loadLatestPaceByContext(
this.props.coursePace.context_type,
this.props.coursePace.context_id
)
this.props.setEditingBlackoutDates(false)
}
@ -176,16 +176,16 @@ export class Settings extends React.Component<ComponentProps, LocalState> {
}
render() {
if (this.props.pacePlan.context_type === 'Enrollment') {
if (this.props.coursePace.context_type === 'Enrollment') {
return null
}
return (
<div style={{display: 'inline-block'}}>
{this.renderBlackoutDatesModal()}
<UpdateExistingPlansModal
open={this.state.showUpdateExistingPlansModal}
onDismiss={this.onCloseUpdateExistingPlansModal}
confirm={this.republishAllPlans}
<UpdateExistingPacesModal
open={this.state.showUpdateExistingPacesModal}
onDismiss={this.onCloseUpdateExistingPacesModal}
confirm={this.republishAllPaces}
/>
<Popover
on="click"
@ -206,7 +206,7 @@ export class Settings extends React.Component<ComponentProps, LocalState> {
data-testid="skip-weekends-toggle"
label={I18n.t('Skip Weekends')}
checked={this.props.excludeWeekends}
disabled={this.props.planPublishing}
disabled={this.props.pacePublishing}
onChange={() => this.props.toggleExcludeWeekends()}
/>
</View>
@ -235,15 +235,15 @@ const mapStateToProps = (state: StoreState): StoreProps => {
course: getCourse(state),
courseId: getCourse(state).id,
excludeWeekends: getExcludeWeekends(state),
pacePlan: getPacePlan(state),
planPublishing: getPlanPublishing(state)
coursePace: getCoursePace(state),
pacePublishing: getPacePublishing(state)
}
}
export default connect(mapStateToProps, {
loadLatestPlanByContext: pacePlanActions.loadLatestPlanByContext,
loadLatestPaceByContext: coursePaceActions.loadLatestPaceByContext,
setEditingBlackoutDates: uiActions.setEditingBlackoutDates,
setEndDate: pacePlanActions.setEndDate,
setEndDate: coursePaceActions.setEndDate,
showLoadingOverlay: uiActions.showLoadingOverlay,
toggleExcludeWeekends: pacePlanActions.toggleExcludeWeekends
toggleExcludeWeekends: coursePaceActions.toggleExcludeWeekends
})(Settings)

View File

@ -18,23 +18,23 @@
import React from 'react'
// @ts-ignore: TS doesn't understand i18n scoped imports
import { useScope as useI18nScope } from '@canvas/i18n';
import {useScope as useI18nScope} from '@canvas/i18n'
import {connect} from 'react-redux'
import {Button, IconButton} from '@instructure/ui-buttons'
import {IconEyeLine, IconOffLine} from '@instructure/ui-icons'
import {isStudentPlan} from '../../reducers/pace_plans'
import {isStudentPace} from '../../reducers/course_paces'
import {getResponsiveSize, getShowProjections} from '../../reducers/ui'
import {ResponsiveSizes, StoreState} from '../../types'
import {actions as uiActions} from '../../actions/ui'
const I18n = useI18nScope('pace_plans_show_projections_button');
const I18n = useI18nScope('course_paces_show_projections_button')
interface StoreProps {
readonly responsiveSize: ResponsiveSizes
readonly showProjections: boolean
readonly studentPlan: boolean
readonly studentPace: boolean
}
interface DispatchProps {
@ -51,11 +51,11 @@ export const ShowProjectionsButton: React.FC<ComponentProps> = ({
margin,
responsiveSize,
showProjections,
studentPlan,
studentPace,
toggleShowProjections
}) => {
// Don't show the projections button on student plans
if (studentPlan) return null
// Don't show the projections button on student paces
if (studentPace) return null
const buttonText = showProjections ? I18n.t('Hide Projections') : I18n.t('Show Projections')
const Icon = showProjections ? IconOffLine : IconEyeLine
@ -88,7 +88,7 @@ const mapStateToProps = (state: StoreState): StoreProps => {
return {
responsiveSize: getResponsiveSize(state),
showProjections: getShowProjections(state),
studentPlan: isStudentPlan(state)
studentPace: isStudentPace(state)
}
}

View File

@ -45,7 +45,7 @@ const UnpublishedWarningModal = ({open, onCancel, onConfirm}) => {
<View>
<Text>
{I18n.t(
'You have unpublished changes to your Course Pace Plan. Continuing will discard these changes.'
'You have unpublished changes to your Course Pace. Continuing will discard these changes.'
)}
</Text>
</View>

View File

@ -19,8 +19,8 @@
import React, {useEffect} from 'react'
import {CondensedButton} from '@instructure/ui-buttons'
// @ts-ignore: TS doesn't understand i18n scoped imports
import { useScope as useI18nScope } from '@canvas/i18n';
import {getPacePlan, getPlanPublishing, getUnpublishedChangeCount} from '../reducers/pace_plans'
import {useScope as useI18nScope} from '@canvas/i18n'
import {getCoursePace, getPacePublishing, getUnpublishedChangeCount} from '../reducers/course_paces'
import {StoreState} from '../types'
import {connect} from 'react-redux'
import {getCategoryError} from '../reducers/ui'
@ -29,12 +29,12 @@ import {PresentationContent} from '@instructure/ui-a11y-content'
import {Text} from '@instructure/ui-text'
import {View} from '@instructure/ui-view'
const I18n = useI18nScope('unpublished_changes_button_props');
const I18n = useI18nScope('unpublished_changes_button_props')
type StateProps = {
changeCount: number
planPublishing: boolean
newPlan: boolean
pacePublishing: boolean
newPace: boolean
publishError?: string
}
@ -69,9 +69,9 @@ const triggerBrowserWarning = (e: BeforeUnloadEvent) => {
export const UnpublishedChangesIndicator = ({
changeCount,
margin,
newPlan,
newPace,
onClick,
planPublishing,
pacePublishing,
publishError,
onUnpublishedNavigation = triggerBrowserWarning
}: UnpublishedChangesIndicatorProps) => {
@ -84,7 +84,7 @@ export const UnpublishedChangesIndicator = ({
}
}, [hasChanges, onUnpublishedNavigation])
if (newPlan) return null
if (newPace) return null
if (publishError !== undefined) {
return (
@ -94,12 +94,12 @@ export const UnpublishedChangesIndicator = ({
)
}
if (planPublishing) {
if (pacePublishing) {
return (
<View>
<Spinner size="x-small" margin="0 x-small 0" renderTitle={I18n.t('Publishing plan...')} />
<Spinner size="x-small" margin="0 x-small 0" renderTitle={I18n.t('Publishing pace...')} />
<PresentationContent>
<Text>{I18n.t('Publishing plan...')}</Text>
<Text>{I18n.t('Publishing pace...')}</Text>
</PresentationContent>
</View>
)
@ -118,8 +118,8 @@ export const UnpublishedChangesIndicator = ({
const mapStateToProps = (state: StoreState) => ({
changeCount: getUnpublishedChangeCount(state),
planPublishing: getPlanPublishing(state),
newPlan: !getPacePlan(state)?.id,
pacePublishing: getPacePublishing(state),
newPace: !getCoursePace(state)?.id,
publishError: getCategoryError(state, 'publish')
})

View File

@ -16,16 +16,19 @@
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import {PacePlanItem, Module} from '../types'
import {Constants as PacePlanItemConstants, PacePlanItemAction} from '../actions/pace_plan_items'
import {CoursePaceItem, Module} from '../types'
import {
Constants as CoursePaceItemConstants,
CoursePaceItemAction
} from '../actions/course_pace_items'
/* Reducers */
const itemsReducer = (state: PacePlanItem[], action: PacePlanItemAction): PacePlanItem[] => {
const itemsReducer = (state: CoursePaceItem[], action: CoursePaceItemAction): CoursePaceItem[] => {
switch (action.type) {
case PacePlanItemConstants.SET_PLAN_ITEM_DURATION:
case CoursePaceItemConstants.SET_PACE_ITEM_DURATION:
return state.map(item => {
return item.module_item_id === action.payload.planItemId
return item.module_item_id === action.payload.paceItemId
? {...item, duration: action.payload.duration}
: item
})
@ -36,7 +39,7 @@ const itemsReducer = (state: PacePlanItem[], action: PacePlanItemAction): PacePl
// Modules are read-only currently, so this is just deferring to the itemsReducer for
// each module's item.
export default (state: Module[], action: PacePlanItemAction): Module[] => {
export default (state: Module[], action: CoursePaceItemAction): Module[] => {
if (!state) return state
return state.map(module => ({...module, items: itemsReducer(module.items, action)}))
}

View File

@ -20,17 +20,17 @@ import {createSelector, createSelectorCreator, defaultMemoize} from 'reselect'
import {deepEqual} from '@instructure/ui-utils'
import moment from 'moment-timezone'
import {Constants as PacePlanConstants, PacePlanAction} from '../actions/pace_plans'
import pacePlanItemsReducer from './pace_plan_items'
import {Constants as CoursePaceConstants, CoursePaceAction} from '../actions/course_paces'
import coursePaceItemsReducer from './course_pace_items'
import * as DateHelpers from '../utils/date_stuff/date_helpers'
import * as PlanDueDatesCalculator from '../utils/date_stuff/plan_due_dates_calculator'
import * as PaceDueDatesCalculator from '../utils/date_stuff/pace_due_dates_calculator'
import {
PacePlansState,
PacePlan,
PlanContextTypes,
CoursePacesState,
CoursePace,
PaceContextTypes,
StoreState,
PacePlanItem,
PacePlanItemDueDates,
CoursePaceItem,
CoursePaceItemDueDates,
Enrollment,
Sections,
Enrollments,
@ -38,24 +38,24 @@ import {
Module
} from '../types'
import {BlackoutDate, Course} from '../shared/types'
import {Constants as UIConstants, SetSelectedPlanType} from '../actions/ui'
import {Constants as UIConstants, SetSelectedPaceType} from '../actions/ui'
import {getCourse} from './course'
import {getEnrollments} from './enrollments'
import {getSections} from './sections'
import {getBlackoutDates} from '../shared/reducers/blackout_dates'
import {Change, summarizeChanges} from '../utils/change_tracking'
const initialProgress = window.ENV.PACE_PLAN_PROGRESS
const initialProgress = window.ENV.COURSE_PACE_PROGRESS
export const initialState: PacePlansState = ({
...window.ENV.PACE_PLAN,
export const initialState: CoursePacesState = ({
...window.ENV.COURSE_PACE,
course: window.ENV.COURSE,
originalPlan: window.ENV.PACE_PLAN,
originalPace: window.ENV.COURSE_PACE,
publishingProgress: initialProgress
} || {}) as PacePlansState
} || {}) as CoursePacesState
const getModuleItems = (modules: Module[]) =>
([] as PacePlanItem[]).concat(...modules.map(m => m.items))
([] as CoursePaceItem[]).concat(...modules.map(m => m.items))
/* Selectors */
@ -68,61 +68,62 @@ const getModuleItems = (modules: Module[]) =>
// calculations.
const createDeepEqualSelector = createSelectorCreator(defaultMemoize, deepEqual)
export const getExcludeWeekends = (state: StoreState): boolean => state.pacePlan.exclude_weekends
export const getOriginalPlan = (state: StoreState) => state.pacePlan.originalPlan
export const getPacePlan = (state: StoreState): PacePlansState => state.pacePlan
export const getPacePlanModules = (state: StoreState) => state.pacePlan.modules
export const getPacePlanType = (state: StoreState): PlanContextTypes => state.pacePlan.context_type
export const getHardEndDates = (state: StoreState): boolean => state.pacePlan.hard_end_dates
export const getPlanPublishing = (state: StoreState): boolean => {
const progress = state.pacePlan.publishingProgress
export const getExcludeWeekends = (state: StoreState): boolean => state.coursePace.exclude_weekends
export const getOriginalPace = (state: StoreState) => state.coursePace.originalPace
export const getCoursePace = (state: StoreState): CoursePacesState => state.coursePace
export const getCoursePaceModules = (state: StoreState) => state.coursePace.modules
export const getCoursePaceType = (state: StoreState): PaceContextTypes =>
state.coursePace.context_type
export const getHardEndDates = (state: StoreState): boolean => state.coursePace.hard_end_dates
export const getPacePublishing = (state: StoreState): boolean => {
const progress = state.coursePace.publishingProgress
if (!progress) return false
return !!progress.id && ['queued', 'running'].includes(progress.workflow_state)
}
export const getPublishingError = (state: StoreState): string | undefined => {
const progress = state.pacePlan.publishingProgress
const progress = state.coursePace.publishingProgress
if (!progress || progress.workflow_state !== 'failed') return undefined
return progress.message
}
export const getEndDate = (state: StoreState): string | undefined => state.pacePlan.end_date
export const isStudentPlan = (state: StoreState) => state.pacePlan.context_type === 'Enrollment'
export const getIsPlanCompressed = (state: StoreState): boolean =>
!!state.pacePlan.compressed_due_dates
export const getPlanCompressedDates = (state: StoreState): PacePlanItemDueDates | undefined =>
state.pacePlan.compressed_due_dates
export const getEndDate = (state: StoreState): string | undefined => state.coursePace.end_date
export const isStudentPace = (state: StoreState) => state.coursePace.context_type === 'Enrollment'
export const getIsPaceCompressed = (state: StoreState): boolean =>
!!state.coursePace.compressed_due_dates
export const getPaceCompressedDates = (state: StoreState): CoursePaceItemDueDates | undefined =>
state.coursePace.compressed_due_dates
export const getPacePlanItems = createSelector(getPacePlanModules, getModuleItems)
export const getCoursePaceItems = createSelector(getCoursePaceModules, getModuleItems)
export const getSettingChanges = createDeepEqualSelector(
getExcludeWeekends,
getHardEndDates,
getOriginalPlan,
getOriginalPace,
getEndDate,
(excludeWeekends, hardEndDates, originalPlan, endDate) => {
(excludeWeekends, hardEndDates, originalPace, endDate) => {
const changes: Change[] = []
if (excludeWeekends !== originalPlan.exclude_weekends)
if (excludeWeekends !== originalPace.exclude_weekends)
changes.push({
id: 'exclude_weekends',
oldValue: originalPlan.exclude_weekends,
oldValue: originalPace.exclude_weekends,
newValue: excludeWeekends
})
// we want to validate that if hardEndDates is true that the endDate is a valid date
if (
hardEndDates !== originalPlan.hard_end_dates &&
hardEndDates !== originalPace.hard_end_dates &&
(!hardEndDates || (hardEndDates && endDate))
)
changes.push({
id: 'hard_end_dates',
oldValue: originalPlan.hard_end_dates,
oldValue: originalPace.hard_end_dates,
newValue: hardEndDates
})
if (endDate && endDate !== originalPlan.end_date)
if (endDate && endDate !== originalPace.end_date)
changes.push({
id: 'end_date',
oldValue: originalPlan.end_date,
oldValue: originalPace.end_date,
newValue: endDate
})
@ -130,16 +131,16 @@ export const getSettingChanges = createDeepEqualSelector(
}
)
export const getPacePlanItemChanges = createDeepEqualSelector(
getPacePlanItems,
getOriginalPlan,
(pacePlanItems, originalPlan) => {
const originalItems = getModuleItems(originalPlan.modules)
const changes: Change<PacePlanItem>[] = []
export const getCoursePaceItemChanges = createDeepEqualSelector(
getCoursePaceItems,
getOriginalPace,
(coursePaceItems, originalPace) => {
const originalItems = getModuleItems(originalPace.modules)
const changes: Change<CoursePaceItem>[] = []
for (const i in pacePlanItems) {
for (const i in coursePaceItems) {
const originalItem = originalItems[i]
const currentItem = pacePlanItems[i]
const currentItem = coursePaceItems[i]
if (originalItem.duration !== currentItem.duration) {
changes.push({id: originalItem.id, oldValue: originalItem, newValue: currentItem})
@ -152,25 +153,25 @@ export const getPacePlanItemChanges = createDeepEqualSelector(
export const getUnpublishedChangeCount = createSelector(
getSettingChanges,
getPacePlanItemChanges,
(settingChanges, pacePlanItemChanges) => settingChanges.length + pacePlanItemChanges.length
getCoursePaceItemChanges,
(settingChanges, coursePaceItemChanges) => settingChanges.length + coursePaceItemChanges.length
)
export const getSummarizedChanges = createSelector(
getSettingChanges,
getPacePlanItemChanges,
getCoursePaceItemChanges,
summarizeChanges
)
export const getPacePlanItemPosition = createDeepEqualSelector(
getPacePlanItems,
(_, props): PacePlanItem => props.pacePlanItem,
(pacePlanItems: PacePlanItem[], pacePlanItem: PacePlanItem): number => {
export const getCoursePaceItemPosition = createDeepEqualSelector(
getCoursePaceItems,
(_, props): CoursePaceItem => props.coursePaceItem,
(coursePaceItems: CoursePaceItem[], coursePaceItem: CoursePaceItem): number => {
let position = 0
for (let i = 0; i < pacePlanItems.length; i++) {
for (let i = 0; i < coursePaceItems.length; i++) {
position = i
if (pacePlanItems[i].id === pacePlanItem.id) {
if (coursePaceItems[i].id === coursePaceItem.id) {
break
}
}
@ -179,71 +180,71 @@ export const getPacePlanItemPosition = createDeepEqualSelector(
}
)
export const getPacePlanDurationTotal = createDeepEqualSelector(
getPacePlanItems,
(pacePlanItems: PacePlanItem[]): number =>
pacePlanItems.reduce((total, item) => total + item.duration, 0)
export const getCoursePaceDurationTotal = createDeepEqualSelector(
getCoursePaceItems,
(coursePaceItems: CoursePaceItem[]): number =>
coursePaceItems.reduce((total, item) => total + item.duration, 0)
)
export const getStartDate = createDeepEqualSelector(
getPacePlan,
getOriginalPlan,
(pacePlan: PacePlan): string | undefined => {
return pacePlan.start_date
getCoursePace,
getOriginalPace,
(coursePace: CoursePace): string | undefined => {
return coursePace.start_date
}
)
// Wrapping this in a selector makes sure the result is memoized
export const getDueDates = createDeepEqualSelector(
getPacePlanItems,
getCoursePaceItems,
getExcludeWeekends,
getBlackoutDates,
getStartDate,
getPlanCompressedDates,
getPaceCompressedDates,
(
items: PacePlanItem[],
items: CoursePaceItem[],
excludeWeekends: boolean,
blackoutDates: BlackoutDate[],
startDate?: string,
compressedDueDates?: PacePlanItemDueDates
): PacePlanItemDueDates => {
compressedDueDates?: CoursePaceItemDueDates
): CoursePaceItemDueDates => {
if (compressedDueDates) {
return compressedDueDates
}
return PlanDueDatesCalculator.getDueDates(items, excludeWeekends, blackoutDates, startDate)
return PaceDueDatesCalculator.getDueDates(items, excludeWeekends, blackoutDates, startDate)
}
)
export const getUncompressedDueDates = createDeepEqualSelector(
getPacePlanItems,
getCoursePaceItems,
getExcludeWeekends,
getBlackoutDates,
getStartDate,
(
items: PacePlanItem[],
items: CoursePaceItem[],
excludeWeekends: boolean,
blackoutDates: BlackoutDate[],
startDate?: string
): PacePlanItemDueDates => {
return PlanDueDatesCalculator.getDueDates(items, excludeWeekends, blackoutDates, startDate)
): CoursePaceItemDueDates => {
return PaceDueDatesCalculator.getDueDates(items, excludeWeekends, blackoutDates, startDate)
}
)
export const getDueDate = createSelector(
getDueDates,
(_, props): PacePlanItem => props.pacePlanItem,
(dueDates: PacePlanItemDueDates, pacePlanItem: PacePlanItem): string => {
return dueDates[pacePlanItem.module_item_id]
(_, props): CoursePaceItem => props.coursePaceItem,
(dueDates: CoursePaceItemDueDates, coursePaceItem: CoursePaceItem): string => {
return dueDates[coursePaceItem.module_item_id]
}
)
export const getProjectedEndDate = createDeepEqualSelector(
getUncompressedDueDates,
getPacePlanItems,
getCoursePaceItems,
getStartDate,
(
dueDates: PacePlanItemDueDates,
items: PacePlanItem[],
dueDates: CoursePaceItemDueDates,
items: CoursePaceItem[],
startDate?: string
): string | undefined => {
if (!startDate || !Object.keys(dueDates) || !items.length) return startDate
@ -254,30 +255,30 @@ export const getProjectedEndDate = createDeepEqualSelector(
}
)
export const getPlanDays = createDeepEqualSelector(
getPacePlan,
export const getPaceDays = createDeepEqualSelector(
getCoursePace,
getExcludeWeekends,
getBlackoutDates,
getProjectedEndDate,
(
pacePlan: PacePlan,
coursePace: CoursePace,
excludeWeekends: boolean,
blackoutDates: BlackoutDate[],
projectedEndDate?: string
): number => {
if (!pacePlan.start_date) return 0
if (!coursePace.start_date) return 0
const endDate = pacePlan.end_date || projectedEndDate || pacePlan.start_date
return DateHelpers.daysBetween(pacePlan.start_date, endDate, excludeWeekends, blackoutDates)
const endDate = coursePace.end_date || projectedEndDate || coursePace.start_date
return DateHelpers.daysBetween(coursePace.start_date, endDate, excludeWeekends, blackoutDates)
}
)
export const getPlanWeeks = createSelector(
getPlanDays,
export const getPaceWeeks = createSelector(
getPaceDays,
getExcludeWeekends,
(planDays: number, excludeWeekends: boolean): number => {
(paceDays: number, excludeWeekends: boolean): number => {
const weekLength = excludeWeekends ? 5 : 7
return Math.floor(planDays / weekLength)
return Math.floor(paceDays / weekLength)
}
)
@ -288,22 +289,22 @@ export const getWeekLength = createSelector(
}
)
export const getActivePlanContext = createSelector(
getPacePlan,
export const getActivePaceContext = createSelector(
getCoursePace,
getCourse,
getEnrollments,
getSections,
(
activePacePlan: PacePlan,
activeCoursePace: CoursePace,
course: Course,
enrollments: Enrollments,
sections: Sections
): Course | Section | Enrollment => {
switch (activePacePlan.context_type) {
switch (activeCoursePace.context_type) {
case 'Section':
return sections[activePacePlan.context_id]
return sections[activeCoursePace.context_id]
case 'Enrollment':
return enrollments[activePacePlan.context_id]
return enrollments[activeCoursePace.context_id]
default:
return course
}
@ -311,15 +312,15 @@ export const getActivePlanContext = createSelector(
)
export const getIsCompressing = createSelector(
getPacePlan,
getCoursePace,
getHardEndDates,
getProjectedEndDate,
(
pacePlan: PacePlansState,
coursePace: CoursePacesState,
hardEndDates: boolean,
projectedEndDate: string | undefined
): boolean => {
const realEnd = hardEndDates ? pacePlan.end_date : ENV.VALID_DATE_RANGE.end_at.date
const realEnd = hardEndDates ? coursePace.end_date : ENV.VALID_DATE_RANGE.end_at.date
return !!projectedEndDate && projectedEndDate > realEnd
}
)
@ -328,39 +329,39 @@ export const getIsCompressing = createSelector(
export default (
state = initialState,
action: PacePlanAction | SetSelectedPlanType
): PacePlansState => {
action: CoursePaceAction | SetSelectedPaceType
): CoursePacesState => {
switch (action.type) {
case PacePlanConstants.SET_PACE_PLAN:
case CoursePaceConstants.SET_COURSE_PACE:
return {...state, ...action.payload}
case PacePlanConstants.SET_START_DATE:
case CoursePaceConstants.SET_START_DATE:
return {...state, start_date: DateHelpers.formatDate(action.payload)}
case PacePlanConstants.SET_END_DATE:
case CoursePaceConstants.SET_END_DATE:
return {
...state,
end_date: action.payload ? DateHelpers.formatDate(action.payload) : undefined
}
case PacePlanConstants.PLAN_CREATED:
// Could use a *REFACTOR* to better handle new plans and updating the ui properly
case CoursePaceConstants.PACE_CREATED:
// Could use a *REFACTOR* to better handle new paces and updating the ui properly
return {
...state,
id: action.payload.id,
modules: action.payload.modules,
published_at: action.payload.published_at
}
case UIConstants.SET_SELECTED_PLAN_CONTEXT:
return {...action.payload.newSelectedPlan, originalPlan: action.payload.newSelectedPlan}
case PacePlanConstants.TOGGLE_EXCLUDE_WEEKENDS:
case UIConstants.SET_SELECTED_PACE_CONTEXT:
return {...action.payload.newSelectedPace, originalPace: action.payload.newSelectedPace}
case CoursePaceConstants.TOGGLE_EXCLUDE_WEEKENDS:
if (state.exclude_weekends) {
return {...state, exclude_weekends: false}
} else {
return {...state, exclude_weekends: true}
}
case PacePlanConstants.TOGGLE_HARD_END_DATES:
case CoursePaceConstants.TOGGLE_HARD_END_DATES:
if (state.hard_end_dates) {
return {...state, hard_end_dates: false, end_date: ''}
} else {
let endDate = state.originalPlan.end_date
let endDate = state.originalPace.end_date
if (!endDate) {
if (state.course.end_at) {
endDate = state.course.end_at
@ -371,22 +372,22 @@ export default (
return {...state, hard_end_dates: true, end_date: endDate}
}
case PacePlanConstants.RESET_PLAN:
case CoursePaceConstants.RESET_PACE:
return {
...state.originalPlan,
originalPlan: state.originalPlan,
...state.originalPace,
originalPace: state.originalPace,
updated_at: new Date().toISOString() // kicks react into re-rendering the assignment_rows
}
case PacePlanConstants.SET_PROGRESS:
case CoursePaceConstants.SET_PROGRESS:
return {...state, publishingProgress: action.payload}
case PacePlanConstants.SET_COMPRESSED_ITEM_DATES: {
case CoursePaceConstants.SET_COMPRESSED_ITEM_DATES: {
const newState = {...state}
newState.compressed_due_dates = action.payload
return newState
}
case PacePlanConstants.UNCOMPRESS_DATES:
case CoursePaceConstants.UNCOMPRESS_DATES:
return {...state, compressed_due_dates: undefined}
default:
return {...state, modules: pacePlanItemsReducer(state.modules, action)}
return {...state, modules: coursePaceItemsReducer(state.modules, action)}
}
}

View File

@ -19,7 +19,7 @@
import {combineReducers} from 'redux'
import {StoreState} from '../types'
import pacePlansReducer from './pace_plans'
import coursePacesReducer from './course_paces'
import {courseReducer} from './course'
import {sectionsReducer} from './sections'
import {enrollmentsReducer} from './enrollments'
@ -27,7 +27,7 @@ import {blackoutDatesReducer} from '../shared/reducers/blackout_dates'
import uiReducer from './ui'
export default combineReducers<StoreState>({
pacePlan: pacePlansReducer,
coursePace: coursePacesReducer,
enrollments: enrollmentsReducer,
sections: sectionsReducer,
ui: uiReducer,

Some files were not shown because too many files have changed in this diff Show More