Update pace plans header for new design

- remove the Show Projections button
- show static text for dates
- move "x Assignments | y weeks" to its new location

refs LS-3008
flag=course_pacing

test plan:
  - in a course in the default term  with a pace plan
    (doesn't have to be the default term, but know that
     you can't have a specified term with missing start or
     end dates, so those tests have to be run for the course
     dates only)

  *** for the course plan ***
  - in settings, set participation to Term or Course but leave
    start and end dates empty
  > expect today as the start date
  > expect the last assignment's due date as the end date

  - give the course or term a start date
  > expect the course or term start to be the start date
  . expect the last assignment's due date as the end date

  - give the course a start and end date
  > expect the course or term start to be the start date
  > expet the term or course to be the end date

  - give the course a end date only
  > expect today as the start date
  > expect a Course or Term completion as the end date

  - switch to a student plan
  - redo the above
  > expect the start date to always be
    the student enrollment date
  > expect the end date to always be determined by
    course pacing

Change-Id: I11cbd8c460dffdda94d161c62d8e97c468db442e
Reviewed-on: https://gerrit.instructure.com/c/canvas-lms/+/287160
Tested-by: Service Cloud Jenkins <svc.cloudjenkins@instructure.com>
Reviewed-by: Eric Saupe <eric.saupe@instructure.com>
QA-Review: Eric Saupe <eric.saupe@instructure.com>
Product-Review: Jody Sailor
This commit is contained in:
Ed Schiebel 2022-03-15 17:05:21 -04:00
parent d7ea5464a6
commit 426a6e97d9
15 changed files with 729 additions and 175 deletions

View File

@ -205,17 +205,42 @@ class CoursePace < ActiveRecord::Base
end
end
def start_date
def start_date(with_context: false)
valid_date_range = CourseDateRange.new(course)
student_enrollment = course.student_enrollments.find_by(user_id: user_id) if user_id
# always put course pace dates in the course time zone
Time.at(
(
student_enrollment&.start_at || course_section&.start_at || course.start_at ||
course.enrollment_term&.start_at ||
course.created_at
).to_i,
in: course.time_zone
).to_date
# always put pace plan dates in the course time zone
date = student_enrollment&.start_at || course_section&.start_at || valid_date_range.start_at[:date]
date = Time.at(date.to_time.to_i, in: course.time_zone).to_date if date
today = Time.at(Time.now.to_i, in: course.time_zone).to_date
if with_context
if date
context = (student_enrollment && "user") || (course_section&.start_at && "section") || (date && valid_date_range.start_at[:date_context])
else
date = today
context = "hypothetical"
end
{ start_date: date, start_date_context: context }
else
date || today
end
end
def end_date(with_context: false)
valid_date_range = CourseDateRange.new(course)
date = (hard_end_dates && self[:end_date]) || valid_date_range.end_at[:date]
date = Time.at(date.to_time.to_i, in: course.time_zone).to_date if date
if with_context
context = if date
hard_end_dates ? "hard" : valid_date_range.end_at[:date_context]
else
"hypothetical"
end
{ end_date: date, end_date_context: context }
else
date
end
end
end

View File

@ -33,8 +33,6 @@ class CoursePacePresenter
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,
@ -44,7 +42,7 @@ class CoursePacePresenter
modules: modules_json,
context_id: context_id,
context_type: context_type
}
}.merge(course_pace.start_date(with_context: true)).merge(course_pace.end_date(with_context: true))
end
private

View File

