Add 'Assign To' tray for ungraded discussion on edit page

closes LX-1494

flag=discussion_create
flag=differentiated_modules

pre-requisites:
- The test plan is applicable when discussion_create
AND differentiated_modules are enabled.
- It's necessary to test that everything is still
working when we disable the flags.

test plan:
- Navigate to a discussion edit page.
- Disabled graded checkbox.
> Verify that ‘Post to’ and ‘Available From’ sections
are no longer visible. Only 'Manage To' button.
> Verify  that ‘Due at’ input does not appear.
Only ‘Available from’ and ‘Until’.
> Verify that you can create/delete/update assignment
cards in the tray.
> Apply the tray and save.
> Verify that on show/edit page you can see the
expected assignment cards.
> Verify the 'Assign To' does not appear for students
on ungraded discussions. The old 'Post To' section
should appear instead.
> Verify that this didn't affect Announcements
edit page, since they are a subclass of Discussions.

Change-Id: I44be7f807d5b29d7b5ea8bc5bc878fc311c3b20b
Reviewed-on: https://gerrit.instructure.com/c/canvas-lms/+/346427
Tested-by: Service Cloud Jenkins <svc.cloudjenkins@instructure.com>
Reviewed-by: Jackson Howe <jackson.howe@instructure.com>
Reviewed-by: Sarah Gerard <sarah.gerard@instructure.com>
QA-Review: Sarah Gerard <sarah.gerard@instructure.com>
Product-Review: Juan Chavez <juan.chavez@instructure.com>
This commit is contained in:
juan.chavez 2024-04-29 20:20:16 -04:00 committed by Juan Chavez
parent 4c65927b53
commit d8484b4156
25 changed files with 857 additions and 125 deletions

View File

@ -34,18 +34,24 @@ class Types::DiscussionTopicAnonymousStateType < Types::BaseEnum
end
class Mutations::CreateDiscussionTopic < Mutations::DiscussionBase
include Api
include Api::V1::AssignmentOverride
graphql_name "CreateDiscussionTopic"
argument :is_announcement, Boolean, required: false
argument :is_anonymous_author, Boolean, required: false
argument :anonymous_state, Types::DiscussionTopicAnonymousStateType, required: false
argument :context_id, ID, required: true, prepare: GraphQLHelpers.relay_or_legacy_id_prepare_func("Context")
argument :context_id, GraphQL::Schema::Object::ID, required: true, prepare: GraphQLHelpers.relay_or_legacy_id_prepare_func("Context")
argument :context_type, Types::DiscussionTopicContextType, required: true
argument :assignment, Mutations::AssignmentBase::AssignmentCreate, required: false
argument :ungraded_discussion_overrides, [Mutations::AssignmentBase::AssignmentOverrideCreateOrUpdate], required: false
# most arguments inherited from DiscussionBase
def resolve(input:)
@current_user = current_user
discussion_topic_context = find_context(input)
return validation_error(I18n.t("Invalid context")) unless discussion_topic_context
@ -110,7 +116,7 @@ class Mutations::CreateDiscussionTopic < Mutations::DiscussionBase
end
process_common_inputs(input, is_announcement, discussion_topic)
process_future_date_inputs(input[:delayed_post_at], input[:lock_at], discussion_topic)
process_future_date_inputs(input.slice(:delayed_post_at, :lock_at), discussion_topic)
process_locked_parameter(input[:locked], discussion_topic)
if input.key?(:assignment) && input[:assignment].present?
@ -144,6 +150,11 @@ class Mutations::CreateDiscussionTopic < Mutations::DiscussionBase
discussion_topic.saved_by = :assignment if discussion_topic.assignment.present?
return errors_for(discussion_topic) unless discussion_topic.save!
if input.key?(:ungraded_discussion_overrides)
overrides = input[:ungraded_discussion_overrides] || []
update_ungraded_discussion(discussion_topic, overrides)
end
{ discussion_topic: }
rescue Checkpoints::DiscussionCheckpointError => e
raise GraphQL::ExecutionError, e.message

View File

@ -72,6 +72,7 @@ class Mutations::DiscussionBase < Mutations::BaseMutation
argument :locked, Boolean, required: false
argument :message, String, required: false
argument :only_graders_can_rate, Boolean, required: false
argument :only_visible_to_overrides, Boolean, required: false
argument :published, Boolean, required: false
argument :require_initial_post, Boolean, required: false
argument :title, String, required: false
@ -85,7 +86,7 @@ class Mutations::DiscussionBase < Mutations::BaseMutation
field :discussion_topic, Types::DiscussionType, null:
# These are inputs that are allowed to be directly assigned from graphql to the model without additional processing or logic involved
ALLOWED_INPUTS = %i[title message require_initial_post allow_rating only_graders_can_rate podcast_enabled podcast_has_student_posts].freeze
ALLOWED_INPUTS = %i[title message require_initial_post allow_rating only_graders_can_rate only_visible_to_overrides podcast_enabled podcast_has_student_posts].freeze
def process_common_inputs(input, is_announcement, discussion_topic)
model_attrs = input.to_h.slice(*ALLOWED_INPUTS)
@ -110,9 +111,10 @@ class Mutations::DiscussionBase < Mutations::BaseMutation
end
end
def process_future_date_inputs(delayed_post_at, lock_at, discussion_topic)
discussion_topic.delayed_post_at = delayed_post_at if delayed_post_at
discussion_topic.lock_at = lock_at if lock_at
def process_future_date_inputs(dates, discussion_topic)
# if dates contain delayed_post_at or lock_at set it even if is nil
discussion_topic.delayed_post_at = dates[:delayed_post_at] if dates.key?(:delayed_post_at)
discussion_topic.lock_at = dates[:lock_at] if dates.key?(:lock_at)
if discussion_topic.unlock_at_changed? || discussion_topic.delayed_post_at_changed? || discussion_topic.lock_at_changed?
# only apply post_delayed if the topic is set to published
@ -167,4 +169,18 @@ class Mutations::DiscussionBase < Mutations::BaseMutation
discussion_topic.course_sections.map(&:id) - visibilities
end
end
# Adapted from LearningObjectDatesController#update_ungraded_object
def update_ungraded_discussion(discussion_topic, overrides)
batch = prepare_assignment_overrides_for_batch_update(discussion_topic, overrides, @current_user) if overrides
discussion_topic.transaction do
perform_batch_update_assignment_overrides(discussion_topic, batch) if overrides
# this is temporary until we are able to remove the dicussion_topic_section_visibilities table
if discussion_topic.is_section_specific
discussion_topic.discussion_topic_section_visibilities.destroy_all
discussion_topic.update!(is_section_specific: false)
end
end
discussion_topic.clear_cache_key(:availability)
end
end

View File

