diff --git a/app/models/course_pace.rb b/app/models/course_pace.rb index d5f62924b5a..7f571d5707f 100644 --- a/app/models/course_pace.rb +++ b/app/models/course_pace.rb @@ -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 diff --git a/app/presenters/course_pace_presenter.rb b/app/presenters/course_pace_presenter.rb index 645363723dd..f01f44294ec 100644 --- a/app/presenters/course_pace_presenter.rb +++ b/app/presenters/course_pace_presenter.rb @@ -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 diff --git a/spec/controllers/course_paces_controller_spec.rb b/spec/controllers/course_paces_controller_spec.rb index 81580d4ecde..5564fafceb9 100644 --- a/spec/controllers/course_paces_controller_spec.rb +++ b/spec/controllers/course_paces_controller_spec.rb @@ -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: [ diff --git a/spec/lib/course_pace_due_dates_calculator_spec.rb b/spec/lib/course_pace_due_dates_calculator_spec.rb index 72a98372a21..69d45766fa0 100644 --- a/spec/lib/course_pace_due_dates_calculator_spec.rb +++ b/spec/lib/course_pace_due_dates_calculator_spec.rb @@ -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" diff --git a/spec/lib/course_pace_hard_end_date_compressor_spec.rb b/spec/lib/course_pace_hard_end_date_compressor_spec.rb index 90e61ec6fdd..3c3a826144b 100644 --- a/spec/lib/course_pace_hard_end_date_compressor_spec.rb +++ b/spec/lib/course_pace_hard_end_date_compressor_spec.rb @@ -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 diff --git a/spec/models/course_pace_spec.rb b/spec/models/course_pace_spec.rb index 9af2cb9ab43..a0a0ff3eb72 100644 --- a/spec/models/course_pace_spec.rb +++ b/spec/models/course_pace_spec.rb @@ -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 diff --git a/spec/selenium/course_paces/coursepaces_projections_2_spec.rb b/spec/selenium/course_paces/coursepaces_projections_2_spec.rb new file mode 100644 index 00000000000..e9b60296fc4 --- /dev/null +++ b/spec/selenium/course_paces/coursepaces_projections_2_spec.rb @@ -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 . + +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 diff --git a/spec/selenium/course_paces/coursepaces_projections_spec.rb b/spec/selenium/course_paces/coursepaces_projections_spec.rb index 2f63e1e5681..3e42c461c93 100644 --- a/spec/selenium/course_paces/coursepaces_projections_spec.rb +++ b/spec/selenium/course_paces/coursepaces_projections_spec.rb @@ -17,183 +17,187 @@ # You should have received a copy of the GNU Affero General Public License along # with this program. If not, see . -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 diff --git a/spec/selenium/course_paces/pages/coursepaces_page.rb b/spec/selenium/course_paces/pages/coursepaces_page.rb index 8d8744deff2..43c2e738bc3 100644 --- a/spec/selenium/course_paces/pages/coursepaces_page.rb +++ b/spec/selenium/course_paces/pages/coursepaces_page.rb @@ -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 diff --git a/ui/features/course_paces/react/__tests__/fixtures.ts b/ui/features/course_paces/react/__tests__/fixtures.ts index 1dd791d11b1..493e181b82c 100644 --- a/ui/features/course_paces/react/__tests__/fixtures.ts +++ b/ui/features/course_paces/react/__tests__/fixtures.ts @@ -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 = { diff --git a/ui/features/course_paces/react/components/header/header.tsx b/ui/features/course_paces/react/components/header/header.tsx index 2bc99a067f8..5529a955f79 100644 --- a/ui/features/course_paces/react/components/header/header.tsx +++ b/ui/features/course_paces/react/components/header/header.tsx @@ -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) => ( - diff --git a/ui/features/course_paces/react/components/header/projected_dates/__tests__/projected_dates_2.test.tsx b/ui/features/course_paces/react/components/header/projected_dates/__tests__/projected_dates_2.test.tsx new file mode 100644 index 00000000000..cd9b0597b63 --- /dev/null +++ b/ui/features/course_paces/react/components/header/projected_dates/__tests__/projected_dates_2.test.tsx @@ -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 . + */ + +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() + + 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() + + 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( + + ) + + 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() + + 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( + + ) + + 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( + + ) + + 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() + }) +}) diff --git a/ui/features/course_paces/react/components/header/projected_dates/projected_dates_2.tsx b/ui/features/course_paces/react/components/header/projected_dates/projected_dates_2.tsx new file mode 100644 index 00000000000..af1331daf84 --- /dev/null +++ b/ui/features/course_paces/react/components/header/projected_dates/projected_dates_2.tsx @@ -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 . + */ + +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 = ({ + 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 ( +
+ + {label} + + + {dateValue ? ( + formatDate(moment.tz(dateValue, coursePaceTimezone).toISOString(true)) + ) : ( + + {DASH} {I18n.t('Not Specified')} {DASH} + + )} + +
+ + {helpText} + +
+
+ ) + } + + const renderSummary = () => { + return ( + + + + + {I18n.t( + { + one: '1 assignment', + other: '%{count} assignments' + }, + {count: assignments} + )} + + + + | + + + + {I18n.t( + { + one: '1 week', + other: '%{count} weeks' + }, + {count: paceWeeks} + )} + + + + + + {I18n.t('Dates shown in course time zone')} + + + + ) + } + + return ( +
+ + {hasAtLeastOneDate() && ( + <> + + {renderDate( + I18n.t('Start Date'), + startDateValue, + startHelpText, + 'coursepace-start-date' + )} + + + {renderDate(I18n.t('End Date'), endDateValue, endHelpText, 'coursepace-end-date')} + + + )} + + {renderSummary()} + + +
+ ) +} + +const mapStateToProps = (state: StoreState) => { + return { + coursePace: getCoursePace(state), + assignments: getCoursePaceItems(state).length, + paceWeeks: getPaceWeeks(state), + projectedEndDate: getProjectedEndDate(state) + } +} +export default connect(mapStateToProps)(ProjectedDates) diff --git a/ui/features/course_paces/react/reducers/ui.ts b/ui/features/course_paces/react/reducers/ui.ts index 64dcb59329e..970e199d03a 100644 --- a/ui/features/course_paces/react/reducers/ui.ts +++ b/ui/features/course_paces/react/reducers/ui.ts @@ -32,7 +32,7 @@ export const initialState: UIState = { editingBlackoutDates: false, showLoadingOverlay: false, responsiveSize: 'large', - showProjections: false + showProjections: true } /* Selectors */ diff --git a/ui/features/course_paces/react/types.ts b/ui/features/course_paces/react/types.ts index c90f72c045b..43cb4bec0ce 100644 --- a/ui/features/course_paces/react/types.ts +++ b/ui/features/course_paces/react/types.ts @@ -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