remove concluded enrollments from course roster table
fixes VICE-3051 flag=react_people_page Test Plan: - Tests pass - With FF turned off, visit a course people page as a user with the teacher role - For an existing user, add a new enrollment (role and/or section) that is distinct from its existing enrollments - Visit the user's user detail page and conclude the enrollment that you just added - Turn on 'People Page Upgrade' FF - Visit the course people page and verify that the concluded enrollment is not visible in the roster table Change-Id: I0f9ce2f7d2c065f47215d2c0187d5b8856e58c6e Reviewed-on: https://gerrit.instructure.com/c/canvas-lms/+/298420 Tested-by: Service Cloud Jenkins <svc.cloudjenkins@instructure.com> Reviewed-by: Omar Soto-Fortuño <omar.soto@instructure.com> Product-Review: Omar Soto-Fortuño <omar.soto@instructure.com> QA-Review: Jason Gillett <jason.gillett@instructure.com>
This commit is contained in:
parent
cda83fd89d
commit
c62a32194e
|
@ -112,6 +112,9 @@ module Types
|
||||||
argument :order_by, [String],
|
argument :order_by, [String],
|
||||||
"The fields to order the results by",
|
"The fields to order the results by",
|
||||||
required: false
|
required: false
|
||||||
|
argument :exclude_concluded, Boolean,
|
||||||
|
"Whether or not to exclude `completed` enrollments",
|
||||||
|
required: false
|
||||||
end
|
end
|
||||||
|
|
||||||
field :login_id, String, null: true
|
field :login_id, String, null: true
|
||||||
|
@ -132,12 +135,13 @@ module Types
|
||||||
pseudonym.unique_id
|
pseudonym.unique_id
|
||||||
end
|
end
|
||||||
|
|
||||||
def enrollments(course_id: nil, current_only: false, order_by: [])
|
def enrollments(course_id: nil, current_only: false, order_by: [], exclude_concluded: false)
|
||||||
course_ids = [course_id].compact
|
course_ids = [course_id].compact
|
||||||
Loaders::UserCourseEnrollmentLoader.for(
|
Loaders::UserCourseEnrollmentLoader.for(
|
||||||
course_ids: course_ids,
|
course_ids: course_ids,
|
||||||
order_by: order_by,
|
order_by: order_by,
|
||||||
current_only: current_only
|
current_only: current_only,
|
||||||
|
exclude_concluded: exclude_concluded
|
||||||
).load(object.id).then do |enrollments|
|
).load(object.id).then do |enrollments|
|
||||||
(enrollments || []).select do |enrollment|
|
(enrollments || []).select do |enrollment|
|
||||||
object == context[:current_user] ||
|
object == context[:current_user] ||
|
||||||
|
@ -434,7 +438,7 @@ end
|
||||||
|
|
||||||
module Loaders
|
module Loaders
|
||||||
class UserCourseEnrollmentLoader < Loaders::ForeignKeyLoader
|
class UserCourseEnrollmentLoader < Loaders::ForeignKeyLoader
|
||||||
def initialize(course_ids:, order_by: [], current_only: false)
|
def initialize(course_ids:, order_by: [], current_only: false, exclude_concluded: false)
|
||||||
scope = Enrollment.joins(:course)
|
scope = Enrollment.joins(:course)
|
||||||
|
|
||||||
scope = if current_only
|
scope = if current_only
|
||||||
|
@ -446,6 +450,8 @@ module Loaders
|
||||||
|
|
||||||
scope = scope.where(course_id: course_ids) if course_ids.present?
|
scope = scope.where(course_id: course_ids) if course_ids.present?
|
||||||
|
|
||||||
|
scope = scope.where.not(enrollments: { workflow_state: "completed" }) if exclude_concluded
|
||||||
|
|
||||||
order_by.each { |o| scope = scope.order(o) }
|
order_by.each { |o| scope = scope.order(o) }
|
||||||
|
|
||||||
super(scope, :user_id)
|
super(scope, :user_id)
|
||||||
|
|
|
@ -283,6 +283,12 @@ describe Types::UserType do
|
||||||
user_type.resolve(%|enrollments(courseId: "#{@course2.id}") { _id }|)
|
user_type.resolve(%|enrollments(courseId: "#{@course2.id}") { _id }|)
|
||||||
).to eq []
|
).to eq []
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it "excludes concluded enrollments when excludeConcluded is true" do
|
||||||
|
expect(user_type.resolve("enrollments(excludeConcluded: true) { _id }").length).to eq 1
|
||||||
|
@student.enrollments.update_all workflow_state: "completed"
|
||||||
|
expect(user_type.resolve("enrollments(excludeConcluded: true) { _id }")).to eq []
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context "email" do
|
context "email" do
|
||||||
|
|
|
@ -35,37 +35,34 @@ export const mockUser = ({
|
||||||
sectionName = 'Section 1',
|
sectionName = 'Section 1',
|
||||||
additionalEnrollments = []
|
additionalEnrollments = []
|
||||||
} = {}) => ({
|
} = {}) => ({
|
||||||
node: {
|
name,
|
||||||
name,
|
_id,
|
||||||
_id,
|
id: Buffer.from(`User-${_id}`).toString('base64'),
|
||||||
id: Buffer.from(`User-${_id}`).toString('base64'),
|
sisId,
|
||||||
sisId,
|
avatarUrl,
|
||||||
avatarUrl,
|
pronouns,
|
||||||
pronouns,
|
loginId,
|
||||||
loginId,
|
__typename: 'user',
|
||||||
__typename: 'user',
|
enrollments: [
|
||||||
enrollments: [
|
{
|
||||||
{
|
id: Buffer.from(`Enrollment-${_id}`).toString('base64'),
|
||||||
id: Buffer.from(`Enrollment-${_id}`).toString('base64'),
|
type: enrollmentType,
|
||||||
type: enrollmentType,
|
state: enrollmentStatus,
|
||||||
state: enrollmentStatus,
|
lastActivityAt,
|
||||||
lastActivityAt,
|
htmlUrl: `http://test.host/courses/${courseID}/users/${_id}`,
|
||||||
htmlUrl: `http://test.host/courses/${courseID}/users/${_id}`,
|
totalActivityTime,
|
||||||
totalActivityTime,
|
canBeRemoved,
|
||||||
canBeRemoved,
|
associatedUser: null, // always null for user's own enrollment
|
||||||
associatedUser: null, // always null for user's own enrollment
|
__typename: 'enrollment',
|
||||||
__typename: 'enrollment',
|
section: {
|
||||||
section: {
|
_id: sectionID,
|
||||||
_id: sectionID,
|
id: Buffer.from(`Section-${sectionID}`).toString('base64'),
|
||||||
id: Buffer.from(`Section-${sectionID}`).toString('base64'),
|
name: sectionName,
|
||||||
name: sectionName,
|
__typename: 'section'
|
||||||
__typename: 'section'
|
}
|
||||||
}
|
},
|
||||||
},
|
...additionalEnrollments
|
||||||
...additionalEnrollments
|
]
|
||||||
]
|
|
||||||
},
|
|
||||||
__typename: 'usersConnectionEdge'
|
|
||||||
})
|
})
|
||||||
|
|
||||||
export const mockEnrollment = ({
|
export const mockEnrollment = ({
|
||||||
|
@ -115,7 +112,7 @@ export const getRosterQueryMock = ({mockUsers = [], courseID = '1', shouldError
|
||||||
data: {
|
data: {
|
||||||
course: {
|
course: {
|
||||||
usersConnection: {
|
usersConnection: {
|
||||||
edges: [...mockUsers],
|
nodes: [...mockUsers],
|
||||||
__typename: 'usersConnection'
|
__typename: 'usersConnection'
|
||||||
},
|
},
|
||||||
__typename: 'course'
|
__typename: 'course'
|
||||||
|
|
|
@ -22,33 +22,31 @@ export const ROSTER_QUERY = gql`
|
||||||
query getRosterQuery($courseID: ID!) {
|
query getRosterQuery($courseID: ID!) {
|
||||||
course(id: $courseID) {
|
course(id: $courseID) {
|
||||||
usersConnection {
|
usersConnection {
|
||||||
edges {
|
nodes {
|
||||||
node {
|
name
|
||||||
name
|
_id
|
||||||
_id
|
id
|
||||||
|
sisId
|
||||||
|
avatarUrl
|
||||||
|
pronouns
|
||||||
|
loginId
|
||||||
|
enrollments(courseId: $courseID, excludeConcluded: true) {
|
||||||
id
|
id
|
||||||
sisId
|
type
|
||||||
avatarUrl
|
state
|
||||||
pronouns
|
lastActivityAt
|
||||||
loginId
|
htmlUrl
|
||||||
enrollments(courseId: $courseID) {
|
totalActivityTime
|
||||||
|
canBeRemoved
|
||||||
|
associatedUser {
|
||||||
|
_id
|
||||||
id
|
id
|
||||||
type
|
name
|
||||||
state
|
}
|
||||||
lastActivityAt
|
section {
|
||||||
htmlUrl
|
_id
|
||||||
totalActivityTime
|
id
|
||||||
canBeRemoved
|
name
|
||||||
associatedUser {
|
|
||||||
_id
|
|
||||||
id
|
|
||||||
name
|
|
||||||
}
|
|
||||||
section {
|
|
||||||
_id
|
|
||||||
id
|
|
||||||
name
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -51,8 +51,8 @@ const RosterTable = () => {
|
||||||
|
|
||||||
const {view_user_logins, read_sis} = ENV?.permissions || {}
|
const {view_user_logins, read_sis} = ENV?.permissions || {}
|
||||||
|
|
||||||
const tableRows = data.course.usersConnection.edges.map(edge => {
|
const tableRows = data.course.usersConnection.nodes.map(node => {
|
||||||
const {name, _id, sisId, enrollments, loginId, avatarUrl, pronouns} = edge.node
|
const {name, _id, sisId, enrollments, loginId, avatarUrl, pronouns} = node
|
||||||
const {totalActivityTime, htmlUrl, state} = enrollments[0]
|
const {totalActivityTime, htmlUrl, state} = enrollments[0]
|
||||||
|
|
||||||
const sectionNames = enrollments.map(enrollment => {
|
const sectionNames = enrollments.map(enrollment => {
|
||||||
|
|
|
@ -169,7 +169,7 @@ describe('RosterTable', () => {
|
||||||
it('should wrap the name of each user in a button', async () => {
|
it('should wrap the name of each user in a button', async () => {
|
||||||
const container = setup(getRosterQueryMock({mockUsers}))
|
const container = setup(getRosterQueryMock({mockUsers}))
|
||||||
const cells = await container.findAllByTestId('roster-table-name-cell')
|
const cells = await container.findAllByTestId('roster-table-name-cell')
|
||||||
const names = mockUsers.map(user => user.node.name)
|
const names = mockUsers.map(user => user.name)
|
||||||
cells.forEach((cell, index) => {
|
cells.forEach((cell, index) => {
|
||||||
const nameMatch = new RegExp(names[index])
|
const nameMatch = new RegExp(names[index])
|
||||||
const button = within(cell).getByRole('button', {name: nameMatch})
|
const button = within(cell).getByRole('button', {name: nameMatch})
|
||||||
|
@ -186,13 +186,13 @@ describe('RosterTable', () => {
|
||||||
const container = setup(getRosterQueryMock({mockUsers: [mockSelf]}))
|
const container = setup(getRosterQueryMock({mockUsers: [mockSelf]}))
|
||||||
const cells = await container.findByTestId('roster-table-name-cell')
|
const cells = await container.findByTestId('roster-table-name-cell')
|
||||||
const link = within(cells).getByRole('link', {name: nameMatch})
|
const link = within(cells).getByRole('link', {name: nameMatch})
|
||||||
expect(link).toHaveAttribute('href', mockSelf.node.enrollments.htmlUrl)
|
expect(link).toHaveAttribute('href', mockSelf.enrollments.htmlUrl)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should not link the current_user to the user detail page when clicking a name that is not their own', async () => {
|
it('should not link the current_user to the user detail page when clicking a name that is not their own', async () => {
|
||||||
const container = setup(getRosterQueryMock({mockUsers}))
|
const container = setup(getRosterQueryMock({mockUsers}))
|
||||||
const cells = await container.findAllByTestId('roster-table-name-cell')
|
const cells = await container.findAllByTestId('roster-table-name-cell')
|
||||||
const names = mockUsers.map(user => user.node.name)
|
const names = mockUsers.map(user => user.name)
|
||||||
cells.forEach((cell, index) => {
|
cells.forEach((cell, index) => {
|
||||||
const nameMatch = new RegExp(names[index])
|
const nameMatch = new RegExp(names[index])
|
||||||
const button = within(cell).getByRole('button', {name: nameMatch})
|
const button = within(cell).getByRole('button', {name: nameMatch})
|
||||||
|
@ -208,9 +208,9 @@ describe('RosterTable', () => {
|
||||||
const container = setup(getRosterQueryMock({mockUsers}))
|
const container = setup(getRosterQueryMock({mockUsers}))
|
||||||
const rows = await container.findAllByTestId('roster-table-data-row')
|
const rows = await container.findAllByTestId('roster-table-data-row')
|
||||||
const lastActivityByUser = mockUsers.map(user => {
|
const lastActivityByUser = mockUsers.map(user => {
|
||||||
return user.node.enrollments[0].type === 'ObserverEnrollment'
|
return user.enrollments[0].type === 'ObserverEnrollment'
|
||||||
? null
|
? null
|
||||||
: user.node.enrollments[0].lastActivityAt
|
: user.enrollments[0].lastActivityAt
|
||||||
})
|
})
|
||||||
rows.forEach((row, index) => {
|
rows.forEach((row, index) => {
|
||||||
const lastActivity = queryAllByText(row, datetimePattern)
|
const lastActivity = queryAllByText(row, datetimePattern)
|
||||||
|
@ -221,7 +221,7 @@ describe('RosterTable', () => {
|
||||||
it('should display users total activity time only if total time is greater than zero', async () => {
|
it('should display users total activity time only if total time is greater than zero', async () => {
|
||||||
const container = setup(getRosterQueryMock({mockUsers}))
|
const container = setup(getRosterQueryMock({mockUsers}))
|
||||||
const rows = await container.findAllByTestId('roster-table-data-row')
|
const rows = await container.findAllByTestId('roster-table-data-row')
|
||||||
const totalActivityByUser = mockUsers.map(user => user.node.enrollments[0].totalActivityTime)
|
const totalActivityByUser = mockUsers.map(user => user.enrollments[0].totalActivityTime)
|
||||||
rows.forEach((row, index) => {
|
rows.forEach((row, index) => {
|
||||||
const totalActivity = queryAllByText(row, /^[0-9]+(:[0-5][0-9]){1,2}$/) // 00:00 or 00:00:00
|
const totalActivity = queryAllByText(row, /^[0-9]+(:[0-5][0-9]){1,2}$/) // 00:00 or 00:00:00
|
||||||
expect(totalActivity).toHaveLength(totalActivityByUser[index] ? 1 : 0)
|
expect(totalActivity).toHaveLength(totalActivityByUser[index] ? 1 : 0)
|
||||||
|
@ -231,7 +231,7 @@ describe('RosterTable', () => {
|
||||||
it('should list the user pronouns if available', async () => {
|
it('should list the user pronouns if available', async () => {
|
||||||
const container = setup(getRosterQueryMock({mockUsers}))
|
const container = setup(getRosterQueryMock({mockUsers}))
|
||||||
const cells = await container.findAllByTestId('roster-table-name-cell')
|
const cells = await container.findAllByTestId('roster-table-name-cell')
|
||||||
const userPronouns = mockUsers.map(user => user.node.pronouns)
|
const userPronouns = mockUsers.map(user => user.pronouns)
|
||||||
cells.forEach((cell, index) => {
|
cells.forEach((cell, index) => {
|
||||||
if (userPronouns[index]) {
|
if (userPronouns[index]) {
|
||||||
const pronounMatch = new RegExp(userPronouns[index], 'i')
|
const pronounMatch = new RegExp(userPronouns[index], 'i')
|
||||||
|
@ -243,7 +243,7 @@ describe('RosterTable', () => {
|
||||||
it('should list the user status if not active', async () => {
|
it('should list the user status if not active', async () => {
|
||||||
const container = setup(getRosterQueryMock({mockUsers}))
|
const container = setup(getRosterQueryMock({mockUsers}))
|
||||||
const cells = await container.findAllByTestId('roster-table-name-cell')
|
const cells = await container.findAllByTestId('roster-table-name-cell')
|
||||||
const userStatus = mockUsers.map(user => user.node.enrollments[0].state)
|
const userStatus = mockUsers.map(user => user.enrollments[0].state)
|
||||||
cells.forEach((cell, index) => {
|
cells.forEach((cell, index) => {
|
||||||
if (userStatus[index] !== ACTIVE_STATE) {
|
if (userStatus[index] !== ACTIVE_STATE) {
|
||||||
const status = PILL_MAP[userStatus[index]].text
|
const status = PILL_MAP[userStatus[index]].text
|
||||||
|
@ -261,7 +261,7 @@ describe('RosterTable', () => {
|
||||||
expect(container.queryAllByTestId('colheader-login-id')).toHaveLength(0)
|
expect(container.queryAllByTestId('colheader-login-id')).toHaveLength(0)
|
||||||
|
|
||||||
// Check there is no login id data
|
// Check there is no login id data
|
||||||
const loginIdByUser = mockUsers.map(user => user.node.enrollments[0].loginId)
|
const loginIdByUser = mockUsers.map(user => user.enrollments[0].loginId)
|
||||||
rows.forEach((row, index) => {
|
rows.forEach((row, index) => {
|
||||||
loginIdByUser[index] && expect(queryAllByText(row, loginIdByUser[index])).toHaveLength(0)
|
loginIdByUser[index] && expect(queryAllByText(row, loginIdByUser[index])).toHaveLength(0)
|
||||||
})
|
})
|
||||||
|
@ -271,7 +271,7 @@ describe('RosterTable', () => {
|
||||||
window.ENV.permissions.read_sis = false
|
window.ENV.permissions.read_sis = false
|
||||||
const container = setup(getRosterQueryMock({mockUsers}))
|
const container = setup(getRosterQueryMock({mockUsers}))
|
||||||
const rows = await container.findAllByTestId('roster-table-data-row')
|
const rows = await container.findAllByTestId('roster-table-data-row')
|
||||||
const sisIdByUser = mockUsers.map(user => user.node.enrollments[0].sisId)
|
const sisIdByUser = mockUsers.map(user => user.enrollments[0].sisId)
|
||||||
|
|
||||||
// Check there is no column header
|
// Check there is no column header
|
||||||
expect(container.queryAllByTestId('colheader-sis-id')).toHaveLength(0)
|
expect(container.queryAllByTestId('colheader-sis-id')).toHaveLength(0)
|
||||||
|
|
Loading…
Reference in New Issue