gradingStandard.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374
  1. /*
  2. * Copyright (C) 2014 - 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 ReactDOM from 'react-dom'
  20. import update from 'immutability-helper'
  21. import DataRow from 'jsx/grading/dataRow'
  22. import $ from 'jquery'
  23. import I18n from 'i18n!external_tools'
  24. import _ from 'underscore'
  25. import splitAssetString from 'compiled/str/splitAssetString'
  26. var GradingStandard = React.createClass({
  27. getInitialState: function() {
  28. return {
  29. editingStandard: $.extend(true, {}, this.props.standard),
  30. saving: false,
  31. showAlert: false
  32. };
  33. },
  34. componentWillReceiveProps: function(nextProps) {
  35. this.setState({
  36. editingStandard: $.extend(true, {}, this.props.standard),
  37. saving: nextProps.saving,
  38. showAlert: false
  39. });
  40. },
  41. componentDidMount: function() {
  42. if(this.props.justAdded) ReactDOM.findDOMNode(this.refs.title).focus();
  43. },
  44. componentDidUpdate: function(prevProps, prevState) {
  45. if(this.props.editing !== prevProps.editing){
  46. ReactDOM.findDOMNode(this.refs.title).focus();
  47. this.setState({editingStandard: $.extend(true, {}, this.props.standard)})
  48. }
  49. },
  50. triggerEditGradingStandard: function(event) {
  51. this.props.onSetEditingStatus(this.props.uniqueId, true);
  52. },
  53. triggerStopEditingGradingStandard: function() {
  54. this.props.onSetEditingStatus(this.props.uniqueId, false);
  55. },
  56. triggerDeleteGradingStandard: function(event) {
  57. return this.props.onDeleteGradingStandard(event, this.props.uniqueId);
  58. },
  59. triggerSaveGradingStandard: function() {
  60. if(this.standardIsValid()){
  61. this.setState({saving: true}, function() {
  62. this.props.onSaveGradingStandard(this.state.editingStandard);
  63. });
  64. }else{
  65. this.setState({showAlert: true}, function() {
  66. ReactDOM.findDOMNode(this.refs.invalidStandardAlert).focus();
  67. });
  68. }
  69. },
  70. assessedAssignment: function() {
  71. return !!(this.props.standard && this.props.standard["assessed_assignment?"]);
  72. },
  73. deleteDataRow: function(index) {
  74. if(this.moreThanOneDataRowRemains()){
  75. var newEditingStandard = update(this.state.editingStandard, {data: {$splice: [[index, 1]]}});
  76. this.setState({editingStandard: newEditingStandard});
  77. }
  78. },
  79. moreThanOneDataRowRemains: function() {
  80. return this.state.editingStandard.data.length > 1;
  81. },
  82. insertGradingStandardRow: function(index) {
  83. var newEditingStandard = update(this.state.editingStandard, {data: {$splice: [[index + 1, 0, ["", ""]]]}});
  84. this.setState({editingStandard: newEditingStandard});
  85. },
  86. changeTitle: function(event) {
  87. var newEditingStandard = $.extend(true, {}, this.state.editingStandard);
  88. newEditingStandard.title = (event.target.value);
  89. this.setState({editingStandard: newEditingStandard});
  90. },
  91. changeRowMinScore: function(index, inputVal) {
  92. var newEditingStandard = $.extend(true, {}, this.state.editingStandard);
  93. var lastChar = inputVal.substr(inputVal.length - 1);
  94. newEditingStandard.data[index][1] = inputVal;
  95. this.setState({editingStandard: newEditingStandard});
  96. },
  97. changeRowName: function(index, newRowName) {
  98. var newEditingStandard = $.extend(true, {}, this.state.editingStandard);
  99. newEditingStandard.data[index][0] = newRowName;
  100. this.setState({editingStandard: newEditingStandard});
  101. },
  102. hideAlert: function() {
  103. this.setState({showAlert: false}, function(){
  104. ReactDOM.findDOMNode(this.refs.title).focus();
  105. });
  106. },
  107. standardIsValid: function() {
  108. return this.rowDataIsValid() && this.rowNamesAreValid();
  109. },
  110. rowDataIsValid: function() {
  111. if(this.state.editingStandard.data.length <= 1) return true;
  112. var rowValues = _.map(this.state.editingStandard.data, function(dataRow){ return String(dataRow[1]).trim() });
  113. var sanitizedRowValues = _.chain(rowValues).compact().uniq().value();
  114. var inputsAreUniqueAndNonEmpty = (sanitizedRowValues.length === rowValues.length);
  115. var valuesDoNotOverlap = !_.any(this.state.editingStandard.data, function(element, index, list){
  116. if(index < 1) return false;
  117. var thisMinScore = this.props.round(element[1]);
  118. var aboveMinScore = this.props.round(list[index-1][1]);
  119. return (thisMinScore >= aboveMinScore);
  120. }, this);
  121. return inputsAreUniqueAndNonEmpty && valuesDoNotOverlap;
  122. },
  123. rowNamesAreValid: function() {
  124. var rowNames = _.map(this.state.editingStandard.data, function(dataRow){ return dataRow[0].trim() });
  125. var sanitizedRowNames = _.chain(rowNames).compact().uniq().value();
  126. return sanitizedRowNames.length === rowNames.length;
  127. },
  128. renderCannotManageMessage: function() {
  129. if(this.props.permissions.manage && this.props.othersEditing) return null;
  130. if(this.props.standard.context_name){
  131. return (
  132. <div ref="cannotManageMessage">
  133. {I18n.t("(%{context}: %{contextName})", { context: this.props.standard.context_type.toLowerCase(), contextName: this.props.standard.context_name })}
  134. </div>
  135. );
  136. }
  137. return (
  138. <div ref="cannotManageMessage">
  139. {I18n.t("(%{context} level)", { context: this.props.standard.context_type.toLowerCase() })}
  140. </div>
  141. );
  142. },
  143. renderIdNames: function() {
  144. if(this.assessedAssignment()) return "grading_standard_blank";
  145. return "grading_standard_" + (this.props.standard ? this.props.standard.id : "blank");
  146. },
  147. renderTitle: function() {
  148. if(this.props.editing){
  149. return (
  150. <div className="pull-left">
  151. <input type="text" onChange={this.changeTitle}
  152. name="grading_standard[title]" className="scheme_name" title={I18n.t("Grading standard title")}
  153. value={this.state.editingStandard.title} ref="title"/>
  154. </div>
  155. );
  156. }
  157. return (
  158. <div className="pull-left">
  159. <div className="title" ref="title">
  160. <span className="screenreader-only">{I18n.t("Grading standard title")}</span>
  161. {this.props.standard.title}
  162. </div>
  163. </div>
  164. );
  165. },
  166. renderDataRows: function() {
  167. var data = this.props.editing ? this.state.editingStandard.data : this.props.standard.data;
  168. return data.map(function(item, idx, array){
  169. return (
  170. <DataRow key={idx} uniqueId={idx} row={item} siblingRow={array[idx - 1]} editing={this.props.editing}
  171. onDeleteRow={this.deleteDataRow} onInsertRow={this.insertGradingStandardRow}
  172. onlyDataRowRemaining={!this.moreThanOneDataRowRemains()} round={this.props.round}
  173. onRowMinScoreChange={this.changeRowMinScore} onRowNameChange={this.changeRowName}/>
  174. );
  175. }, this);
  176. },
  177. renderSaveButton: function() {
  178. if(this.state.saving){
  179. return (
  180. <button type="button" ref="saveButton" className="btn btn-primary save_button" disabled="true">
  181. {I18n.t("Saving...")}
  182. </button>
  183. );
  184. }
  185. return (
  186. <button type="button" ref="saveButton" onClick={this.triggerSaveGradingStandard} className="btn btn-primary save_button">
  187. {I18n.t("Save")}
  188. </button>
  189. );
  190. },
  191. renderSaveAndCancelButtons: function() {
  192. if(this.props.editing){
  193. return (
  194. <div className="form-actions">
  195. <button type="button" ref="cancelButton" onClick={this.triggerStopEditingGradingStandard} className="btn cancel_button">
  196. {I18n.t("Cancel")}
  197. </button>
  198. {this.renderSaveButton()}
  199. </div>
  200. );
  201. }
  202. return null;
  203. },
  204. renderEditAndDeleteIcons: function() {
  205. if(!this.props.editing){
  206. return(
  207. <div>
  208. <button ref="editButton"
  209. className={"Button Button--icon-action edit_grading_standard_button " + (this.assessedAssignment() ? "read_only" : "")}
  210. onClick={this.triggerEditGradingStandard} type="button">
  211. <span className="screenreader-only">{I18n.t("Edit Grading Scheme")}</span>
  212. <i className="icon-edit"/>
  213. </button>
  214. <button ref="deleteButton" className="Button Button--icon-action delete_grading_standard_button"
  215. onClick={this.triggerDeleteGradingStandard} type="button">
  216. <span className="screenreader-only">{I18n.t("Delete Grading Scheme")}</span>
  217. <i className="icon-trash"/>
  218. </button>
  219. </div>
  220. );
  221. }
  222. return null;
  223. },
  224. renderIconsAndTitle: function() {
  225. if(this.props.permissions.manage && !this.props.othersEditing &&
  226. (!this.props.standard.context_code || this.props.standard.context_code == ENV.context_asset_string)){
  227. return (
  228. <div>
  229. {this.renderTitle()}
  230. <div className="links">
  231. {this.renderEditAndDeleteIcons()}
  232. </div>
  233. </div>
  234. );
  235. }
  236. return (
  237. <div>
  238. {this.renderTitle()}
  239. {this.renderDisabledIcons()}
  240. <div className="pull-left cannot-manage-notification">
  241. {this.renderCannotManageMessage()}
  242. </div>
  243. </div>
  244. );
  245. },
  246. renderDisabledIcons: function() {
  247. if (this.props.permissions.manage &&
  248. this.props.standard.context_code && (this.props.standard.context_code != ENV.context_asset_string)) {
  249. var url = "/" + splitAssetString(this.props.standard.context_code).join('/') + "/grading_standards";
  250. var titleText = I18n.t('Manage grading schemes in %{context_name}',
  251. {context_name: this.props.standard.context_name || this.props.standard.context_type.toLowerCase()});
  252. return (<a className='links cannot-manage-notification'
  253. href={url}
  254. title={titleText}
  255. data-tooltip='left'>
  256. <span className="screenreader-only">{titleText}</span>
  257. <i className="icon-more standalone-icon"/>
  258. </a>);
  259. } else {
  260. return (<div className="disabled-buttons" ref="disabledButtons">
  261. <i className="icon-edit"/>
  262. <i className="icon-trash"/>
  263. </div>);
  264. }
  265. },
  266. renderInvalidStandardMessage: function() {
  267. var message = "Invalid grading scheme";
  268. if (!this.rowDataIsValid()) message = "Cannot have overlapping or empty ranges. Fix the ranges and try clicking 'Save' again.";
  269. if (!this.rowNamesAreValid()) message = "Cannot have duplicate or empty row names. Fix the names and try clicking 'Save' again.";
  270. return (
  271. <div id={"invalid_standard_message_" + this.props.uniqueId} className="alert-message" tabIndex="-1" ref="invalidStandardAlert">
  272. {I18n.t("%{message}", { message: message })}
  273. </div>
  274. );
  275. },
  276. renderStandardAlert: function() {
  277. if(!this.state.showAlert) return null;
  278. if(this.standardIsValid()){
  279. return (
  280. <div id="valid_standard" className="alert alert-success">
  281. <button aria-label="Close" className="dismiss_alert close" onClick={this.hideAlert}>×</button>
  282. <div className="alert-message">
  283. {I18n.t("Looks great!")}
  284. </div>
  285. </div>
  286. );
  287. }
  288. return (
  289. <div id="invalid_standard" className="alert alert-error">
  290. <button aria-label="Close" className="dismiss_alert close" onClick={this.hideAlert}>×</button>
  291. {this.renderInvalidStandardMessage()}
  292. </div>
  293. );
  294. },
  295. render: function () {
  296. return (
  297. <div>
  298. <div className="grading_standard react_grading_standard pad-box-mini border border-trbl border-round"
  299. id={this.renderIdNames()}>
  300. {this.renderStandardAlert()}
  301. <div>
  302. <table>
  303. <caption className="screenreader-only">
  304. {I18n.t("A table containing the name of the grading scheme and buttons for editing or deleting the scheme.")}
  305. </caption>
  306. <thead>
  307. <tr>
  308. <th scope="col" className="insert_row_container" tabIndex="-1"/>
  309. <th scope="col" colSpan="5" className="standard_title">
  310. {this.renderIconsAndTitle()}
  311. </th>
  312. </tr>
  313. <tr>
  314. <th scope="col" className="insert_row_container"/>
  315. <th scope="col" className="name_header">{I18n.t("Name")}</th>
  316. <th scope="col" className="range_container" colSpan="2">
  317. <div className="range_label">{I18n.t("Range")}</div>
  318. <div className="clear"></div>
  319. </th>
  320. </tr>
  321. </thead>
  322. </table>
  323. <table className="grading_standard_data">
  324. <caption className="screenreader-only">
  325. {I18n.t("A table that contains the grading scheme data. Each row contains a name, a maximum percentage, and a minimum percentage. In addition, each row contains a button to add a new row below, and a button to delete the current row.")}
  326. </caption>
  327. <thead ariaHidden="true"><tr><td></td><td></td><td></td><td></td><td></td></tr></thead>
  328. <tbody>
  329. {this.renderDataRows()}
  330. </tbody>
  331. </table>
  332. {this.renderSaveAndCancelButtons()}
  333. </div>
  334. </div>
  335. </div>
  336. );
  337. }
  338. });
  339. export default GradingStandard