include “Term” column in course search results

Closes: CNVS-39755 CNVS-39647

Test plan:
* enable user/course search feature flag
* go to /accounts/x
* on the “Courses” tab, there should be a “Term”
  Column on the results table
* clicking it should sort the results by term
  name properly

Change-Id: Idb27356f6eeb66380f739b8d4550cf2fdd908dc2
Reviewed-on: https://gerrit.instructure.com/128347
Tested-by: Jenkins
Reviewed-by: Clay Diffrient <cdiffrient@instructure.com>
Product-Review: Ryan Shaw <ryan@instructure.com>
QA-Review: Ryan Shaw <ryan@instructure.com>
This commit is contained in:
Ryan Shaw 2017-10-03 16:29:32 -06:00
parent ce29dd4fe0
commit aac714911f
6 changed files with 200 additions and 349 deletions

View File

@ -378,6 +378,10 @@ class AccountsController < ApplicationController
"(SELECT #{name_col} FROM #{Account.quoted_table_name}
WHERE #{Account.quoted_table_name}.id
= #{Course.quoted_table_name}.account_id)"
elsif params[:sort] == 'term'
"(SELECT #{EnrollmentTerm.quoted_table_name}.name FROM #{EnrollmentTerm.quoted_table_name}
WHERE #{EnrollmentTerm.quoted_table_name}.id
= #{Course.quoted_table_name}.enrollment_term_id)"
else
"id"
end

View File