@ -20,6 +20,7 @@
describe CoursePacesController, type: :controller do
let(:valid_update_params) do
{
hard_end_dates: true,
end_date: 1.year.from_now.strftime("%Y-%m-%d"),
workflow_state: "active",
course_pace_module_items_attributes: [
@ -39,7 +40,7 @@ describe CoursePacesController, type: :controller do
before :once do
course_with_teacher(active_all: true)
@course.update(start_at: "2021-09-30")
@course.update(start_at: "2021-09-30", restrict_enrollments_to_course_dates: true)
student_in_course(active_all: true)
course_pace_model(course: @course)
@student_enrollment = @student.enrollments.first
@ -66,6 +67,7 @@ describe CoursePacesController, type: :controller do
@course_section = @course.course_sections.first
@valid_params = {
hard_end_dates: true,
end_date: 1.year.from_now.strftime("%Y-%m-%d"),
workflow_state: "active",
course_pace_module_items_attributes: [

View File

@ -21,7 +21,7 @@
describe CoursePaceDueDatesCalculator do
before :once do
course_with_student active_all: true
@course.update start_at: "2021-09-01"
@course.update start_at: "2021-09-01", restrict_enrollments_to_course_dates: true
@module = @course.context_modules.create!
@assignment = @course.assignments.create!
@tag = @assignment.context_module_tags.create! context_module: @module, context: @course, tag_type: "context_module"

View File

@ -21,8 +21,8 @@
describe CoursePaceHardEndDateCompressor do
before :once do
course_with_student active_all: true
@course.update start_at: "2021-09-01"
@course_pace = @course.course_paces.create! workflow_state: "active", end_date: "2021-09-10"
@course.update start_at: "2021-09-01", restrict_enrollments_to_course_dates: true
@course_pace = @course.course_paces.create! workflow_state: "active", end_date: "2021-09-10", hard_end_dates: true
@module = @course.context_modules.create!
end
@ -64,14 +64,15 @@ describe CoursePaceHardEndDateCompressor do
context "implicit end dates" do
before :once do
@course.update(start_at: "2021-12-27")
@course_pace.update(end_date: nil, exclude_weekends: true)
@course_pace.update(end_date: nil, hard_end_dates: false, 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")
@course.update(restrict_enrollments_to_course_dates: false)
@course.enrollment_term.update(start_at: "2021-12-27", end_at: "2021-12-31")
compressed = CoursePaceHardEndDateCompressor.compress(@course_pace, @course_pace.course_pace_module_items)
expect(compressed.pluck(:duration)).to eq([1, 1, 2])
end

View File

@ -22,7 +22,7 @@ require_relative "../spec_helper"
describe CoursePace do
before :once do
course_with_student active_all: true
@course.update start_at: "2021-09-01"
@course.update start_at: "2021-09-01", restrict_enrollments_to_course_dates: true
@module = @course.context_modules.create!
@assignment = @course.assignments.create!
@course_section = @course.course_sections.first
@ -313,26 +313,86 @@ describe CoursePace do
enrollment.update start_at: "2022-01-29"
@course_pace.user_id = student3.id
expect(@course_pace.start_date.to_date).to eq(Date.parse("2022-01-29"))
result = @course_pace.start_date(with_context: true)
expect(result[:start_date].to_date).to eq(Date.parse("2022-01-29"))
expect(result[:start_date_context]).to eq("user")
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.course_paces.create! course_section: other_section
expect(section_plan.start_date.to_date).to eq(Date.parse("2022-01-30"))
result = section_plan.start_date(with_context: true)
expect(result[:start_date].to_date).to eq(Date.parse("2022-01-30"))
expect(result[:start_date_context]).to eq("section")
end
it "returns course start if available" do
@course.update start_at: "2022-01-28"
expect(@course_pace.start_date.to_date).to eq(Date.parse("2022-01-28"))
result = @course_pace.start_date(with_context: true)
expect(result[:start_date].to_date).to eq(Date.parse("2022-01-28"))
expect(result[:start_date_context]).to eq("course")
end
it "returns course's term start if available" do
@course.enrollment_term.update start_at: "2022-01-27"
expect(@course_pace.start_date.to_date).to eq(Date.parse("2022-01-27"))
result = @course_pace.start_date(with_context: true)
expect(result[:start_date].to_date).to eq(Date.parse("2022-01-27"))
expect(result[:start_date_context]).to eq("term")
end
it "returns course created_at date as a last resort" do
expect(@course_pace.start_date.to_date).to eq(@course.created_at.to_date)
it "returns today date as a last resort" do
# there's an extremely tiny window where the date may have changed between
# when start_date called Time.now and now causing this to fail
# I don't think it's worth worrying about.
expect(@course_pace.start_date.to_date).to eq(Time.now.to_date)
result = @course_pace.start_date(with_context: true)
expect(result[:start_date].to_date).to eq(Time.now.to_date)
expect(result[:start_date_context]).to eq("hypothetical")
end
end
describe "default plan end_at" do
before do
@course.update start_at: nil
@course_pace.user_id = nil
end
it "returns hard end date if set" do
@course_pace.hard_end_dates = true
@course_pace[:end_date] = "2022-03-17"
result = @course_pace.end_date(with_context: true)
expect(result[:end_date].to_date).to eq(Date.parse("2022-03-17"))
expect(result[:end_date_context]).to eq("hard")
end
it "returns course end if available" do
@course.update conclude_at: "2022-01-28"
result = @course_pace.end_date(with_context: true)
expect(result[:end_date].to_date).to eq(Date.parse("2022-01-28"))
expect(result[:end_date_context]).to eq("course")
end
it "returns course's term end if available" do
@course.enrollment_term.update end_at: "2022-01-27"
result = @course_pace.end_date(with_context: true)
expect(result[:end_date].to_date).to eq(Date.parse("2022-01-27"))
expect(result[:end_date_context]).to eq("term")
end
it "returns nil if no fixed date is available" do
@course.restrict_enrollments_to_course_dates = false
@course.enrollment_term.update start_at: nil, end_at: nil
result = @course_pace.end_date(with_context: true)
expect(result[:end_date]).to be_nil
expect(result[:end_date_context]).to eq("hypothetical")
end
end
end

View File

@ -0,0 +1,131 @@
# 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/>.
require_relative "../common"
require_relative "pages/coursepaces_common_page"
require_relative "pages/coursepaces_page"
require_relative "../courses/pages/courses_home_page"
describe "course pacing page" do
include_context "in-process server selenium tests"
include CoursePacesCommonPageObject
include CoursePacesPageObject
include CoursesHomePage
before do
teacher_setup
course_with_student(
active_all: true,
name: "Jessi Jenkins",
course: @course
)
enable_course_paces_in_course
user_session @teacher
end
context "course pacing dates visibility" do
it "shows start and end dates" do
@course.start_at = Date.today
@course.conclude_at = Date.today + 1.month
@course.restrict_enrollments_to_course_dates = true
@course.save!
visit_course_paces_page
expect(course_pace_start_date).to be_displayed
expect(course_pace_end_date).to be_displayed
end
it "shows a due date tooltip when plan is compressed" do
@course_module = create_course_module("New Module", "active")
@assignment = create_assignment(@course, "Module Assignment", "Module Assignment Description", 10, "published")
@module_item = @course_module.add_item(id: @assignment.id, type: "assignment")
today = Date.today
@course.start_at = today
@course.conclude_at = today + 10.days
@course.restrict_enrollments_to_course_dates = true
@course.save!
visit_course_paces_page
update_module_item_duration(0, "15")
wait_for(method: nil, timeout: 10) { compression_tooltip.displayed? }
expect(compression_tooltip).to be_displayed
end
it "shows the number of assignments and how many weeks used in plan" do
@course_module = create_course_module("New Module", "active")
@assignment = create_assignment(@course, "Module Assignment", "Module Assignment Description", 10, "published")
@module_item = @course_module.add_item(id: @assignment.id, type: "assignment")
discussion_assignment = create_graded_discussion(@course, "Module Discussion", "published")
@course_module.add_item(id: discussion_assignment.id, type: "discussion_topic")
visit_course_paces_page
expect(number_of_assignments.text).to eq("2 assignments")
expect(number_of_weeks.text).to eq("0 weeks")
update_module_item_duration(0, 6)
expect(number_of_weeks.text).to eq("1 week")
end
it "shows Dates shown in course time zone text" do
@course_module = create_course_module("New Module", "active")
@assignment = create_assignment(@course, "Module Assignment", "Module Assignment Description", 10, "published")
@module_item = @course_module.add_item(id: @assignment.id, type: "assignment")
visit_course_paces_page
expect(dates_shown).to be_displayed
end
end
context "Skip Weekend Interactions" do
let(:today) { Date.today }
before do
@course_module = create_course_module("New Module", "active")
@assignment = create_assignment(@course, "Module Assignment", "Module Assignment Description", 10, "published")
@module_item = @course_module.add_item(id: @assignment.id, type: "assignment")
@course.start_at = today
@course.conclude_at = today + 1.month
@course.restrict_enrollments_to_course_dates = true
@course.save!
end
it "shows dates with weekends included in calculation" do
visit_course_paces_page
click_settings_button
click_weekends_checkbox
update_module_item_duration(0, 7)
expect(assignment_due_date_text).to eq(format_date_for_view(today + 7.days, "%a, %b %-d, %Y"))
end
it "shows dates with weekends not included in calculation" do
visit_course_paces_page
click_settings_button
today = Date.today
update_module_item_duration(0, 7)
expect(assignment_due_date_text).to eq(format_date_for_view(skip_weekends(today, 7), "%a, %b %-d, %Y"))
end
end
end

View File

@ -17,183 +17,187 @@
# 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/>.
require_relative "../common"
require_relative "pages/coursepaces_common_page"
require_relative "pages/coursepaces_page"
require_relative "../courses/pages/courses_home_page"
################################################################################
# most tests not valid for MVP simplification. See paceplans_projections_2_spec.rb
###############################################################################
describe "course pace page" do
include_context "in-process server selenium tests"
include CoursePacesCommonPageObject
include CoursePacesPageObject
include CoursesHomePage
# require_relative "../common"
# require_relative "pages/coursepaces_common_page"
# require_relative "pages/coursepaces_page"
# require_relative "../courses/pages/courses_home_page"
before :once do
teacher_setup
course_with_student(
active_all: true,
name: "Jessi Jenkins",
course: @course
)
enable_course_paces_in_course
end
# describe "course pace page" do
# include_context "in-process server selenium tests"
# include CoursePacesCommonPageObject
# include CoursePacesPageObject
# include CoursesHomePage
before do
user_session @teacher
end
# before :once do
# teacher_setup
# course_with_student(
# active_all: true,
# name: "Jessi Jenkins",
# course: @course
# )
# enable_course_paces_in_course
# end
context "course paces show/hide projections" do
it "have a projections button that changes text from hide to show when pressed" do
visit_course_paces_page
# before do
# user_session @teacher
# end
expect(show_hide_course_paces_button_text).to eq("Show Projections")
# context "course paces show/hide projections" do
# it "have a projections button that changes text from hide to show when pressed" do
# visit_course_paces_page
click_show_hide_projections_button
# expect(show_hide_course_paces_button_text).to eq("Show Projections")
expect(show_hide_course_paces_button_text).to eq("Hide Projections")
end
# click_show_hide_projections_button
it "shows start and end date fields when Show Projections button is clicked" do
visit_course_paces_page
# expect(show_hide_course_paces_button_text).to eq("Hide Projections")
# end
click_show_hide_projections_button
# it "shows start and end date fields when Show Projections button is clicked" do
# visit_course_paces_page
expect(course_pace_start_date).to be_displayed
expect(course_pace_end_date).to be_displayed
end
# click_show_hide_projections_button
it "does not show date fields when Hide Projections button is clicked" do
visit_course_paces_page
# expect(course_pace_start_date).to be_displayed
# expect(course_pace_end_date).to be_displayed
# end
click_show_hide_projections_button
click_show_hide_projections_button
# it "does not show date fields when Hide Projections button is clicked" do
# visit_course_paces_page
expect(course_pace_start_date_exists?).to be_falsey
expect(course_pace_end_date_exists?).to be_falsey
end
# click_show_hide_projections_button
# click_show_hide_projections_button
it "shows only a projection icon when window size is narrowed" do
visit_course_paces_page
# expect(course_pace_start_date_exists?).to be_falsey
# expect(course_pace_end_date_exists?).to be_falsey
# end
window_size_width = driver.manage.window.size.width
window_size_height = driver.manage.window.size.height
driver.manage.window.resize_to((window_size_width / 2).to_i, window_size_height)
scroll_to_element(show_hide_button_with_icon)
# it "shows only a projection icon when window size is narrowed" do
# visit_course_paces_page
expect(show_hide_icon_button_exists?).to be_truthy
expect(show_hide_course_paces_exists?).to be_falsey
end
# window_size_width = driver.manage.window.size.width
# window_size_height = driver.manage.window.size.height
# driver.manage.window.resize_to((window_size_width / 2).to_i, window_size_height)
# scroll_to_element(show_hide_button_with_icon)
it "shows an error message when weekend date is input and skip weekends is toggled on" do
visit_course_paces_page
click_show_hide_projections_button
add_start_date(calculate_saturday_date)
# expect(show_hide_icon_button_exists?).to be_truthy
# expect(show_hide_course_paces_exists?).to be_falsey
# end
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 an error message when weekend date is input and skip weekends is toggled on" do
# visit_course_paces_page
# click_show_hide_projections_button
# add_start_date(calculate_saturday_date)
it "shows a due date tooltip when plan is compressed" do
@course_module = create_course_module("New Module", "active")
@assignment = create_assignment(@course, "Module Assignment", "Module Assignment Description", 10, "published")
@module_item = @course_module.add_item(id: @assignment.id, type: "assignment")
# expect { course_paces_page_text.include?("The selected date is on a weekend and this course pace skips weekends.") }.to become(true)
# end
visit_course_paces_page
click_show_hide_projections_button
click_require_end_date_checkbox
# it "shows a due date tooltip when plan is compressed" do
# @course_module = create_course_module("New Module", "active")
# @assignment = create_assignment(@course, "Module Assignment", "Module Assignment Description", 10, "published")
# @module_item = @course_module.add_item(id: @assignment.id, type: "assignment")
today = Date.today
add_start_date(today)
add_required_end_date(today + 10.days)
update_module_item_duration(0, "15")
wait_for(method: nil, timeout: 10) { compression_tooltip.displayed? }
expect(compression_tooltip).to be_displayed
end
# visit_course_paces_page
# click_show_hide_projections_button
# click_require_end_date_checkbox
it "shows the number of assignments and how many weeks used in plan" do
@course_module = create_course_module("New Module", "active")
@assignment = create_assignment(@course, "Module Assignment", "Module Assignment Description", 10, "published")
@module_item = @course_module.add_item(id: @assignment.id, type: "assignment")
discussion_assignment = create_graded_discussion(@course, "Module Discussion", "published")
@course_module.add_item(id: discussion_assignment.id, type: "discussion_topic")
# today = Date.today
# add_start_date(today)
# add_required_end_date(today + 10.days)
# update_module_item_duration(0, "15")
# wait_for(method: nil, timeout: 10) { compression_tooltip.displayed? }
# expect(compression_tooltip).to be_displayed
# end
visit_course_paces_page
click_show_hide_projections_button
# it "shows the number of assignments and how many weeks used in plan" do
# @course_module = create_course_module("New Module", "active")
# @assignment = create_assignment(@course, "Module Assignment", "Module Assignment Description", 10, "published")
# @module_item = @course_module.add_item(id: @assignment.id, type: "assignment")
# discussion_assignment = create_graded_discussion(@course, "Module Discussion", "published")
# @course_module.add_item(id: discussion_assignment.id, type: "discussion_topic")
expect(number_of_assignments.text).to eq("2 assignments")
expect(number_of_weeks.text).to eq("0 weeks")
# visit_course_paces_page
# click_show_hide_projections_button
update_module_item_duration(0, 6)
# expect(number_of_assignments.text).to eq("2 assignments")
# expect(number_of_weeks.text).to eq("0 weeks")
expect(number_of_weeks.text).to eq("1 week")
end
# update_module_item_duration(0, 6)
it "shows Dates shown in course time zone text" do
@course_module = create_course_module("New Module", "active")
@assignment = create_assignment(@course, "Module Assignment", "Module Assignment Description", 10, "published")
@module_item = @course_module.add_item(id: @assignment.id, type: "assignment")
# expect(number_of_weeks.text).to eq("1 week")
# end
visit_course_paces_page
click_show_hide_projections_button
# it "shows Dates shown in course time zone text" do
# @course_module = create_course_module("New Module", "active")
# @assignment = create_assignment(@course, "Module Assignment", "Module Assignment Description", 10, "published")
# @module_item = @course_module.add_item(id: @assignment.id, type: "assignment")
expect(dates_shown).to be_displayed
end
end
# visit_course_paces_page
# click_show_hide_projections_button
context "Projected Dates" do
it "toggles provides input field for required end date when clicked" do
visit_course_paces_page
click_show_hide_projections_button
# expect(dates_shown).to be_displayed
# end
# end
click_require_end_date_checkbox
expect(is_checked(require_end_date_checkbox_selector)).to be_truthy
expect(required_end_date_input_exists?).to be_truthy
expect(required_end_date_message).to be_displayed
# context "Projected Dates" do
# it "toggles provides input field for required end date when clicked" do
# visit_course_paces_page
# click_show_hide_projections_button
click_require_end_date_checkbox
expect(is_checked(require_end_date_checkbox_selector)).to be_falsey
expect(hypothetical_end_date).to be_displayed
end
# click_require_end_date_checkbox
# expect(is_checked(require_end_date_checkbox_selector)).to be_truthy
# expect(required_end_date_input_exists?).to be_truthy
# expect(required_end_date_message).to be_displayed
it "allows inputting a date in the required date field" do
later_date = Time.zone.now + 2.weeks
visit_course_paces_page
click_show_hide_projections_button
# click_require_end_date_checkbox
# expect(is_checked(require_end_date_checkbox_selector)).to be_falsey
# expect(hypothetical_end_date).to be_displayed
# end
click_require_end_date_checkbox
add_required_end_date(later_date)
# it "allows inputting a date in the required date field" do
# later_date = Time.zone.now + 2.weeks
# visit_course_paces_page
# click_show_hide_projections_button
expect(required_end_date_value).to eq(format_date_for_view(later_date, "%B %-d, %Y"))
end
end
# click_require_end_date_checkbox
# add_required_end_date(later_date)
context "Skip Weekend Interactions" do
before :once do
@course_module = create_course_module("New Module", "active")
@assignment = create_assignment(@course, "Module Assignment", "Module Assignment Description", 10, "published")
@module_item = @course_module.add_item(id: @assignment.id, type: "assignment")
end
# expect(required_end_date_value).to eq(format_date_for_view(later_date, "%B %-d, %Y"))
# end
# end
it "shows dates with weekends included in calculation" do
visit_course_paces_page
click_settings_button
click_weekends_checkbox
click_show_hide_projections_button
today = Date.today
add_start_date(today)
update_module_item_duration(0, 7)
# context "Skip Weekend Interactions" do
# before :once do
# @course_module = create_course_module("New Module", "active")
# @assignment = create_assignment(@course, "Module Assignment", "Module Assignment Description", 10, "published")
# @module_item = @course_module.add_item(id: @assignment.id, type: "assignment")
# end
expect(assignment_due_date_text).to eq(format_date_for_view(today + 7.days, "%a, %b %-d, %Y"))
end
# it "shows dates with weekends included in calculation" do
# visit_course_paces_page
# click_settings_button
# click_weekends_checkbox
# click_show_hide_projections_button
# today = Date.today
# add_start_date(today)
# update_module_item_duration(0, 7)
it "shows dates with weekends not included in calculation" do
visit_course_paces_page
click_settings_button
click_show_hide_projections_button
today = Date.today
add_start_date(today)
update_module_item_duration(0, 7)
# expect(assignment_due_date_text).to eq(format_date_for_view(today + 7.days, "%a, %b %-d, %Y"))
# end
expect(assignment_due_date_text).to eq(format_date_for_view(skip_weekends(today, 7), "%a, %b %-d, %Y"))
end
end
end
# it "shows dates with weekends not included in calculation" do
# visit_course_paces_page
# click_settings_button
# click_show_hide_projections_button
# today = Date.today
# add_start_date(today)
# update_module_item_duration(0, 7)
# expect(assignment_due_date_text).to eq(format_date_for_view(skip_weekends(today, 7), "%a, %b %-d, %Y"))
# end
# end
# end

View File

@ -70,15 +70,15 @@ module CoursePacesPageObject
end
def number_of_assignments_selector
"[data-testid='number-of-assignments'] i"
"[data-testid='number-of-assignments']"
end
def number_of_weeks_selector
"[data-testid='number-of-weeks'] i"
"[data-testid='number-of-weeks']"
end
def course_pace_end_date_selector
"[data-testid='coursepace-date-text']"
"[data-testid='coursepace-end-date']"
end
def course_pace_menu_selector
@ -98,7 +98,7 @@ module CoursePacesPageObject
end
def course_pace_start_date_selector
"[data-testid='course-pace-date']"
"[data-testid='coursepace-start-date']"
end
def course_pace_table_module_selector

View File

@ -149,11 +149,17 @@ export const PRIMARY_PACE: CoursePace = {
context_type: 'Course',
context_id: COURSE.id,
start_date: '2021-09-01',
start_date_context: 'course',
end_date: '2021-12-15',
end_date_context: 'course',
workflow_state: 'active',
exclude_weekends: true,
hard_end_dates: true,
modules: [PACE_MODULE_1, PACE_MODULE_2]
modules: [PACE_MODULE_1, PACE_MODULE_2],
// @ts-ignore
course: undefined,
compressed_due_dates: undefined,
updated_at: ''
}
export const SECTION_PACE: CoursePace = {
@ -164,11 +170,17 @@ export const SECTION_PACE: CoursePace = {
context_type: 'Section',
context_id: SECTION_1.id,
start_date: '2021-09-15',
start_date_context: 'course',
end_date: '2021-12-15',
end_date_context: 'course',
workflow_state: 'active',
exclude_weekends: false,
hard_end_dates: true,
modules: [PACE_MODULE_1, PACE_MODULE_2]
modules: [PACE_MODULE_1, PACE_MODULE_2],
// @ts-ignore
course: undefined,
compressed_due_dates: undefined,
updated_at: ''
}
export const STUDENT_PACE: CoursePace = {
@ -179,11 +191,17 @@ export const STUDENT_PACE: CoursePace = {
context_type: 'Enrollment',
context_id: ENROLLMENT_1.user_id,
start_date: '2021-10-01',
start_date_context: 'user',
end_date: '2021-12-15',
end_date_context: 'course',
workflow_state: 'active',
exclude_weekends: true,
hard_end_dates: true,
modules: [PACE_MODULE_1, PACE_MODULE_2]
modules: [PACE_MODULE_1, PACE_MODULE_2],
// @ts-ignore
course: undefined,
compressed_due_dates: undefined,
updated_at: ''
}
export const PROGRESS_RUNNING = {

View File

@ -21,9 +21,8 @@ import {Flex} from '@instructure/ui-flex'
import {View} from '@instructure/ui-view'
import PacePicker from './pace_picker'
import ProjectedDates from './projected_dates/projected_dates'
import ProjectedDates from './projected_dates/projected_dates_2'
import Settings from './settings/settings'
import ShowProjectionsButton from './show_projections_button'
import UnpublishedChangesIndicator from '../unpublished_changes_indicator'
export type HeaderProps = {
@ -39,7 +38,6 @@ const Header = (props: HeaderProps) => (
</Flex.Item>
<Flex.Item margin="0 0 small" shouldGrow>
<Settings margin="0 0 0 small" />
<ShowProjectionsButton margin="0 auto 0 small" />
</Flex.Item>
<Flex.Item textAlign="end" margin="0 0 small small">
<UnpublishedChangesIndicator onClick={props.handleDrawerToggle} />

View File

@ -0,0 +1,127 @@
/*
* 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/>.
*/
import React from 'react'
import {within} from '@testing-library/dom'
import {renderConnected} from '../../../../__tests__/utils'
import {PRIMARY_PACE, STUDENT_PACE} from '../../../../__tests__/fixtures'
import {ProjectedDates} from '../projected_dates_2'
const defaultProps = {
coursePace: PRIMARY_PACE,
assignments: 5,
paceWeeks: 8,
projectedEndDate: '2021-12-01',
blackoutDates: [],
weekendsDisabled: false,
setStartDate: () => {},
compressDates: jest.fn(),
uncompressDates: jest.fn(),
toggleHardEndDates: jest.fn()
}
afterEach(() => {
jest.clearAllMocks()
})
describe('ProjectedDates', () => {
it('shows course start and end date when given', () => {
const {getByText} = renderConnected(<ProjectedDates {...defaultProps} />)
expect(getByText('Start Date')).toBeInTheDocument()
expect(getByText('Determined by course start date')).toBeInTheDocument()
expect(getByText('End Date')).toBeInTheDocument()
expect(getByText('Determined by course end date')).toBeInTheDocument()
expect(getByText(/\d+ assignments/)).toBeInTheDocument()
expect(getByText(/\d+ weeks/)).toBeInTheDocument()
expect(getByText('Dates shown in course time zone')).toBeInTheDocument()
})
it('shows term start and end date when given', () => {
const cpace = {...defaultProps.coursePace, start_date_context: 'term', end_date_context: 'term'}
const {getByText} = renderConnected(<ProjectedDates {...defaultProps} coursePace={cpace} />)
expect(getByText('Start Date')).toBeInTheDocument()
expect(getByText('Determined by course start date')).toBeInTheDocument()
expect(getByText('End Date')).toBeInTheDocument()
expect(getByText('Determined by course end date')).toBeInTheDocument()
expect(getByText(/\d+ assignments/)).toBeInTheDocument()
expect(getByText(/\d+ weeks/)).toBeInTheDocument()
expect(getByText('Dates shown in course time zone')).toBeInTheDocument()
})
it('shows student enrollment dates when given', () => {
const {getByText} = renderConnected(
<ProjectedDates {...defaultProps} coursePace={STUDENT_PACE} />
)
expect(getByText('Start Date')).toBeInTheDocument()
expect(getByText('Student enrollment date')).toBeInTheDocument()
expect(getByText('End Date')).toBeInTheDocument()
expect(getByText('Determined by course pace')).toBeInTheDocument()
expect(getByText(/\d+ assignments/)).toBeInTheDocument()
expect(getByText(/\d+ weeks/)).toBeInTheDocument()
expect(getByText('Dates shown in course time zone')).toBeInTheDocument()
})
// this can't happen any more
it('shows no dates for a course with no start and end dates', () => {
const cpace = {...defaultProps.coursePace, start_date: null, end_date: null}
const {queryByText} = renderConnected(<ProjectedDates {...defaultProps} coursePace={cpace} />)
expect(queryByText('Start Date')).not.toBeInTheDocument()
expect(queryByText('End Date')).not.toBeInTheDocument()
expect(queryByText(/\d+ assignments/)).toBeInTheDocument()
expect(queryByText(/\d+ weeks/)).toBeInTheDocument()
expect(queryByText('Dates shown in course time zone')).toBeInTheDocument()
})
it("shows not specified end if start date is all that's given", () => {
const cpace = {...defaultProps.coursePace, end_date: null}
const {getByTestId, getByText} = renderConnected(
<ProjectedDates {...defaultProps} coursePace={cpace} />
)
expect(getByText('Start Date')).toBeInTheDocument()
expect(getByText('End Date')).toBeInTheDocument()
const end = getByTestId('coursepace-end-date')
expect(within(end).getByText(/Not Specified/)).toBeInTheDocument()
expect(getByText(/\d+ assignments/)).toBeInTheDocument()
expect(getByText(/\d+ weeks/)).toBeInTheDocument()
expect(getByText('Dates shown in course time zone')).toBeInTheDocument()
})
it('captions the end date to match the start', () => {
const cpace = {...defaultProps.coursePace, end_date: null, end_date_context: 'term'}
const {getByTestId, getByText} = renderConnected(
<ProjectedDates {...defaultProps} coursePace={cpace} />
)
expect(getByText('Start Date')).toBeInTheDocument()
expect(getByText('End Date')).toBeInTheDocument()
const end = getByTestId('coursepace-end-date')
expect(within(end).getByText(/Not Specified/)).toBeInTheDocument()
expect(within(end).getByText('Determined by course end date')).toBeInTheDocument()
expect(getByText(/\d+ assignments/)).toBeInTheDocument()
expect(getByText(/\d+ weeks/)).toBeInTheDocument()
expect(getByText('Dates shown in course time zone')).toBeInTheDocument()
})
})

View File

@ -0,0 +1,187 @@
/*
* 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/>.
*/
import React from 'react'
import {connect} from 'react-redux'
import moment from 'moment-timezone'
import {useScope as useI18nScope} from '@canvas/i18n'
import useDateTimeFormat from '@canvas/use-date-time-format-hook'
import {Flex} from '@instructure/ui-flex'
import {PresentationContent} from '@instructure/ui-a11y-content'
import {Text} from '@instructure/ui-text'
import {View} from '@instructure/ui-view'
import {StoreState, CoursePace} from '../../../types'
import {
getCoursePace,
getCoursePaceItems,
getPaceWeeks,
getProjectedEndDate
} from '../../../reducers/course_paces'
import {coursePaceTimezone} from '../../../shared/api/backend_serializer'
const I18n = useI18nScope('course_paces_projected_dates')
const DASH = String.fromCharCode(0x2013)
const START_DATE_CAPTIONS = {
user: I18n.t('Student enrollment date'),
course: I18n.t('Determined by course start date'),
// always refer to the start and end dates as "course"
// because the course does whether it's bounded
// by the term or course dates
term: I18n.t('Determined by course start date'),
section: I18n.t('Determined by section stat date'),
hypothetical: I18n.t("Determined by today's date")
}
const END_DATE_CAPTIONS = {
hard: I18n.t('Reqired end date'),
user: I18n.t('Determined by course pace'),
course: I18n.t('Determined by course end date'),
term: I18n.t('Determined by course end date'),
section: I18n.t('Determined by section end date'),
hypothetical: I18n.t('Determined by course pace')
}
type ComponentProps = {
readonly coursePace: CoursePace
readonly assignments: number
readonly paceWeeks: number
readonly projectedEndDate: string
}
export const ProjectedDates: React.FC<ComponentProps> = ({
coursePace,
assignments,
paceWeeks,
projectedEndDate
}) => {
const formatDate = useDateTimeFormat('date.formats.long', coursePaceTimezone, ENV.LOCALE)
const enrollmentType = coursePace.context_type === 'Enrollment'
const startDateValue = coursePace.start_date
const startHelpText = START_DATE_CAPTIONS[coursePace.start_date_context]
let endDateValue, endHelpText
if (enrollmentType) {
endDateValue = projectedEndDate
endHelpText = END_DATE_CAPTIONS.user
} else {
endDateValue =
coursePace.end_date_context === 'hypothetical' ? projectedEndDate : coursePace.end_date
endHelpText = END_DATE_CAPTIONS[coursePace.end_date_context]
}
const hasAtLeastOneDate = () => !!(startDateValue || endDateValue)
const renderDate = (label, dateValue, helpText, testid) => {
return (
<div data-testid={testid} style={{display: 'inline-block', lineHeight: '1.125rem'}}>
<View as="div" margin="0">
<Text weight="bold">{label}</Text>
</View>
<View data-testid="coursepace-date-text" as="div" margin="small 0 x-small 0">
{dateValue ? (
formatDate(moment.tz(dateValue, coursePaceTimezone).toISOString(true))
) : (
<Text>
{DASH} {I18n.t('Not Specified')} {DASH}
</Text>
)}
</View>
<div style={{whiteSpace: 'nowrap'}}>
<Text fontStyle="italic" size="small">
<span style={{whiteSpace: 'nowrap'}}>{helpText}</span>
</Text>
</div>
</div>
)
}
const renderSummary = () => {
return (
<Flex as="section" direction="column" alignItems="end" wrap="wrap">
<Flex.Item margin="0">
<View padding="0 xxx-small 0 0" margin="0 x-small 0 0">
<Text data-testid="number-of-assignments" size="small" fontStyle="italic">
{I18n.t(
{
one: '1 assignment',
other: '%{count} assignments'
},
{count: assignments}
)}
</Text>
</View>
<PresentationContent>
<Text color="secondary">|</Text>
</PresentationContent>
<View margin="0 0 0 x-small">
<Text data-testid="number-of-weeks" size="small" fontStyle="italic">
{I18n.t(
{
one: '1 week',
other: '%{count} weeks'
},
{count: paceWeeks}
)}
</Text>
</View>
</Flex.Item>
<Flex.Item margin="0">
<Text data-testid="dates-shown-time-zone" fontStyle="italic" size="small">
{I18n.t('Dates shown in course time zone')}
</Text>
</Flex.Item>
</Flex>
)
}
return (
<div style={{lineHeight: '1.125rem'}}>
<Flex as="section" alignItems="end" margin="0" wrap="wrap">
{hasAtLeastOneDate() && (
<>
<Flex.Item margin="0 medium medium 0">
{renderDate(
I18n.t('Start Date'),
startDateValue,
startHelpText,
'coursepace-start-date'
)}
</Flex.Item>
<Flex.Item margin="0 medium medium 0" shouldGrow>
{renderDate(I18n.t('End Date'), endDateValue, endHelpText, 'coursepace-end-date')}
</Flex.Item>
</>
)}
<Flex.Item margin="0 0 x-small 0" shouldGrow>
{renderSummary()}
</Flex.Item>
</Flex>
</div>
)
}
const mapStateToProps = (state: StoreState) => {
return {
coursePace: getCoursePace(state),
assignments: getCoursePaceItems(state).length,
paceWeeks: getPaceWeeks(state),
projectedEndDate: getProjectedEndDate(state)
}
}
export default connect(mapStateToProps)(ProjectedDates)

View File

@ -32,7 +32,7 @@ export const initialState: UIState = {
editingBlackoutDates: false,
showLoadingOverlay: false,
responsiveSize: 'large',
showProjections: false
showProjections: true
}
/* Selectors */

View File

@ -69,11 +69,14 @@ export interface Module {
export type PaceContextTypes = 'Course' | 'Section' | 'Enrollment'
export type WorkflowStates = 'unpublished' | 'active' | 'deleted'
export type ProgressStates = 'queued' | 'running' | 'completed' | 'failed'
export type ContextTypes = 'user' | 'course' | 'term' | 'hypothetical'
export interface CoursePace {
readonly id?: string
readonly start_date?: string
readonly end_date?: string
readonly start_date: string
readonly start_date_context: ContextTypes
readonly end_date: string | null
readonly end_date_context: ContextTypes
readonly workflow_state: WorkflowStates
readonly modules: Module[]
readonly exclude_weekends: boolean