gradingPeriodCollection.js 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270
  1. /*
  2. * Copyright (C) 2015 - present Instructure, Inc.
  3. *
  4. * This file is part of Canvas.
  5. *
  6. * Canvas is free software: you can redistribute it and/or modify it under
  7. * the terms of the GNU Affero General Public License as published by the Free
  8. * Software Foundation, version 3 of the License.
  9. *
  10. * Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
  11. * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
  12. * A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
  13. * details.
  14. *
  15. * You should have received a copy of the GNU Affero General Public License along
  16. * with this program. If not, see <http://www.gnu.org/licenses/>.
  17. */
  18. import React from 'react'
  19. import update from 'immutability-helper'
  20. import GradingPeriod from 'jsx/grading/gradingPeriod'
  21. import $ from 'jquery'
  22. import I18n from 'i18n!external_tools'
  23. import _ from 'underscore'
  24. import {camelize} from 'convert_case'
  25. import 'jquery.instructure_misc_plugins'
  26. const periodsAreLoaded = (state) => {
  27. return state.periods !== null;
  28. };
  29. let GradingPeriodCollection = React.createClass({
  30. propTypes: {
  31. // no props
  32. },
  33. getInitialState: function () {
  34. return {
  35. periods: null,
  36. readOnly: false,
  37. disabled: false,
  38. saveDisabled: true
  39. };
  40. },
  41. componentWillMount: function () {
  42. this.getPeriods();
  43. },
  44. getPeriods: function () {
  45. let self = this;
  46. $.getJSON(ENV.GRADING_PERIODS_URL)
  47. .success(function(periods) {
  48. self.setState({
  49. periods: self.deserializePeriods(periods),
  50. readOnly: periods.grading_periods_read_only,
  51. disabled: false,
  52. saveDisabled: _.isEmpty(periods.grading_periods)
  53. });
  54. })
  55. .error(function (){
  56. $.flashError(I18n.t('There was a problem fetching periods'));
  57. });
  58. },
  59. deserializePeriods: function(periods) {
  60. return _.map(periods.grading_periods, period => {
  61. let newPeriod = camelize(period);
  62. newPeriod.startDate = new Date(period.start_date);
  63. newPeriod.endDate = new Date(period.end_date);
  64. newPeriod.closeDate = new Date(period.close_date || period.end_date);
  65. return newPeriod;
  66. });
  67. },
  68. deleteGradingPeriod: function(id) {
  69. if (id.indexOf('new') > -1) {
  70. this.removeDeletedGradingPeriod(id);
  71. } else {
  72. let self = this;
  73. $('#grading-period-' + id).confirmDelete({
  74. url: ENV.GRADING_PERIODS_URL + '/' + id,
  75. message: I18n.t('Are you sure you want to delete this grading period?'),
  76. success: function () {
  77. $.flashMessage(I18n.t('The grading period was deleted'));
  78. if (self.lastRemainingPeriod()) {
  79. self.getPeriods();
  80. } else {
  81. self.removeDeletedGradingPeriod(id);
  82. }
  83. },
  84. error: function () {
  85. $.flashError(I18n.t('There was a problem deleting the grading period'));
  86. }
  87. });
  88. }
  89. },
  90. lastRemainingPeriod: function () {
  91. return this.state.periods.length === 1;
  92. },
  93. removeDeletedGradingPeriod: function(id) {
  94. let newPeriods = _.reject(this.state.periods, period => period.id === id);
  95. this.setState({periods: newPeriods});
  96. },
  97. getPeriodById: function(id) {
  98. return _.find(this.state.periods, period => period.id === id);
  99. },
  100. areGradingPeriodsValid: function () {
  101. return _.every(this.state.periods, (period) => {
  102. return this.isTitleCompleted(period) &&
  103. this.areDatesValid(period) &&
  104. this.isStartDateBeforeEndDate(period) &&
  105. this.areNoDatesOverlapping(period)
  106. });
  107. },
  108. areDatesOverlapping: function(targetPeriod) {
  109. let target = this.getPeriodById(targetPeriod.id);
  110. let otherPeriods = _.reject(this.state.periods, p => (p.id === target.id));
  111. if (_.isEmpty(otherPeriods)) return false;
  112. return _.any(otherPeriods, (period) => {
  113. // http://c2.com/cgi/wiki?TestIfDateRangesOverlap
  114. return (
  115. target.startDate < period.endDate &&
  116. period.startDate < target.endDate
  117. );
  118. });
  119. },
  120. areNoDatesOverlapping: function(targetPeriod) {
  121. if(this.areDatesOverlapping(targetPeriod)) {
  122. $.flashError(I18n.t('Grading periods must not overlap'));
  123. return false;
  124. } else {
  125. return true;
  126. }
  127. },
  128. areDatesValid: function(period) {
  129. if (!isNaN(period.startDate) && !isNaN(period.endDate)) {
  130. return true;
  131. } else {
  132. $.flashError(I18n.t('All dates fields must be present and formatted correctly'));
  133. return false;
  134. }
  135. },
  136. isStartDateBeforeEndDate: function(period) {
  137. if (period.startDate < period.endDate) {
  138. return true;
  139. } else {
  140. $.flashError(I18n.t('All start dates must be before the end date'));
  141. return false;
  142. }
  143. },
  144. isTitleCompleted: function(period) {
  145. if ((period.title).trim().length > 0) {
  146. return true;
  147. } else {
  148. $.flashError(I18n.t('All grading periods must have a title'));
  149. return false;
  150. }
  151. },
  152. updateGradingPeriodCollection: function(updatedGradingPeriodComponent) {
  153. let attrs = $.extend(true, {}, updatedGradingPeriodComponent.props, updatedGradingPeriodComponent.state);
  154. let existingGradingPeriod = this.getPeriodById(attrs.id);
  155. let indexToUpdate = this.state.periods.indexOf(existingGradingPeriod);
  156. let updatedPeriods = update(this.state.periods, {$splice: [[indexToUpdate, 1, attrs]]});
  157. this.setState({ periods: updatedPeriods });
  158. },
  159. serializeDataForSubmission: function () {
  160. let periods = _.map(this.state.periods, function(period) {
  161. return {
  162. id: period.id,
  163. title: period.title,
  164. start_date: period.startDate,
  165. end_date: period.endDate
  166. };
  167. });
  168. return { 'grading_periods': periods };
  169. },
  170. batchUpdatePeriods: function () {
  171. this.setState({disabled: true}, () => {
  172. if (this.areGradingPeriodsValid()) {
  173. $.ajax({
  174. type: 'PATCH',
  175. url: ENV.GRADING_PERIODS_URL + '/batch_update',
  176. dataType: 'json',
  177. contentType: 'application/json',
  178. data: JSON.stringify(this.serializeDataForSubmission()),
  179. context: this
  180. })
  181. .success(function (response) {
  182. $.flashMessage(I18n.t('All changes were saved'));
  183. this.setState({disabled: false, periods: this.deserializePeriods(response)});
  184. })
  185. .error(function (error) {
  186. this.setState({disabled: false});
  187. $.flashError(I18n.t('There was a problem saving the grading period'));
  188. });
  189. } else {
  190. this.setState({disabled: false});
  191. }
  192. });
  193. },
  194. renderSaveButton: function () {
  195. if (periodsAreLoaded(this.state) && !this.state.readOnly && _.all(this.state.periods, period => period.permissions.update)) {
  196. return (
  197. <div className='form-actions'>
  198. <button
  199. className='Button btn-primary btn save_button'
  200. id='update-button'
  201. disabled={this.state.disabled || this.state.saveDisabled}
  202. onClick={this.batchUpdatePeriods}
  203. >
  204. {this.state.disabled ? I18n.t('Updating') : I18n.t('Save')}
  205. </button>
  206. </div>
  207. );
  208. }
  209. },
  210. renderGradingPeriods: function () {
  211. if (!this.state.periods) return null;
  212. return _.map(this.state.periods, period => {
  213. return (
  214. <GradingPeriod
  215. key={period.id}
  216. ref={"grading_period_" + period.id}
  217. id={period.id}
  218. title={period.title}
  219. startDate={period.startDate}
  220. endDate={period.endDate}
  221. closeDate={period.closeDate}
  222. permissions={period.permissions}
  223. readOnly={this.state.readOnly}
  224. disabled={this.state.disabled}
  225. weight={period.weight}
  226. weighted={ENV.GRADING_PERIODS_WEIGHTED}
  227. updateGradingPeriodCollection={this.updateGradingPeriodCollection}
  228. onDeleteGradingPeriod={this.deleteGradingPeriod}
  229. />
  230. );
  231. });
  232. },
  233. render: function () {
  234. return (
  235. <div>
  236. <div id='grading_periods' className='content-box'>
  237. {this.renderGradingPeriods()}
  238. </div>
  239. {this.renderSaveButton()}
  240. </div>
  241. );
  242. }
  243. });
  244. export default GradingPeriodCollection