@ -19,12 +19,16 @@
#
class Mutations::UpdateDiscussionTopic < Mutations::DiscussionBase
include Api
include Api::V1::AssignmentOverride
graphql_name "UpdateDiscussionTopic"
argument :discussion_topic_id, ID, required: true, prepare: GraphQLHelpers.relay_or_legacy_id_prepare_func("DiscussionTopic")
argument :discussion_topic_id, GraphQL::Schema::Object::ID, required: true, prepare: GraphQLHelpers.relay_or_legacy_id_prepare_func("DiscussionTopic")
argument :remove_attachment, Boolean, required: false
argument :assignment, Mutations::AssignmentBase::AssignmentUpdate, required: false
argument :set_checkpoints, Boolean, required: false
argument :ungraded_discussion_overrides, [Mutations::AssignmentBase::AssignmentOverrideCreateOrUpdate], required: false
field :discussion_topic, Types::DiscussionType, null: false
def resolve(input:)
@ -53,7 +57,7 @@ class Mutations::UpdateDiscussionTopic < Mutations::DiscussionBase
end
process_common_inputs(input, discussion_topic.is_announcement, discussion_topic)
process_future_date_inputs(input[:delayed_post_at], input[:lock_at], discussion_topic)
process_future_date_inputs(input.slice(:delayed_post_at, :lock_at), discussion_topic)
# Take care of Assignment update information
if input[:assignment]
@ -133,6 +137,11 @@ class Mutations::UpdateDiscussionTopic < Mutations::DiscussionBase
return errors_for(discussion_topic) unless discussion_topic.save!
if input.key?(:ungraded_discussion_overrides)
overrides = input[:ungraded_discussion_overrides] || []
update_ungraded_discussion(discussion_topic, overrides)
end
discussion_topic.assignment = assignment_result[:assignment] if assignment_result && assignment_result[:assignment]
{

View File

@ -68,7 +68,10 @@ module Types
implements GraphQL::Types::Relay::Node
implements Interfaces::TimestampInterface
implements Interfaces::LegacyIDInterface
# IDs could be nil since DiscussionTopicSectionVisibilities are not persisted
# So we use expect null IDs instead of implementing Interfaces::LegacyIDInterface
field :_id, ID, "legacy canvas id", method: :id, null: true
alias_method :override, :object

View File

@ -68,6 +68,8 @@ module Types
field :is_section_specific, Boolean, null: true
field :require_initial_post, Boolean, null: true
field :can_group, Boolean, null: true, method: :can_group?
field :visible_to_everyone, Boolean, null: true
field :only_visible_to_overrides, Boolean, null: true
field :message, String, null: true
def message
@ -358,6 +360,33 @@ module Types
).load(object)
end
field :ungraded_discussion_overrides, Types::AssignmentOverrideType.connection_type, null: true
def ungraded_discussion_overrides
overrides = AssignmentOverrideApplicator.overrides_for_assignment_and_user(object, current_user)
# this is a temporary check for any discussion_topic_section_visibilities until we eventually backfill that table
if object.is_section_specific
section_overrides = object.assignment_overrides.active.where(set_type: "CourseSection").select(:set_id)
section_visibilities = object.discussion_topic_section_visibilities.active.where.not(course_section_id: section_overrides)
end
if section_visibilities
section_overrides = section_visibilities.map do |section_visibility|
assignment_override = AssignmentOverride.new(
discussion_topic: section_visibility.discussion_topic,
course_section: section_visibility.course_section
)
assignment_override.unlock_at = object.unlock_at if object.unlock_at
assignment_override.lock_at = object.lock_at if object.lock_at
assignment_override
end
end
all_overrides = overrides.to_a
all_overrides += section_overrides if section_visibilities
all_overrides
end
def get_entries(search_term: nil, filter: nil, sort_order: :asc, root_entries: false, user_search_id: nil, unread_before: nil)
return [] if object.initial_post_required?(current_user, session) || !available_for_user

View File

