multiple grading periods enrollment term dropdown

adds the enrollment term dropdown to the account
grading periods page. selecting an enrollment term
from the dropdown will filter the grading period
sets to only show those that contain the selected
enrollment term.

closes CNVS-27108

test plan:
- enable multiple grading periods for an
  account
- create at least three grading period sets
- create at least three grading periods per
  grading period set
- create at least four enrollment terms belonging
  to the account. make sure three of them belong
  to one of the sets above, and one does not belong
  to any set. ensure
    a) one of the enrollment terms has a name and
       a start_at
    b) one of the enrollment terms is missing a
       name but has a start_at
    c) one of the enrollment terms is missing a
       name and a start_at
- go to the account grading periods page at
  /accounts/:id/grading_standards
- verify the enrollment terms are populated in the
  dropdown. the displayed name should be the name,
  or "Term starting **date here**" if no name exists,
  or "Term created **date here**" if no name exists
  and no start_at exists. verify the enrollment term
  that does not belong to a set is not displayed.
- verify the enrollment terms are ordered from top to
  bottom by start_at descending (if start_at exists,
  with all terms having a start_at displayed above
  all terms without a start_at), and then by created_at
  descending for terms without a start_at.
- verify selecting an enrollment term from the dropdown
  filters the grading period sets to only inlcude those
  that are associated with the selected term.
- verify the enrollment term names show below the grading
  period set name (with the correct display name as defined
  three steps before this one).

Change-Id: I920152f1c7a13720ab24e2fae0f4ec811bde5a93
Reviewed-on: https://gerrit.instructure.com/81420
Reviewed-by: Derek Bender <djbender@instructure.com>
Tested-by: Jenkins
Reviewed-by: Keith T. Garner <kgarner@instructure.com>
QA-Review: KC Naegle <knaegle@instructure.com>
Product-Review: Keith T. Garner <kgarner@instructure.com>
This commit is contained in:
Nick Pitrak 2016-05-31 16:57:36 -05:00 committed by Spencer Olson
parent 33c669bc8e
commit 585511c860
20 changed files with 538 additions and 111 deletions

View File