@ -17,9 +17,9 @@
*/
import preventDefault from 'compiled/fn/preventDefault'
import IconArrowUpSolid from 'instructure-icons/lib/Solid/IconArrowUpSolid'
import IconArrowDownSolid from 'instructure-icons/lib/Solid/IconArrowDownSolid'
import Typography from 'instructure-ui/lib/components/Typography'
import IconMiniArrowUpSolid from 'instructure-icons/lib/Solid/IconMiniArrowUpSolid'
import IconMiniArrowDownSolid from 'instructure-icons/lib/Solid/IconMiniArrowDownSolid'
import Link from 'instructure-ui/lib/components/Link'
import Tooltip from 'instructure-ui/lib/components/Tooltip'
import React from 'react'
import PropTypes from 'prop-types'
@ -68,91 +68,24 @@ class CoursesList extends React.Component {
})
}
renderHeader ({id, label, tipDesc, tipAsc}) {
return (
<Tooltip
as={Link}
tip={(this.props.sort === id && this.props.order === 'asc') ? tipAsc : tipDesc}
onClick={preventDefault(() => this.props.onChangeSort(id))}
>
{label}
{this.props.sort === id ?
(this.props.order === 'asc' ? <IconMiniArrowDownSolid /> : <IconMiniArrowUpSolid />) :
''
}
</Tooltip>
)
}
render () {
const sort = this.props.sort
const order = this.props.order
const courseLabel = I18n.t('Course')
const idLabel = I18n.t('SIS ID')
const teacherLabel = I18n.t('Teacher')
const enrollmentsLabel = I18n.t('Enrollments')
const subaccountLabel = I18n.t('Sub-Account')
let courseTip
let idTip
let teacherTip
let enrollmentsTip
let subaccountTip
let courseArrow = ''
let idArrow = ''
let teacherArrow = ''
let enrollmentsArrow = ''
let subaccountArrow = ''
if (sort === 'course_name') {
idTip = I18n.t('Click to sort by SIS ID ascending')
teacherTip = I18n.t('Click to sort by teacher ascending')
enrollmentsTip = I18n.t('Click to sort by enrollments ascending')
subaccountTip = I18n.t('Click to sort by subaccount ascending')
if (order === 'asc') {
courseTip = I18n.t('Click to sort by name descending')
courseArrow = <IconArrowDownSolid />
} else {
courseTip = I18n.t('Click to sort by name ascending')
courseArrow = <IconArrowUpSolid />
}
} else if (sort === 'sis_course_id') {
courseTip = I18n.t('Click to sort by name ascending')
teacherTip = I18n.t('Click to sort by teacher ascending')
enrollmentsTip = I18n.t('Click to sort by enrollments ascending')
subaccountTip = I18n.t('Click to sort by subaccount ascending')
if (order === 'asc') {
idTip = I18n.t('Click to sort by SIS ID descending')
idArrow = <IconArrowDownSolid />
} else {
idTip = I18n.t('Click to sort by SIS ID ascending')
idArrow = <IconArrowUpSolid />
}
} else if (sort === 'teacher') {
courseTip = I18n.t('Click to sort by name ascending')
idTip = I18n.t('Click to sort by SIS ID ascending')
enrollmentsTip = I18n.t('Click to sort by enrollments ascending')
subaccountTip = I18n.t('Click to sort by subaccount ascending')
if (order === 'asc') {
teacherTip = I18n.t('Click to sort by teacher descending')
teacherArrow = <IconArrowDownSolid />
} else {
teacherTip = I18n.t('Click to sort by teacher ascending')
teacherArrow = <IconArrowUpSolid />
}
} else if (sort === 'enrollments') {
courseTip = I18n.t('Click to sort by name ascending')
idTip = I18n.t('Click to sort by SIS ID ascending')
teacherTip = I18n.t('Click to sort by teacher ascending')
subaccountTip = I18n.t('Click to sort by subaccount ascending')
if (order === 'asc') {
enrollmentsTip = I18n.t('Click to sort by enrollments descending')
enrollmentsArrow = <IconArrowDownSolid />
} else {
enrollmentsTip = I18n.t('Click to sort by enrollments ascending')
enrollmentsArrow = <IconArrowUpSolid />
}
} else if (sort === 'subaccount') {
courseTip = I18n.t('Click to sort by name ascending')
idTip = I18n.t('Click to sort by SIS ID ascending')
teacherTip = I18n.t('Click to sort by teacher ascending')
enrollmentsTip = I18n.t('Click to sort by enrollments ascending')
if (order === 'asc') {
subaccountTip = I18n.t('Click to sort by subaccount descending')
subaccountArrow = <IconArrowDownSolid />
} else {
subaccountTip = I18n.t('Click to sort by subaccount ascending')
subaccountArrow = <IconArrowUpSolid />
}
}
const courses = this.props.courses
return (
@ -162,71 +95,54 @@ class CoursesList extends React.Component {
<div className="grid-row">
<div className="col-xs-2" />
<div className="col-xs-10" role="columnheader">
<a
role="button"
href=""
className="courses-user-list-header"
onClick={preventDefault(() => this.props.onChangeSort('course_name'))}
>
<Tooltip as={Typography} tip={courseTip}>
{courseLabel}
{courseArrow}
</Tooltip>
</a>
{this.renderHeader({
id: 'course_name',
label: I18n.t('Course'),
tipDesc: I18n.t('Click to sort by name ascending'),
tipAsc: I18n.t('Click to sort by name descending')
})}
</div>
</div>
</div>
<div role="columnheader" className="col-xs-1">
<a
role="button"
href=""
className="courses-user-list-header"
onClick={preventDefault(() => this.props.onChangeSort('sis_course_id'))}
>
<Tooltip as={Typography} tip={idTip}>
{idLabel}
{idArrow}
</Tooltip>
</a>
</div>
<div role="columnheader" className="col-xs-2">
<a
role="button"
href=""
className="courses-user-list-header"
onClick={preventDefault(() => this.props.onChangeSort('teacher'))}
>
<Tooltip as={Typography} tip={teacherTip}>
{teacherLabel}
{teacherArrow}
</Tooltip>
</a>
</div>
<div role="columnheader" className="col-xs-2">
<a
role="button"
href=""
className="courses-user-list-header"
onClick={preventDefault(() => this.props.onChangeSort('subaccount'))}
>
<Tooltip as={Typography} tip={subaccountTip}>
{subaccountLabel}
{subaccountArrow}
</Tooltip>
</a>
{this.renderHeader({
id: 'sis_course_id',
label: I18n.t('SIS ID'),
tipDesc: I18n.t('Click to sort by SIS ID ascending'),
tipAsc: I18n.t('Click to sort by SIS ID descending')
})}
</div>
<div role="columnheader" className="col-xs-1">
<a
role="button"
href=""
className="courses-user-list-header"
onClick={preventDefault(() => this.props.onChangeSort('enrollments'))}
>
<Tooltip as={Typography} tip={enrollmentsTip}>
{enrollmentsLabel}
{enrollmentsArrow}
</Tooltip>
</a>
{this.renderHeader({
id: 'term',
label: I18n.t('Term'),
tipDesc: I18n.t('Click to sort by term ascending'),
tipAsc: I18n.t('Click to sort by term descending')
})}
</div>
<div role="columnheader" className="col-xs-2">
{this.renderHeader({
id: 'teacher',
label: I18n.t('Teacher'),
tipDesc: I18n.t('Click to sort by teacher ascending'),
tipAsc: I18n.t('Click to sort by teacher descending')
})}
</div>
<div role="columnheader" className="col-xs-2">
{this.renderHeader({
id: 'subaccount',
label: I18n.t('Sub-Account'),
tipDesc: I18n.t('Click to sort by sub-account ascending'),
tipAsc: I18n.t('Click to sort by sub-account descending')
})}
</div>
<div role="columnheader" className="col-xs-1">
{this.renderHeader({
id: 'enrollments',
label: I18n.t('Enrollments'),
tipDesc: I18n.t('Click to sort by enrollments ascending'),
tipAsc: I18n.t('Click to sort by enrollments descending')
})}
</div>
<div role="columnheader" className="col-xs-2">
<span className="screenreader-only">{I18n.t('Course option links')}</span>

View File

@ -41,6 +41,7 @@ class CoursesListRow extends React.Component {
teachers: arrayOf(shape(UserLink.propTypes)).isRequired,
sis_course_id: string.isRequired,
subaccount_name: string.isRequired,
term: shape({name: string.isRequired}).isRequired,
urls: shape({ ENROLL_USERS_URL: string.isRequired, USER_LISTS_URL: string.isRequired }),
roles: arrayOf(shape({ id: string.isRequired })),
sections: arrayOf(shape(UserLink.propTypes)),
@ -145,6 +146,10 @@ class CoursesListRow extends React.Component {
<div className="courseSIS">{sis_course_id}</div>
</div>
<div className="col-xs-1" role="gridcell">
<div className="courseSIS">{this.props.term.name}</div>
</div>
<div className="col-xs-2" role="gridcell">
{this.state.teachersToShow && this.state.teachersToShow.map(teacher => <UserLink key={teacher.id} {...teacher} />)}
{ this.showMoreLink() }

View File

@ -31,7 +31,7 @@ const CoursesStore = createStore({
if (params.sort) payload.sort = params.sort
if (params.order) payload.order = params.order
if (params.search_by) payload.search_by = params.search_by
payload.include = ['total_students', 'teachers', 'subaccount']
payload.include = ['total_students', 'teachers', 'subaccount', 'term']
return payload
}

View File

@ -921,6 +921,33 @@ describe AccountsController do
expect(response.body).to match(/\"sis_course_id\":\"30\".+\"sis_course_id\":\"31\".+\"sis_course_id\":\"42\".+\"sis_course_id\":\"52\"/)
end
context "sorting by term" do
let(:letters_in_random_order) { 'daqwds'.split('') }
before do
@account = Account.create!
create_courses(letters_in_random_order.map { |i|
{enrollment_term_id: @account.enrollment_terms.create!(name: i).id}
}, account: @account)
admin_logged_in(@account)
end
it "should be able to sort courses by term ascending" do
get 'courses_api', params: {account_id: @account.id, sort: "term", order: "asc", include: ['term']}
expect(response).to be_success
term_names = json_parse(response.body).map{|c| c['term']['name']}
expect(term_names).to eq(letters_in_random_order.sort)
end
it "should be able to sort courses by term descending" do
get 'courses_api', params: {account_id: @account.id, sort: "term", order: "desc", include: ['term']}
expect(response).to be_success
term_names = json_parse(response.body).map{|c| c['term']['name']}
expect(term_names).to eq(letters_in_random_order.sort.reverse)
end
end
it "should be able to sort courses by enrollments ascending" do
@c3 = course_factory(account: @account, course_name: "apple", sis_source_id: 30)

View File

@ -32,7 +32,10 @@ const props = {
teachers: [{
id: '1',
display_name: 'Testing Teacher'
}]
}],
term:{
name: "Testing Term"
}
}],
addUserUrls: {
USER_LISTS_URL: 'http://courses/{{id}}/users',
@ -45,7 +48,7 @@ const props = {
base_role_type: 'StudentEnrollment'
}]
}]
};
}
test('renders with the proper urls and roles', () => {
const wrapper = shallow(<CoursesList {...props} />)
@ -69,9 +72,11 @@ const coursesProps = {
teachers: [{
id: '1',
display_name: 'Testing Teacher'
}]
},
{
}],
term: {
name: "A Term"
}
}, {
id: '2',
name: 'Ba',
workflow_state: 'alive',
@ -79,9 +84,11 @@ const coursesProps = {
teachers: [{
id: '1',
display_name: 'Testing Teacher'
}]
},
{
}],
term: {
name: "Ba Term"
}
}, {
id: '3',
name: 'Bb',
workflow_state: 'alive',
@ -89,9 +96,11 @@ const coursesProps = {
teachers: [{
id: '1',
display_name: 'Testing Teacher'
}]
},
{
}],
term: {
name: "Bb Term"
}
}, {
id: '4',
name: 'C',
workflow_state: 'alive',
@ -99,9 +108,11 @@ const coursesProps = {
teachers: [{
id: '1',
display_name: 'Testing Teacher'
}]
},
{
}],
term: {
name: "C Term"
}
}, {
id: '5',
name: 'De',
workflow_state: 'alive',
@ -109,9 +120,11 @@ const coursesProps = {
teachers: [{
id: '1',
display_name: 'Testing Teacher'
}]
},
{
}],
term: {
name: "De Term"
}
}, {
id: '6',
name: 'Dz',
workflow_state: 'alive',
@ -119,7 +132,10 @@ const coursesProps = {
teachers: [{
id: '1',
display_name: 'Testing Teacher'
}]
}],
term: {
name: "Dz Term"
}
}],
addUserUrls: {
USER_LISTS_URL: 'http://courses/{{id}}/users',
@ -136,10 +152,66 @@ const coursesProps = {
order: 'asc'
};
test('displays courses that are passed in as props', () => {
Object.entries({
course_name: 'Course',
sis_course_id: 'SIS ID',
term: 'Term',
teacher: 'Teacher',
subaccount: 'Sub-Account',
enrollments: 'Enrollments'
}).forEach(([columnID, label]) => {
test(`sorting by ${columnID} asc puts down-arrow on ${label} only`, () => {
const wrapper = shallow(<CoursesList {...{
...coursesProps,
sort: columnID,
order: 'asc'
}} />)
equal(wrapper.find('IconMiniArrowUpSolid').length, 0, 'no columns have an up arrow')
const icons = wrapper.find('IconMiniArrowDownSolid')
equal(icons.length, 1, 'only one down arrow')
const header = icons.first().parents('Tooltip')
let expectedTip = `Click to sort by ${label} descending`
if (columnID === 'course_name') {
expectedTip = 'Click to sort by name descending'
}
ok(header.prop('tip').match(RegExp(expectedTip, 'i')), 'has right tooltip')
ok(header.contains(label), `${label} is the one that has the down arrow`)
})
test(`sorting by ${columnID} desc puts up-arrow on ${label} only`, () => {
const wrapper = shallow(<CoursesList {...{
...coursesProps,
sort: columnID,
order: 'desc'
}} />)
equal(wrapper.find('IconMiniArrowDownSolid').length, 0)
const icons = wrapper.find('IconMiniArrowUpSolid', 'no columns have a down arrow')
equal(icons.length, 1, 'only one up arrow')
const header = icons.first().parents('Tooltip')
let expectedTip = `Click to sort by ${label} ascending`
if (columnID === 'course_name') {
expectedTip = 'Click to sort by name ascending'
}
ok(header.prop('tip').match(RegExp(expectedTip, 'i')), 'has right tooltip')
ok(header.contains(label), `${label} is the one that has the up arrow`)
})
test(`clicking the ${label} column header calls onChangeSort with ${columnID}`, function() {
const sortSpy = this.spy()
const wrapper = shallow(<CoursesList {...{
...coursesProps,
onChangeSort: sortSpy
}} />)
const header = wrapper.findWhere(n => n.text() === label).first().parents('Tooltip')
header.simulate('click')
ok(sortSpy.calledOnce)
ok(sortSpy.calledWith(columnID))
})
})
test('displays courses in the right order', () => {
const wrapper = shallow(<CoursesList {...coursesProps} />)
const renderedList = wrapper.find(CoursesListRow)
equal(renderedList.nodes[0].props.name, 'A')
@ -157,186 +229,13 @@ test('displays courses that are passed in as props', () => {
equal(renderedList.nodes[5].props.id, '6')
});
test('sorting by course name ascending puts down-arrow on Name', () => {
const wrapper = shallow(<CoursesList {...coursesProps} />)
const header = wrapper.find('a')
equal(header.nodes[0].props.children.props.children[1].type.name, 'IconArrowDownSolid')
});
test('displays Terms in right order', () => {
const renderedList = shallow(<CoursesList {...coursesProps} />).find(CoursesListRow)
const descIdSinonProps = {
courses: [{
id: '1',
name: 'A',
workflow_state: 'alive',
total_students: 6,
teachers: [{
id: '1',
display_name: 'Testing Teacher'
}]
}],
addUserUrls: {
USER_LISTS_URL: 'http://courses/{{id}}/users',
ENROLL_USERS_URL: 'http://courses/{{id}}/users/enroll'
},
sort: 'sis_course_id',
order: 'desc',
onChangeSort: sinon.spy(),
};
test('sorting by id descending puts up-arrow on SIS ID', () => {
const wrapper = shallow(<CoursesList {...descIdSinonProps} />)
const header = wrapper.find('a')
equal(header.nodes[1].props.children.props.children[1].type.name, 'IconArrowUpSolid')
});
test('clicking the Courses column header calls onChangeSort with course_name', () => {
const wrapper = shallow(<CoursesList {...descIdSinonProps} />)
const header = wrapper.find('a').first()
header.simulate('click')
const sinonCallback = wrapper.unrendered.props.onChangeSort
ok(sinonCallback.calledOnce)
ok(sinonCallback.calledWith('course_name'))
});
const moreSinonProps = {
courses: [{
id: '1',
name: 'A',
workflow_state: 'alive',
total_students: 6,
teachers: [{
id: '1',
display_name: 'Testing Teacher'
}]
}],
addUserUrls: {
USER_LISTS_URL: 'http://courses/{{id}}/users',
ENROLL_USERS_URL: 'http://courses/{{id}}/users/enroll'
},
sort: 'sis_course_id',
order: 'desc',
onChangeSort: sinon.spy(),
};
test('clicking the SIS ID column header calls onChangeSort with sis_source_id', () => {
const wrapper = shallow(<CoursesList {...moreSinonProps} />)
const header = wrapper.find('a').slice(1, 2)
header.simulate('click')
header.simulate('click')
header.simulate('click')
const sinonCallback = wrapper.unrendered.props.onChangeSort
ok(sinonCallback.callCount === 3)
ok(sinonCallback.calledWith('sis_course_id'))
});
const teacherSinonProps = {
courses: [{
id: '1',
name: 'A',
workflow_state: 'alive',
total_students: 6,
teachers: [{
id: '1',
display_name: 'Testing Teacher'
}]
}],
addUserUrls: {
USER_LISTS_URL: 'http://courses/{{id}}/users',
ENROLL_USERS_URL: 'http://courses/{{id}}/users/enroll'
},
sort: 'teacher',
order: 'asc',
onChangeSort: sinon.spy(),
};
test('sorting by teacher ascending puts down-arrow on Teacher', () => {
const wrapper = shallow(<CoursesList {...teacherSinonProps} />)
const header = wrapper.find('a')
equal(header.nodes[2].props.children.props.children[1].type.name, 'IconArrowDownSolid')
});
test('clicking the Teacher column header calls onChangeSort with teacher', () => {
const wrapper = shallow(<CoursesList {...teacherSinonProps} />)
const header = wrapper.find('a').slice(2, 3)
header.simulate('click')
const sinonCallback = wrapper.unrendered.props.onChangeSort
ok(sinonCallback.calledOnce)
ok(sinonCallback.calledWith('teacher'))
});
const subaccountSinonProps = {
courses: [{
id: '1',
name: 'A',
workflow_state: 'alive',
total_students: 6,
teachers: [{
id: '1',
display_name: 'Testing Teacher'
}]
}],
addUserUrls: {
USER_LISTS_URL: 'http://courses/{{id}}/users',
ENROLL_USERS_URL: 'http://courses/{{id}}/users/enroll'
},
sort: 'subaccount',
order: 'asc',
onChangeSort: sinon.spy(),
};
test('sorting by subaccount ascending puts down-arrow on Enrollments', () => {
const wrapper = shallow(<CoursesList {...subaccountSinonProps} />)
const header = wrapper.find('a')
equal(header.nodes[3].props.children.props.children[1].type.name, 'IconArrowDownSolid')
});
test('clicking the Enrollments column header calls onChangeSort with enrollments', () => {
const wrapper = shallow(<CoursesList {...subaccountSinonProps} />)
const header = wrapper.find('a').slice(3, 4)
header.simulate('click')
const sinonCallback = wrapper.unrendered.props.onChangeSort
ok(sinonCallback.calledOnce)
ok(sinonCallback.calledWith('subaccount'))
});
const enrollmentsSinonProps = {
courses: [{
id: '1',
name: 'A',
workflow_state: 'alive',
total_students: 6,
teachers: [{
id: '1',
display_name: 'Testing Teacher'
}]
}],
addUserUrls: {
USER_LISTS_URL: 'http://courses/{{id}}/users',
ENROLL_USERS_URL: 'http://courses/{{id}}/users/enroll'
},
sort: 'enrollments',
order: 'asc',
onChangeSort: sinon.spy(),
};
test('sorting by enrollments ascending puts down-arrow on Enrollments', () => {
const wrapper = shallow(<CoursesList {...enrollmentsSinonProps} />)
const header = wrapper.find('a')
equal(header.nodes[4].props.children.props.children[1].type.name, 'IconArrowDownSolid')
});
test('clicking the Enrollments column header calls onChangeSort with enrollments', () => {
const wrapper = shallow(<CoursesList {...enrollmentsSinonProps} />)
const header = wrapper.find('a').slice(4, 5)
header.simulate('click')
const sinonCallback = wrapper.unrendered.props.onChangeSort
ok(sinonCallback.calledOnce)
ok(sinonCallback.calledWith('enrollments'))
});
equal(renderedList.nodes[0].props.term.name, 'A Term')
equal(renderedList.nodes[1].props.term.name, 'Ba Term')
equal(renderedList.nodes[2].props.term.name, 'Bb Term')
equal(renderedList.nodes[3].props.term.name, 'C Term')
equal(renderedList.nodes[4].props.term.name, 'De Term')
equal(renderedList.nodes[5].props.term.name, 'Dz Term')
})