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:
parent
af20e417dd
commit
6ba814bed7
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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 ||
|
|
@ -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) }
|
|
@ -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])
|
||||
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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"
|
||||
|
|
|
@ -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"))
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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))
|
||||
|
|
|
@ -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"/>
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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" }
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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",
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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?
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -31,5 +31,5 @@ const CoursePage: React.FC = () => (
|
|||
)
|
||||
|
||||
ready(() => {
|
||||
ReactDOM.render(<CoursePage />, document.getElementById('pace_plans'))
|
||||
ReactDOM.render(<CoursePage />, document.getElementById('course_paces'))
|
||||
})
|
|
@ -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
|
||||
}
|
|
@ -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([
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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)
|
|
@ -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()
|
||||
})
|
||||
})
|
|
@ -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()
|
||||
})
|
||||
|
|
@ -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()
|
||||
})
|
||||
})
|
|
@ -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
|
|
@ -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', {
|
|
@ -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()
|
|
@ -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
|
|
@ -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)
|
|
@ -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>
|
|
@ -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)
|
|
@ -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)
|
|
@ -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')
|
||||
})
|
||||
})
|
||||
})
|
|
@ -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()
|
||||
})
|
||||
})
|
|
@ -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" />
|
|
@ -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)
|
|
@ -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'
|
||||
})
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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())
|
||||
|
|
@ -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)
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
|
@ -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')
|
||||
})
|
||||
|
|
@ -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)}))
|
||||
}
|
|
@ -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)}
|
||||
}
|
||||
}
|
|
@ -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
Loading…
Reference in New Issue