# frozen_string_literal: true # # Copyright (C) 2014 - 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 "../helpers/discussions_common" require_relative "../helpers/items_assign_to_tray" require_relative "../helpers/context_modules_common" require_relative "../../helpers/k5_common" require_relative "../dashboard/pages/k5_important_dates_section_page" require_relative "../dashboard/pages/k5_dashboard_common_page" require_relative "../common" require_relative "pages/discussion_page" require_relative "../assignments/page_objects/assignment_create_edit_page" require_relative "../discussions/discussion_helpers" require_relative "../../helpers/selective_release_common" describe "discussions" do include_context "in-process server selenium tests" include DiscussionsCommon include DiscussionHelpers include ItemsAssignToTray include ContextModulesCommon include K5DashboardCommonPageObject include K5Common include K5ImportantDatesSectionPageObject include SelectiveReleaseCommon def create_graded_discussion(discussion_course, assignment_options = {}) default_assignment_options = { name: "Default Assignment", points_possible: 10, assignment_group: discussion_course.assignment_groups.create!(name: "Default Assignment Group"), only_visible_to_overrides: false } options = default_assignment_options.merge(assignment_options) @discussion_assignment = discussion_course.assignments.create!(options) all_graded_discussion_options = { user: teacher, title: "assignment topic title", message: "assignment topic message", discussion_type: "threaded", assignment: @discussion_assignment, } discussion_course.discussion_topics.create!(all_graded_discussion_options) end let(:course) { course_model.tap(&:offer!) } let(:teacher) { teacher_in_course(course:, name: "teacher", active_all: true).user } let(:teacher_topic) { course.discussion_topics.create!(user: teacher, title: "teacher topic title", message: "teacher topic message") } let(:assignment_group) { course.assignment_groups.create!(name: "assignment group") } let(:group_category) { course.group_categories.create!(name: "group category") } let(:assignment) do course.assignments.create!( name: "assignment", points_possible: 10, # submission_types: 'discussion_topic', assignment_group: ) end let(:assignment_topic) do course.discussion_topics.create!(user: teacher, title: "assignment topic title", message: "assignment topic message", assignment:) end context "on the edit page" do let(:url) { "/courses/#{course.id}/discussion_topics/#{topic.id}/edit" } context "when :discussion_create feature flag is OFF" do before do Account.site_admin.disable_feature! :discussion_create end context "as a teacher" do let(:topic) { teacher_topic } before do user_session(teacher) stub_rcs_config end context "graded" do let(:topic) { assignment_topic } it "allows editing the assignment group", priority: "1" do assign_group_2 = course.assignment_groups.create!(name: "Group 2") get url click_option("#assignment_group_id", assign_group_2.name) expect_new_page_load { f(".form-actions button[type=submit]").click } expect(topic.reload.assignment.assignment_group_id).to eq assign_group_2.id end it "allows editing the grading type", priority: "1" do get url click_option("#assignment_grading_type", "Letter Grade") expect_new_page_load { f(".form-actions button[type=submit]").click } expect(topic.reload.assignment.grading_type).to eq "letter_grade" end it "allows editing the group category", priority: "1" do group_cat = course.group_categories.create!(name: "Groupies") get url f("#has_group_category").click click_option("#assignment_group_category_id", group_cat.name) expect_new_page_load { f(".form-actions button[type=submit]").click } expect(topic.reload.group_category_id).to eq group_cat.id end it "allows editing the peer review", priority: "1" do get url f("#assignment_peer_reviews").click expect_new_page_load { f(".form-actions button[type=submit]").click } expect(topic.reload.assignment.peer_reviews).to be true end it "allows editing the due dates", priority: "1" do differentiated_modules_off get url wait_for_tiny(f("textarea[name=message]")) due_at = 3.days.from_now unlock_at = 2.days.from_now lock_at = 4.days.from_now # set due_at, lock_at, unlock_at replace_content(f(".date_field[data-date-type='due_at']"), format_date_for_view(due_at), tab_out: true) replace_content(f(".date_field[data-date-type='unlock_at']"), format_date_for_view(unlock_at), tab_out: true) replace_content(f(".date_field[data-date-type='lock_at']"), format_date_for_view(lock_at), tab_out: true) wait_for_ajaximations expect_new_page_load { f(".form-actions button[type=submit]").click } a = DiscussionTopic.last.assignment expect(a.due_at.to_date).to eq due_at.to_date expect(a.unlock_at.to_date).to eq unlock_at.to_date expect(a.lock_at.to_date).to eq lock_at.to_date end it "adds an attachment to a graded topic", priority: "1" do get url wait_for_tiny(f("textarea[name=message]")) add_attachment_and_validate do # should correctly save changes to the assignment set_value f("#discussion_topic_assignment_points_possible"), "123" end assignment.reload expect(assignment.points_possible).to eq 123 end it "returns focus to add attachment when removed" do get url add_attachment_and_validate get url f(".removeAttachment").click wait_for_ajaximations check_element_has_focus(f("input[name=attachment]")) end it "warns user when leaving page unsaved", priority: "1" do skip_if_safari(:alert) title = "new title" get url wait_for_tiny(f("textarea[name=message]")) replace_content(f("input[name=title]"), title) fln("Home").click expect(alert_present?).to be_truthy driver.switch_to.alert.dismiss end context "with archived grading schemes enabled" do before do Account.site_admin.enable_feature!(:grading_scheme_updates) Account.site_admin.enable_feature!(:archived_grading_schemes) @course = course @account = @course.account @active_grading_standard = @course.grading_standards.create!(title: "Active Grading Scheme", data: { "A" => 0.9, "F" => 0 }, scaling_factor: 1.0, points_based: false, workflow_state: "active") @archived_grading_standard = @course.grading_standards.create!(title: "Archived Grading Scheme", data: { "A" => 0.9, "F" => 0 }, scaling_factor: 1.0, points_based: false, workflow_state: "archived") @account_grading_standard = @account.grading_standards.create!(title: "Account Grading Scheme", data: { "A" => 0.9, "F" => 0 }, scaling_factor: 1.0, points_based: false, workflow_state: "active") discussion_assignment_options = { name: "assignment", points_possible: 10, grading_type: "letter_grade", assignment_group: course.assignment_groups.create!(name: "assignment group"), only_visible_to_overrides: true, } discussion_assignment_peer_review_options = { peer_reviews: true, automatic_peer_reviews: true, peer_reviews_due_at: 1.day.ago, peer_review_count: 2, } discussion_assignment_options = discussion_assignment_options.merge(discussion_assignment_peer_review_options) @graded_discussion = create_graded_discussion(course, discussion_assignment_options) course_override_due_date = 5.days.from_now course_section = course.course_sections.create!(name: "section alpha") @graded_discussion.assignment.assignment_overrides.create!(set_type: "CourseSection", set_id: course_section.id, due_at: course_override_due_date) @assignment = @graded_discussion.assignment end it "shows archived grading scheme if it is the course default twice, once to follow course default scheme and once to choose that scheme to use" do @course.update!(grading_standard_id: @archived_grading_standard.id) @course.reload get "/courses/#{@course.id}/discussion_topics/#{@graded_discussion.id}/edit" expect(f("[data-testid='grading-schemes-selector-dropdown']").attribute("title")).to eq(@archived_grading_standard.title + " (course default)") f("[data-testid='grading-schemes-selector-dropdown']").click expect(f("[data-testid='grading-schemes-selector-option-#{@course.grading_standard.id}']")).to include_text(@course.grading_standard.title) end it "shows archived grading scheme if it is the current assignment grading standard" do @assignment.update!(grading_standard_id: @archived_grading_standard.id) @assignment.reload get "/courses/#{@course.id}/discussion_topics/#{@graded_discussion.id}/edit" wait_for_ajaximations expect(f("[data-testid='grading-schemes-selector-dropdown']").attribute("title")).to eq(@archived_grading_standard.title) end it "removes grading schemes from dropdown after archiving them but still shows them upon reopening the modal" do get "/courses/#{@course.id}/discussion_topics/#{@graded_discussion.id}/edit" wait_for_ajaximations f("[data-testid='grading-schemes-selector-dropdown']").click expect(f("[data-testid='grading-schemes-selector-option-#{@active_grading_standard.id}']")).to be_present f("[data-testid='manage-all-grading-schemes-button']").click wait_for_ajaximations f("[data-testid='grading-scheme-#{@active_grading_standard.id}-archive-button']").click wait_for_ajaximations f("[data-testid='manage-all-grading-schemes-close-button']").click wait_for_ajaximations f("[data-testid='grading-schemes-selector-dropdown']").click expect(f("[data-testid='grading-schemes-selector-dropdown-form']")).not_to contain_css("[data-testid='grading-schemes-selector-option-#{@active_grading_standard.id}']") f("[data-testid='manage-all-grading-schemes-button']").click wait_for_ajaximations expect(f("[data-testid='grading-scheme-row-#{@active_grading_standard.id}']").text).to be_present end it "shows all archived schemes in the manage grading schemes modal" do archived_gs1 = @course.grading_standards.create!(title: "Archived Grading Scheme 1", data: { "A" => 0.9, "F" => 0 }, scaling_factor: 1.0, points_based: false, workflow_state: "archived") archived_gs2 = @course.grading_standards.create!(title: "Archived Grading Scheme 2", data: { "A" => 0.9, "F" => 0 }, scaling_factor: 1.0, points_based: false, workflow_state: "archived") archived_gs3 = @course.grading_standards.create!(title: "Archived Grading Scheme 3", data: { "A" => 0.9, "F" => 0 }, scaling_factor: 1.0, points_based: false, workflow_state: "archived") get "/courses/#{@course.id}/discussion_topics/#{@graded_discussion.id}/edit" wait_for_ajaximations f("[data-testid='manage-all-grading-schemes-button']").click wait_for_ajaximations expect(f("[data-testid='grading-scheme-#{archived_gs1.id}-name']")).to include_text(archived_gs1.title) expect(f("[data-testid='grading-scheme-#{archived_gs2.id}-name']")).to include_text(archived_gs2.title) expect(f("[data-testid='grading-scheme-#{archived_gs3.id}-name']")).to include_text(archived_gs3.title) end it "will still show the assignment grading scheme if you archive it on the edit page in the management modal and persist on reload" do @assignment.update!(grading_standard_id: @active_grading_standard.id) @assignment.reload get "/courses/#{@course.id}/discussion_topics/#{@graded_discussion.id}/edit" wait_for_ajaximations expect(f("[data-testid='grading-schemes-selector-dropdown']").attribute("title")).to eq(@active_grading_standard.title) f("[data-testid='manage-all-grading-schemes-button']").click wait_for_ajaximations f("[data-testid='grading-scheme-#{@active_grading_standard.id}-archive-button']").click wait_for_ajaximations f("[data-testid='manage-all-grading-schemes-close-button']").click wait_for_ajaximations expect(f("[data-testid='grading-schemes-selector-dropdown']").attribute("title")).to eq(@active_grading_standard.title) get "/courses/#{@course.id}/assignments/#{@assignment.id}/edit" wait_for_ajaximations expect(f("[data-testid='grading-schemes-selector-dropdown']").attribute("title")).to eq(@active_grading_standard.title) end it "creates a discussion topic with selected grading scheme/standard" do grading_standard = @course.grading_standards.create!(title: "Win/Lose", data: [["Winner", 0.94], ["Loser", 0]]) get "/courses/#{@course.id}/assignments/#{@assignment.id}/edit" wait_for_ajaximations f("[data-testid='grading-schemes-selector-dropdown']").click f("[data-testid='grading-schemes-selector-option-#{grading_standard.id}']").click f(".form-actions button[type=submit]").click fj(".ui-button-text:contains('Continue')").click a = DiscussionTopic.last.assignment expect(a.grading_standard_id).to eq grading_standard.id end end end context "with a group attached" do let(:graded_topic) { assignment_topic } before do @gc = GroupCategory.create(name: "Sharks", context: @course) @student = student_in_course(course: @course, active_all: true).user group = @course.groups.create!(group_category: @gc) group.users << @student end it "group discussions with entries should lock and display the group name", priority: "1" do topic.group_category = @gc topic.save! topic.child_topics[0].reply_from({ user: @student, text: "I feel pretty" }) @gc.destroy get url expect(f("#assignment_group_category_id")).to be_disabled expect(get_value("#assignment_group_category_id")).to eq topic.group_category.id.to_s end it "prompts for creating a new group category if original group is deleted with no submissions", priority: "1" do topic.group_category = @gc topic.save! @gc.destroy get url wait_for_ajaximations expect(f("#assignment_group_category_id")).not_to be_displayed end context "graded" do let(:topic) { assignment_topic } it "locks and display the group name", priority: "1" do topic.group_category = @gc topic.save! topic.reply_from({ user: @student, text: "I feel pretty" }) @gc.destroy get url expect(f("#assignment_group_category_id")).to be_disabled expect(get_value("#assignment_group_category_id")).to eq topic.group_category.id.to_s end end end it "saves and display all changes", priority: "2" do course.require_assignment_group confirm(:off) toggle(:on) confirm(:on) end it "preserves query parameters in the URL when you CANCEL", :ignore_js_errors do get "/courses/#{course.id}/discussion_topics/#{topic.id}/edit?embed=true" force_click("button:contains('Cancel')") wait_for_ajaximations expect(driver.current_url).not_to include("edit") expect(driver.current_url).to include("?embed=true") end it "shows correct date when saving" do Timecop.freeze do topic.lock_at = 5.days.ago topic.save! teacher.time_zone = "Hawaii" teacher.save! get url f(".form-actions button[type=submit]").click get url expect(topic.reload.lock_at).to eq 5.days.ago.beginning_of_minute end end it "toggles checkboxes when clicking their labels", priority: "1" do get url expect(is_checked("input[type=checkbox][name=threaded]")).not_to be_truthy force_click_native("input#threaded") expect(is_checked("input[type=checkbox][name=threaded]")).to be_truthy end context "locking" do it "sets as active when removing existing delayed_post_at and lock_at dates", priority: "1" do topic.delayed_post_at = 10.days.ago topic.lock_at = 5.days.ago topic.locked = true topic.save! get url wait_for_tiny(f("textarea[name=message]")) expect(f('input[type=text][name="delayed_post_at"]')).to be_displayed f('input[type=text][name="delayed_post_at"]').clear f('input[type=text][name="lock_at"]').clear expect_new_page_load { f(".form-actions button[type=submit]").click } topic.reload expect(topic.delayed_post_at).to be_nil expect(topic.lock_at).to be_nil expect(topic.active?).to be_truthy expect(topic.locked?).to be_falsey end it "is locked when delayed_post_at and lock_at are in past", priority: "2" do topic.delayed_post_at = nil topic.lock_at = nil topic.workflow_state = "active" topic.save! get url wait_for_tiny(f("textarea[name=message]")) delayed_post_at = 10.days.ago lock_at = 5.days.ago replace_content(f('input[type=text][name="delayed_post_at"]'), format_date_for_view(delayed_post_at), tab_out: true) replace_content(f('input[type=text][name="lock_at"]'), format_date_for_view(lock_at), tab_out: true) expect_new_page_load { f(".form-actions button[type=submit]").click } wait_for_ajaximations topic.reload expect(topic.delayed_post_at.to_date).to eq delayed_post_at.to_date expect(topic.lock_at.to_date).to eq lock_at.to_date expect(topic.locked?).to be_truthy end it "sets workflow to active when delayed_post_at in past and lock_at in future", priority: "2" do topic.delayed_post_at = 5.days.from_now topic.lock_at = 10.days.from_now topic.workflow_state = "active" topic.locked = false topic.save! get url wait_for_tiny(f("textarea[name=message]")) delayed_post_at = 5.days.ago replace_content(f('input[type=text][name="delayed_post_at"]'), format_date_for_view(delayed_post_at), tab_out: true) expect_new_page_load { f(".form-actions button[type=submit]").click } wait_for_ajaximations topic.reload expect(topic.delayed_post_at.to_date).to eq delayed_post_at.to_date expect(topic.active?).to be_truthy expect(topic.locked?).to be_falsey end end context "usage rights" do before do course.root_account.enable_feature!(:usage_rights_discussion_topics) course.update!(usage_rights_required: true) end it "validates that usage rights are set" do get url _filename, fullpath, _data = get_file("testfile5.zip") f("input[name=attachment]").send_keys(fullpath) type_in_tiny("textarea[name=message]", "file attachment discussion") f("#edit_discussion_form_buttons .btn-primary[type=submit]").click wait_for_ajaximations error_box = f("div[role='alert'] .error_text") expect(error_box.text).to eq "You must set usage rights" end it "sets usage rights on file attachment" do get url _filename, fullpath, _data = get_file("testfile1.txt") f("input[name=attachment]").send_keys(fullpath) f("#usage_rights_control button").click click_option(".UsageRightsSelectBox__container select", "own_copyright", :value) f(".UsageRightsDialog__Footer-Actions button[type='submit']").click expect_new_page_load { f(".form-actions button[type=submit]").click } expect(topic.reload.attachment.usage_rights).not_to be_nil end it "displays usage rights on file attachment" do usage_rights = @course.usage_rights.create!( legal_copyright: "(C) 2012 Initrode", use_justification: "own_copyright" ) file = @course.attachments.create!( display_name: "hey.txt", uploaded_data: default_uploaded_data, usage_rights: ) file.usage_rights topic.attachment = file topic.save! get url expect(element_exists?("#usage_rights_control i.icon-files-copyright")).to be(true) end end context "in paced course" do let(:topic) { assignment_topic } before do course.enable_course_paces = true course.save! context_module = course.context_modules.create! name: "M" assignment_topic.context_module_tags.create! context_module:, context: @course, tag_type: "context_module" end it "shows the course pacing notice on a graded discussion" do get url expect(Discussion.course_pacing_notice).to be_displayed end it "does not show the course pacing notice on a graded discussion when feature off in account" do course.account.disable_feature!(:course_paces) get url expect(element_exists?(Discussion.course_pacing_notice_selector)).to be_falsey end end context "anonymous topic" do let(:topic) { course.discussion_topics.create!(user: teacher, title: "anonymous topic title", message: "anonymous topic message", anonymous_state: "full_anonymity") } before do Account.site_admin.enable_feature! :react_discussions_post @student = student_in_course(course: @course, active_all: true).user end it "able to save" do get url expect_new_page_load { f(".form-actions button[type=submit]").click } expect(fj("span:contains('anonymous topic title')")).to be_present end it "able to save anon, not graded, quick added from assignments", :ignore_js_errors do get "/courses/#{course.id}/assignments" f(".add_assignment").click click_option(f('[name="submission_types"]'), "Discussion") f(".create_assignment_dialog input[type=text]").send_keys("anon disc from assignment") f(".more_options").click f("input[type=radio][value=partial_anonymity]").click f("input#use_for_grading").click expect_new_page_load { f("button.save_and_publish").click } end it "allow to change the anonymity if there is no reply" do get url expect(f("input[value='full_anonymity']").selected?).to be_truthy force_click_native("input[value='partial_anonymity']") expect_new_page_load { f(".form-actions button[type=submit]").click } end it "should not allow to change the anonymity when there are replys" do topic.reply_from({ user: @student, text: "I feel pretty" }) get url expect(ff("input[name='anonymous_state'][disabled]").count).to eq 3 end end end end context "when :discussion_create feature flag is ON", :ignore_js_errors do before do Account.site_admin.enable_feature!(:discussion_create) Account.site_admin.enable_feature!(:react_discussions_post) user_session(teacher) end context "ungraded" do before do attachment_model # TODO: Update to cover: graded, group discussions, file attachment, any other options later implemented all_discussion_options_enabled = { title: "value for title", message: "value for message", is_anonymous_author: false, anonymous_state: "full_anonymity", todo_date: "Thu, 12 Oct 2023 15:59:59.000000000 UTC +00:00", allow_rating: true, only_graders_can_rate: true, podcast_enabled: true, podcast_has_student_posts: true, require_initial_post: true, discussion_type: "side_comment", delayed_post_at: "Tue, 10 Oct 2023 16:00:00.000000000 UTC +00:00", lock_at: "Wed, 11 Nov 2023 15:59:59.999999000 UTC +00:00", is_section_specific: false, attachment: @attachment } @topic_all_options = course.discussion_topics.create!(all_discussion_options_enabled) @topic_no_options = course.discussion_topics.create!(title: "no options enabled - topic", message: "test") end it "displays all selected options correctly" do get "/courses/#{course.id}/discussion_topics/#{@topic_all_options.id}/edit" expect(f("input[value='full_anonymity']").selected?).to be_truthy expect(f("input[value='full_anonymity']").attribute("disabled")).to be_nil expect(f("input[value='must-respond-before-viewing-replies']").selected?).to be_truthy expect(f("input[value='enable-podcast-feed']").selected?).to be_truthy expect(f("input[value='include-student-replies-in-podcast-feed']").selected?).to be_truthy expect(f("input[value='allow-liking']").selected?).to be_truthy expect(f("input[value='only-graders-can-like']").selected?).to be_truthy expect(f("input[value='add-to-student-to-do']").selected?).to be_truthy # Just checking for a value. Formatting and TZ differences between front-end and back-end # makes an exact comparison too fragile. unless Account.site_admin.feature_enabled?(:selective_release_ui_api) expect(ff("input[placeholder='Select Date']")[0].attribute("value")).to be_truthy expect(ff("input[placeholder='Select Date']")[1].attribute("value")).to be_truthy end end it "does not display the grading and groups not supported in anonymous discussions message in the edit page" do get "/courses/#{course.id}/discussion_topics/#{@topic_all_options.id}/edit" expect(f("input[value='full_anonymity']").selected?).to be_truthy expect(f("input[value='full_anonymity']").attribute("disabled")).to be_nil expect(f("body")).not_to contain_jqcss("[data-testid=groups_grading_not_allowed]") end it "displays all unselected options correctly" do get "/courses/#{course.id}/discussion_topics/#{@topic_no_options.id}/edit" expect(f("input[value='full_anonymity']").selected?).to be_falsey expect(f("input[value='full_anonymity']").attribute("disabled")).to be_nil # There are less checks here because certain options are only visible if their parent input is selected expect(f("input[value='must-respond-before-viewing-replies']").selected?).to be_falsey expect(f("input[value='enable-podcast-feed']").selected?).to be_falsey expect(f("input[value='allow-liking']").selected?).to be_falsey expect(f("input[value='add-to-student-to-do']").selected?).to be_falsey # Just checking for a value. Formatting and TZ differences between front-end and back-end # makes an exact comparison too fragile. unless Account.site_admin.feature_enabled?(:selective_release_ui_api) expect(ff("input[placeholder='Select Date']")[0].attribute("value")).to eq("") expect(ff("input[placeholder='Select Date']")[1].attribute("value")).to eq("") end end context "usage rights" do before do course.root_account.enable_feature!(:usage_rights_discussion_topics) course.update!(usage_rights_required: true) usage_rights = @course.usage_rights.create! use_justification: "creative_commons", legal_copyright: "(C) 2014 XYZ Corp", license: "cc_by_nd" @attachment.usage_rights = usage_rights @attachment.save! end it "displays correct usage rights" do get "/courses/#{course.id}/discussion_topics/#{@topic_all_options.id}/edit" expect(f("button[data-testid='usage-rights-icon']")).to be_truthy f("button[data-testid='usage-rights-icon']").find_element(css: "svg") # Verify that the correct icon appears expect(f("button[data-testid='usage-rights-icon']").find_element(css: "svg").attribute("name")).to eq "IconFilesCreativeCommons" f("button[data-testid='usage-rights-icon']").click expect(f("input[data-testid='usage-select']").attribute("value")).to eq "The material is licensed under Creative Commons" expect(f("input[data-testid='cc-license-select']").attribute("value")).to eq "CC Attribution No Derivatives" expect(f("input[data-testid='legal-copyright']").attribute("value")).to eq "(C) 2014 XYZ Corp" end end it "saves all changes correctly" do get "/courses/#{course.id}/discussion_topics/#{@topic_all_options.id}/edit" replace_content(f("input[placeholder='Topic Title']"), "new title", { tab_out: true }) clear_tiny(f("#discussion-topic-message-body"), "discussion-topic-message-body_ifr") type_in_tiny("#discussion-topic-message-body", "new message") driver.action.move_to(f("span[data-testid='removable-item']")).perform f("[data-testid='remove-button']").click _, fullpath, _data = get_file("testfile5.zip") f("[data-testid='attachment-input']").send_keys(fullpath) unless Account.site_admin.feature_enabled?(:selective_release_ui_api) f("button[title='Remove All Sections']").click f("input[data-testid='section-select']").click fj("li:contains('value for name')").click end # we can change anonymity on edit, if there is no reply expect(ffj("fieldset:contains('Anonymous Discussion') input[type=radio]").count).to eq 3 force_click_native("input[value='must-respond-before-viewing-replies']") force_click_native("input[value='enable-podcast-feed']") expect(f("body")).not_to contain_jqcss("input[value='include-student-replies-in-podcast-feed']") force_click_native("input[value='allow-liking']") expect(f("body")).not_to contain_jqcss("input[value='only-graders-can-like']") force_click_native("input[value='add-to-student-to-do']") fj("button:contains('Save')").click @topic_all_options.reload expect(@topic_all_options.title).to eq "new title" expect(@topic_all_options.message).to include "new message" expect(@topic_all_options.attachment_id).to eq Attachment.last.id unless Account.site_admin.feature_enabled?(:selective_release_ui_api) expect(@topic_all_options.is_section_specific).to be_truthy end expect(@topic_all_options.require_initial_post).to be_falsey expect(@topic_all_options.podcast_enabled).to be_falsey expect(@topic_all_options.allow_rating).to be_falsey expect(@topic_all_options.only_graders_can_rate).to be_falsey expect(@topic_all_options.todo_date).to be_nil end # ignore js errors in unrelated discussion show page it "preserves URL query parameters on CANCEL", :ignore_js_errors do get "/courses/#{course.id}/discussion_topics/#{@topic_all_options.id}/edit?embed=true" fj("button:contains('Cancel')").click wait_for_ajaximations expect(driver.current_url).not_to include("edit") expect(driver.current_url).to include("?embed=true") end context "with selective_release_backend and selective_release_ui_api enabled" do before :once do Account.site_admin.enable_feature!(:selective_release_backend) Account.site_admin.enable_feature!(:selective_release_ui_api) end it "does not show the assign to UI when the user does not have permission even if user can access edit page" do # i.e., they have moderate_forum permission but not admin or unrestricted student enrollment RoleOverride.create!(context: @course.account, permission: "moderate_forum", role: student_role, enabled: true) student_in_course(active_all: true) user_session(@student) get "/courses/#{course.id}/discussion_topics/#{@topic_no_options.id}/edit" expect(element_exists?(Discussion.assign_to_button_selector)).to be_truthy enrollment = @course.enrollments.find_by(user: @student) enrollment.update!(limit_privileges_to_course_section: true) get "/courses/#{course.id}/discussion_topics/#{@topic_no_options.id}/edit" expect(element_exists?(Discussion.assign_to_button_selector)).to be_falsey end it "does not display 'Assign To' section for an ungraded group discussion" do group = course.groups.create!(name: "group") group_ungraded = course.discussion_topics.create!(title: "no options enabled - topic", group_category: group.group_category) get "/courses/#{course.id}/discussion_topics/#{group_ungraded.id}/edit" expect(Discussion.select_date_input_exists?).to be_truthy expect(element_exists?(Discussion.assign_to_button_selector)).to be_falsey end it "does not display 'Post To' section and Available From/Until inputs" do get "/courses/#{course.id}/discussion_topics/#{@topic_no_options.id}/edit" expect(Discussion.select_date_input_exists?).to be_falsey expect(Discussion.section_selection_input_exists?).to be_falsey end it "updates overrides using 'Assign To' tray", :ignore_js_errors do student1 = course.enroll_student(User.create!, enrollment_state: "active").user available_from = 5.days.ago available_until = 5.days.from_now get "/courses/#{course.id}/discussion_topics/#{@topic_no_options.id}/edit" Discussion.click_assign_to_button wait_for_assign_to_tray_spinner keep_trying_until { expect(item_tray_exists?).to be_truthy } click_add_assign_to_card expect(element_exists?(due_date_input_selector)).to be_falsey select_module_item_assignee(1, student1.name) update_available_date(1, format_date_for_view(available_from, "%-m/%-d/%Y"), true) update_available_time(1, "8:00 AM", true) update_until_date(1, format_date_for_view(available_until, "%-m/%-d/%Y"), true) update_until_time(1, "9:00 PM", true) click_save_button("Apply") keep_trying_until { expect(element_exists?(module_item_edit_tray_selector)).to be_falsey } Discussion.save_button.click wait_for_ajaximations @topic_no_options.reload new_override = @topic_no_options.active_assignment_overrides.last expect(new_override.set_type).to eq("ADHOC") expect(new_override.set_id).to be_nil expect(new_override.set.map(&:id)).to match_array([student1.id]) end it "shows pending changes when overrides have been added", :ignore_js_errors, custom_timeout: 45 do student1 = course.enroll_student(User.create!, enrollment_state: "active").user available_from = 5.days.ago available_until = 5.days.from_now get "/courses/#{course.id}/discussion_topics/#{@topic_no_options.id}/edit" Discussion.click_assign_to_button wait_for_assign_to_tray_spinner keep_trying_until { expect(item_tray_exists?).to be_truthy } click_add_assign_to_card select_module_item_assignee(1, student1.name) update_available_date(1, format_date_for_view(available_from, "%-m/%-d/%Y"), true) update_available_time(1, "8:00 AM", true) update_until_date(1, format_date_for_view(available_until, "%-m/%-d/%Y"), true) update_until_time(1, "9:00 PM", true) click_save_button("Apply") keep_trying_until { expect(element_exists?(module_item_edit_tray_selector)).to be_falsey } expect(Discussion.pending_changes_pill_exists?).to be_truthy end it "shows no pending changes when override tray cancelled", :ignore_js_errors do student1 = course.enroll_student(User.create!, enrollment_state: "active").user available_from = 5.days.ago available_until = 5.days.from_now get "/courses/#{course.id}/discussion_topics/#{@topic_no_options.id}/edit" Discussion.click_assign_to_button wait_for_assign_to_tray_spinner keep_trying_until { expect(item_tray_exists?).to be_truthy } click_add_assign_to_card select_module_item_assignee(1, student1.name) update_available_date(1, format_date_for_view(available_from, "%-m/%-d/%Y"), true) update_available_time(1, "8:00 AM", true) update_until_date(1, format_date_for_view(available_until, "%-m/%-d/%Y"), true) update_until_time(1, "9:00 PM", true) click_cancel_button keep_trying_until { expect(element_exists?(module_item_edit_tray_selector)).to be_falsey } expect(Discussion.pending_changes_pill_exists?).to be_falsey end it "transitions from ungraded to graded and overrides are ok", :ignore_js_errors do discussion_topic = DiscussionHelpers.create_discussion_topic( course, teacher, "Teacher Discussion 1 Title", "Teacher Discussion 1 message", nil ) student1 = course.enroll_student(User.create!, enrollment_state: "active").user available_from = 5.days.ago available_until = 5.days.from_now discussion_topic.assignment_overrides.create!(set_type: "ADHOC", unlock_at: available_from, lock_at: available_until) discussion_topic.assignment_overrides.last.assignment_override_students.create!(user: student1) get "/courses/#{course.id}/discussion_topics/#{discussion_topic.id}/edit" expect(is_checked(Discussion.graded_checkbox)).to be_falsey Discussion.click_graded_checkbox Discussion.save_button.click wait_for_ajaximations get "/courses/#{course.id}/discussion_topics/#{discussion_topic.id}/edit" Discussion.click_assign_to_button wait_for_assign_to_tray_spinner keep_trying_until { expect(item_tray_exists?).to be_truthy } expect(assign_to_due_date(1).attribute("value")).to eq("") expect(assign_to_due_time(1).attribute("value")).to eq("") expect(assign_to_available_from_date(1).attribute("value")).to eq(format_date_for_view(available_from, "%b %-e, %Y")) expect(assign_to_available_from_time(1).attribute("value")).to eq(available_from.strftime("%-l:%M %p")) expect(assign_to_until_date(1).attribute("value")).to eq(format_date_for_view(available_until, "%b %-e, %Y")) expect(assign_to_until_time(1).attribute("value")).to eq(available_until.strftime("%-l:%M %p")) end it "transitions from graded to ungraded and overrides are ok", :ignore_js_errors do discussion_assignment_options = { name: "assignment", points_possible: 10, } discussion_topic = create_graded_discussion(course, discussion_assignment_options) student1 = course.enroll_student(User.create!, enrollment_state: "active").user due_at = 3.days.from_now available_from = 5.days.ago available_until = 5.days.from_now @discussion_assignment.assignment_overrides.create!(set_type: "ADHOC", due_at:, unlock_at: available_from, lock_at: available_until) @discussion_assignment.assignment_overrides.last.assignment_override_students.create!(user: student1) get "/courses/#{course.id}/discussion_topics/#{discussion_topic.id}/edit" expect(is_checked(Discussion.graded_checkbox)).to be_truthy Discussion.click_graded_checkbox Discussion.save_button.click wait_for_ajaximations get "/courses/#{course.id}/discussion_topics/#{discussion_topic.id}/edit" Discussion.click_assign_to_button wait_for_assign_to_tray_spinner keep_trying_until { expect(item_tray_exists?).to be_truthy } expect(assign_to_date_and_time[1].text).not_to include("Due Date") expect(assign_to_available_from_date(1, true).attribute("value")).to eq(format_date_for_view(available_from, "%b %-e, %Y")) expect(assign_to_available_from_time(1, true).attribute("value")).to eq(available_from.strftime("%-l:%M %p")) expect(assign_to_until_date(1, true).attribute("value")).to eq(format_date_for_view(available_until, "%b %-e, %Y")) expect(assign_to_until_time(1, true).attribute("value")).to eq(available_until.strftime("%-l:%M %p")) end it "does not recover a deleted card when adding an assignee", :ignore_js_errors do # Bug fix of LX-1619 student1 = course.enroll_student(User.create!, enrollment_state: "active").user get "/courses/#{course.id}/discussion_topics/#{@topic_all_options.id}/edit" Discussion.click_assign_to_button wait_for_assign_to_tray_spinner keep_trying_until { expect(item_tray_exists?).to be_truthy } click_add_assign_to_card click_delete_assign_to_card(0) select_module_item_assignee(0, student1.name) expect(selected_assignee_options.count).to be(1) end end end context "ungraded group" do it "displays the selected group category correctly" do group_category = course.group_categories.create!(name: "group category 1") discussion_with_group_category = { title: "value for title", message: "value for message", group_category_id: group_category.id, } group_topic = course.discussion_topics.create!(discussion_with_group_category) group_topic.update!(group_category_id: group_category.id) get "/courses/#{course.id}/discussion_topics/#{group_topic.id}/edit" expect(f("input[value='group-discussion']").selected?).to be_truthy expect(f("input[placeholder='Select a group category']").attribute("title")).to eq group_category.name end end context "announcement" do before do # TODO: Update to cover: file attachments and any other options later implemented all_announcement_options = { title: "value for title", message: "value for message", delayed_post_at: "Thu, 16 Nov 2023 17:00:00.000000000 UTC +00:00", podcast_enabled: true, podcast_has_student_posts: true, require_initial_post: true, discussion_type: "side_comment", allow_rating: true, only_graders_can_rate: true, locked: false, } @announcement_all_options = course.announcements.create!(all_announcement_options) # In this case, locked: true displays itself as an unchecked "allow participants to comment" option @announcement_no_options = course.announcements.create!({ title: "no options", message: "nothing else", locked: true }) end it "displays all selected options correctly" do get "/courses/#{course.id}/discussion_topics/#{@announcement_all_options.id}/edit" expect(f("input[value='enable-participants-commenting']").selected?).to be_truthy expect(f("input[value='allow-liking']").selected?).to be_truthy expect(f("input[value='only-graders-can-like']").selected?).to be_truthy expect(f("input[value='enable-podcast-feed']").selected?).to be_truthy expect(f("input[value='include-student-replies-in-podcast-feed']").selected?).to be_truthy # Just checking for a value. Formatting and TZ differences between front-end and back-end # makes an exact comparison too fragile. expect(ff("input[placeholder='Select Date']")[0].attribute("value")).to be_truthy end it "displays all unselected options correctly" do get "/courses/#{course.id}/discussion_topics/#{@announcement_no_options.id}/edit" expect(f("input[value='enable-participants-commenting']").selected?).to be_falsey expect(f("input[value='allow-liking']").selected?).to be_falsey expect(f("input[value='enable-podcast-feed']").selected?).to be_falsey end end context "graded" do it "displays graded assignment options correctly when initially opening edit page with archived grading schemes disabled" do Account.site_admin.disable_feature!(:archived_grading_schemes) grading_standard = course.grading_standards.create!(title: "Win/Lose", data: [["Winner", 0.94], ["Loser", 0]]) # Create a grading standard and make sure it is selected discussion_assignment_options = { name: "assignment", points_possible: 10, grading_type: "letter_grade", assignment_group: course.assignment_groups.create!(name: "assignment group"), grading_standard_id: grading_standard.id, only_visible_to_overrides: true, } discussion_assignment_peer_review_options = { peer_reviews: true, automatic_peer_reviews: true, peer_reviews_due_at: 1.day.ago, peer_review_count: 2, } discussion_assignment_options = discussion_assignment_options.merge(discussion_assignment_peer_review_options) graded_discussion = create_graded_discussion(course, discussion_assignment_options) course_override_due_date = 5.days.from_now course_section = course.course_sections.create!(name: "section alpha") graded_discussion.assignment.assignment_overrides.create!(set_type: "CourseSection", set_id: course_section.id, due_at: course_override_due_date) get "/courses/#{course.id}/discussion_topics/#{graded_discussion.id}/edit" # Grading scheme sub menu is selected expect(fj("span:contains('#{grading_standard.title}')").present?).to be_truthy expect(fj("span:contains('Manage All Grading Schemes')").present?).to be_truthy # Graded checkbox expect(is_checked(f("input[data-testid='graded-checkbox']"))).to be_truthy # Points possible expect(f("input[data-testid='points-possible-input']").attribute("value")).to eq "10" # Grading type expect(f("input[data-testid='display-grade-input']").attribute("value")).to eq "Letter Grade" # Assignment Group expect(f("input[data-testid='assignment-group-input']").attribute("value")).to eq "assignment group" # Peer review checkboxes expect(is_checked(f("input[data-testid='peer_review_manual']"))).to be_falsey expect(is_checked(f("input[data-testid='peer_review_off']"))).to be_falsey expect(is_checked(f("input[data-testid='peer_review_auto']"))).to be_truthy # peer review count expect(f("input[data-testid='peer-review-count-input']").attribute("value")).to eq "2" # Peer review date # Just checking for a value. Formatting and TZ differences between front-end and back-end # makes an exact comparison too fragile. expect(ff("input[placeholder='Select Date']")[0].attribute("value")).not_to be_empty if Account.site_admin.feature_enabled?(:selective_release_ui_api) expect(Discussion.assign_to_button).to be_displayed Discussion.assign_to_button.click expect(assign_to_in_tray("Remove #{course_section.name}")[0]).to be_displayed else expect(f("span[data-testid='assign-to-select-span']").present?).to be_truthy expect(fj("span:contains('#{course_section.name}')").present?).to be_truthy # Verify that the only_visible_to_overrides field is being respected expect(f("body")).not_to contain_jqcss("span:contains('Everyone')") # Just checking for a value. Formatting and TZ differences between front-end and back-end # makes an exact comparison too fragile. expect(f("input[placeholder='Select Assignment Due Date']").attribute("value")).not_to be_empty end end it "allows settings a graded discussion to an ungraded discussion" do graded_discussion = create_graded_discussion(course) get "/courses/#{course.id}/discussion_topics/#{graded_discussion.id}/edit" # Uncheck the "graded" checkbox force_click_native('input[type=checkbox][value="graded"]') fj("button:contains('Save')").click expect(DiscussionTopic.last.assignment).to be_nil end it "sets the mark important dates checkbox for discussion edit when differentiated modules ff is off" do feature_setup graded_discussion = create_graded_discussion(course) course_override_due_date = 5.days.from_now course_section = course.course_sections.create!(name: "section alpha") graded_discussion.assignment.assignment_overrides.create!(set_type: "CourseSection", set_id: course_section.id, due_at: course_override_due_date) get "/courses/#{course.id}/discussion_topics/#{graded_discussion.id}/edit" expect(mark_important_dates).to be_displayed scroll_to_element(mark_important_dates) click_mark_important_dates Discussion.save_button.click wait_for_ajaximations assignment = Assignment.last expect(assignment.important_dates).to be(true) end context "with archived grading schemes enabled" do before do Account.site_admin.enable_feature!(:grading_scheme_updates) Account.site_admin.enable_feature!(:archived_grading_schemes) @course = course @account = @course.account @active_grading_standard = @course.grading_standards.create!(title: "Active Grading Scheme", data: { "A" => 0.9, "F" => 0 }, scaling_factor: 1.0, points_based: false, workflow_state: "active") @archived_grading_standard = @course.grading_standards.create!(title: "Archived Grading Scheme", data: { "A" => 0.9, "F" => 0 }, scaling_factor: 1.0, points_based: false, workflow_state: "archived") @account_grading_standard = @account.grading_standards.create!(title: "Account Grading Scheme", data: { "A" => 0.9, "F" => 0 }, scaling_factor: 1.0, points_based: false, workflow_state: "active") discussion_assignment_options = { name: "assignment", points_possible: 10, grading_type: "letter_grade", assignment_group: course.assignment_groups.create!(name: "assignment group"), only_visible_to_overrides: true, } @graded_discussion = create_graded_discussion(course, discussion_assignment_options) @assignment = @graded_discussion.assignment end it "shows archived grading scheme if it is the course default twice, once to follow course default scheme and once to choose that scheme to use" do @course.update!(grading_standard_id: @archived_grading_standard.id) @course.reload get "/courses/#{@course.id}/discussion_topics/#{@graded_discussion.id}/edit" wait_for_ajaximations expect(f("[data-testid='grading-schemes-selector-dropdown']").attribute("title")).to eq(@archived_grading_standard.title + " (course default)") f("[data-testid='grading-schemes-selector-dropdown']").click expect(f("[data-testid='grading-schemes-selector-option-#{@course.grading_standard.id}']")).to include_text(@course.grading_standard.title) end it "shows archived grading scheme if it is the current assignment grading standard" do @assignment.update!(grading_standard_id: @archived_grading_standard.id) @assignment.reload get "/courses/#{@course.id}/discussion_topics/#{@graded_discussion.id}/edit" wait_for_ajaximations expect(f("[data-testid='grading-schemes-selector-dropdown']").attribute("title")).to eq(@archived_grading_standard.title) end it "removes grading schemes from dropdown after archiving them but still shows them upon reopening the modal" do get "/courses/#{@course.id}/discussion_topics/#{@graded_discussion.id}/edit" wait_for_ajaximations f("[data-testid='grading-schemes-selector-dropdown']").click expect(f("[data-testid='grading-schemes-selector-option-#{@active_grading_standard.id}']")).to be_present f("[data-testid='manage-all-grading-schemes-button']").click wait_for_ajaximations f("[data-testid='grading-scheme-#{@active_grading_standard.id}-archive-button']").click wait_for_ajaximations f("[data-testid='manage-all-grading-schemes-close-button']").click wait_for_ajaximations f("[data-testid='grading-schemes-selector-dropdown']").click expect(f("[data-testid='grading-schemes-selector-dropdown-form']")).not_to contain_css("[data-testid='grading-schemes-selector-option-#{@active_grading_standard.id}']") f("[data-testid='manage-all-grading-schemes-button']").click wait_for_ajaximations expect(f("[data-testid='grading-scheme-row-#{@active_grading_standard.id}']").text).to be_present end it "shows all archived schemes in the manage grading schemes modal" do archived_gs1 = @course.grading_standards.create!(title: "Archived Grading Scheme 1", data: { "A" => 0.9, "F" => 0 }, scaling_factor: 1.0, points_based: false, workflow_state: "archived") archived_gs2 = @course.grading_standards.create!(title: "Archived Grading Scheme 2", data: { "A" => 0.9, "F" => 0 }, scaling_factor: 1.0, points_based: false, workflow_state: "archived") archived_gs3 = @course.grading_standards.create!(title: "Archived Grading Scheme 3", data: { "A" => 0.9, "F" => 0 }, scaling_factor: 1.0, points_based: false, workflow_state: "archived") get "/courses/#{@course.id}/discussion_topics/#{@graded_discussion.id}/edit" wait_for_ajaximations f("[data-testid='manage-all-grading-schemes-button']").click wait_for_ajaximations expect(f("[data-testid='grading-scheme-#{archived_gs1.id}-name']")).to include_text(archived_gs1.title) expect(f("[data-testid='grading-scheme-#{archived_gs2.id}-name']")).to include_text(archived_gs2.title) expect(f("[data-testid='grading-scheme-#{archived_gs3.id}-name']")).to include_text(archived_gs3.title) end it "will still show the assignment grading scheme if you archive it on the edit page in the management modal and persist on reload" do @assignment.update!(grading_standard_id: @active_grading_standard.id) @assignment.reload get "/courses/#{@course.id}/discussion_topics/#{@graded_discussion.id}/edit" wait_for_ajaximations expect(f("[data-testid='grading-schemes-selector-dropdown']").attribute("title")).to eq(@active_grading_standard.title) f("[data-testid='manage-all-grading-schemes-button']").click wait_for_ajaximations f("[data-testid='grading-scheme-#{@active_grading_standard.id}-archive-button']").click wait_for_ajaximations f("[data-testid='manage-all-grading-schemes-close-button']").click wait_for_ajaximations expect(f("[data-testid='grading-schemes-selector-dropdown']").attribute("title")).to eq(@active_grading_standard.title) get "/courses/#{@course.id}/assignments/#{@assignment.id}/edit" wait_for_ajaximations expect(f("[data-testid='grading-schemes-selector-dropdown']").attribute("title")).to eq(@active_grading_standard.title) end it "creates a discussion topic with selected grading scheme/standard" do grading_standard = @course.grading_standards.create!(title: "Win/Lose", data: [["Winner", 0.94], ["Loser", 0]]) get "/courses/#{@course.id}/discussion_topics/#{@graded_discussion.id}/edit" wait_for_ajaximations f("[data-testid='grading-schemes-selector-dropdown']").click f("[data-testid='grading-schemes-selector-option-#{grading_standard.id}']").click f("[data-testid='save-button']").click wait_for_ajaximations expect_new_page_load { f("[data-testid='continue-button']").click } a = DiscussionTopic.last.assignment expect(a.grading_standard_id).to eq grading_standard.id end end it "allows editing the assignment group for the graded discussion" do assign_group_2 = course.assignment_groups.create!(name: "Group 2") get "/courses/#{course.id}/discussion_topics/#{assignment_topic.id}/edit" force_click_native("input[title='assignment group']") wait_for(method: nil, timeout: 3) { fj("li:contains('Group 2')").present? } fj("li:contains('Group 2')").click fj("button:contains('Save')").click expect(assignment.reload.assignment_group_id).to eq assign_group_2.id end it "allows editing the points possible, grading type, group category, and peer review for the graded discussion" do pp_string = "80" group_cat = course.group_categories.create!(name: "another group set") get "/courses/#{course.id}/discussion_topics/#{assignment_topic.id}/edit" # change points possible from 10 to 80. selenium's clear method does not completely remove the previous value # so we use backspace instead pp_string.each_char { f("input[data-testid='points-possible-input']").send_keys(:backspace) } f("input[data-testid='points-possible-input']").send_keys(pp_string) force_click_native("input[value='Points']") fj("li:contains('Letter Grade')").click force_click_native("input[data-testid='peer_review_manual']") force_click_native("input[data-testid='group-discussion-checkbox']") force_click_native("input[placeholder='Select a group category']") fj("li:contains('#{group_cat.name}')").click fj("button:contains('Save')").click updated_assignment = assignment.reload expect(updated_assignment.points_possible).to eq 80 expect(updated_assignment.grading_type).to eq "letter_grade" expect(updated_assignment.peer_reviews).to be true expect(updated_assignment.automatic_peer_reviews).to be false expect(updated_assignment.peer_reviews_assign_at).to be_nil expect(assignment.effective_group_category_id).to eq group_cat.id end it "adds an attachment to a graded discussion" do get "/courses/#{course.id}/discussion_topics/#{assignment_topic.id}/edit" _filename, fullpath, _data = get_file("testfile5.zip") f("input[data-testid='attachment-input']").send_keys(fullpath) fj("button:contains('Save')").click expect(assignment_topic.reload.attachment_id).to eq Attachment.last.id end context "differentiated modules Feature Flag", :ignore_js_errors do before do differentiated_modules_on @student1 = student_in_course(course:, active_all: true).user @student2 = student_in_course(course:, active_all: true).user @course_section = course.course_sections.create!(name: "section alpha") @course_section_2 = course.course_sections.create!(name: "section Beta") end it "displays Everyone correctly", custom_timeout: 30 do graded_discussion = create_graded_discussion(course) due_date = "Sat, 06 Apr 2024 00:00:00.000000000 UTC +00:00" unlock_at = "Fri, 05 Apr 2024 00:00:00.000000000 UTC +00:00" lock_at = "Sun, 07 Apr 2024 00:00:00.000000000 UTC +00:00" graded_discussion.assignment.update!(due_at: due_date, unlock_at:, lock_at:) # Open page and assignTo tray get "/courses/#{course.id}/discussion_topics/#{graded_discussion.id}/edit" Discussion.assign_to_button.click # Expect check card and override count/content expect(module_item_assign_to_card.count).to eq 1 expect(selected_assignee_options.count).to eq 1 expect(selected_assignee_options.first.find("span").text).to eq "Everyone" # Find the date inputs, extract their values, combine date and time values, and parse into DateTime objects displayed_override_dates = all_displayed_assign_to_date_and_time # Check that the due dates are correctly displayed expect(displayed_override_dates.include?(DateTime.parse(due_date))).to be_truthy expect(displayed_override_dates.include?(DateTime.parse(unlock_at))).to be_truthy expect(displayed_override_dates.include?(DateTime.parse(lock_at))).to be_truthy end it "displays everyone and section and student overrides correctly", custom_timeout: 30 do graded_discussion = create_graded_discussion(course) # Create overrides # Card 1 = ["Everyone else"], Set by: only_visible_to_overrides: false # Card 2 graded_discussion.assignment.assignment_overrides.create!(set_type: "CourseSection", set_id: @course_section.id) # Card 3 graded_discussion.assignment.assignment_overrides.create!(set_type: "CourseSection", set_id: @course_section_2.id) # Card 4 graded_discussion.assignment.assignment_overrides.create!(set_type: "ADHOC") graded_discussion.assignment.assignment_overrides.last.assignment_override_students.create!(user: @student1) graded_discussion.assignment.assignment_overrides.last.assignment_override_students.create!(user: @student2) # Open edit page and AssignTo Tray get "/courses/#{course.id}/discussion_topics/#{graded_discussion.id}/edit" Discussion.assign_to_button.click # Check that displayed cards and overrides are correct expect(module_item_assign_to_card.count).to eq 4 displayed_overrides = module_item_assign_to_card.map do |card| card.find_all(assignee_selected_option_selector).map(&:text) end expected_overrides = generate_expected_overrides(graded_discussion.assignment) expect(displayed_overrides).to match_array(expected_overrides) end it "displays visible to overrides only correctly", custom_timeout: 30 do # The main difference in this test is that only_visible_to_overrides is true discussion_assignment_options = { only_visible_to_overrides: true, } graded_discussion = create_graded_discussion(course, discussion_assignment_options) # Create overrides # Card 1 graded_discussion.assignment.assignment_overrides.create!(set_type: "CourseSection", set_id: @course_section.id) # Card 2 graded_discussion.assignment.assignment_overrides.create!(set_type: "CourseSection", set_id: @course_section_2.id) # Card 3 graded_discussion.assignment.assignment_overrides.create!(set_type: "ADHOC") graded_discussion.assignment.assignment_overrides.last.assignment_override_students.create!(user: @student1) graded_discussion.assignment.assignment_overrides.last.assignment_override_students.create!(user: @student2) # Open edit page and AssignTo Tray get "/courses/#{course.id}/discussion_topics/#{graded_discussion.id}/edit" Discussion.assign_to_button.click # Check that displayed cards and overrides are correct expect(module_item_assign_to_card.count).to eq 3 displayed_overrides = module_item_assign_to_card.map do |card| card.find_all(assignee_selected_option_selector).map(&:text) end expected_overrides = generate_expected_overrides(graded_discussion.assignment) expect(displayed_overrides).to match_array(expected_overrides) end it "allows adding overrides" do graded_discussion = create_graded_discussion(course) due_date = "Sat, 06 Apr 2024 00:00:00.000000000 UTC +00:00" unlock_at = "Fri, 05 Apr 2024 00:00:00.000000000 UTC +00:00" lock_at = "Sun, 07 Apr 2024 00:00:00.000000000 UTC +00:00" graded_discussion.assignment.update!(due_at: due_date, unlock_at:, lock_at:) course.reload # Open page and assignTo tray get "/courses/#{course.id}/discussion_topics/#{graded_discussion.id}/edit" Discussion.assign_to_button.click wait_for_assign_to_tray_spinner keep_trying_until { expect(item_tray_exists?).to be_truthy } click_add_assign_to_card select_module_item_assignee(1, @student1.name) click_save_button("Apply") keep_trying_until { expect(element_exists?(module_item_edit_tray_selector)).to be_falsey } expect(AssignmentCreateEditPage.pending_changes_pill_exists?).to be_truthy Discussion.save_button.click wait_for_ajaximations assignment = Assignment.last expect(assignment.assignment_overrides.active.count).to eq 1 end it "allows removing overrides" do graded_discussion = create_graded_discussion(course) due_date = "Sat, 06 Apr 2024 00:00:00.000000000 UTC +00:00" unlock_at = "Fri, 05 Apr 2024 00:00:00.000000000 UTC +00:00" lock_at = "Sun, 07 Apr 2024 00:00:00.000000000 UTC +00:00" graded_discussion.assignment.update!(due_at: due_date, unlock_at:, lock_at:) # Create section override course_section = course.course_sections.create!(name: "section alpha") graded_discussion.assignment.assignment_overrides.create!(set_type: "CourseSection", set_id: course_section.id) course.reload # Open page and assignTo tray get "/courses/#{course.id}/discussion_topics/#{graded_discussion.id}/edit" Discussion.assign_to_button.click wait_for_assign_to_tray_spinner keep_trying_until { expect(item_tray_exists?).to be_truthy } # Remove the section override click_delete_assign_to_card(1) click_save_button("Apply") keep_trying_until { expect(element_exists?(module_item_edit_tray_selector)).to be_falsey } expect(AssignmentCreateEditPage.pending_changes_pill_exists?).to be_truthy Discussion.save_button.click wait_for_ajaximations assignment = Assignment.last assignment.reload expect(assignment.assignment_overrides.active.count).to eq 0 expect(assignment.only_visible_to_overrides).to be_falsey end it "displays module overrides correctly" do graded_discussion = create_graded_discussion(course) module1 = course.context_modules.create!(name: "Module 1") graded_discussion.context_module_tags.create! context_module: module1, context: course, tag_type: "context_module" override = module1.assignment_overrides.create! override.assignment_override_students.create!(user: @student1) # Open page and assignTo tray get "/courses/#{course.id}/discussion_topics/#{graded_discussion.id}/edit" Discussion.assign_to_button.click wait_for_assign_to_tray_spinner # Verify that Everyone tag does not appear expect(module_item_assign_to_card.count).to eq 1 expect(module_item_assign_to_card[0].find_all(assignee_selected_option_selector).map(&:text)).to eq ["User"] expect(inherited_from.last.text).to eq("Inherited from #{module1.name}") # Update the inherited card due date, should remove inherited update_due_date(0, "12/31/2022") update_due_time(0, "5:00 PM") click_save_button("Apply") Discussion.assign_to_button.click expect(f("body")).not_to contain_jqcss(inherited_from_selector) click_save_button("Apply") Discussion.save_button.click Discussion.section_warning_continue_button.click wait_for_ajaximations # Expect the module override to be overridden and not appear get "/courses/#{course.id}/discussion_topics/#{graded_discussion.id}/edit" Discussion.assign_to_button.click expect(module_item_assign_to_card.count).to eq 1 expect(f("body")).not_to contain_jqcss(inherited_from_selector) end it "displays module and course overrides correctly" do graded_discussion = create_graded_discussion(course) module1 = course.context_modules.create!(name: "Module 1") graded_discussion.context_module_tags.create! context_module: module1, context: course, tag_type: "context_module" override = module1.assignment_overrides.create! override.assignment_override_students.create!(user: @student1) graded_discussion.assignment.assignment_overrides.create!(set: course, due_at: 1.day.from_now) # Open page and assignTo tray get "/courses/#{course.id}/discussion_topics/#{graded_discussion.id}/edit" Discussion.assign_to_button.click wait_for_assign_to_tray_spinner # Verify that Everyone tag does not appear expect(module_item_assign_to_card.count).to eq 2 expect(module_item_assign_to_card[0].find_all(assignee_selected_option_selector).map(&:text)).to eq ["Everyone else"] expect(module_item_assign_to_card[1].find_all(assignee_selected_option_selector).map(&:text)).to eq ["User"] expect(inherited_from.last.text).to eq("Inherited from #{module1.name}") end it "creates a course override if everyone is added with a module override" do graded_discussion = create_graded_discussion(course) module1 = course.context_modules.create!(name: "Module 1") graded_discussion.context_module_tags.create! context_module: module1, context: course, tag_type: "context_module" override = module1.assignment_overrides.create! override.assignment_override_students.create!(user: @student1) # Open page and assignTo tray get "/courses/#{course.id}/discussion_topics/#{graded_discussion.id}/edit" Discussion.assign_to_button.click wait_for_assign_to_tray_spinner # Verify the module override is shown expect(module_item_assign_to_card.count).to eq 1 expect(module_item_assign_to_card[0].find_all(assignee_selected_option_selector).map(&:text)).to eq ["User"] expect(inherited_from.last.text).to eq("Inherited from #{module1.name}") click_add_assign_to_card select_module_item_assignee(1, "Everyone else") click_save_button("Apply") # Save the discussion without changing the inherited module override Discussion.save_button.click Discussion.section_warning_continue_button.click wait_for_ajaximations assignment = graded_discussion.assignment assignment.reload # Expect the existing override to be the module override expect(assignment.assignment_overrides.active.count).to eq 1 expect(assignment.all_assignment_overrides.active.count).to eq 2 expect(assignment.assignment_overrides.first.set_type).to eq "Course" expect(assignment.only_visible_to_overrides).to be_truthy end it "does not display module override if an unassigned override exists" do graded_discussion = create_graded_discussion(course) module1 = course.context_modules.create!(name: "Module 1") graded_discussion.context_module_tags.create! context_module: module1, context: course, tag_type: "context_module" override = module1.assignment_overrides.create! override.assignment_override_students.create!(user: @student1) unassigned_override = graded_discussion.assignment.assignment_overrides.create! unassigned_override.assignment_override_students.create!(user: @student1) unassigned_override.update(unassign_item: true) assigned_override = graded_discussion.assignment.assignment_overrides.create! assigned_override.assignment_override_students.create!(user: @student2) # Open page and assignTo tray get "/courses/#{course.id}/discussion_topics/#{graded_discussion.id}/edit" Discussion.assign_to_button.click wait_for_assign_to_tray_spinner # Verify the module override is not shown expect(module_item_assign_to_card.count).to eq 1 expect(module_item_assign_to_card[0].find_all(assignee_selected_option_selector).map(&:text)).to eq ["User"] expect(module_item_assign_to_card[0]).not_to contain_css(inherited_from_selector) end it "does not create an override if the modules override is not updated" do graded_discussion = create_graded_discussion(course) module1 = course.context_modules.create!(name: "Module 1") graded_discussion.context_module_tags.create! context_module: module1, context: course, tag_type: "context_module" override = module1.assignment_overrides.create! override.assignment_override_students.create!(user: @student1) # Open page and assignTo tray get "/courses/#{course.id}/discussion_topics/#{graded_discussion.id}/edit" Discussion.assign_to_button.click wait_for_assign_to_tray_spinner # Verify the module override is shown expect(module_item_assign_to_card.count).to eq 1 expect(module_item_assign_to_card[0].find_all(assignee_selected_option_selector).map(&:text)).to eq ["User"] expect(inherited_from.last.text).to eq("Inherited from #{module1.name}") click_save_button("Apply") # Save the discussion without changing the inherited module override Discussion.save_button.click Discussion.section_warning_continue_button.click wait_for_ajaximations assignment = graded_discussion.assignment assignment.reload # Expect the existing override to be the module override expect(assignment.assignment_overrides.active.count).to eq 0 expect(assignment.all_assignment_overrides.active.count).to eq 1 expect(assignment.all_assignment_overrides.first.context_module_id).to eq module1.id expect(assignment.only_visible_to_overrides).to be_falsey end it "displays highighted cards correctly" do graded_discussion = create_graded_discussion(course) get "/courses/#{course.id}/discussion_topics/#{graded_discussion.id}/edit" Discussion.assign_to_button.click wait_for_assign_to_tray_spinner # Expect there to be no highlighted cards expect(module_item_assign_to_card.count).to eq 1 expect(f("body")).not_to contain_jqcss(highlighted_card_selector) click_save_button("Apply") # Expect that if no changes were made, that the apply button doens't highlight old cards Discussion.assign_to_button.click wait_for_assign_to_tray_spinner expect(module_item_assign_to_card.count).to eq 1 expect(f("body")).not_to contain_jqcss(highlighted_card_selector) # Expect highlighted card after making a change update_due_date(0, "12/31/2022") click_save_button("Apply") Discussion.assign_to_button.click wait_for_assign_to_tray_spinner expect(highlighted_item_assign_to_card.count).to eq 1 end it "cancels correctly" do graded_discussion = create_graded_discussion(course) # Open page and assignTo tray get "/courses/#{course.id}/discussion_topics/#{graded_discussion.id}/edit" Discussion.assign_to_button.click wait_for_assign_to_tray_spinner # Create a new card, don't apply it click_add_assign_to_card select_module_item_assignee(1, @student1.name) expect(module_item_assign_to_card.count).to eq 2 cancel_button.click # Reopen, expect new card to not be there Discussion.assign_to_button.click expect(module_item_assign_to_card.count).to eq 1 # Add a new card, apply it click_add_assign_to_card select_module_item_assignee(1, @student1.name) click_save_button("Apply") expect(AssignmentCreateEditPage.pending_changes_pill_exists?).to be_truthy # Expect both cards to be there Discussion.assign_to_button.click expect(module_item_assign_to_card.count).to eq 2 end it "sets the mark important dates checkbox for discussion edit" do feature_setup graded_discussion = create_graded_discussion(course) get "/courses/#{course.id}/discussion_topics/#{graded_discussion.id}/edit" Discussion.assign_to_button.click wait_for_assign_to_tray_spinner keep_trying_until { expect(item_tray_exists?).to be_truthy } formatted_date = format_date_for_view(2.days.from_now(Time.zone.now), "%m/%d/%Y") update_due_date(0, formatted_date) update_due_time(0, "5:00 PM") click_save_button("Apply") expect(mark_important_dates).to be_displayed scroll_to_element(mark_important_dates) click_mark_important_dates Discussion.save_button.click wait_for_ajaximations assignment = Assignment.last expect(assignment.important_dates).to be(true) end it "does not show the assign to UI when the user does not have permission even if user can access edit page" do # i.e., they have moderate_forum permission but not manage_assignments_edit discussion = create_graded_discussion(course) get "/courses/#{course.id}/discussion_topics/#{discussion.id}/edit" expect(element_exists?(Discussion.assign_to_button_selector)).to be_truthy RoleOverride.create!(context: @course.account, permission: "manage_assignments_edit", role: teacher_role, enabled: false) get "/courses/#{course.id}/discussion_topics/#{discussion.id}/edit" expect(element_exists?(Discussion.assign_to_button_selector)).to be_falsey end it "does not recover a deleted card when adding an assignee", :ignore_js_errors do # Bug fix of LX-1619 discussion = create_graded_discussion(course) get "/courses/#{course.id}/discussion_topics/#{discussion.id}/edit" Discussion.click_assign_to_button wait_for_assign_to_tray_spinner keep_trying_until { expect(item_tray_exists?).to be_truthy } click_add_assign_to_card click_delete_assign_to_card(0) select_module_item_assignee(0, @course_section_2.name) expect(selected_assignee_options.count).to be(1) end context "checkpoints" do it "shows reply to topic input on graded discussion with sub assignments" do Account.site_admin.enable_feature!(:discussion_checkpoints) @course.root_account.enable_feature!(:discussion_checkpoints) assignment = @course.assignments.create!( name: "Assignment", submission_types: ["online_text_entry"], points_possible: 20 ) assignment.update!(has_sub_assignments: true) assignment.sub_assignments.create!(context: assignment.context, sub_assignment_tag: CheckpointLabels::REPLY_TO_TOPIC, points_possible: 10, due_at: 3.days.from_now) assignment.sub_assignments.create!(context: assignment.context, sub_assignment_tag: CheckpointLabels::REPLY_TO_ENTRY, points_possible: 10, due_at: 5.days.from_now) graded_discussion = @course.discussion_topics.create!( title: "Graded Discussion", discussion_type: "threaded", posted_at: "2017-07-09 16:32:34", user: @teacher, assignment:, reply_to_entry_required_count: 1 ) # Open page and assignTo tray get "/courses/#{@course.id}/discussion_topics/#{graded_discussion.id}/edit" Discussion.assign_to_button.click wait_for_assign_to_tray_spinner expect(module_item_assign_to_card.last).to contain_css(reply_to_topic_due_date_input_selector) end it "shows required replies input on graded discussion with sub assignments" do Account.site_admin.enable_feature!(:discussion_checkpoints) @course.root_account.enable_feature!(:discussion_checkpoints) @student1 = student_in_course(course:, active_all: true).user @student2 = student_in_course(course:, active_all: true).user @course_section = course.course_sections.create!(name: "section alpha") @course_section_2 = course.course_sections.create!(name: "section Beta") # Open page and assignTo tray get "/courses/#{@course.id}/discussion_topics/new" title = "Graded Discussion Topic with letter grade type" message = "replying to topic" f("input[placeholder='Topic Title']").send_keys title type_in_tiny("textarea", message) force_click_native('input[type=checkbox][value="graded"]') force_click_native('input[type=checkbox][value="checkpoints"]') Discussion.assign_to_button.click click_add_assign_to_card click_delete_assign_to_card(0) select_module_item_assignee(0, @course_section_2.name) reply_to_topic_date = 3.days.from_now(Time.zone.now).to_date + 17.hours reply_to_topic_date_formatted = format_date_for_view(reply_to_topic_date, "%m/%d/%Y") update_reply_to_topic_date(0, reply_to_topic_date_formatted) update_reply_to_topic_time(0, "5:00 PM") # required replies required_replies_date = 4.days.from_now(Time.zone.now).to_date + 17.hours required_replies_date_formatted = format_date_for_view(required_replies_date, "%m/%d/%Y") update_required_replies_date(0, required_replies_date_formatted) update_required_replies_time(0, "5:00 PM") # available from available_from_date = 2.days.from_now(Time.zone.now).to_date + 17.hours available_from_date_formatted = format_date_for_view(available_from_date, "%m/%d/%Y") update_available_date(0, available_from_date_formatted, true, false) update_available_time(0, "5:00 PM", true, false) # available until until_date = 5.days.from_now(Time.zone.now).to_date + 17.hours until_date_formatted = format_date_for_view(until_date, "%m/%d/%Y") update_until_date(0, until_date_formatted, true, false) update_until_time(0, "5:00 PM", true, false) click_save_button("Apply") wait_for_assign_to_tray_spinner fj("button:contains('Save')").click Discussion.section_warning_continue_button.click wait_for_ajaximations graded_discussion = DiscussionTopic.last sub_assignments = graded_discussion.assignment.sub_assignments sub_assignment1 = sub_assignments.find_by(sub_assignment_tag: CheckpointLabels::REPLY_TO_TOPIC) sub_assignment2 = sub_assignments.find_by(sub_assignment_tag: CheckpointLabels::REPLY_TO_ENTRY) expect(graded_discussion.assignment.sub_assignments.count).to eq(2) expect(format_date_for_view(sub_assignment1.assignment_overrides.active.first.due_at, "%m/%d/%Y")).to eq(reply_to_topic_date_formatted) expect(format_date_for_view(sub_assignment2.assignment_overrides.active.first.due_at, "%m/%d/%Y")).to eq(required_replies_date_formatted) # renders update get "/courses/#{@course.id}/discussion_topics/#{graded_discussion.id}/edit" Discussion.assign_to_button.click displayed_override_dates = all_displayed_assign_to_date_and_time # Check that the due dates are correctly displayed expect(displayed_override_dates.include?(reply_to_topic_date)).to be_truthy expect(displayed_override_dates.include?(required_replies_date)).to be_truthy expect(displayed_override_dates.include?(available_from_date)).to be_truthy expect(displayed_override_dates.include?(until_date)).to be_truthy # updates dates and saves reply_to_topic_date = 4.days.from_now(Time.zone.now).to_date + 17.hours reply_to_topic_date_formatted = format_date_for_view(reply_to_topic_date, "%m/%d/%Y") update_reply_to_topic_date(0, reply_to_topic_date_formatted) update_reply_to_topic_time(0, "5:00 PM") # required replies required_replies_date = 5.days.from_now(Time.zone.now).to_date + 17.hours required_replies_date_formatted = format_date_for_view(required_replies_date, "%m/%d/%Y") update_required_replies_date(0, required_replies_date_formatted) update_required_replies_time(0, "5:00 PM") # available from available_from_date = 3.days.from_now(Time.zone.now).to_date + 17.hours available_from_date_formatted = format_date_for_view(available_from_date, "%m/%d/%Y") update_available_date(0, available_from_date_formatted, true, false) update_available_time(0, "5:00 PM", true, false) # available until until_date = 6.days.from_now(Time.zone.now).to_date + 17.hours until_date_formatted = format_date_for_view(until_date, "%m/%d/%Y") update_until_date(0, until_date_formatted, true, false) update_until_time(0, "5:00 PM", true, false) click_save_button("Apply") wait_for_assign_to_tray_spinner fj("button:contains('Save')").click Discussion.section_warning_continue_button.click wait_for_ajaximations graded_discussion.reload sub_assignments = graded_discussion.assignment.sub_assignments sub_assignment1 = sub_assignments.find_by(sub_assignment_tag: CheckpointLabels::REPLY_TO_TOPIC) sub_assignment2 = sub_assignments.find_by(sub_assignment_tag: CheckpointLabels::REPLY_TO_ENTRY) expect(graded_discussion.assignment.sub_assignments.count).to eq(2) expect(format_date_for_view(sub_assignment1.assignment_overrides.active.first.due_at, "%m/%d/%Y")).to eq(reply_to_topic_date_formatted) expect(format_date_for_view(sub_assignment2.assignment_overrides.active.first.due_at, "%m/%d/%Y")).to eq(required_replies_date_formatted) end it "displays an error when the availability date is after the due date" do Account.site_admin.enable_feature!(:discussion_checkpoints) @course.root_account.enable_feature!(:discussion_checkpoints) assignment = @course.assignments.create!( name: "Assignment", submission_types: ["online_text_entry"], points_possible: 20 ) assignment.update!(has_sub_assignments: true) assignment.sub_assignments.create!(context: assignment.context, sub_assignment_tag: CheckpointLabels::REPLY_TO_TOPIC, points_possible: 10, due_at: 3.days.from_now) assignment.sub_assignments.create!(context: assignment.context, sub_assignment_tag: CheckpointLabels::REPLY_TO_ENTRY, points_possible: 10, due_at: 5.days.from_now) graded_discussion = @course.discussion_topics.create!( title: "Graded Discussion", discussion_type: "threaded", posted_at: "2017-07-09 16:32:34", user: @teacher, assignment:, reply_to_entry_required_count: 1 ) # Open page and assignTo tray get "/courses/#{@course.id}/discussion_topics/#{graded_discussion.id}/edit" Discussion.assign_to_button.click reply_to_topic_date_formatted = format_date_for_view(1.day.from_now(Time.zone.now).to_date, "%m/%d/%Y") update_reply_to_topic_date(0, reply_to_topic_date_formatted) update_reply_to_topic_time(0, "5:00 PM") # available from available_from_date_formatted = format_date_for_view(2.days.from_now(Time.zone.now).to_date, "%m/%d/%Y") update_available_date(0, available_from_date_formatted, true, false) update_available_time(0, "5:00 PM", true, false) expect(assign_to_date_and_time[2].text).to include("Unlock date cannot be after reply to topic due date") # correct reply to topic reply_to_topic_date_formatted = format_date_for_view(3.days.from_now(Time.zone.now).to_date, "%m/%d/%Y") update_reply_to_topic_date(0, reply_to_topic_date_formatted) update_reply_to_topic_time(0, "5:00 PM") # required replies required_replies_date_formatted = format_date_for_view(1.day.from_now(Time.zone.now).to_date, "%m/%d/%Y") update_required_replies_date(0, required_replies_date_formatted) update_required_replies_time(0, "5:00 PM") expect(assign_to_date_and_time[2].text).to include("Unlock date cannot be after required replies due date") # available until until_date = 5.days.from_now(Time.zone.now).to_date + 17.hours until_date_formatted = format_date_for_view(until_date, "%m/%d/%Y") update_until_date(0, until_date_formatted, true, false) update_until_time(0, "5:00 PM", true, false) reply_to_topic_date_formatted = format_date_for_view(6.days.from_now(Time.zone.now).to_date, "%m/%d/%Y") update_reply_to_topic_date(0, reply_to_topic_date_formatted) update_reply_to_topic_time(0, "5:00 PM") expect(assign_to_date_and_time[3].text).to include("Lock date cannot be before reply to topic due date") # correct reply to topic reply_to_topic_date_formatted = format_date_for_view(3.days.from_now(Time.zone.now).to_date, "%m/%d/%Y") update_reply_to_topic_date(0, reply_to_topic_date_formatted) update_reply_to_topic_time(0, "5:00 PM") # required replies required_replies_date_formatted = format_date_for_view(6.days.from_now(Time.zone.now).to_date, "%m/%d/%Y") update_required_replies_date(0, required_replies_date_formatted) update_required_replies_time(0, "5:00 PM") expect(assign_to_date_and_time[3].text).to include("Lock date cannot be before required replies due date") end end context "post to sis" do before do course.account.set_feature_flag! "post_grades", "on" course.account.set_feature_flag! :new_sis_integrations, "on" course.account.settings[:sis_syncing] = { value: true, locked: false } course.account.settings[:sis_require_assignment_due_date] = { value: true } course.account.save! end it "blocks when enabled", :ignore_js_errors do graded_discussion = create_graded_discussion(course) get "/courses/#{course.id}/discussion_topics/#{graded_discussion.id}/edit" Discussion.click_sync_to_sis_checkbox Discussion.save_button.click wait_for_ajaximations expect(driver.current_url).to include("edit") expect_instui_flash_message("Please set a due date or change your selection for the “Sync to SIS” option.") Discussion.click_assign_to_button wait_for_assign_to_tray_spinner keep_trying_until { expect(item_tray_exists?).to be_truthy } expect(assign_to_date_and_time[0].text).to include("Please add a due date") update_due_date(0, format_date_for_view(Time.zone.now, "%-m/%-d/%Y")) update_due_time(0, "11:59 PM") click_save_button("Apply") keep_trying_until { expect(element_exists?(module_item_edit_tray_selector)).to be_falsey } expect_new_page_load { Discussion.save_button.click } expect(driver.current_url).not_to include("edit") expect(graded_discussion.reload.assignment.post_to_sis).to be_truthy get "/courses/#{course.id}/discussion_topics/#{graded_discussion.id}/edit" expect(is_checked(Discussion.sync_to_sis_checkbox_selector)).to be_truthy end it "does not block when disabled" do graded_discussion = create_graded_discussion(course) get "/courses/#{course.id}/discussion_topics/#{graded_discussion.id}/edit" expect_new_page_load { Discussion.save_button.click } expect(driver.current_url).not_to include("edit") expect(graded_discussion.reload.assignment.post_to_sis).to be_falsey get "/courses/#{course.id}/discussion_topics/#{graded_discussion.id}/edit" expect(is_checked(Discussion.sync_to_sis_checkbox_selector)).to be_falsey end it "validates due date when user checks/unchecks the box", :ignore_js_errors do graded_discussion = create_graded_discussion(course) get "/courses/#{course.id}/discussion_topics/#{graded_discussion.id}/edit" Discussion.click_assign_to_button wait_for_assign_to_tray_spinner keep_trying_until { expect(item_tray_exists?).to be_truthy } expect(assign_to_date_and_time[0].text).not_to include("Please add a due date") click_cancel_button keep_trying_until { expect(element_exists?(module_item_edit_tray_selector)).to be_falsey } Discussion.click_sync_to_sis_checkbox Discussion.click_assign_to_button wait_for_assign_to_tray_spinner keep_trying_until { expect(item_tray_exists?).to be_truthy } expect(assign_to_date_and_time[0].text).to include("Please add a due date") update_due_date(0, format_date_for_view(Time.zone.now, "%-m/%-d/%Y")) update_due_time(0, "11:59 PM") click_save_button("Apply") keep_trying_until { expect(element_exists?(module_item_edit_tray_selector)).to be_falsey } expect_new_page_load { Discussion.save_button.click } expect(driver.current_url).not_to include("edit") expect(graded_discussion.reload.assignment.post_to_sis).to be_truthy end end end context "checkpoints" do before do course.root_account.enable_feature!(:discussion_checkpoints) @checkpointed_discussion = DiscussionTopic.create_graded_topic!(course:, title: "checkpointed discussion") Checkpoints::DiscussionCheckpointCreatorService.call( discussion_topic: @checkpointed_discussion, checkpoint_label: CheckpointLabels::REPLY_TO_TOPIC, dates: [{ type: "everyone", due_at: 2.days.from_now }], points_possible: 6 ) Checkpoints::DiscussionCheckpointCreatorService.call( discussion_topic: @checkpointed_discussion, checkpoint_label: CheckpointLabels::REPLY_TO_ENTRY, dates: [{ type: "everyone", due_at: 2.days.from_now }], points_possible: 7, replies_required: 5 ) end it "displays checkpoint settings values correctly when there are existing checkpoints" do get "/courses/#{course.id}/discussion_topics/#{@checkpointed_discussion.id}/edit" expect(f("input[data-testid='points-possible-input-reply-to-topic']").attribute("value")).to eq "6" expect(f("input[data-testid='points-possible-input-reply-to-entry']").attribute("value")).to eq "7" expect(f("input[data-testid='reply-to-entry-required-count']").attribute("value")).to eq "5" end it "allows for a discussion with checkpoints to be updated" do get "/courses/#{course.id}/discussion_topics/#{@checkpointed_discussion.id}/edit" f("input[data-testid='points-possible-input-reply-to-topic']").send_keys :backspace f("input[data-testid='points-possible-input-reply-to-topic']").send_keys "5" f("input[data-testid='reply-to-entry-required-count']").send_keys :backspace f("input[data-testid='reply-to-entry-required-count']").send_keys "6" f("input[data-testid='points-possible-input-reply-to-entry']").send_keys :backspace f("input[data-testid='points-possible-input-reply-to-entry']").send_keys "7" fj("button:contains('Save')").click expect(DiscussionTopic.last.reply_to_entry_required_count).to eq 6 assignment = Assignment.last sub_assignments = SubAssignment.where(parent_assignment_id: assignment.id) sub_assignment1 = sub_assignments.find_by(sub_assignment_tag: CheckpointLabels::REPLY_TO_TOPIC) sub_assignment2 = sub_assignments.find_by(sub_assignment_tag: CheckpointLabels::REPLY_TO_ENTRY) expect(sub_assignment1.points_possible).to eq 5 expect(sub_assignment2.points_possible).to eq 7 end it "deletes checkpoints if the checkpoint checkbox is unselected on an existing discussion with checkpoints" do assignment = Assignment.last expect(assignment.sub_assignments.count).to eq 2 get "/courses/#{course.id}/discussion_topics/#{@checkpointed_discussion.id}/edit" force_click_native('input[type=checkbox][value="checkpoints"]') fj("button:contains('Save')").click expect(DiscussionTopic.last.reply_to_entry_required_count).to eq 0 expect(assignment.sub_assignments.count).to eq 0 expect(Assignment.last.has_sub_assignments).to be(false) end it "can edit a non-checkpointed discussion into a checkpointed discussion" do graded_discussion = create_graded_discussion(course) get "/courses/#{course.id}/discussion_topics/#{graded_discussion.id}/edit" force_click_native('input[type=checkbox][value="checkpoints"]') f("input[data-testid='points-possible-input-reply-to-topic']").send_keys :backspace f("input[data-testid='points-possible-input-reply-to-topic']").send_keys "5" f("input[data-testid='reply-to-entry-required-count']").send_keys :backspace f("input[data-testid='reply-to-entry-required-count']").send_keys "6" f("input[data-testid='points-possible-input-reply-to-entry']").send_keys :backspace f("input[data-testid='points-possible-input-reply-to-entry']").send_keys "7" fj("button:contains('Save')").click assignment = Assignment.last expect(assignment.has_sub_assignments?).to be true expect(DiscussionTopic.last.reply_to_entry_required_count).to eq 6 sub_assignments = SubAssignment.where(parent_assignment_id: assignment.id) sub_assignment1 = sub_assignments.find_by(sub_assignment_tag: CheckpointLabels::REPLY_TO_TOPIC) sub_assignment2 = sub_assignments.find_by(sub_assignment_tag: CheckpointLabels::REPLY_TO_ENTRY) expect(sub_assignment1.points_possible).to eq 5 expect(sub_assignment2.points_possible).to eq 7 end it "deletes checkpoints if the graded checkbox is unselected on an exisitng discussion with checkpoints" do assignment = Assignment.last expect(assignment.sub_assignments.count).to eq 2 get "/courses/#{course.id}/discussion_topics/#{@checkpointed_discussion.id}/edit" # Uncheck the "graded" checkbox force_click_native('input[type=checkbox][value="graded"]') fj("button:contains('Save')").click # Expect the assignment an the checkpoints to no longer exist expect(DiscussionTopic.last.reply_to_entry_required_count).to eq 0 expect(assignment.sub_assignments.count).to eq 0 expect(Assignment.last.has_sub_assignments).to be(false) expect(DiscussionTopic.last.assignment).to be_nil end end context "mastery paths aka cyoe ake conditional release" do def create_assignment(course, title, points_possible = 10) course.assignments.create!( title: "#{title} #{SecureRandom.alphanumeric(10)}", description: "General Assignment", points_possible:, submission_types: "online_text_entry", workflow_state: "published" ) end def create_discussion(course, creator, workflow_state = "published") discussion_assignment = create_assignment(@course, "Discussion Assignment", 10) course.discussion_topics.create!( user: creator, title: "Discussion Topic #{SecureRandom.alphanumeric(10)}", message: "Discussion topic message", assignment: discussion_assignment, workflow_state: ) end it "loads connected mastery paths immediately is requested in url" do course_with_teacher_logged_in @course.conditional_release = true @course.save! @trigger_assignment = create_assignment(@course, "Mastery Path Main Assignment", 10) @set1_assmt1 = create_assignment(@course, "Set 1 Assessment 1", 10) @set2_assmt1 = create_assignment(@course, "Set 2 Assessment 1", 10) @set2_assmt2 = create_assignment(@course, "Set 2 Assessment 2", 10) @set3a_assmt = create_assignment(@course, "Set 3a Assessment", 10) @set3b_assmt = create_assignment(@course, "Set 3b Assessment", 10) graded_discussion = create_discussion(@course, @teacher) course_module = @course.context_modules.create!(name: "Mastery Path Module") course_module.add_item(id: @trigger_assignment.id, type: "assignment") course_module.add_item(id: @set1_assmt1.id, type: "assignment") course_module.add_item(id: graded_discussion.id, type: "discussion_topic") course_module.add_item(id: @set2_assmt1.id, type: "assignment") course_module.add_item(id: @set2_assmt2.id, type: "assignment") course_module.add_item(id: @set3a_assmt.id, type: "assignment") course_module.add_item(id: @set3b_assmt.id, type: "assignment") ranges = [ ConditionalRelease::ScoringRange.new(lower_bound: 0.7, upper_bound: 1.0, assignment_sets: [ ConditionalRelease::AssignmentSet.new(assignment_set_associations: [ ConditionalRelease::AssignmentSetAssociation.new(assignment_id: @set1_assmt1.id), ConditionalRelease::AssignmentSetAssociation.new(assignment_id: graded_discussion.assignment_id) ]) ]), ConditionalRelease::ScoringRange.new(lower_bound: 0.4, upper_bound: 0.7, assignment_sets: [ ConditionalRelease::AssignmentSet.new(assignment_set_associations: [ ConditionalRelease::AssignmentSetAssociation.new(assignment_id: @set2_assmt1.id), ConditionalRelease::AssignmentSetAssociation.new(assignment_id: @set2_assmt2.id) ]) ]), ConditionalRelease::ScoringRange.new(lower_bound: 0, upper_bound: 0.4, assignment_sets: [ ConditionalRelease::AssignmentSet.new( assignment_set_associations: [ConditionalRelease::AssignmentSetAssociation.new( assignment_id: @set3a_assmt.id )] ), ConditionalRelease::AssignmentSet.new( assignment_set_associations: [ConditionalRelease::AssignmentSetAssociation.new( assignment_id: @set3b_assmt.id )] ) ]) ] @rule = @course.conditional_release_rules.create!(trigger_assignment: @trigger_assignment, scoring_ranges: ranges) mp_discussion = @course.discussion_topics.create!(assignment: @trigger_assignment, title: "graded discussion") get "/courses/#{@course.id}/discussion_topics/#{mp_discussion.id}/edit#mastery-paths-editor" fj("div[role='tab']:contains('Mastery Paths')").click ui_ranges = ff("div.cr-scoring-range") expect(ui_ranges[0].text).to include @set1_assmt1.title expect(ui_ranges[0].text).to include graded_discussion.title expect(ui_ranges[1].text).to include @set2_assmt1.title expect(ui_ranges[1].text).to include @set2_assmt2.title expect(ui_ranges[2].text).to include @set3a_assmt.title expect(ui_ranges[2].text).to include @set3b_assmt.title end end end end end end