@ -42,7 +42,8 @@ RSpec.describe Mutations::UpdateDiscussionTopic do
assignment: nil,
checkpoints: nil,
set_checkpoints: nil,
group_category_id: nil
group_category_id: nil,
ungraded_discussion_overrides: nil
)
<<~GQL
mutation {
@ -62,12 +63,25 @@ RSpec.describe Mutations::UpdateDiscussionTopic do
#{assignment_str(assignment)}
#{checkpoints_str(checkpoints)}
#{"setCheckpoints: #{set_checkpoints}" unless set_checkpoints.nil?}
#{"ungradedDiscussionOverrides: #{ungraded_discussion_overrides_str(ungraded_discussion_overrides)}" unless ungraded_discussion_overrides.nil?}
}) {
discussionTopic {
_id
published
locked
replyToEntryRequiredCount
ungradedDiscussionOverrides {
nodes {
_id
createdAt
dueAt
id
lockAt
title
unlockAt
updatedAt
}
}
assignment {
_id
pointsPossible
@ -203,6 +217,17 @@ RSpec.describe Mutations::UpdateDiscussionTopic do
"assignmentOverrides: { #{args.join(", ")} }"
end
def ungraded_discussion_overrides_str(overrides)
return "" unless overrides
args = []
args << "sectionId: \"#{overrides[:sectionId]}\"" if overrides[:sectionId]
args << "studentIds: [\"#{overrides[:studentIds].join('", "')}\"]" if overrides[:studentIds]
# Add other override input fields if you want to test them
"{ #{args.join(", ")} }"
end
def run_mutation(opts = {}, current_user = @teacher)
result = CanvasSchema.execute(
mutation_str(**opts),
@ -707,4 +732,22 @@ RSpec.describe Mutations::UpdateDiscussionTopic do
expect(@graded_topic.reload.assignment).to be_nil
end
end
it "updates ungraded assignment overrides" do
student1 = @course.enroll_student(User.create!, enrollment_state: "active").user
student2 = @course.enroll_student(User.create!, enrollment_state: "active").user
@course.enroll_student(User.create!, enrollment_state: "active").user
ungraded_discussion_overrides = {
studentIds: [student1.id, student2.id]
}
result = run_mutation(id: @topic.id, ungraded_discussion_overrides:)
expect(result["errors"]).to be_nil
new_override = DiscussionTopic.last.active_assignment_overrides.first
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, student2.id])
end
end

View File

@ -22,6 +22,7 @@ import gql from 'graphql-tag'
import {Attachment} from './Attachment'
import {GroupSet} from './GroupSet'
import {Assignment} from './Assignment'
import {AssignmentOverride} from './AssignmentOverride'
export const DiscussionTopic = {
fragment: gql`
@ -45,6 +46,8 @@ export const DiscussionTopic = {
published
canGroup
replyToEntryRequiredCount
visibleToEveryone
onlyVisibleToOverrides
courseSections {
...Section
}
@ -57,11 +60,17 @@ export const DiscussionTopic = {
assignment {
...Assignment
}
ungradedDiscussionOverrides {
nodes {
...AssignmentOverride
}
}
}
${Attachment.fragment}
${Assignment.fragment}
${Section.fragment}
${GroupSet.fragment}
${AssignmentOverride.fragment}
`,
shape: shape({
@ -82,11 +91,14 @@ export const DiscussionTopic = {
lockAt: string,
published: bool,
replyToEntryRequiredCount: number,
visibleToEveryone: bool,
onlyVisibleToOverrides: bool,
courseSections: arrayOf(Section.shape),
groupSet: GroupSet.shape,
attachment: Attachment.shape,
assignment: Assignment.shape,
canGroup: bool,
ungradedDiscussionOverrides: AssignmentOverride.shape(),
}),
mock: ({
@ -107,11 +119,14 @@ export const DiscussionTopic = {
lockAt = null,
published = true,
replyToEntryRequiredCount = 1,
visibleToEveryone = false,
onlyVisibleToOverrides = false,
courseSections = [Section.mock()],
groupSet = GroupSet.mock(),
attachment = Attachment.mock(),
assignment = null,
canGroup = false,
ungradedDiscussionOverrides = null,
} = {}) => ({
_id,
id,
@ -130,11 +145,14 @@ export const DiscussionTopic = {
lockAt,
published,
replyToEntryRequiredCount,
visibleToEveryone,
onlyVisibleToOverrides,
courseSections,
groupSet,
attachment,
assignment,
canGroup,
ungradedDiscussionOverrides,
__typename: 'Discussion',
}),
}

View File

@ -34,6 +34,7 @@ export const CREATE_DISCUSSION_TOPIC = gql`
$isAnonymousAuthor: Boolean
$allowRating: Boolean
$onlyGradersCanRate: Boolean
$onlyVisibleToOverrides: Boolean
$todoDate: DateTime
$podcastEnabled: Boolean
$podcastHasStudentPosts: Boolean
@ -44,6 +45,7 @@ export const CREATE_DISCUSSION_TOPIC = gql`
$assignment: AssignmentCreate
$checkpoints: [DiscussionCheckpoints!]
$fileId: ID
$ungradedDiscussionOverrides: [AssignmentOverrideCreateOrUpdate!]
) {
createDiscussionTopic(
input: {
@ -59,6 +61,7 @@ export const CREATE_DISCUSSION_TOPIC = gql`
isAnonymousAuthor: $isAnonymousAuthor
allowRating: $allowRating
onlyGradersCanRate: $onlyGradersCanRate
onlyVisibleToOverrides: $onlyVisibleToOverrides
todoDate: $todoDate
podcastEnabled: $podcastEnabled
podcastHasStudentPosts: $podcastHasStudentPosts
@ -69,6 +72,7 @@ export const CREATE_DISCUSSION_TOPIC = gql`
assignment: $assignment
checkpoints: $checkpoints
fileId: $fileId
ungradedDiscussionOverrides: $ungradedDiscussionOverrides
}
) {
discussionTopic {
@ -84,6 +88,7 @@ export const CREATE_DISCUSSION_TOPIC = gql`
isAnonymousAuthor
allowRating
onlyGradersCanRate
onlyVisibleToOverrides
todoDate
podcastEnabled
podcastHasStudentPosts
@ -145,6 +150,7 @@ export const UPDATE_DISCUSSION_TOPIC = gql`
$lockAt: DateTime
$allowRating: Boolean
$onlyGradersCanRate: Boolean
$onlyVisibleToOverrides: Boolean
$todoDate: DateTime
$podcastEnabled: Boolean
$podcastHasStudentPosts: Boolean
@ -156,6 +162,7 @@ export const UPDATE_DISCUSSION_TOPIC = gql`
$assignment: AssignmentUpdate
$checkpoints: [DiscussionCheckpoints!]
$setCheckpoints: Boolean
$ungradedDiscussionOverrides: [AssignmentOverrideCreateOrUpdate!]
) {
updateDiscussionTopic(
input: {
@ -168,6 +175,7 @@ export const UPDATE_DISCUSSION_TOPIC = gql`
lockAt: $lockAt
allowRating: $allowRating
onlyGradersCanRate: $onlyGradersCanRate
onlyVisibleToOverrides: $onlyVisibleToOverrides
todoDate: $todoDate
podcastEnabled: $podcastEnabled
podcastHasStudentPosts: $podcastHasStudentPosts
@ -179,6 +187,7 @@ export const UPDATE_DISCUSSION_TOPIC = gql`
assignment: $assignment
checkpoints: $checkpoints
setCheckpoints: $setCheckpoints
ungradedDiscussionOverrides: $ungradedDiscussionOverrides
}
) {
discussionTopic {
@ -194,6 +203,7 @@ export const UPDATE_DISCUSSION_TOPIC = gql`
isAnonymousAuthor
allowRating
onlyGradersCanRate
onlyVisibleToOverrides
todoDate
podcastEnabled
podcastHasStudentPosts

View File

@ -24,7 +24,7 @@ import {Alert} from '@instructure/ui-alerts'
import {Select} from '@instructure/ui-select'
import {IconCheckSolid} from '@instructure/ui-icons'
import {View} from '@instructure/ui-view'
import {GradedDiscussionDueDatesContext} from '../../util/constants'
import {DiscussionDueDatesContext} from '../../util/constants'
const I18n = useI18nScope('discussion_create')
const liveRegion = () => document.getElementById('flash_screenreader_holder')
@ -56,9 +56,8 @@ export const AssignedTo = ({
.find(option => initialAssignedToInformation.includes(option.assetCode)) || []
)
const {groupCategoryId, groups, gradedDiscussionRefMap, setGradedDiscussionRefMap} = useContext(
GradedDiscussionDueDatesContext
)
const {groupCategoryId, groups, gradedDiscussionRefMap, setGradedDiscussionRefMap} =
useContext(DiscussionDueDatesContext)
// Add the checkmark icon to the selected options
const addIconToOption = (option, isSelected) => ({

View File

@ -22,7 +22,7 @@ import {useScope as useI18nScope} from '@canvas/i18n'
import {DateTimeInput} from '@instructure/ui-date-time-input'
import {FormFieldGroup} from '@instructure/ui-form-field'
import {AssignedTo} from './AssignedTo'
import {GradedDiscussionDueDatesContext} from '../../util/constants'
import {DiscussionDueDatesContext} from '../../util/constants'
const I18n = useI18nScope('discussion_create')
@ -38,9 +38,7 @@ export const AssignmentDueDate = ({
const [dueDateErrorMessage, setDueDateErrorMessage] = useState([])
const [availableFromAndUntilErrorMessage, setAvailableFromAndUntilErrorMessage] = useState([])
const {gradedDiscussionRefMap, setGradedDiscussionRefMap} = useContext(
GradedDiscussionDueDatesContext
)
const {gradedDiscussionRefMap, setGradedDiscussionRefMap} = useContext(DiscussionDueDatesContext)
const validateDueDate = (dueDate, availableFrom, availableUntil) => {
const due = new Date(dueDate)

View File

@ -28,7 +28,7 @@ import {Flex} from '@instructure/ui-flex'
import {IconAddLine} from '@instructure/ui-icons'
import theme from '@instructure/canvas-theme'
import {
GradedDiscussionDueDatesContext,
DiscussionDueDatesContext,
defaultEveryoneOption,
defaultEveryoneElseOption,
masteryPathsOption,
@ -52,7 +52,7 @@ export const AssignmentDueDatesManager = () => {
setGradedDiscussionRefMap,
importantDates,
setImportantDates,
} = useContext(GradedDiscussionDueDatesContext)
} = useContext(DiscussionDueDatesContext)
const [listOptions, setListOptions] = useState({
'': getDefaultBaseOptions(ENV.CONDITIONAL_RELEASE_SERVICE_ENABLED, defaultEveryoneOption),
'Course Sections': sections.map(section => {

View File

@ -26,7 +26,7 @@ import theme from '@instructure/canvas-theme'
import {ScreenReaderContent} from '@instructure/ui-a11y-content'
import {PointsPossible} from './PointsPossible'
import {
GradedDiscussionDueDatesContext,
DiscussionDueDatesContext,
minimumReplyToEntryRequiredCount,
maximumReplyToEntryRequiredCount,
} from '../../util/constants'
@ -49,7 +49,7 @@ export const CheckpointsSettings = () => {
setPointsPossibleReplyToEntry,
replyToEntryRequiredCount,
setReplyToEntryRequiredCount,
} = useContext(GradedDiscussionDueDatesContext)
} = useContext(DiscussionDueDatesContext)
return (
<>

View File

@ -17,7 +17,7 @@
*/
import React, {useContext, useEffect, useState} from 'react'
import {GradedDiscussionDueDatesContext} from '../../util/constants'
import {DiscussionDueDatesContext} from '../../util/constants'
import DifferentiatedModulesSection from '@canvas/due-dates/react/DifferentiatedModulesSection'
import LoadingIndicator from '@canvas/loading-indicator'
@ -32,7 +32,8 @@ export const ItemAssignToTrayWrapper = () => {
importantDates,
setImportantDates,
pointsPossible,
} = useContext(GradedDiscussionDueDatesContext)
isGraded,
} = useContext(DiscussionDueDatesContext)
const [overrides, setOverrides] = useState([])
const [loading, setLoading] = useState(true)
@ -186,6 +187,7 @@ export const ItemAssignToTrayWrapper = () => {
type="discussion"
importantDates={importantDates}
defaultSectionId={DEFAULT_SECTION_ID}
removeDueDateInput={!isGraded}
/>
)
}

View File

@ -20,7 +20,7 @@ import {render, fireEvent, screen} from '@testing-library/react'
import React from 'react'
import {AssignmentDueDatesManager} from '../AssignmentDueDatesManager'
import {
GradedDiscussionDueDatesContext,
DiscussionDueDatesContext,
defaultEveryoneOption,
defaultEveryoneElseOption,
masteryPathsOption,
@ -58,7 +58,7 @@ const setup = ({
setImportantDates = () => {},
} = {}) => {
return render(
<GradedDiscussionDueDatesContext.Provider
<DiscussionDueDatesContext.Provider
value={{
assignedInfoList,
setAssignedInfoList,
@ -72,7 +72,7 @@ const setup = ({
}}
>
<AssignmentDueDatesManager />
</GradedDiscussionDueDatesContext.Provider>
</DiscussionDueDatesContext.Provider>
)
}

View File

@ -21,7 +21,7 @@ import React from 'react'
import {CheckpointsSettings} from '../CheckpointsSettings'
import {GradedDiscussionDueDatesContext} from '../../../util/constants'
import {DiscussionDueDatesContext} from '../../../util/constants'
const setup = ({
pointsPossibleReplyToTopic = 0,
@ -32,7 +32,7 @@ const setup = ({
setReplyToEntryRequiredCount = () => {},
} = {}) => {
return render(
<GradedDiscussionDueDatesContext.Provider
<DiscussionDueDatesContext.Provider
value={{
pointsPossibleReplyToTopic,
setPointsPossibleReplyToTopic,
@ -43,7 +43,7 @@ const setup = ({
}}
>
<CheckpointsSettings />
</GradedDiscussionDueDatesContext.Provider>
</DiscussionDueDatesContext.Provider>
)
}

View File

@ -16,7 +16,7 @@
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import React, {useState, useRef, useEffect, useContext} from 'react'
import React, {useState, useRef, useEffect, useContext, useCallback} from 'react'
import PropTypes from 'prop-types'
import {CreateOrEditSetModal} from '@canvas/groups/react/CreateOrEditSetModal'
import {useScope as useI18nScope} from '@canvas/i18n'
@ -45,7 +45,7 @@ import {GradedDiscussionOptions} from '../DiscussionOptions/GradedDiscussionOpti
import {NonGradedDateOptions} from '../DiscussionOptions/NonGradedDateOptions'
import {AnonymousSelector} from '../DiscussionOptions/AnonymousSelector'
import {
GradedDiscussionDueDatesContext,
DiscussionDueDatesContext,
defaultEveryoneOption,
defaultEveryoneElseOption,
masteryPathsOption,
@ -60,7 +60,11 @@ import {UsageRightsContainer} from '../../containers/usageRights/UsageRightsCont
import {AlertManagerContext} from '@canvas/alerts/react/AlertManager'
import {ScreenReaderContent} from '@instructure/ui-a11y-content'
import {prepareAssignmentPayload, prepareCheckpointsPayload} from '../../util/payloadPreparations'
import {
prepareAssignmentPayload,
prepareCheckpointsPayload,
prepareUngradedDiscussionOverridesPayload,
} from '../../util/payloadPreparations'
import {validateTitle, validateFormFields} from '../../util/formValidation'
import AssignmentExternalTools from '@canvas/assignments/react/AssignmentExternalTools'
@ -73,8 +77,9 @@ import {
import {MissingSectionsWarningModal} from '../MissingSectionsWarningModal/MissingSectionsWarningModal'
import {flushSync} from 'react-dom'
import {SavingDiscussionTopicOverlay} from '../SavingDiscussionTopicOverlay/SavingDiscussionTopicOverlay'
import {Heading} from "@instructure/ui-heading";
import {Heading} from '@instructure/ui-heading'
import WithBreakpoints, {breakpointsShape} from '@canvas/with-breakpoints'
import {ItemAssignToTrayWrapper} from '../DiscussionOptions/ItemAssignToTrayWrapper'
const I18n = useI18nScope('discussion_create')
@ -247,9 +252,7 @@ function DiscussionTopicForm({
currentDiscussionTopic?.assignment?.peerReviews?.dueAt || ''
)
const [assignedInfoList, setAssignedInfoList] = useState(
isEditing
? buildAssignmentOverrides(currentDiscussionTopic?.assignment)
: buildDefaultAssignmentOverride()
isEditing ? buildAssignmentOverrides(currentDiscussionTopic) : buildDefaultAssignmentOverride()
)
const [gradedDiscussionRefMap, setGradedDiscussionRefMap] = useState(new Map())
@ -302,6 +305,7 @@ function DiscussionTopicForm({
importantDates,
setImportantDates,
pointsPossible,
isGraded,
}
const [showGroupCategoryModal, setShowGroupCategoryModal] = useState(false)
@ -385,6 +389,7 @@ function DiscussionTopicForm({
shouldShowSaveAndPublishButton,
shouldShowPodcastFeedOption,
shouldShowCheckpointsOptions,
shouldShowAssignToForUngradedDiscussions,
} = useShouldShowContent(
isGraded,
isAnnouncement,
@ -451,6 +456,18 @@ function DiscussionTopicForm({
...(shouldShowUsageRightsOption && {usageRightsData}),
}
if (!isGraded && !currentDiscussionTopic?.assignment && ENV.FEATURES?.differentiated_modules) {
Object.assign(
payload,
prepareUngradedDiscussionOverridesPayload(
assignedInfoList,
defaultEveryoneOption,
defaultEveryoneElseOption,
masteryPathsOption
)
)
}
// Additional properties for editing mode
if (isEditing) {
const editingPayload = {
@ -524,7 +541,7 @@ function DiscussionTopicForm({
}
const submitForm = shouldPublish => {
if (shouldShowAvailabilityOptions && isGraded) {
if (shouldShowAvailabilityOptions) {
const selectedAssignedTo = assignedInfoList.map(info => info.assignedList).flatMap(x => x)
const isEveryoneOrEveryoneElseSelected = selectedAssignedTo.some(
assignedTo =>
@ -609,21 +626,95 @@ function DiscussionTopicForm({
const itemMargin = breakpoints.desktopOnly ? '0 0 large' : '0 0 medium'
const headerText = isAnnouncement ? I18n.t('Create Announcement') : I18n.t('Create Discussion')
const titleContent = title ?? headerText
return (
instUINavEnabled() ? (
<Flex direction="column" as="div">
<Flex.Item margin={itemMargin} overflow="hidden">
<Heading level="h1">{headerText}</Heading>
</Flex.Item>
</Flex>
) : (
<ScreenReaderContent>
<h1>{titleContent}</h1>
</ScreenReaderContent>
)
return instUINavEnabled() ? (
<Flex direction="column" as="div">
<Flex.Item margin={itemMargin} overflow="hidden">
<Heading level="h1">{headerText}</Heading>
</Flex.Item>
</Flex>
) : (
<ScreenReaderContent>
<h1>{titleContent}</h1>
</ScreenReaderContent>
)
}
const renderAvailabilityOptions = useCallback(() => {
if (isGraded) {
return (
<View as="div" data-testid="assignment-settings-section">
<DiscussionDueDatesContext.Provider value={assignmentDueDateContext}>
<GradedDiscussionOptions
assignmentGroups={assignmentGroups}
pointsPossible={pointsPossible}
setPointsPossible={setPointsPossible}
displayGradeAs={displayGradeAs}
setDisplayGradeAs={setDisplayGradeAs}
assignmentGroup={assignmentGroup}
setAssignmentGroup={setAssignmentGroup}
peerReviewAssignment={peerReviewAssignment}
setPeerReviewAssignment={setPeerReviewAssignment}
peerReviewsPerStudent={peerReviewsPerStudent}
setPeerReviewsPerStudent={setPeerReviewsPerStudent}
peerReviewDueDate={peerReviewDueDate}
setPeerReviewDueDate={setPeerReviewDueDate}
postToSis={postToSis}
setPostToSis={setPostToSis}
gradingSchemeId={gradingSchemeId}
setGradingSchemeId={setGradingSchemeId}
intraGroupPeerReviews={intraGroupPeerReviews}
setIntraGroupPeerReviews={setIntraGroupPeerReviews}
isCheckpoints={isCheckpoints && ENV.DISCUSSION_CHECKPOINTS_ENABLED}
/>
</DiscussionDueDatesContext.Provider>
</View>
)
} else if (shouldShowAssignToForUngradedDiscussions) {
return (
<View as="div" data-testid="assignment-settings-section">
<Text weight="bold">{I18n.t('Assign Access')}</Text>
<DiscussionDueDatesContext.Provider value={assignmentDueDateContext}>
<ItemAssignToTrayWrapper />
</DiscussionDueDatesContext.Provider>
</View>
)
} else {
return (
<NonGradedDateOptions
availableFrom={availableFrom}
setAvailableFrom={setAvailableFrom}
availableUntil={availableUntil}
setAvailableUntil={setAvailableUntil}
isGraded={isGraded}
setAvailabilityValidationMessages={setAvailabilityValidationMessages}
availabilityValidationMessages={availabilityValidationMessages}
inputWidth={inputWidth}
setDateInputRef={ref => {
dateInputRef.current = ref
}}
/>
)
}
}, [
assignmentDueDateContext,
assignmentGroup,
assignmentGroups,
availabilityValidationMessages,
availableFrom,
availableUntil,
displayGradeAs,
gradingSchemeId,
intraGroupPeerReviews,
isCheckpoints,
isGraded,
peerReviewAssignment,
peerReviewDueDate,
peerReviewsPerStudent,
pointsPossible,
postToSis,
shouldShowAssignToForUngradedDiscussions,
])
return (
<>
{renderHeading()}
@ -674,7 +765,7 @@ function DiscussionTopicForm({
canAttach={ENV.DISCUSSION_TOPIC?.PERMISSIONS.CAN_ATTACH}
/>
)}
{shouldShowPostToSectionOption && (
{shouldShowPostToSectionOption && !shouldShowAssignToForUngradedDiscussions && (
<View display="block" padding="medium none">
<CanvasMultiSelect
data-testid="section-select"
@ -985,49 +1076,7 @@ function DiscussionTopicForm({
</Alert>
</View>
)}
{shouldShowAvailabilityOptions &&
(isGraded ? (
<View as="div" data-testid="assignment-settings-section">
<GradedDiscussionDueDatesContext.Provider value={assignmentDueDateContext}>
<GradedDiscussionOptions
assignmentGroups={assignmentGroups}
pointsPossible={pointsPossible}
setPointsPossible={setPointsPossible}
displayGradeAs={displayGradeAs}
setDisplayGradeAs={setDisplayGradeAs}
assignmentGroup={assignmentGroup}
setAssignmentGroup={setAssignmentGroup}
peerReviewAssignment={peerReviewAssignment}
setPeerReviewAssignment={setPeerReviewAssignment}
peerReviewsPerStudent={peerReviewsPerStudent}
setPeerReviewsPerStudent={setPeerReviewsPerStudent}
peerReviewDueDate={peerReviewDueDate}
setPeerReviewDueDate={setPeerReviewDueDate}
postToSis={postToSis}
setPostToSis={setPostToSis}
gradingSchemeId={gradingSchemeId}
setGradingSchemeId={setGradingSchemeId}
intraGroupPeerReviews={intraGroupPeerReviews}
setIntraGroupPeerReviews={setIntraGroupPeerReviews}
isCheckpoints={isCheckpoints && ENV.DISCUSSION_CHECKPOINTS_ENABLED}
/>
</GradedDiscussionDueDatesContext.Provider>
</View>
) : (
<NonGradedDateOptions
availableFrom={availableFrom}
setAvailableFrom={setAvailableFrom}
availableUntil={availableUntil}
setAvailableUntil={setAvailableUntil}
isGraded={isGraded}
setAvailabilityValidationMessages={setAvailabilityValidationMessages}
availabilityValidationMessages={availabilityValidationMessages}
inputWidth={inputWidth}
setDateInputRef={ref => {
dateInputRef.current = ref
}}
/>
))}
{shouldShowAvailabilityOptions && renderAvailabilityOptions()}
{(!isAnnouncement || !ENV.ASSIGNMENT_EDIT_PLACEMENT_NOT_ON_ANNOUNCEMENTS) &&
ENV.context_is_not_group && (
<div id="assignment_external_tools" data-testid="assignment-external-tools" />

View File

@ -745,4 +745,60 @@ describe('DiscussionTopicForm', () => {
})
})
})
describe('Ungraded', () => {
describe('differentiated_modules flag is ON', () => {
beforeAll(() => {
window.ENV.FEATURES.differentiated_modules = true
})
it('renders expected default teacher discussion options', () => {
window.ENV.DISCUSSION_TOPIC.PERMISSIONS.CAN_CREATE_ASSIGNMENT = true
window.ENV.DISCUSSION_TOPIC.PERMISSIONS.CAN_UPDATE_ASSIGNMENT = true
window.ENV.DISCUSSION_TOPIC.PERMISSIONS.CAN_MODERATE = true
window.ENV.DISCUSSION_TOPIC.PERMISSIONS.CAN_MANAGE_CONTENT = true
const document = setup()
// Default teacher options in order top to bottom
expect(document.getByText('Topic Title')).toBeInTheDocument()
expect(document.queryByText('Attach')).toBeTruthy()
expect(document.queryByTestId('section-select')).toBeTruthy()
expect(document.queryAllByText('Anonymous Discussion')).toBeTruthy()
expect(document.queryByTestId('require-initial-post-checkbox')).toBeTruthy()
expect(document.queryByLabelText('Enable podcast feed')).toBeInTheDocument()
expect(document.queryByTestId('graded-checkbox')).toBeTruthy()
expect(document.queryByLabelText('Allow liking')).toBeInTheDocument()
expect(document.queryByLabelText('Add to student to-do')).toBeInTheDocument()
expect(document.queryByTestId('group-discussion-checkbox')).toBeTruthy()
expect(document.queryAllByText('Manage Assign To')).toBeTruthy()
// Hides announcement options
expect(document.queryByLabelText('Delay Posting')).not.toBeInTheDocument()
expect(document.queryByLabelText('Allow Participants to Comment')).not.toBeInTheDocument()
})
it('renders expected default student discussion options', () => {
window.ENV.DISCUSSION_TOPIC.PERMISSIONS.CAN_CREATE_ASSIGNMENT = false
window.ENV.DISCUSSION_TOPIC.PERMISSIONS.CAN_UPDATE_ASSIGNMENT = false
window.ENV.DISCUSSION_TOPIC.PERMISSIONS.CAN_MODERATE = false
window.ENV.DISCUSSION_TOPIC.PERMISSIONS.CAN_MANAGE_CONTENT = false
const document = setup()
// Default teacher options in order top to bottom
expect(document.getByText('Topic Title')).toBeInTheDocument()
expect(document.queryByText('Attach')).toBeTruthy()
expect(document.queryByTestId('section-select')).toBeTruthy()
expect(document.queryAllByText('Anonymous Discussion')).toBeTruthy()
expect(document.queryByTestId('require-initial-post-checkbox')).toBeTruthy()
expect(document.queryByLabelText('Allow liking')).toBeInTheDocument()
expect(document.queryByTestId('group-discussion-checkbox')).toBeTruthy()
expect(document.queryAllByText('Available from')).toBeTruthy()
expect(document.queryAllByText('Until')).toBeTruthy()
// Hides announcement options
expect(document.queryByLabelText('Delay Posting')).not.toBeInTheDocument()
expect(document.queryByLabelText('Allow Participants to Comment')).not.toBeInTheDocument()
})
})
})
})

View File

@ -0,0 +1,126 @@
/*
* Copyright (C) 2024 - present Instructure, Inc.
*
* This file is part of Canvas.
*
* Canvas is free software: you can redistribute it and/or modify it under
* the terms of the GNU Affero General Public License as published by the Free
* Software Foundation, version 3 of the License.
*
* Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
* A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
* details.
*
* You should have received a copy of the GNU Affero General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import {defaultEveryoneElseOption, defaultEveryoneOption, masteryPathsOption} from '../constants'
import {prepareUngradedDiscussionOverridesPayload} from '../payloadPreparations'
describe('prepareUngradedDiscussionOverridesPayload', () => {
it('returns payload only for everyone', () => {
const assignedInfoList = [
{
assignedList: ['everyone'],
dueDate: '2024-04-15T00:00:00.000Z',
availableFrom: '2024-04-10T00:00:00.000Z',
availableUntil: '2024-04-20T00:00:00.000Z',
},
]
const payload = prepareUngradedDiscussionOverridesPayload(
assignedInfoList,
defaultEveryoneOption,
defaultEveryoneElseOption,
masteryPathsOption
)
expect(payload).toEqual({
delayedPostAt: '2024-04-10T00:00:00.000Z',
dueAt: '2024-04-15T00:00:00.000Z',
lockAt: '2024-04-20T00:00:00.000Z',
onlyVisibleToOverrides: false,
ungradedDiscussionOverrides: null,
})
})
it('returns payload for sections', () => {
const assignedInfoList = [
{
assignedList: ['course_section_2'],
dueDate: null,
availableFrom: '2024-04-10T00:00:00.000Z',
availableUntil: '2024-04-20T00:00:00.000Z',
},
]
const payload = prepareUngradedDiscussionOverridesPayload(
assignedInfoList,
defaultEveryoneOption,
defaultEveryoneElseOption,
masteryPathsOption
)
expect(payload).toEqual({
delayedPostAt: null,
dueAt: null,
lockAt: null,
onlyVisibleToOverrides: true,
ungradedDiscussionOverrides: [
{
courseId: null,
courseSectionId: '2',
dueAt: null,
groupId: null,
lockAt: '2024-04-20T00:00:00.000Z',
noopId: null,
studentIds: null,
title: null,
unassignItem: false,
unlockAt: '2024-04-10T00:00:00.000Z',
},
],
})
})
it('returns payload for section and everyone else', () => {
const assignedInfoList = [
{
assignedList: ['course_section_2'],
dueDate: null,
availableFrom: '2024-04-10T00:00:00.000Z',
availableUntil: '2024-04-20T00:00:00.000Z',
},
{
assignedList: ['everyone'],
dueDate: '2024-04-15T00:00:00.000Z',
availableFrom: '2024-04-10T00:00:00.000Z',
availableUntil: '2024-04-20T00:00:00.000Z',
},
]
const payload = prepareUngradedDiscussionOverridesPayload(
assignedInfoList,
defaultEveryoneOption,
defaultEveryoneElseOption,
masteryPathsOption
)
expect(payload).toEqual({
delayedPostAt: '2024-04-10T00:00:00.000Z',
dueAt: '2024-04-15T00:00:00.000Z',
lockAt: '2024-04-20T00:00:00.000Z',
onlyVisibleToOverrides: false,
ungradedDiscussionOverrides: [
{
courseId: null,
courseSectionId: '2',
dueAt: null,
groupId: null,
lockAt: '2024-04-20T00:00:00.000Z',
noopId: null,
studentIds: null,
title: null,
unassignItem: false,
unlockAt: '2024-04-10T00:00:00.000Z',
},
],
})
})
})

View File

@ -0,0 +1,286 @@
/*
* Copyright (C) 2024 - present Instructure, Inc.
*
* This file is part of Canvas.
*
* Canvas is free software: you can redistribute it and/or modify it under
* the terms of the GNU Affero General Public License as published by the Free
* Software Foundation, version 3 of the License.
*
* Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
* A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
* details.
*
* You should have received a copy of the GNU Affero General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import {buildAssignmentOverrides, buildDefaultAssignmentOverride} from '../utils'
import {DiscussionTopic} from '../../../graphql/DiscussionTopic'
import {Assignment} from '../../../graphql/Assignment'
import {AssignmentOverride} from '../../../graphql/AssignmentOverride'
describe('buildDefaultAssignmentOverride', () => {
it('returns default object', () => {
const overrides = buildDefaultAssignmentOverride()
expect(overrides).toEqual([
{
dueDateId: expect.any(String),
assignedList: ['everyone'],
dueDate: '',
availableFrom: '',
availableUntil: '',
},
])
})
})
describe('buildAssignmentOverrides', () => {
it('returns default for null assignment or no ungraded overrides', () => {
const discussion = DiscussionTopic.mock()
const overrides = buildAssignmentOverrides(discussion)
expect(overrides).toEqual([])
})
describe('for graded assignments', () => {
it('returns overrides when onlyVisibleToOverrides is true', () => {
const assignment = Assignment.mock({onlyVisibleToOverrides: true})
const discussion = DiscussionTopic.mock()
assignment.assignmentOverrides = {nodes: [AssignmentOverride.mock()]}
discussion.assignment = assignment
const overrides = buildAssignmentOverrides(discussion)
expect(overrides).toEqual([
{
assignedList: ['course_section_1'],
availableFrom: '2020-01-01',
availableUntil: '2020-01-01',
dueDate: '2020-01-01',
dueDateId: expect.any(String),
unassignItem: false,
},
])
})
it('returns overrides when visibleToEveryone is false', () => {
const assignment = Assignment.mock({visibleToEveryone: false})
const discussion = DiscussionTopic.mock()
assignment.assignmentOverrides = {nodes: [AssignmentOverride.mock()]}
discussion.assignment = assignment
const overrides = buildAssignmentOverrides(discussion)
expect(overrides).toEqual([
{
assignedList: ['course_section_1'],
availableFrom: '2020-01-01',
availableUntil: '2020-01-01',
dueDate: '2020-01-01',
dueDateId: expect.any(String),
unassignItem: false,
},
])
})
it('returns overrides when it has course overrides', () => {
const assignment = Assignment.mock()
const discussion = DiscussionTopic.mock()
assignment.assignmentOverrides = {
nodes: [
AssignmentOverride.mock({
set: {
__typename: 'Course',
id: '1',
name: 'Course Name',
_id: '1',
},
}),
],
}
discussion.assignment = assignment
const overrides = buildAssignmentOverrides(discussion)
expect(overrides).toEqual([
{
assignedList: ['course_1'],
availableFrom: '2020-01-01',
availableUntil: '2020-01-01',
dueDate: '2020-01-01',
dueDateId: expect.any(String),
unassignItem: false,
},
])
})
it('returns overrides with everyone', () => {
const assignment = Assignment.mock({
dueAt: '2024-04-15',
lockAt: '2024-04-12',
unlockAt: '2024-04-16',
})
const discussion = DiscussionTopic.mock()
discussion.assignment = assignment
const overrides = buildAssignmentOverrides(discussion)
expect(overrides).toEqual([
{
assignedList: ['everyone'],
availableFrom: '2024-04-16',
availableUntil: '2024-04-12',
dueDate: '2024-04-15',
dueDateId: expect.any(String),
},
])
})
it('returns overrides with everyone else', () => {
const assignment = Assignment.mock({
dueAt: '2024-04-15',
lockAt: '2024-04-12',
unlockAt: '2024-04-16',
})
const discussion = DiscussionTopic.mock()
assignment.assignmentOverrides = {nodes: [AssignmentOverride.mock()]}
discussion.assignment = assignment
const overrides = buildAssignmentOverrides(discussion)
expect(overrides).toEqual([
{
assignedList: ['course_section_1'],
availableFrom: '2020-01-01',
availableUntil: '2020-01-01',
dueDate: '2020-01-01',
dueDateId: expect.any(String),
unassignItem: false,
},
{
assignedList: ['everyone'],
availableFrom: '2024-04-16',
availableUntil: '2024-04-12',
dueDate: '2024-04-15',
dueDateId: expect.any(String),
},
])
})
})
describe('for ungraded assignments', () => {
it('returns overrides when onlyVisibleToOverrides is true', () => {
const discussion = DiscussionTopic.mock({onlyVisibleToOverrides: true})
discussion.ungradedDiscussionOverrides = {nodes: [AssignmentOverride.mock()]}
const overrides = buildAssignmentOverrides(discussion)
expect(overrides).toEqual([
{
assignedList: ['course_section_1'],
availableFrom: '2020-01-01',
availableUntil: '2020-01-01',
dueDate: '2020-01-01',
dueDateId: expect.any(String),
unassignItem: false,
},
])
})
it('returns overrides when visibleToEveryone is false', () => {
const discussion = DiscussionTopic.mock({visibleToEveryone: false})
discussion.ungradedDiscussionOverrides = {nodes: [AssignmentOverride.mock()]}
const overrides = buildAssignmentOverrides(discussion)
expect(overrides).toEqual([
{
assignedList: ['course_section_1'],
availableFrom: '2020-01-01',
availableUntil: '2020-01-01',
dueDate: '2020-01-01',
dueDateId: expect.any(String),
unassignItem: false,
},
])
})
it('returns overrides when it has course overrides', () => {
const discussion = DiscussionTopic.mock()
discussion.ungradedDiscussionOverrides = {
nodes: [
AssignmentOverride.mock({
set: {
__typename: 'Course',
id: '1',
name: 'Course Name',
_id: '1',
},
}),
],
}
const overrides = buildAssignmentOverrides(discussion)
expect(overrides).toEqual([
{
assignedList: ['course_1'],
availableFrom: '2020-01-01',
availableUntil: '2020-01-01',
dueDate: '2020-01-01',
dueDateId: expect.any(String),
unassignItem: false,
},
])
})
it('returns overrides with everyone', () => {
const discussion = DiscussionTopic.mock({
lockAt: '2024-04-12',
delayedPostAt: '2024-04-15',
visibleToEveryone: true,
})
const overrides = buildAssignmentOverrides(discussion)
expect(overrides).toEqual([
{
assignedList: ['everyone'],
availableFrom: '2024-04-15',
availableUntil: '2024-04-12',
dueDate: undefined,
dueDateId: expect.any(String),
},
])
})
it('returns overrides with everyone else', () => {
const discussion = DiscussionTopic.mock({
lockAt: '2024-04-12',
delayedPostAt: '2024-04-15',
visibleToEveryone: true,
})
discussion.ungradedDiscussionOverrides = {nodes: [AssignmentOverride.mock()]}
const overrides = buildAssignmentOverrides(discussion)
expect(overrides).toEqual([
{
assignedList: ['course_section_1'],
availableFrom: '2020-01-01',
availableUntil: '2020-01-01',
dueDate: '2020-01-01',
dueDateId: expect.any(String),
unassignItem: false,
},
{
assignedList: ['everyone'],
availableFrom: '2024-04-15',
availableUntil: '2024-04-12',
dueDate: undefined,
dueDateId: expect.any(String),
},
])
})
})
})

View File

@ -34,7 +34,7 @@ export const masteryPathsOption = {
label: I18n.t('Mastery Paths'),
}
const GradedDiscussionDueDateDefaultValues = {
const DiscussionDueDateDefaultValues = {
assignedInfoList: [],
setAssignedInfoList: () => {},
studentEnrollments: [],
@ -52,9 +52,7 @@ const GradedDiscussionDueDateDefaultValues = {
setImportantDates: newImportantDatesValue => {},
}
export const GradedDiscussionDueDatesContext = React.createContext(
GradedDiscussionDueDateDefaultValues
)
export const DiscussionDueDatesContext = React.createContext(DiscussionDueDateDefaultValues)
export const ASSIGNMENT_OVERRIDE_GRAPHQL_TYPENAMES = {
ADHOC: 'AdhocStudents',
@ -129,6 +127,17 @@ export const useShouldShowContent = (
const shouldShowCheckpointsOptions = isGraded && ENV.DISCUSSION_CHECKPOINTS_ENABLED
const canCreateGradedDiscussion =
!isEditing && ENV?.DISCUSSION_TOPIC?.PERMISSIONS?.CAN_CREATE_ASSIGNMENT
const canEditDiscussionAssignment =
isEditing && ENV?.DISCUSSION_TOPIC?.PERMISSIONS?.CAN_UPDATE_ASSIGNMENT
const shouldShowAssignToForUngradedDiscussions =
!isAnnouncement &&
!isGraded &&
ENV?.FEATURES?.differentiated_modules &&
(canCreateGradedDiscussion || canEditDiscussionAssignment)
return {
shouldShowTodoSettings,
shouldShowPostToSectionOption,
@ -143,5 +152,6 @@ export const useShouldShowContent = (
shouldShowSaveAndPublishButton,
shouldShowPodcastFeedOption,
shouldShowCheckpointsOptions,
shouldShowAssignToForUngradedDiscussions,
}
}

View File

@ -211,6 +211,17 @@ export const prepareCheckpointsPayload = (
: []
}
const prepareEveryoneOrEveryoneElseOverride = (
assignedInfoList,
defaultEveryoneOption,
defaultEveryoneElseOption
) =>
assignedInfoList.find(
info =>
info.assignedList.includes(defaultEveryoneOption.assetCode) ||
info.assignedList.includes(defaultEveryoneElseOption.assetCode)
) || {}
export const prepareAssignmentPayload = (
abGuid,
isEditing,
@ -240,12 +251,11 @@ export const prepareAssignmentPayload = (
*/
if (!isGraded && !existingAssignment) return null
const everyoneOverride =
assignedInfoList.find(
info =>
info.assignedList.includes(defaultEveryoneOption.assetCode) ||
info.assignedList.includes(defaultEveryoneElseOption.assetCode)
) || {}
const everyoneOverride = prepareEveryoneOrEveryoneElseOverride(
assignedInfoList,
defaultEveryoneOption,
defaultEveryoneElseOption
)
// Common payload properties for graded assignments
let payload = {
postToSis,
@ -301,3 +311,28 @@ export const prepareAssignmentPayload = (
}
return payload
}
export const prepareUngradedDiscussionOverridesPayload = (
assignedInfoList,
defaultEveryoneOption,
defaultEveryoneElseOption,
masteryPathsOption
) => {
const everyoneOverride = prepareEveryoneOrEveryoneElseOverride(
assignedInfoList,
defaultEveryoneOption,
defaultEveryoneElseOption
)
return {
dueAt: everyoneOverride.dueDate || null,
lockAt: everyoneOverride.availableUntil || null,
delayedPostAt: everyoneOverride.availableFrom || null,
onlyVisibleToOverrides: setOnlyVisibleToOverrides(assignedInfoList, everyoneOverride),
ungradedDiscussionOverrides: prepareAssignmentOverridesPayload(
assignedInfoList,
defaultEveryoneOption,
masteryPathsOption
),
}
}

View File

@ -88,12 +88,18 @@ export const buildDefaultAssignmentOverride = () => {
},
]
}
export const buildAssignmentOverrides = discussion => {
const target = discussion.assignment || discussion
export const buildAssignmentOverrides = assignment => {
if (!assignment) return buildDefaultAssignmentOverride()
if (!target) return buildDefaultAssignmentOverride()
const overrides =
assignment?.assignmentOverrides?.nodes?.map(override => ({
let overrides =
target === discussion.assignment
? target.assignmentOverrides
: target.ungradedDiscussionOverrides
overrides =
overrides?.nodes?.map(override => ({
dueDateId: override.id,
assignedList: getAssignedList(override),
dueDate: override.dueAt,
@ -110,7 +116,7 @@ export const buildAssignmentOverrides = assignment => {
obj.assignedList.some(item => item.includes('course') && !item.includes('section'))
)
// When this is true, then we do not have a everyone/everyone else option
if (assignment.onlyVisibleToOverrides || !assignment.visibleToEveryone || hasCourseOverride)
if (target.onlyVisibleToOverrides || !target.visibleToEveryone || hasCourseOverride)
return overrides
overrides.push({
@ -119,9 +125,9 @@ export const buildAssignmentOverrides = assignment => {
overrides.length > 0
? [defaultEveryoneElseOption.assetCode]
: [defaultEveryoneOption.assetCode],
dueDate: assignment.dueAt,
availableFrom: assignment.unlockAt,
availableUntil: assignment.lockAt,
dueDate: target.dueAt,
availableFrom: target.unlockAt || target.delayedPostAt,
availableUntil: target.lockAt,
})
return overrides.length > 0 ? overrides : buildDefaultAssignmentOverride()

View File

@ -80,9 +80,6 @@
{
"name": "AssignmentGroup"
},
{
"name": "AssignmentOverride"
},
{
"name": "CommentBankItem"
},

View File

@ -279,7 +279,8 @@ describe('ItemAssignToCard', () => {
expect(getByLabelText('Due Date')).not.toBeDisabled()
})
it('renders error when date change to a closed grading period for teacher', async () => {
it.skip('renders error when date change to a closed grading period for teacher', async () => {
// Flakey spec
withWithGradingPeriodsMock()
window.ENV.current_user_is_admin = false

View File

@ -58,6 +58,7 @@ const DifferentiatedModulesSection = ({
importantDates,
onTrayOpen,
onTrayClose,
removeDueDateInput = false,
}) => {
const [open, setOpen] = useState(false)
// stagedCards are the itemAssignToCards that will be saved when the assignment is saved
@ -78,11 +79,15 @@ const DifferentiatedModulesSection = ({
const [moduleAssignees, setModuleAssignees] = useState([])
const linkRef = useRef()
const formData = useMemo(() => ({
assignmentName: getAssignmentName(),
pointsPossible: getPointsPossible(),
groupCategoryId: getGroupCategoryId?.()
}), [open]);
const formData = useMemo(
() => ({
assignmentName: getAssignmentName(),
pointsPossible: getPointsPossible(),
groupCategoryId: getGroupCategoryId?.(),
}),
// eslint-disable-next-line react-hooks/exhaustive-deps
[open]
)
useEffect(() => {
const updatedOverrides = overrides.map(override => {
@ -99,7 +104,11 @@ const DifferentiatedModulesSection = ({
useEffect(() => {
if (stagedOverrides === null) return
const parsedOverrides = getParsedOverrides(stagedOverrides, stagedCards, formData.groupCategoryId)
const parsedOverrides = getParsedOverrides(
stagedOverrides,
stagedCards,
formData.groupCategoryId
)
const uniqueOverrides = removeOverriddenAssignees(overrides, parsedOverrides)
setStagedCards(uniqueOverrides)
if (initialState === null) {
@ -219,7 +228,12 @@ const DifferentiatedModulesSection = ({
)
const defaultState = getParsedOverrides(preSaved, checkPoint)
const checkPointOverrides = getAllOverridesFromCards(defaultState).filter(
card => card.course_section_id || card.student_ids || card.noop_id || card.course_id || card.group_id
card =>
card.course_section_id ||
card.student_ids ||
card.noop_id ||
card.course_id ||
card.group_id
)
setStagedOverrides(checkPointOverrides)
const newStagedCards = resetStagedCards(stagedCards, checkPoint, defaultState)
@ -233,7 +247,12 @@ const DifferentiatedModulesSection = ({
newCard.draft = true
newCard.index = stagedOverrides.length + 1
const oldOverrides = getAllOverridesFromCards(stagedCards).filter(
card => card.course_section_id || card.student_ids || card.noop_id || card.course_id || card.group_id
card =>
card.course_section_id ||
card.student_ids ||
card.noop_id ||
card.course_id ||
card.group_id
)
const newStageOverrides = [...oldOverrides, newCard]
setStagedOverrides(newStageOverrides)
@ -283,6 +302,7 @@ const DifferentiatedModulesSection = ({
return {
...override,
[dateType]: date,
[`${dateType}_overridden`]: !!date,
}
})
@ -370,7 +390,12 @@ const DifferentiatedModulesSection = ({
const handleSave = () => {
const newOverrides = getAllOverridesFromCards(stagedCards).filter(
card => card.course_section_id || card.student_ids || card.noop_id || card.course_id || card.group_id
card =>
card.course_section_id ||
card.student_ids ||
card.noop_id ||
card.course_id ||
card.group_id
)
const deletedModuleAssignees = moduleAssignees.filter(
@ -485,6 +510,7 @@ const DifferentiatedModulesSection = ({
onAssigneesChange={handleChange}
onDatesChange={handleDatesUpdate}
onCardRemove={handleCardRemove}
removeDueDateInput={removeDueDateInput}
/>
</>
)
@ -502,5 +528,7 @@ DifferentiatedModulesSection.propTypes = {
getGroupCategoryId: func,
onTrayOpen: func,
onTrayClose: func,
removeDueDateInput: bool,
}
export default DifferentiatedModulesSection