@ -6,14 +6,15 @@ require [
mgpEnabled = ENV.MULTIPLE_GRADING_PERIODS
readOnly = ENV.GRADING_PERIODS_READ_ONLY
urls =
gradingPeriodSetsURL: ENV.GRADING_PERIOD_SETS_URL,
gradingPeriodSetsURL: ENV.GRADING_PERIOD_SETS_URL
gradingPeriodsUpdateURL: ENV.GRADING_PERIODS_UPDATE_URL
enrollmentTermsURL: ENV.ENROLLMENT_TERMS_URL
React.render(
TabContainerFactory(
multipleGradingPeriodsEnabled: mgpEnabled
readOnly: readOnly
URLs: urls
urls: urls
),
document.getElementById("react_grading_tabs")
)

View File

@ -26,6 +26,7 @@ class GradingStandardsController < ApplicationController
client_env = {
:GRADING_STANDARDS_URL => context_url(@context, :context_grading_standards_url),
:GRADING_PERIOD_SETS_URL => api_v1_account_grading_period_sets_url(@context),
:ENROLLMENT_TERMS_URL => api_v1_enrollment_terms_url(@context),
:MULTIPLE_GRADING_PERIODS => multiple_grading_periods?,
:DEFAULT_GRADING_STANDARD_DATA => GradingStandard.default_grading_standard,
:CONTEXT_SETTINGS_URL => context_url(@context, :context_settings_url)

View File

@ -13,10 +13,14 @@ define([
return object;
},
formatDateForDisplay: function(date) {
formatDatetimeForDisplay: function(date) {
return $.datetimeString(date, { format: 'medium', timezone: ENV.CONTEXT_TIMEZONE });
},
formatDateForDisplay: function(date) {
return $.dateString(date, { format: 'medium', timezone: ENV.CONTEXT_TIMEZONE });
},
isMidnight: function(date) {
return tz.isMidnight(date, { timezone: ENV.CONTEXT_TIMEZONE });
}

View File

@ -24,10 +24,10 @@ define([
<span tabIndex="0">{this.props.period.title}</span>
</div>
<div className="GradingPeriodList__period__attribute col-xs-12 col-md-8 col-lg-4">
<span tabIndex="0" ref="startDate">{I18n.t("Start Date:")} {DatesHelper.formatDateForDisplay(this.props.period.startDate)}</span>
<span tabIndex="0" ref="startDate">{I18n.t("Start Date:")} {DatesHelper.formatDatetimeForDisplay(this.props.period.startDate)}</span>
</div>
<div className="GradingPeriodList__period__attribute col-xs-12 col-md-8 col-lg-4">
<span tabIndex="0" ref="endDate">{I18n.t("End Date:")} {DatesHelper.formatDateForDisplay(this.props.period.endDate)}</span>
<span tabIndex="0" ref="endDate">{I18n.t("End Date:")} {DatesHelper.formatDatetimeForDisplay(this.props.period.endDate)}</span>
</div>
</div>
<div className="GradingPeriodList__period__actions">

View File

@ -11,9 +11,10 @@ define([
propTypes: {
multipleGradingPeriodsEnabled: types.bool.isRequired,
readOnly: types.bool.isRequired,
URLs: types.shape({
urls: types.shape({
gradingPeriodSetsURL: types.string.isRequired,
gradingPeriodsUpdateURL: types.string.isRequired
gradingPeriodsUpdateURL: types.string.isRequired,
enrollmentTermsURL: types.string.isRequired
}).isRequired
},
@ -31,7 +32,7 @@ define([
<li><a href="#grading-standards-tab" className="grading_standards_tab"> {I18n.t('Grading Schemes')}</a></li>
</ul>
<div ref="gradingPeriods" id="grading-periods-tab">
<GradingPeriodSetCollection URLs={this.props.URLs}
<GradingPeriodSetCollection urls={this.props.urls}
readOnly={this.props.readOnly} />
</div>
<div ref="gradingStandards" id="grading-standards-tab">

View File

@ -0,0 +1,53 @@
define([
'react',
'underscore',
'i18n!grading_periods',
], function(React, _, I18n) {
let EnrollmentTermsDropdown = React.createClass({
propTypes: {
terms: React.PropTypes.array.isRequired,
changeSelectedEnrollmentTerm: React.PropTypes.func.isRequired
},
termsBelongingToSets(terms) {
return _.select(terms, term => term.gradingPeriodGroupId);
},
sortedTerms(terms) {
const dated = _.select(terms, term => term.startAt);
const datedTermsSortedByStart = _.sortBy(dated, term => term.startAt).reverse();
const undated = _.select(terms, term => !term.startAt);
const undatedTermsSortedByCreate = _.sortBy(undated, term => term.createdAt).reverse();
return datedTermsSortedByStart.concat(undatedTermsSortedByCreate);
},
termOptions(terms) {
const allTermsOption = (<option key={0} value={0}>{I18n.t("All Terms")}</option>);
const termsWithSets = this.termsBelongingToSets(terms);
let options = _.map(this.sortedTerms(termsWithSets), function(term) {
return (<option key={term.id} value={term.id}>{term.displayName}</option>);
});
options.unshift(allTermsOption);
return options;
},
render() {
return (
<select
className="EnrollmentTerms__dropdown"
name="enrollment_term"
data-view="termSelect"
aria-label="Enrollment Term"
ref="termsDropdown"
onChange={this.props.changeSelectedEnrollmentTerm} >
{this.termOptions(this.props.terms)}
</select>
);
}
});
return EnrollmentTermsDropdown;
});

View File

@ -65,6 +65,7 @@ define([
title: types.string
}).isRequired,
gradingPeriods: types.array.isRequired,
terms: types.array.isRequired,
urls: types.shape({
batchUpdateUrl: types.string.isRequired
}).isRequired,
@ -104,6 +105,17 @@ define([
e.stopPropagation();
},
setTerms() {
return _.filter(this.props.terms, (term) => {
return term.gradingPeriodGroupId === parseInt(this.props.set.id);
});
},
termNames() {
const names = _.pluck(this.setTerms(), "displayName");
return I18n.t("Terms: ") + names.join(", ");
},
editSet(e) {
e.stopPropagation();
},
@ -130,6 +142,8 @@ define([
},
renderSetBody() {
if(!this.state.expanded) return null;
return (
<div ref="setBody" className="ig-body">
<div className="GradingPeriodList" ref="gradingPeriodList">
@ -156,20 +170,25 @@ define([
<div className="ItemGroup__header"
ref="toggleSetBody"
onClick={this.toggleSetBody}>
<div className="ItemGroup__header__title">
<button className={"Button Button--icon-action GradingPeriodSet__toggle"}
aria-expanded={this.state.expanded}
aria-label="Toggle grading period visibility">
<i className={"icon-mini-arrow-" + arrow}/>
</button>
<span className="screenreader-only">{I18n.t("Grading period title")}</span>
<h2 tabIndex="0" className="GradingPeriodSet__title">
{this.props.set.title}
</h2>
<div>
<div className="ItemGroup__header__title">
<button className={"Button Button--icon-action GradingPeriodSet__toggle"}
aria-expanded={this.state.expanded}
aria-label="Toggle grading period visibility">
<i className={"icon-mini-arrow-" + arrow}/>
</button>
<span className="screenreader-only">{I18n.t("Grading period title")}</span>
<h2 tabIndex="0" className="GradingPeriodSet__title">
{this.props.set.title}
</h2>
</div>
{this.renderEditAndDeleteIcons()}
</div>
<div className="EnrollmentTerms__list" tabIndex="0">
{this.termNames()}
</div>
{this.renderEditAndDeleteIcons()}
</div>
{this.state.expanded && this.renderSetBody()}
{this.renderSetBody()}
</div>
);
},

View File

@ -7,8 +7,11 @@ define([
'convert_case',
'jsx/grading/GradingPeriodSet',
'jsx/grading/SearchGradingPeriodsField',
'jsx/shared/helpers/searchHelpers'
], function(React, _, $, axios, I18n, ConvertCase, GradingPeriodSet, SearchGradingPeriodsField, SearchHelpers) {
'jsx/grading/EnrollmentTermsDropdown',
'jsx/shared/helpers/searchHelpers',
'jsx/gradebook/grid/helpers/datesHelper'
], function(React, _, $, axios, I18n, ConvertCase, GradingPeriodSet, SearchGradingPeriodsField, EnrollmentTermsDropdown, SearchHelpers, DatesHelper) {
const deserializeSets = function(sets) {
return _.map(sets, function(set) {
let newSet = ConvertCase.camelize(set);
@ -28,31 +31,55 @@ define([
});
};
const deserializeEnrollmentTerms = function(enrollmentTerms) {
return _.map(enrollmentTerms, term => {
let newTerm = ConvertCase.camelize(term);
if(term.start_at) newTerm.startAt = new Date(term.start_at);
if(term.end_at) newTerm.endAt = new Date(term.end_at);
if(term.created_at) newTerm.createdAt = new Date(term.created_at);
if(newTerm.name) {
newTerm.displayName = newTerm.name;
} else if(_.isDate(newTerm.startAt)) {
let started = DatesHelper.formatDateForDisplay(newTerm.startAt);
newTerm.displayName = I18n.t("Term starting ") + started;
} else {
let created = DatesHelper.formatDateForDisplay(newTerm.createdAt);
newTerm.displayName = I18n.t("Term created ") + created;
}
return newTerm;
});
};
const types = React.PropTypes;
let GradingPeriodSetCollection = React.createClass({
propTypes: {
readOnly: types.bool.isRequired,
URLs: types.shape({
urls: types.shape({
gradingPeriodSetsURL: types.string.isRequired,
gradingPeriodsUpdateURL: types.string.isRequired
gradingPeriodsUpdateURL: types.string.isRequired,
enrollmentTermsURL: types.string.isRequired
}).isRequired
},
getInitialState: function() {
getInitialState() {
return {
enrollmentTerms: [],
sets: [],
showNewSetForm: false,
searchText: ""
searchText: "",
selectedTermID: 0
};
},
componentWillMount: function() {
componentWillMount() {
this.getTerms();
this.getSets();
},
getSets: function() {
axios.get(this.props.URLs.gradingPeriodSetsURL)
getSets() {
axios.get(this.props.urls.gradingPeriodSetsURL)
.then((response) => {
this.setState({
sets: deserializeSets(response.data.grading_period_sets)
@ -63,17 +90,16 @@ define([
});
},
renderNewGradingPeriodSetForm: function() {
if(!this.state.showNewSetForm) return null;
return (
<NewGradingPeriodSetForm
ref="newSetForm"
closeForm={this.closeNewSetForm}
URLs={this.props.URLs}
/>
);
},
getTerms() {
axios.get(this.props.urls.enrollmentTermsURL)
.then((response) => {
const enrollmentTerms = deserializeEnrollmentTerms(response.data.enrollment_terms);
this.setState({ enrollmentTerms: enrollmentTerms });
})
.catch(function (response) {
$.flashError(I18n.t("An error occured while fetching enrollment terms."));
});
},
setAndGradingPeriodTitles(set) {
let titles = _.pluck(set.gradingPeriods, 'title');
@ -87,24 +113,42 @@ define([
});
},
filterSetsBySearchText: function() {
if (this.state.searchText === "") return this.state.sets;
filterSetsBySearchText(sets, searchText) {
if (searchText === "") return sets;
return _.filter(this.state.sets, (set) => {
return _.filter(sets, (set) => {
let titles = this.setAndGradingPeriodTitles(set);
return this.searchTextMatchesTitles(titles);
});
},
changeSearchText: function(searchText) {
changeSearchText(searchText) {
if (searchText !== this.state.searchText) {
this.setState({ searchText: searchText });
}
},
renderSets: function() {
let urls = { batchUpdateUrl: this.props.URLs.gradingPeriodsUpdateURL };
let visibleSets = this.filterSetsBySearchText();
filterSetsByActiveTerm(sets, terms, selectedTermID) {
if (selectedTermID === 0) return sets;
const activeTerm = _.findWhere(terms, { id: selectedTermID });
const setID = activeTerm.gradingPeriodGroupId;
return _.where(sets, { id: setID.toString() });
},
changeSelectedEnrollmentTerm(event) {
this.setState({ selectedTermID: parseInt(event.target.value) });
},
getVisibleSets() {
let setsFilteredBySearchText = this.filterSetsBySearchText(this.state.sets, this.state.searchText);
let filterByTermArgs = [setsFilteredBySearchText, this.state.enrollmentTerms, this.state.selectedTermID];
return this.filterSetsByActiveTerm(...filterByTermArgs);
},
renderSets() {
const urls = { batchUpdateUrl: this.props.urls.gradingPeriodsUpdateURL };
let visibleSets = this.getVisibleSets();
return _.map(visibleSets, set => {
return (
<GradingPeriodSet key={set.id}
@ -112,18 +156,20 @@ define([
gradingPeriods={set.gradingPeriods}
urls={urls}
readOnly={this.props.readOnly}
permissions={set.permissions} />
permissions={set.permissions}
terms={this.state.enrollmentTerms} />
);
});
},
render: function() {
render() {
return (
<div>
<div className="GradingPeriodSets__toolbar header-bar no-line">
<EnrollmentTermsDropdown
terms={this.state.enrollmentTerms}
changeSelectedEnrollmentTerm={this.changeSelectedEnrollmentTerm} />
<SearchGradingPeriodsField changeSearchText={this.changeSearchText} />
<div className="header-bar-right">
</div>
</div>
<div>
{this.renderSets()}

View File

@ -71,7 +71,7 @@ define([
replaceInputWithDate: function(dateType, dateElement) {
var date = this.state[dateType];
dateElement.val(DatesHelper.formatDateForDisplay(date));
dateElement.val(DatesHelper.formatDatetimeForDisplay(date));
},
render: function () {

View File

@ -112,13 +112,13 @@ define([
ref="startDate"
name="startDate"
className="input-grading-period-date date_field"
defaultValue={DatesHelper.formatDateForDisplay(this.props.startDate)}
defaultValue={DatesHelper.formatDatetimeForDisplay(this.props.startDate)}
disabled={this.props.disabled}/>
);
} else {
return (
<div id={this.addIdToText("period_start_date_")} ref="startDate">
{DatesHelper.formatDateForDisplay(this.props.startDate)}
{DatesHelper.formatDatetimeForDisplay(this.props.startDate)}
</div>
);
}
@ -131,13 +131,13 @@ define([
className="input-grading-period-date date_field"
ref="endDate"
name="endDate"
defaultValue={DatesHelper.formatDateForDisplay(this.props.endDate)}
defaultValue={DatesHelper.formatDatetimeForDisplay(this.props.endDate)}
disabled={this.props.disabled}/>
);
} else {
return (
<div id={this.addIdToText("period_end_date_")} ref="endDate">
{DatesHelper.formatDateForDisplay(this.props.endDate)}
{DatesHelper.formatDatetimeForDisplay(this.props.endDate)}
</div>
);
}

View File

@ -0,0 +1,11 @@
@import "base/environment";
.EnrollmentTerms__dropdown {
background-color: $lightBackground;
margin-bottom: 2px;
}
.EnrollmentTerms__list {
margin-left: 24px;
@include fontSize($ic-font-size--xsmall);
}

View File

@ -3,12 +3,23 @@
.ItemGroup__header {
background-color: $lightBackground;
border: 1px solid $ic-border-light;
display: flex;
min-height: 30px;
padding: $ic-sp;
position: relative;
}
.ItemGroup__header__toggle {
display: inline-block;
flex: 0 0 auto;
margin: auto;
text-decoration: none;
}
.ItemGroup__header__details {
display: flex;
flex: 1 1 auto;
}
.ItemGroup__header__title {
display: inline-block;
flex: 1 1 auto;
@ -20,7 +31,8 @@
.ItemGroup__header__admin {
align-self: center;
display: flex;
float: right;
display: inline-block;
flex: 0 0 auto;
}
@ -43,6 +55,9 @@
.GradingPeriodSearchField {
width: 250px;
display: inline-block;
padding-left: 12px;
margin-bottom: 0px;
}
// GRADING PERIOD SET

View File

@ -1,5 +1,5 @@
<% js_bundle :account_grading_standards %>
<% css_bundle :grading_standards, :grading_period_sets %>
<% css_bundle :grading_standards, :grading_period_sets, :enrollment_terms %>
<% content_for :page_title, t(:title, "Grading Standards") %>
<div id="react_grading_tabs"></div>

View File

@ -2,7 +2,7 @@ module Api::V1::EnrollmentTerm
include Api::V1::Json
def enrollment_term_json(enrollment_term, user, session, enrollments=[], includes=[])
api_json(enrollment_term, user, session, :only => %w(id name start_at end_at workflow_state)).tap do |hash|
api_json(enrollment_term, user, session, :only => %w(id name start_at end_at workflow_state grading_period_group_id created_at)).tap do |hash|
hash['sis_term_id'] = enrollment_term.sis_source_id if enrollment_term.root_account.grants_any_right?(user, :read_sis, :manage_sis)
hash['start_at'], hash['end_at'] = enrollment_term.overridden_term_dates(enrollments) if enrollments.present?
end

View File

@ -77,8 +77,8 @@ define [
@gradingPeriod.onTitleChange(fakeEvent)
ok @gradingPeriod.props.updateGradingPeriodCollection.calledOnce
test 'replaceInputWithDate calls formatDateForDisplay', ->
formatDate = @stub(DatesHelper, 'formatDateForDisplay')
test 'replaceInputWithDate calls formatDatetimeForDisplay', ->
formatDatetime = @stub(DatesHelper, 'formatDatetimeForDisplay')
fakeDateElement = { val: -> }
@gradingPeriod.replaceInputWithDate("startDate", fakeDateElement)
ok formatDate.calledOnce
ok formatDatetime.calledOnce

View File

@ -42,7 +42,7 @@ define [
ok _.isDate(assignment.created_at)
ok _.isUndefined(assignment.undefined_due_at)
module 'DatesHelper#formatDateForDisplay',
module 'DatesHelper#formatDatetimeForDisplay',
setup: ->
@snapshot = tz.snapshot()
teardown: ->
@ -51,13 +51,29 @@ define [
test 'formats the date for display, adjusted for the timezone', ->
assignment = defaultAssignment()
tz.changeZone(detroit, 'America/Detroit')
formattedDate = DatesHelper.formatDateForDisplay(assignment.due_at)
formattedDate = DatesHelper.formatDatetimeForDisplay(assignment.due_at)
equal formattedDate, "Jul 14, 2015 at 2:35pm"
tz.changeZone(juneau, 'America/Juneau')
formattedDate = DatesHelper.formatDateForDisplay(assignment.due_at)
formattedDate = DatesHelper.formatDatetimeForDisplay(assignment.due_at)
equal formattedDate, "Jul 14, 2015 at 10:35am"
module 'DatesHelper#formatDateForDisplay',
setup: ->
@snapshot = tz.snapshot()
teardown: ->
tz.restore(@snapshot)
test 'formats the date for display, adjusted for the timezone, excluding the time', ->
assignment = defaultAssignment()
tz.changeZone(detroit, 'America/Detroit')
formattedDate = DatesHelper.formatDateForDisplay(assignment.due_at)
equal formattedDate, "Jul 14, 2015"
tz.changeZone(juneau, 'America/Juneau')
formattedDate = DatesHelper.formatDateForDisplay(assignment.due_at)
equal formattedDate, "Jul 14, 2015"
module 'DatesHelper#isMidnight',
setup: ->
@snapshot = tz.snapshot()

View File

@ -10,7 +10,7 @@ define([
renderComponent: function(props={}) {
const defaults = {
readOnly: false,
URLs: {
urls: {
gradingPeriodSetsURL: "api/v1/accounts/1/grading_period_sets",
gradingPeriodsUpdateURL: "api/v1/grading_period_sets/{{ set_id }}/grading_periods/batch_update",
enrollmentTermsURL: "api/v1/accounts/1/enrollment_terms"

View File

@ -0,0 +1,101 @@
define([
'react',
'underscore',
'jsx/grading/EnrollmentTermsDropdown'
], (React, _, Dropdown) => {
const wrapper = document.getElementById('fixtures');
const Simulate = React.addons.TestUtils.Simulate;
module('EnrollmentTermsDropdown', {
renderComponent() {
const props = {
terms: this.terms(),
changeSelectedEnrollmentTerm: this.spy()
};
const element = React.createElement(Dropdown, props);
return React.render(element, wrapper);
},
terms() {
return [
{
id: 18,
name: "Fall 2013 - Art",
startAt: new Date("2013-08-03T02:57:42.000Z"),
endAt: new Date("2013-11-03T02:57:53.000Z"),
createdAt: new Date("2013-07-27T16:51:41.000Z"),
gradingPeriodGroupId: 3,
displayName: "Fall 2013 - Art"
},
{
id: 21,
name: "Winter 2013 - Art",
startAt: new Date("2013-12-03T02:57:42.000Z"),
endAt: new Date("2014-01-21T02:57:53.000Z"),
createdAt: new Date("2013-08-27T16:51:41.000Z"),
gradingPeriodGroupId: 3,
displayName: "Winter 2013 - Art"
},
{
id: 2,
name: null,
startAt: null,
endAt: new Date("2013-10-21T02:57:53.000Z"),
createdAt: new Date("2013-08-22T16:51:41.000Z"),
gradingPeriodGroupId: 2,
displayName: "Term starting Sep 3, 2013"
},
{
id: 7,
name: null,
startAt: null,
endAt: null,
createdAt: new Date("2013-08-23T16:51:41.000Z"),
gradingPeriodGroupId: 2,
displayName: "Term created Aug 23, 2013"
},
{
id: 22,
name: null,
startAt: null,
endAt: null,
createdAt: new Date("2013-08-23T16:51:41.000Z"),
gradingPeriodGroupId: null,
displayName: "Term created Aug 23, 2013"
}
];
},
teardown() {
React.unmountComponentAtNode(wrapper);
}
});
test('includes "number of terms belonging to sets + 1" options', function () {
let dropdown = this.renderComponent();
let node = React.findDOMNode(dropdown.refs.termsDropdown);
equal(node.length, 5);
});
test('starts by showing all enrollment terms', function () {
let dropdown = this.renderComponent();
let node = React.findDOMNode(dropdown.refs.termsDropdown);
const ALL_TERMS_ID = 0;
equal(node.value, ALL_TERMS_ID);
});
test("calls changeSelectedEnrollmentTerm when a selection is made", function() {
let dropdown = this.renderComponent();
let node = React.findDOMNode(dropdown.refs.termsDropdown);
node.value = 3;
Simulate.change(node);
ok(dropdown.props.changeSelectedEnrollmentTerm.calledOnce);
});
test("displays the terms in descending order by start date then created date if start date doesn't exist", function() {
let dropdown = this.renderComponent();
let node = React.findDOMNode(dropdown.refs.termsDropdown);
let optionIDs = _.pluck(node.getElementsByTagName("OPTION"), "value");
propEqual(optionIDs, ["0", "21", "18", "7", "2"]);
});
});

View File

@ -10,23 +10,27 @@ define([
module("GradingPeriodSetCollection", {
renderComponent() {
const props = {
URLs: {
gradingPeriodSetsURL: "api/v1/accounts/1/grading_period_sets"
}
urls: {
gradingPeriodSetsURL: "api/v1/accounts/1/grading_period_sets",
enrollmentTermsURL: "api/v1/accounts/1/terms",
gradingPeriodsUpdateURL: "api/v1/accounts/1/grading_period_sets"
},
readOnly: false
};
const element = React.createElement(SetCollection, props);
return React.render(element, wrapper);
},
successResponse() {
setsResponse() {
return {
data: {
grading_period_sets: [
{
id: "1",
title: "Macarena",
grading_periods: []
grading_periods: [],
permissions: { read: true, create: true, update: true, delete: true }
},
{
id: "2",
@ -34,15 +38,111 @@ define([
grading_periods: [
{ id: 9, title: "Febrero", start_date: "2014-06-08T15:44:25Z", end_date: "2014-07-08T15:44:25Z" },
{ id: 11, title: "Marzo", start_date: "2014-08-08T15:44:25Z", end_date: "2014-09-08T15:44:25Z" }
]
],
permissions: { read: true, create: true, update: true, delete: true }
}
]
}
};
},
stubAJAXSuccess() {
const response = this.successResponse();
deserializedSets() {
return [
{
id: "1",
title: "Macarena",
gradingPeriods: [],
permissions: { read: true, create: true, update: true, delete: true }
},
{
id: "2",
title: "Mambo Numero Cinco",
gradingPeriods: [
{
id: "9",
title: "Febrero",
startDate: new Date("2014-06-08T15:44:25Z"),
endDate: new Date("2014-07-08T15:44:25Z")
},
{
id: "11",
title: "Marzo",
startDate: new Date("2014-08-08T15:44:25Z"),
endDate: new Date("2014-09-08T15:44:25Z")
}
],
permissions: { read: true, create: true, update: true, delete: true }
}
];
},
termsResponse() {
return {
data: {
enrollment_terms: [
{
id: 1,
name: "Fall 2013 - Art",
start_at: "2013-06-03T02:57:42Z",
end_at: "2013-12-03T02:57:53Z",
created_at: "2015-10-27T16:51:41Z",
grading_period_group_id: 2
},
{
id: 3,
name: null,
start_at: "2014-01-03T02:58:36Z",
end_at: "2014-03-03T02:58:42Z",
created_at: "2013-06-02T17:29:19Z",
grading_period_group_id: 2
},
{
id: 4,
name: null,
start_at: null,
end_at: null,
created_at: "2014-05-02T17:29:19Z",
grading_period_group_id: 1
}
]
}
};
},
deserializedTerms() {
return [
{
id: 1,
name: "Fall 2013 - Art",
startAt: new Date("2013-06-03T02:57:42Z"),
endAt: new Date("2013-12-03T02:57:53Z"),
createdAt: new Date("2015-10-27T16:51:41Z"),
gradingPeriodGroupId: 2,
displayName: "Fall 2013 - Art"
},
{
id: 3,
name: null,
startAt: new Date("2014-01-03T02:58:36Z"),
endAt: new Date("2014-03-03T02:58:42Z"),
createdAt: new Date("2013-06-02T17:29:19Z"),
gradingPeriodGroupId: 2,
displayName: "Term starting Jan 3, 2014"
},
{
id: 4,
name: null,
startAt: null,
endAt: null,
createdAt: new Date("2014-05-02T17:29:19Z"),
gradingPeriodGroupId: 1,
displayName: "Term created May 2, 2014"
}
];
},
stubAJAXSuccess(opts={ type: "sets" }) {
const response = opts.type === "sets" ? this.setsResponse() : this.termsResponse();
const successPromise = new Promise(resolve => resolve(response));
this.stub(axios, "get").returns(successPromise);
return successPromise;
@ -64,25 +164,7 @@ define([
asyncTest("deserializes sets and grading periods if the AJAX call is successful", function() {
const success = this.stubAJAXSuccess();
const deserializedSet = {
id: "2",
title: "Mambo Numero Cinco",
gradingPeriods: [
{
id: "9",
title: "Febrero",
startDate: new Date("2014-06-08T15:44:25Z"),
endDate: new Date("2014-07-08T15:44:25Z")
},
{
id: "11",
title: "Marzo",
startDate: new Date("2014-08-08T15:44:25Z"),
endDate: new Date("2014-09-08T15:44:25Z")
}
]
};
const deserializedSet = this.deserializedSets()[1];
let collection = this.renderComponent();
success.then(function() {
@ -103,14 +185,14 @@ define([
});
test("setAndGradingPeriodTitles returns an array of set and grading period title names", function() {
let set = { title: "Set!", gradingPeriods: [{ title: "Grading Period 1" }, { title: "Grading Period 2" }] };
const set = { title: "Set!", gradingPeriods: [{ title: "Grading Period 1" }, { title: "Grading Period 2" }] };
let collection = this.renderComponent();
let titles = collection.setAndGradingPeriodTitles(set);
const titles = collection.setAndGradingPeriodTitles(set);
propEqual(titles, ["Set!", "Grading Period 1", "Grading Period 2"]);
});
test("setAndGradingPeriodTitles filters out empty, null, and undefined titles", function() {
let set = {
const set = {
title: null,
gradingPeriods: [
{ title: "Grading Period 1" },
@ -121,52 +203,126 @@ define([
};
let collection = this.renderComponent();
let titles = collection.setAndGradingPeriodTitles(set);
const titles = collection.setAndGradingPeriodTitles(set);
propEqual(titles, ["Grading Period 1", "Grading Period 2"]);
});
test("changeSearchText calls setState if the new search text differs from the old search text", function() {
const titles = ["hello world", "goodbye friend"];
let collection = this.renderComponent();
const setStateSpy = this.spy(collection, "setState");
collection.changeSearchText("hello world");
collection.changeSearchText("goodbye world");
ok(setStateSpy.calledTwice)
});
test("changeSearchText does not call setState if the new search text equals the old search text", function() {
const titles = ["hello world", "goodbye friend"];
let collection = this.renderComponent();
const setStateSpy = this.spy(collection, "setState");
collection.changeSearchText("hello world");
collection.changeSearchText("hello world");
ok(setStateSpy.calledOnce)
});
test("searchTextMatchesTitles returns true if the search text exactly matches one of the titles", function() {
let titles = ["hello world", "goodbye friend"];
const titles = ["hello world", "goodbye friend"];
let collection = this.renderComponent();
collection.changeSearchText("hello world");
equal(collection.searchTextMatchesTitles(titles), true)
});
test("searchTextMatchesTitles returns true if the search text exactly matches one of the titles", function() {
const titles = ["hello world", "goodbye friend"];
let collection = this.renderComponent();
collection.changeSearchText("hello world");
equal(collection.searchTextMatchesTitles(titles), true)
});
test("searchTextMatchesTitles returns true if the search text is a substring of one of the titles", function() {
let titles = ["hello world", "goodbye friend"];
const titles = ["hello world", "goodbye friend"];
let collection = this.renderComponent();
collection.changeSearchText("orl");
equal(collection.searchTextMatchesTitles(titles), true)
});
test("searchTextMatchesTitles returns false if the search text is a not a substring of any of the titles", function() {
let titles = ["hello world", "goodbye friend"];
const titles = ["hello world", "goodbye friend"];
let collection = this.renderComponent();
collection.changeSearchText("olr");
equal(collection.searchTextMatchesTitles(titles), false)
});
asyncTest("filterSetsBySearchText returns sets that match the search text", function() {
asyncTest("getVisibleSets returns sets that match the search text", function() {
const success = this.stubAJAXSuccess();
let collection = this.renderComponent();
success.then(function() {
collection.changeSearchText("ma");
let filteredIDs = _.pluck(collection.filterSetsBySearchText(), "id");
let filteredIDs = _.pluck(collection.getVisibleSets(), "id");
propEqual(filteredIDs, ["1", "2"]);
collection.changeSearchText("rz");
filteredIDs = _.pluck(collection.filterSetsBySearchText(), "id");
filteredIDs = _.pluck(collection.getVisibleSets(), "id");
propEqual(filteredIDs, ["2"]);
collection.changeSearchText("Mac");
filteredIDs = _.pluck(collection.filterSetsBySearchText(), "id");
filteredIDs = _.pluck(collection.getVisibleSets(), "id");
propEqual(filteredIDs, ["1"]);
collection.changeSearchText("dora the explorer");
filteredIDs = _.pluck(collection.filterSetsBySearchText(), "id");
propEqual(collection.filterSetsBySearchText(), []);
filteredIDs = _.pluck(collection.getVisibleSets(), "id");
propEqual(collection.getVisibleSets(), []);
start();
});
});
asyncTest("deserializes enrollment terms if the AJAX call is successful", function() {
const success = this.stubAJAXSuccess({ type: "terms" });
const deserializedTerm = this.deserializedTerms()[0];
let collection = this.renderComponent();
success.then(function() {
const term = collection.state.enrollmentTerms[0];
propEqual(term, deserializedTerm);
start();
});
});
asyncTest("uses the name, start date (if no name), or creation date (if no start) for the display name", function() {
const success = this.stubAJAXSuccess({ type: "terms" });
const expectedNames = _.pluck(this.deserializedTerms(), "displayName");
let collection = this.renderComponent();
success.then(function() {
const names = _.pluck(collection.state.enrollmentTerms, "displayName");
propEqual(names, expectedNames);
start();
});
});
test("filterSetsByActiveTerm returns all the sets if 'All Terms' is selected", function() {
const ALL_TERMS_ID = 0;
const sets = this.deserializedSets();
const terms = this.deserializedTerms();
const selectedTermID = ALL_TERMS_ID;
let collection = this.renderComponent();
const filteredSets = collection.filterSetsByActiveTerm(sets, terms, selectedTermID);
propEqual(filteredSets, sets);
});
test("filterSetsByActiveTerm filters to only show the set that the selected term belongs to", function() {
const sets = this.deserializedSets();
const terms = this.deserializedTerms();
let selectedTermID = 3;
let collection = this.renderComponent();
let filteredSets = collection.filterSetsByActiveTerm(sets, terms, selectedTermID);
let expectedSets = _.where(sets, { id: "2" });
propEqual(filteredSets, expectedSets);
selectedTermID = 4;
filteredSets = collection.filterSetsByActiveTerm(sets, terms, selectedTermID);
expectedSets = _.where(sets, { id: "1" });
propEqual(filteredSets, expectedSets);
});
});

View File

@ -54,7 +54,8 @@ define([
gradingPeriods: examplePeriods,
readOnly: false,
urls: urls,
permissions: allPermissions
permissions: allPermissions,
terms: []
};
module("GradingPeriodSet", {
@ -101,7 +102,8 @@ define([
gradingPeriods: [],
urls: urls,
permissions: _.defaults(permissions, allPermissions),
readOnly: readOnly
readOnly: readOnly,
terms: []
};
const element = React.createElement(GradingPeriodSet, set);
let component = React.render(element, wrapper);
@ -157,7 +159,8 @@ define([
gradingPeriods: [],
urls: urls,
readOnly: false,
permissions: allPermissions
permissions: allPermissions,
terms: []
};
const element = React.createElement(GradingPeriodSet, set);
let component = React.render(element, wrapper);