diff --git a/app/coffeescripts/bundles/account_grading_standards.coffee b/app/coffeescripts/bundles/account_grading_standards.coffee index 4f6bffe6492..15006126dce 100644 --- a/app/coffeescripts/bundles/account_grading_standards.coffee +++ b/app/coffeescripts/bundles/account_grading_standards.coffee @@ -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") ) diff --git a/app/controllers/grading_standards_controller.rb b/app/controllers/grading_standards_controller.rb index 1930fbe51e9..e28adaf0034 100644 --- a/app/controllers/grading_standards_controller.rb +++ b/app/controllers/grading_standards_controller.rb @@ -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) diff --git a/app/jsx/gradebook/grid/helpers/datesHelper.jsx b/app/jsx/gradebook/grid/helpers/datesHelper.jsx index bccc99ef806..b9dfeb31f98 100644 --- a/app/jsx/gradebook/grid/helpers/datesHelper.jsx +++ b/app/jsx/gradebook/grid/helpers/datesHelper.jsx @@ -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 }); } diff --git a/app/jsx/grading/AccountGradingPeriod.jsx b/app/jsx/grading/AccountGradingPeriod.jsx index 647661e808e..34d3510183c 100644 --- a/app/jsx/grading/AccountGradingPeriod.jsx +++ b/app/jsx/grading/AccountGradingPeriod.jsx @@ -24,10 +24,10 @@ define([ {this.props.period.title}
- {I18n.t("Start Date:")} {DatesHelper.formatDateForDisplay(this.props.period.startDate)} + {I18n.t("Start Date:")} {DatesHelper.formatDatetimeForDisplay(this.props.period.startDate)}
- {I18n.t("End Date:")} {DatesHelper.formatDateForDisplay(this.props.period.endDate)} + {I18n.t("End Date:")} {DatesHelper.formatDatetimeForDisplay(this.props.period.endDate)}
diff --git a/app/jsx/grading/AccountTabContainer.jsx b/app/jsx/grading/AccountTabContainer.jsx index a9d5c1105f1..9b2181db464 100644 --- a/app/jsx/grading/AccountTabContainer.jsx +++ b/app/jsx/grading/AccountTabContainer.jsx @@ -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([
  • {I18n.t('Grading Schemes')}
  • -
    diff --git a/app/jsx/grading/EnrollmentTermsDropdown.jsx b/app/jsx/grading/EnrollmentTermsDropdown.jsx new file mode 100644 index 00000000000..bc5f023d150 --- /dev/null +++ b/app/jsx/grading/EnrollmentTermsDropdown.jsx @@ -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 = (); + const termsWithSets = this.termsBelongingToSets(terms); + let options = _.map(this.sortedTerms(termsWithSets), function(term) { + return (); + }); + + options.unshift(allTermsOption); + return options; + }, + + render() { + return ( + + ); + } + }); + + return EnrollmentTermsDropdown; +}); diff --git a/app/jsx/grading/GradingPeriodSet.jsx b/app/jsx/grading/GradingPeriodSet.jsx index 104835a96cd..8e516f7942c 100644 --- a/app/jsx/grading/GradingPeriodSet.jsx +++ b/app/jsx/grading/GradingPeriodSet.jsx @@ -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 (
    @@ -156,20 +170,25 @@ define([
    -
    - - {I18n.t("Grading period title")} -

    - {this.props.set.title} -

    +
    +
    + + {I18n.t("Grading period title")} +

    + {this.props.set.title} +

    +
    + {this.renderEditAndDeleteIcons()} +
    +
    + {this.termNames()}
    - {this.renderEditAndDeleteIcons()}
    - {this.state.expanded && this.renderSetBody()} + {this.renderSetBody()}
    ); }, diff --git a/app/jsx/grading/GradingPeriodSetCollection.jsx b/app/jsx/grading/GradingPeriodSetCollection.jsx index 0b4782fec99..2ae8c178e38 100644 --- a/app/jsx/grading/GradingPeriodSetCollection.jsx +++ b/app/jsx/grading/GradingPeriodSetCollection.jsx @@ -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 ( - - ); - }, + 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 ( + permissions={set.permissions} + terms={this.state.enrollmentTerms} /> ); }); }, - render: function() { + render() { return (
    + -
    -
    {this.renderSets()} diff --git a/app/jsx/grading/gradingPeriod.jsx b/app/jsx/grading/gradingPeriod.jsx index 144278aacab..0d8d93a2492 100644 --- a/app/jsx/grading/gradingPeriod.jsx +++ b/app/jsx/grading/gradingPeriod.jsx @@ -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 () { diff --git a/app/jsx/grading/gradingPeriodTemplate.jsx b/app/jsx/grading/gradingPeriodTemplate.jsx index f4f7dd8137e..1c278f403ae 100644 --- a/app/jsx/grading/gradingPeriodTemplate.jsx +++ b/app/jsx/grading/gradingPeriodTemplate.jsx @@ -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 (
    - {DatesHelper.formatDateForDisplay(this.props.startDate)} + {DatesHelper.formatDatetimeForDisplay(this.props.startDate)}
    ); } @@ -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 (
    - {DatesHelper.formatDateForDisplay(this.props.endDate)} + {DatesHelper.formatDatetimeForDisplay(this.props.endDate)}
    ); } diff --git a/app/stylesheets/bundles/enrollment_terms.scss b/app/stylesheets/bundles/enrollment_terms.scss new file mode 100644 index 00000000000..7098452a9be --- /dev/null +++ b/app/stylesheets/bundles/enrollment_terms.scss @@ -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); +} diff --git a/app/stylesheets/bundles/grading_period_sets.scss b/app/stylesheets/bundles/grading_period_sets.scss index d4595c85c2b..f4f19f3b0e4 100644 --- a/app/stylesheets/bundles/grading_period_sets.scss +++ b/app/stylesheets/bundles/grading_period_sets.scss @@ -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 diff --git a/app/views/grading_standards/account_index.html.erb b/app/views/grading_standards/account_index.html.erb index f5bb98ad753..520a132092f 100644 --- a/app/views/grading_standards/account_index.html.erb +++ b/app/views/grading_standards/account_index.html.erb @@ -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") %>
    diff --git a/lib/api/v1/enrollment_term.rb b/lib/api/v1/enrollment_term.rb index bd84abd9dff..1052884ba2a 100644 --- a/lib/api/v1/enrollment_term.rb +++ b/lib/api/v1/enrollment_term.rb @@ -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 diff --git a/spec/coffeescripts/jsx/gradebook/GradingPeriodSpec.coffee b/spec/coffeescripts/jsx/gradebook/GradingPeriodSpec.coffee index 06e72cf7b77..0ad48369e5d 100644 --- a/spec/coffeescripts/jsx/gradebook/GradingPeriodSpec.coffee +++ b/spec/coffeescripts/jsx/gradebook/GradingPeriodSpec.coffee @@ -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 diff --git a/spec/coffeescripts/jsx/gradebook/grid/helpers/datesHelperSpec.coffee b/spec/coffeescripts/jsx/gradebook/grid/helpers/datesHelperSpec.coffee index e84c3269755..f2288bcd388 100644 --- a/spec/coffeescripts/jsx/gradebook/grid/helpers/datesHelperSpec.coffee +++ b/spec/coffeescripts/jsx/gradebook/grid/helpers/datesHelperSpec.coffee @@ -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() diff --git a/spec/javascripts/jsx/grading/AccountTabContainerSpec.jsx b/spec/javascripts/jsx/grading/AccountTabContainerSpec.jsx index a77704b6ae9..589788f4e59 100644 --- a/spec/javascripts/jsx/grading/AccountTabContainerSpec.jsx +++ b/spec/javascripts/jsx/grading/AccountTabContainerSpec.jsx @@ -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" diff --git a/spec/javascripts/jsx/grading/EnrollmentTermsDropdownSpec.jsx b/spec/javascripts/jsx/grading/EnrollmentTermsDropdownSpec.jsx new file mode 100644 index 00000000000..781b10adb92 --- /dev/null +++ b/spec/javascripts/jsx/grading/EnrollmentTermsDropdownSpec.jsx @@ -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"]); + }); +}); diff --git a/spec/javascripts/jsx/grading/GradingPeriodSetCollectionSpec.jsx b/spec/javascripts/jsx/grading/GradingPeriodSetCollectionSpec.jsx index 10b6c37518b..3c690f8d05a 100644 --- a/spec/javascripts/jsx/grading/GradingPeriodSetCollectionSpec.jsx +++ b/spec/javascripts/jsx/grading/GradingPeriodSetCollectionSpec.jsx @@ -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); + }); }); diff --git a/spec/javascripts/jsx/grading/GradingPeriodSetSpec.jsx b/spec/javascripts/jsx/grading/GradingPeriodSetSpec.jsx index 44c414b3c6f..ebf482edc6b 100644 --- a/spec/javascripts/jsx/grading/GradingPeriodSetSpec.jsx +++ b/spec/javascripts/jsx/grading/GradingPeriodSetSpec.jsx @@ -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);