PageRenderTime 29ms CodeModel.GetById 13ms RepoModel.GetById 1ms app.codeStats 0ms

/app/jsx/grading/GradingPeriodSet.jsx

https://gitlab.com/ykazemi/canvas-lms
JSX | 411 lines | 364 code | 47 blank | 0 comment | 36 complexity | da0f43e794adcb4f4f9318c644872abe MD5 | raw file
  1. define([
  2. 'react',
  3. 'jquery',
  4. 'underscore',
  5. 'axios',
  6. 'convert_case',
  7. 'i18n!grading_periods',
  8. 'jsx/grading/AccountGradingPeriod',
  9. 'jsx/grading/GradingPeriodForm',
  10. 'compiled/api/gradingPeriodsApi',
  11. 'jquery.instructure_misc_helpers'
  12. ], function(React, $, _, axios, ConvertCase, I18n, GradingPeriod, GradingPeriodForm, gradingPeriodsApi) {
  13. const sortPeriods = function(periods) {
  14. return _.sortBy(periods, "startDate");
  15. };
  16. const anyPeriodsOverlap = function(periods) {
  17. if (_.isEmpty(periods)) {
  18. return false;
  19. }
  20. let firstPeriod = _.first(periods);
  21. let otherPeriods = _.rest(periods);
  22. let overlapping = _.some(otherPeriods, function(otherPeriod) {
  23. return otherPeriod.startDate < firstPeriod.endDate && firstPeriod.startDate < otherPeriod.endDate;
  24. });
  25. return overlapping || anyPeriodsOverlap(otherPeriods);
  26. };
  27. const isValidDate = function(date) {
  28. return Object.prototype.toString.call(date) === "[object Date]" &&
  29. !isNaN(date.getTime());
  30. };
  31. const validatePeriods = function(periods) {
  32. if (_.any(periods, (period) => { return !(period.title || "").trim() })) {
  33. return [I18n.t('All grading periods must have a title')];
  34. }
  35. let validDates = _.all(periods, (period) => {
  36. return isValidDate(period.startDate) && isValidDate(period.endDate);
  37. });
  38. if (!validDates) {
  39. return [I18n.t('All dates fields must be present and formatted correctly')];
  40. }
  41. let orderedDates = _.all(periods, (period) => {
  42. return period.startDate < period.endDate;
  43. });
  44. if (!orderedDates) {
  45. return [I18n.t('All start dates must be before the end date')];
  46. }
  47. if (anyPeriodsOverlap(periods)) {
  48. return [I18n.t('Grading periods must not overlap')];
  49. }
  50. };
  51. const isEditingPeriod = function(state) {
  52. return !!state.editPeriod.id;
  53. };
  54. const setFocus = function(ref) {
  55. React.findDOMNode(ref).focus();
  56. };
  57. const getShowGradingPeriodRef = function(period) {
  58. return "show-grading-period-" + period.id;
  59. };
  60. const getEditGradingPeriodRef = function(period) {
  61. return "edit-grading-period-" + period.id;
  62. };
  63. const { shape, number, string, array, bool, func } = React.PropTypes;
  64. let GradingPeriodSet = React.createClass({
  65. propTypes: {
  66. gradingPeriods: array.isRequired,
  67. terms: array.isRequired,
  68. readOnly: bool.isRequired,
  69. expanded: bool,
  70. actionsDisabled: bool,
  71. onEdit: func.isRequired,
  72. onDelete: func.isRequired,
  73. onPeriodsChange: func.isRequired,
  74. onToggleBody: func.isRequired,
  75. set: shape({
  76. id: string.isRequired,
  77. title: string.isRequired
  78. }).isRequired,
  79. urls: shape({
  80. batchUpdateURL: string.isRequired,
  81. deleteGradingPeriodURL: string.isRequired,
  82. gradingPeriodSetsURL: string.isRequired
  83. }).isRequired,
  84. permissions: shape({
  85. read: bool.isRequired,
  86. create: bool.isRequired,
  87. update: bool.isRequired,
  88. delete: bool.isRequired
  89. }).isRequired
  90. },
  91. getInitialState() {
  92. return {
  93. title: this.props.set.title,
  94. gradingPeriods: sortPeriods(this.props.gradingPeriods),
  95. newPeriod: {
  96. period: null,
  97. saving: false
  98. },
  99. editPeriod: {
  100. id: null,
  101. saving: false
  102. }
  103. };
  104. },
  105. componentDidUpdate(prevProps, prevState) {
  106. if (prevState.newPeriod.period && !this.state.newPeriod.period) {
  107. setFocus(this.refs.addPeriodButton);
  108. } else if (isEditingPeriod(prevState) && !isEditingPeriod(this.state)) {
  109. let period = { id: prevState.editPeriod.id };
  110. setFocus(this.refs[getShowGradingPeriodRef(period)].refs.editButton);
  111. }
  112. },
  113. toggleSetBody() {
  114. if (!isEditingPeriod(this.state)) {
  115. this.props.onToggleBody();
  116. }
  117. },
  118. promptDeleteSet(event) {
  119. event.stopPropagation();
  120. const confirmMessage = I18n.t("Are you sure you want to delete this grading period set?");
  121. if (!window.confirm(confirmMessage)) return null;
  122. const url = this.props.urls.gradingPeriodSetsURL + "/" + this.props.set.id;
  123. axios.delete(url)
  124. .then(() => {
  125. $.flashMessage(I18n.t('The grading period set was deleted'));
  126. this.props.onDelete(this.props.set.id);
  127. })
  128. .catch(() => {
  129. $.flashError(I18n.t("An error occured while deleting the grading period set"));
  130. });
  131. },
  132. setTerms() {
  133. return _.where(this.props.terms, { gradingPeriodGroupId: this.props.set.id });
  134. },
  135. termNames() {
  136. const names = _.pluck(this.setTerms(), "displayName");
  137. return I18n.t("Terms: ") + names.join(", ");
  138. },
  139. editSet(e) {
  140. e.stopPropagation();
  141. this.props.onEdit(this.props.set);
  142. },
  143. changePeriods(periods) {
  144. let sortedPeriods = sortPeriods(periods);
  145. this.setState({ gradingPeriods: sortedPeriods });
  146. this.props.onPeriodsChange(this.props.set.id, sortedPeriods);
  147. },
  148. removeGradingPeriod(idToRemove) {
  149. let periods = _.reject(this.state.gradingPeriods, period => period.id === idToRemove);
  150. this.setState({ gradingPeriods: periods });
  151. },
  152. showNewPeriodForm() {
  153. this.setNewPeriod({ period: {} });
  154. },
  155. saveNewPeriod(period) {
  156. let periods = this.state.gradingPeriods.concat([period]);
  157. let validations = validatePeriods(periods);
  158. if (_.isEmpty(validations)) {
  159. this.setNewPeriod({saving: true});
  160. gradingPeriodsApi.batchUpdate(this.props.set.id, periods)
  161. .then((periods) => {
  162. $.flashMessage(I18n.t('All changes were saved'));
  163. this.removeNewPeriodForm();
  164. this.changePeriods(periods);
  165. })
  166. .catch((_) => {
  167. $.flashError(I18n.t('There was a problem saving the grading period'));
  168. this.setNewPeriod({ saving: false });
  169. });
  170. } else {
  171. _.each(validations, function(message) {
  172. $.flashError(message);
  173. });
  174. }
  175. },
  176. removeNewPeriodForm() {
  177. this.setNewPeriod({ saving: false, period: null });
  178. },
  179. setNewPeriod(attr) {
  180. let period = $.extend(true, {}, this.state.newPeriod, attr);
  181. this.setState({ newPeriod: period });
  182. },
  183. editPeriod(period) {
  184. this.setEditPeriod({ id: period.id, saving: false });
  185. },
  186. updatePeriod(period) {
  187. let periods = _.reject(this.state.gradingPeriods, function(_period) {
  188. return period.id === _period.id;
  189. }).concat([period]);
  190. let validations = validatePeriods(periods);
  191. if (_.isEmpty(validations)) {
  192. this.setEditPeriod({ saving: true });
  193. gradingPeriodsApi.batchUpdate(this.props.set.id, periods)
  194. .then((periods) => {
  195. $.flashMessage(I18n.t('All changes were saved'));
  196. this.setEditPeriod({ id: null, saving: false });
  197. this.changePeriods(periods);
  198. })
  199. .catch((_) => {
  200. $.flashError(I18n.t('There was a problem saving the grading period'));
  201. this.setNewPeriod({saving: false});
  202. });
  203. } else {
  204. _.each(validations, function(message) {
  205. $.flashError(message);
  206. });
  207. }
  208. },
  209. cancelEditPeriod() {
  210. this.setEditPeriod({ id: null, saving: false });
  211. },
  212. setEditPeriod(attr) {
  213. let period = $.extend(true, {}, this.state.editPeriod, attr);
  214. this.setState({ editPeriod: period });
  215. },
  216. renderEditButton() {
  217. if (!this.props.readOnly && this.props.permissions.update) {
  218. let disabled = !!(this.props.actionsDisabled || isEditingPeriod(this.state));
  219. let baseClasses = 'Button Button--icon-action edit_grading_period_set_button';
  220. return (
  221. <button ref="editButton"
  222. className={baseClasses + (disabled ? " disabled" : "")}
  223. aria-disabled={disabled}
  224. type="button"
  225. onClick={this.editSet}>
  226. <span className="screenreader-only">{I18n.t("Edit Grading Period Set")}</span>
  227. <i className="icon-edit"/>
  228. </button>
  229. );
  230. }
  231. },
  232. renderDeleteButton() {
  233. if (!this.props.readOnly && this.props.permissions.delete) {
  234. let disabled = !!(this.props.actionsDisabled || isEditingPeriod(this.state));
  235. let baseClasses = 'Button Button--icon-action delete_grading_period_set_button';
  236. return (
  237. <button ref="deleteButton"
  238. className={baseClasses + (disabled ? " disabled" : "")}
  239. aria-disabled={disabled}
  240. type="button"
  241. onClick={this.promptDeleteSet}>
  242. <span className="screenreader-only">{I18n.t("Delete Grading Period Set")}</span>
  243. <i className="icon-trash"/>
  244. </button>
  245. );
  246. }
  247. },
  248. renderEditAndDeleteButtons() {
  249. return (
  250. <div className="ItemGroup__header__admin">
  251. {this.renderEditButton()}
  252. {this.renderDeleteButton()}
  253. </div>
  254. );
  255. },
  256. renderSetBody() {
  257. if (!this.props.expanded) return null;
  258. return (
  259. <div ref="setBody" className="ig-body">
  260. <div className="GradingPeriodList" ref="gradingPeriodList">
  261. {this.renderGradingPeriods()}
  262. </div>
  263. {this.renderNewPeriod()}
  264. </div>
  265. );
  266. },
  267. renderGradingPeriods() {
  268. let actionsDisabled = !!(this.props.actionsDisabled || isEditingPeriod(this.state) || this.state.newPeriod.period);
  269. return _.map(this.state.gradingPeriods, (period) => {
  270. if (period.id === this.state.editPeriod.id) {
  271. return (
  272. <div key = {"edit-grading-period-" + period.id}
  273. className = 'GradingPeriodList__period--editing pad-box'>
  274. <GradingPeriodForm ref = "editPeriodForm"
  275. period = {period}
  276. disabled = {this.state.editPeriod.saving}
  277. onSave = {this.updatePeriod}
  278. onCancel = {this.cancelEditPeriod} />
  279. </div>
  280. );
  281. } else {
  282. return (
  283. <GradingPeriod key={"show-grading-period-" + period.id}
  284. ref={getShowGradingPeriodRef(period)}
  285. period={period}
  286. actionsDisabled={actionsDisabled}
  287. onEdit={this.editPeriod}
  288. readOnly={this.props.readOnly}
  289. onDelete={this.removeGradingPeriod}
  290. deleteGradingPeriodURL={this.props.urls.deleteGradingPeriodURL}
  291. permissions={this.props.permissions} />
  292. );
  293. }
  294. });
  295. },
  296. renderNewPeriod() {
  297. if (this.props.permissions.create && !this.props.readOnly) {
  298. if (this.state.newPeriod.period) {
  299. return this.renderNewPeriodForm();
  300. } else {
  301. return this.renderNewPeriodButton();
  302. }
  303. }
  304. },
  305. renderNewPeriodButton() {
  306. let disabled = !!(this.props.actionsDisabled || isEditingPeriod(this.state));
  307. let classList = 'Button Button--link GradingPeriodList__new-period__add-button' + (disabled ? " disabled" : "");
  308. return (
  309. <div className='GradingPeriodList__new-period center-xs border-rbl border-round-b'>
  310. <button className={classList}
  311. ref='addPeriodButton'
  312. aria-disabled={disabled}
  313. aria-label={I18n.t('Add Grading Period')}
  314. onClick={this.showNewPeriodForm}>
  315. <i className='icon-plus GradingPeriodList__new-period__add-icon'/>
  316. {I18n.t('Grading Period')}
  317. </button>
  318. </div>
  319. );
  320. },
  321. renderNewPeriodForm() {
  322. return (
  323. <div className='GradingPeriodList__new-period--editing border border-rbl border-round-b pad-box'>
  324. <GradingPeriodForm key = 'new-grading-period'
  325. ref = 'newPeriodForm'
  326. disabled = {this.state.newPeriod.saving}
  327. onSave = {this.saveNewPeriod}
  328. onCancel = {this.removeNewPeriodForm} />
  329. </div>
  330. );
  331. },
  332. render() {
  333. const setStateSuffix = this.props.expanded ? "expanded" : "collapsed";
  334. const arrow = this.props.expanded ? "down" : "right";
  335. return (
  336. <div className={"GradingPeriodSet--" + setStateSuffix}>
  337. <div className="ItemGroup__header"
  338. ref="toggleSetBody"
  339. onClick={this.toggleSetBody}>
  340. <div>
  341. <div className="ItemGroup__header__title">
  342. <button className={"Button Button--icon-action GradingPeriodSet__toggle"}
  343. aria-expanded={this.props.expanded}
  344. aria-label="Toggle grading period visibility">
  345. <i className={"icon-mini-arrow-" + arrow}/>
  346. </button>
  347. <span className="screenreader-only">{I18n.t("Grading period title")}</span>
  348. <h2 ref="title" tabIndex="0" className="GradingPeriodSet__title">
  349. {this.props.set.title}
  350. </h2>
  351. </div>
  352. {this.renderEditAndDeleteButtons()}
  353. </div>
  354. <div className="EnrollmentTerms__list" tabIndex="0">
  355. {this.termNames()}
  356. </div>
  357. </div>
  358. {this.renderSetBody()}
  359. </div>
  360. );
  361. }
  362. });
  363. return GradingPeriodSet;
  364. });