From 54e25d6a83fc7efeeded1ad58a6f626be7dc4f44 Mon Sep 17 00:00:00 2001 From: Nathan Stitt Date: Wed, 9 Jun 2021 17:58:47 -0500 Subject: [PATCH 01/18] convert string helpers to TS and move to shared exercises needs part of it --- shared/src/helpers/string.ts | 128 +++++++++++++++++++++++++++++++++++ tutor/src/helpers/string.js | 114 ------------------------------- tutor/src/helpers/string.ts | 6 ++ 3 files changed, 134 insertions(+), 114 deletions(-) create mode 100644 shared/src/helpers/string.ts delete mode 100644 tutor/src/helpers/string.js create mode 100644 tutor/src/helpers/string.ts diff --git a/shared/src/helpers/string.ts b/shared/src/helpers/string.ts new file mode 100644 index 0000000000..219b25e364 --- /dev/null +++ b/shared/src/helpers/string.ts @@ -0,0 +1,128 @@ +import { isNaN, isString, isNumber, trimStart, trimEnd } from 'lodash'; + +const SMALL_WORDS = /^(a|an|and|as|at|but|by|en|for|if|in|nor|of|on|or|per|the|to|vs?\.?|via)$/i; +const UUID_REGEX = /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/; + + +export function toNumber(value: number | string) { + return isNumber(value) ? value : parseFloat(value) +} +export function toInt(string: string) { + const int = parseInt(string); + if (isNaN(int)) { + return 0; + } + return int; +} +export function asPercent(num: number) { + return Math.round(num * 100) +} + +export function numberWithOneDecimalPlace(value: number | string) { + const num = toNumber(value) + return (Math.round(num * 10) / 10).toFixed(1) +} +export function numberWithTwoDecimalPlaces(val: number | string) { + return (Math.round(toNumber(val) * 100) / 100).toFixed(2) +} + +export function capitalize(string: string, lowerOthers = true) { + const other = lowerOthers ? string.substring(1).toLowerCase() : string.substring(1); + return string.charAt(0).toUpperCase() + other; +} + +export function replaceAt(string: string, index: number, character: string) { + return string.substr(0, index) + character + string.substr(index + character.length); +} + +export function insertAt(string: string, index: number, character: string) { + return string.substr(0, index) + character + string.substr(index); +} + +export function removeAt(string: string, index: number, length = 1) { + return string.substr(0, index) + string.substr(index + length); +} + +export function getNumberAndStringOrder(string: string) { + const parsedInt = parseFloat(string); + if (isNaN(parsedInt)) { return string.toLowerCase(); } else { return parsedInt; } +} +export function dasherize(string: string) { + return String(string) + .replace(/[A-Z]/g, (char, index) => (index !== 0 ? '-' : '') + char.toLowerCase()) + .replace(/[-_\s]+/g, '-'); +} + +// originated from http://individed.com/code/to-title-case/ +export function titleize(string = '') { + return String(string) + .replace(/_/g, ' ') + .replace(/[A-Za-z0-9\u00C0-\u00FF]+[^\s-]*/g, function(match, index, title) { + if ((index > 0) && ((index + match.length) !== title.length) && + (match.search(SMALL_WORDS) > -1) && + (title.charAt(index - 2) !== ':') && + ( (title.charAt(index + match.length) !== '-') || (title.charAt(index - 1) === '-') ) && + (title.charAt(index - 1).search(/[^\s-]/) < 0)) { + + return match.toLowerCase(); + } + + if (match.substr(1).search(/[A-Z]|\../) > -1) { + return match; + } + + return match.charAt(0).toUpperCase() + match.substr(1); + }); +} + +export function toSentence(arry: string | string[], join = 'and') { + if (isString(arry)) { arry = arry.split(' '); } + if (arry.length > 1) { + return `${arry.slice(0, arry.length - 1).join(', ')} ${join} ${arry.slice(-1)}`; + } else { + return arry[0]; + } +} + +export function isEmpty(s: string | null | undefined): boolean { + return Boolean( + isEmpty(s) || (isString(s) && !s.match(/\S/)) + ); +} + +export function isUUID(uuid = '') { return UUID_REGEX.test(uuid); } + +export function countWords(text: string) { + if(!isString(text)) return 0; + + let trimmedText = trimStart(text); + trimmedText = trimEnd(trimmedText); + //https://css-tricks.com/build-word-counter-app/ + const words = trimmedText.match(/\b[-?(\w+)?]+\b/gi); + if(!words) return 0; + return words.length; +} + +export function stripHTMLTags(text: string) { + return isString(text) ? text.replace(/(<([^>]+)>)/ig, '') : text; +} + +export default { + toNumber, + toInt, + asPercent, + numberWithOneDecimalPlace, + numberWithTwoDecimalPlaces, + capitalize, + replaceAt, + insertAt, + removeAt, + getNumberAndStringOrder, + dasherize, + titleize, + toSentence, + isEmpty, + isUUID, + countWords, + stripHTMLTags, +}; diff --git a/tutor/src/helpers/string.js b/tutor/src/helpers/string.js deleted file mode 100644 index 226e9fb024..0000000000 --- a/tutor/src/helpers/string.js +++ /dev/null @@ -1,114 +0,0 @@ -import { isNaN, isString, isEmpty, trimStart, trimEnd } from 'lodash'; - -const SMALL_WORDS = /^(a|an|and|as|at|but|by|en|for|if|in|nor|of|on|or|per|the|to|vs?\.?|via)$/i; -const UUID_REGEX = /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/; - -export default { - asPercent(num) { - return Math.round(num * 100); - }, - - numberWithOneDecimalPlace(num) { - return parseFloat(Math.round(parseFloat(num) * 10) / 10).toFixed(1); - }, - - numberWithTwoDecimalPlaces(num) { - return parseFloat(Math.round(parseFloat(num) * 100) / 100).toFixed(2); - }, - - capitalize(string, lowerOthers = true) { - const other = lowerOthers ? string.substring(1).toLowerCase() : string.substring(1); - return string.charAt(0).toUpperCase() + other; - }, - - replaceAt(string, index, character) { - return string.substr(0, index) + character + string.substr(index + character.length); - }, - - insertAt(string, index, character) { - return string.substr(0, index) + character + string.substr(index); - }, - - removeAt(string, index, length = 1) { - return string.substr(0, index) + string.substr(index + length); - }, - - getNumberAndStringOrder(string) { - const parsedInt = parseFloat(string); - if (isNaN(parsedInt)) { return string.toLowerCase(); } else { return parsedInt; } - }, - - dasherize(string) { - return String(string) - .replace(/[A-Z]/g, (char, index) => (index !== 0 ? '-' : '') + char.toLowerCase()) - .replace(/[-_\s]+/g, '-'); - }, - - // originated from http://individed.com/code/to-title-case/ - titleize(string = '') { - return String(string) - .replace(/_/g, ' ') - .replace(/[A-Za-z0-9\u00C0-\u00FF]+[^\s-]*/g, function(match, index, title) { - if ((index > 0) && ((index + match.length) !== title.length) && - (match.search(SMALL_WORDS) > -1) && - (title.charAt(index - 2) !== ':') && - ( (title.charAt(index + match.length) !== '-') || (title.charAt(index - 1) === '-') ) && - (title.charAt(index - 1).search(/[^\s-]/) < 0)) { - - return match.toLowerCase(); - } - - if (match.substr(1).search(/[A-Z]|\../) > -1) { - return match; - } - - return match.charAt(0).toUpperCase() + match.substr(1); - }); - }, - - toSentence(arry, join = 'and') { - if (isString(arry)) { arry = arry.split(' '); } - if (arry.length > 1) { - return `${arry.slice(0, arry.length - 1).join(', ')} ${join} ${arry.slice(-1)}`; - } else { - return arry[0]; - } - }, - - isEmpty(s) { - return Boolean( - isEmpty(s) || (isString(s) && !s.match(/\S/)) - ); - }, - - isUUID(uuid = '') { return UUID_REGEX.test(uuid); }, - - stringToInt(string) { - const int = parseInt(string); - if (isNaN(int)) { - return 0; - } - return int; - }, - - countWords(text) { - if(!isString(text)) return 0; - - let trimmedText = trimStart(text); - trimmedText = trimEnd(trimmedText); - //https://css-tricks.com/build-word-counter-app/ - const words = trimmedText.match(/\b[-?(\w+)?]+\b/gi); - if(!words) return 0; - return words.length; - }, - - stripHTMLTags(text) { - return isString(text) ? text.replace(/(<([^>]+)>)/ig, '') : text; - }, - - assignmentHeaderText(type) { - if(type === 'external') return 'external assignment'; - return type; - }, - -}; diff --git a/tutor/src/helpers/string.ts b/tutor/src/helpers/string.ts new file mode 100644 index 0000000000..72a823fddb --- /dev/null +++ b/tutor/src/helpers/string.ts @@ -0,0 +1,6 @@ +export function assignmentHeaderText(type: string) { + if(type === 'external') return 'external assignment'; + return type; +} + +export * from 'shared/helpers/string' From 62c1776200280cb6c23b271273a85352d2edf602 Mon Sep 17 00:00:00 2001 From: Nathan Stitt Date: Wed, 9 Jun 2021 17:59:45 -0500 Subject: [PATCH 02/18] add print styles to exercises --- exercises/resources/styles/app.scss | 1 + exercises/resources/styles/print.scss | 22 +++++++++++++++++++ .../src/components/{search.jsx => search.tsx} | 15 +++++++++++-- exercises/src/components/search/clause.jsx | 4 ++-- exercises/src/models/exercises.ts | 10 +++++---- exercises/src/models/search.ts | 4 ++++ tutor/src/components/dropped-question.tsx | 18 ++++++++------- tutor/src/screens/assignment-edit/index.js | 4 ++-- .../screens/teacher-dashboard/add-menu.jsx | 10 ++++----- 9 files changed, 65 insertions(+), 23 deletions(-) create mode 100644 exercises/resources/styles/print.scss rename exercises/src/components/{search.jsx => search.tsx} (77%) diff --git a/exercises/resources/styles/app.scss b/exercises/resources/styles/app.scss index 5b7a65a738..3a3c85b044 100644 --- a/exercises/resources/styles/app.scss +++ b/exercises/resources/styles/app.scss @@ -16,6 +16,7 @@ $input-height: 1.5rem; @import "./vocabulary"; @import './tags'; @import './preview'; +@import './print'; .exercises-body { margin-top: 60px; diff --git a/exercises/resources/styles/print.scss b/exercises/resources/styles/print.scss new file mode 100644 index 0000000000..06ce5a4beb --- /dev/null +++ b/exercises/resources/styles/print.scss @@ -0,0 +1,22 @@ +@media print { + .panel.search { + + .card-footer, + .pagination, + .search-filter + { + display: none; + } + + .openstax-exercise-preview { + page-break-inside: avoid; + border: 1px solid black; + } + .search-title { + position: fixed; + top: 0; + display: none; + } + } + +} diff --git a/exercises/src/components/search.jsx b/exercises/src/components/search.tsx similarity index 77% rename from exercises/src/components/search.jsx rename to exercises/src/components/search.tsx index d51d70c7d5..44b5cea484 100644 --- a/exercises/src/components/search.jsx +++ b/exercises/src/components/search.tsx @@ -9,13 +9,23 @@ import BSPagination from 'shared/components/pagination'; import Loading from 'shared/components/loading-animation'; import { modelize, action } from 'shared/model'; import UX from '../ux'; +import pluralize from 'pluralize'; +import { toSentence } from 'shared/helpers/string' const Pagination = styled(BSPagination)` justify-content: center; margin-top: 2rem; - `; +const Title = ({ exercises, clauses }) => { + if (!exercises.length) { + return null; + } + return ( +
{pluralize('exercise', exercises.length, true)} found for {toSentence(clauses.map(c => c.asString))}
+ ) +} + @inject('ux') @observer class Search extends React.Component { @@ -49,9 +59,10 @@ class Search extends React.Component { exercises.map((e) => ); return ( -
+
{clauses.map((c, i) => )} {pagination && } + {body} </div> ); diff --git a/exercises/src/components/search/clause.jsx b/exercises/src/components/search/clause.jsx index 24f2624eef..dec77ff39d 100644 --- a/exercises/src/components/search/clause.jsx +++ b/exercises/src/components/search/clause.jsx @@ -18,7 +18,7 @@ class Clause extends React.Component { const { clause } = this.props; return ( - <Row> + <Row className="search-filter"> <Col xs={8}> <InputGroup> <DropdownButton @@ -61,7 +61,7 @@ class Clause extends React.Component { waitingText="Searching…" onClick={clause.search.execute} > - Go + Go </AsyncButton> </InputGroup.Append> diff --git a/exercises/src/models/exercises.ts b/exercises/src/models/exercises.ts index 132c939c7d..6787e936cf 100644 --- a/exercises/src/models/exercises.ts +++ b/exercises/src/models/exercises.ts @@ -78,9 +78,11 @@ export class ExercisesMap extends Map<ID, Exercise | ExerciseVersions> { } } - publish(exercise: Exercise) { - const data = exercise.toJSON() - return { uid: exercise.uid, data } ; + async publish(exercise: Exercise) { + this.onSaved( + await this.api.request(urlFor('publish', { uid: exercise.uid }), { data: exercise.toJSON() }), + exercise + ) } async saveDraft(exercise: Exercise) { @@ -91,7 +93,7 @@ export class ExercisesMap extends Map<ID, Exercise | ExerciseVersions> { url = urlFor('saveExistingDraft', { number: exercise.number }) } this.onSaved( - await this.api.request(url, exercise.toJSON()), + await this.api.request(url, { data: exercise.toJSON() }), exercise ) } diff --git a/exercises/src/models/search.ts b/exercises/src/models/search.ts index fdff09179e..6a4f00c50b 100644 --- a/exercises/src/models/search.ts +++ b/exercises/src/models/search.ts @@ -21,6 +21,10 @@ class Clause extends BaseModel { return `Search by ${this.filter}`; } + @computed get asString() { + return `${this.filter}="${this.value}"`; + } + @action.bound setFilter(filter: string) { this.filter = filter; this.search.currentPage = 1; diff --git a/tutor/src/components/dropped-question.tsx b/tutor/src/components/dropped-question.tsx index 472172b317..802204610e 100644 --- a/tutor/src/components/dropped-question.tsx +++ b/tutor/src/components/dropped-question.tsx @@ -91,18 +91,20 @@ const DroppedQuestionHeadingIndicator: React.FC<DroppedQuestionHeadingIndicatorP }) => { if (!heading.someQuestionsDropped) return null + const isHomework = heading.tasking.scores.type == 'homework' + let tooltip - const isHomework = heading.tasking.scores.type == 'homework' + if (heading.droppedQuestion.drop_method == 'full_credit') { + tooltip = 'Full credit given ' + } else { + tooltip = 'Points changed to 0 ' + } - if (isHomework) { - if (heading.everyQuestionFullCredit) { - tooltip = 'Full credit given to all students' - } else { - tooltip = 'Points changed to 0 for all students' - } + if (isHomework && heading.isCore) { + tooltip += 'to all students' } else { - tooltip = `Question dropped for ${pluralize('student', responseCount, true)}` + tooltip += `for ${pluralize('student', responseCount, true)}` } return ( diff --git a/tutor/src/screens/assignment-edit/index.js b/tutor/src/screens/assignment-edit/index.js index 0e44d5c778..5ee01fd648 100644 --- a/tutor/src/screens/assignment-edit/index.js +++ b/tutor/src/screens/assignment-edit/index.js @@ -9,7 +9,7 @@ import { withRouter } from 'react-router'; import UX from './ux'; import { BackgroundWrapper, ContentWrapper } from '../../helpers/background-wrapper'; import CourseBreadcrumb from '../../components/course-breadcrumb'; -import S from '../../helpers/string'; +import { assignmentHeaderText } from '../../helpers/string'; import './styles.scss'; @@ -80,7 +80,7 @@ class AssignmentBuilder extends React.Component { courseId={ux.course.id} > <ContentWrapper> - <CourseBreadcrumb course={this.ux.course} currentTitle={`Add ${S.assignmentHeaderText(ux.plan.type)}`} /> + <CourseBreadcrumb course={this.ux.course} currentTitle={`Add ${assignmentHeaderText(ux.plan.type)}`} /> <Formik initialValues={ux.formValues} validateOnMount={true} diff --git a/tutor/src/screens/teacher-dashboard/add-menu.jsx b/tutor/src/screens/teacher-dashboard/add-menu.jsx index 1bf4c57f5c..b74abeca90 100644 --- a/tutor/src/screens/teacher-dashboard/add-menu.jsx +++ b/tutor/src/screens/teacher-dashboard/add-menu.jsx @@ -6,7 +6,7 @@ import { autobind } from 'core-decorators'; import Router from '../../helpers/router'; import CourseGroupingLabel from '../../components/course-grouping-label'; import { colors } from 'theme'; -import S from '../../helpers/string'; +import { assignmentHeaderText } from '../../helpers/string'; const StyledMenuContainer = styled.div` & hr { @@ -37,25 +37,25 @@ export default class CourseAddMenu { if (hasPeriods) { links = [ { - text: `Add ${S.assignmentHeaderText('reading')}`, + text: `Add ${assignmentHeaderText('reading')}`, to: 'editAssignment', params: { type: 'reading', courseId: course.id, id: 'new' }, type: 'reading', query: { due_at }, }, { - text: `Add ${S.assignmentHeaderText('homework')}`, + text: `Add ${assignmentHeaderText('homework')}`, to: 'editAssignment', params: { type: 'homework', courseId: course.id, id: 'new' }, type: 'homework', query: { due_at }, }, { - text: `Add ${S.assignmentHeaderText('external')}`, + text: `Add ${assignmentHeaderText('external')}`, to: 'editAssignment', params: { type: 'external', courseId: course.id, id: 'new' }, type: 'external', query: { due_at }, }, { - text: `Add ${S.assignmentHeaderText('event')}`, + text: `Add ${assignmentHeaderText('event')}`, to: 'editAssignment', params: { type: 'event', courseId: course.id, id: 'new' }, type: 'event', From 9d23d5402bb92ad7dae562afc9dcc47a2ff7adab Mon Sep 17 00:00:00 2001 From: Nathan Stitt <nathan@stitt.org> Date: Thu, 10 Jun 2021 11:00:27 -0500 Subject: [PATCH 03/18] set title on document so it prints on PDF --- exercises/src/components/search.tsx | 23 ++++++++++------------- exercises/src/models/search.ts | 8 ++++++++ 2 files changed, 18 insertions(+), 13 deletions(-) diff --git a/exercises/src/components/search.tsx b/exercises/src/components/search.tsx index 44b5cea484..63a2d1d991 100644 --- a/exercises/src/components/search.tsx +++ b/exercises/src/components/search.tsx @@ -7,25 +7,15 @@ import Controls from './search/controls'; import { observer, inject } from 'mobx-react'; import BSPagination from 'shared/components/pagination'; import Loading from 'shared/components/loading-animation'; -import { modelize, action } from 'shared/model'; +import { modelize, action, autorun } from 'shared/model'; import UX from '../ux'; -import pluralize from 'pluralize'; -import { toSentence } from 'shared/helpers/string' + const Pagination = styled(BSPagination)` justify-content: center; margin-top: 2rem; `; -const Title = ({ exercises, clauses }) => { - if (!exercises.length) { - return null; - } - return ( - <h6 className="search-title">{pluralize('exercise', exercises.length, true)} found for {toSentence(clauses.map(c => c.asString))}</h6> - ) -} - @inject('ux') @observer class Search extends React.Component { @@ -41,6 +31,14 @@ class Search extends React.Component { constructor(props) { super(props); modelize(this); + this.titleChangeDisposer = autorun(() => { + document.title = this.search.title + }) + } + + componentWillUnmount() { + this.titleChangeDisposer() + document.title = 'OpenStax Exercises' } get search() { @@ -62,7 +60,6 @@ class Search extends React.Component { <div className="panel search"> {clauses.map((c, i) => <Clause key={i} clause={c} />)} {pagination && <Pagination hideFirstAndLastPageLinks {...pagination} />} - <Title clauses={clauses} exercises={exercises} /> {body} </div> ); diff --git a/exercises/src/models/search.ts b/exercises/src/models/search.ts index 6a4f00c50b..6a7015def3 100644 --- a/exercises/src/models/search.ts +++ b/exercises/src/models/search.ts @@ -4,6 +4,8 @@ import { } from 'shared/model'; import Exercise from './exercises/exercise'; import urlFor from '../api' +import pluralize from 'pluralize'; +import { toSentence } from 'shared/helpers/string' class Clause extends BaseModel { @@ -76,6 +78,12 @@ class Search extends BaseModel { this.perform(); } + @computed get title() { + if (!this.exercises.length) { + return 'Exercise Search'; + } + return `${pluralize('exercise', this.exercises.length, true)} found for ${toSentence(this.clauses.map(c => c.asString))}` + } @action.bound onPageChange(pg: number) { this.currentPage = pg; From 6b7e48c351cd5d2984c8d02cb6ce81fa6991140d Mon Sep 17 00:00:00 2001 From: Nathan Stitt <nathan@stitt.org> Date: Thu, 10 Jun 2021 12:47:43 -0500 Subject: [PATCH 04/18] swap formats and solution, display solution inline --- .../resources/styles/components/question.scss | 22 +++++++++---------- shared/src/components/question/index.jsx | 4 ++-- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/shared/resources/styles/components/question.scss b/shared/resources/styles/components/question.scss index 41aba9d0f7..566c8c133c 100644 --- a/shared/resources/styles/components/question.scss +++ b/shared/resources/styles/components/question.scss @@ -3,18 +3,18 @@ //@include clearfix; .detailed-solution { - margin-bottom: 1.5rem; - - .header { - color: #5e6062; - font-weight: bold; - margin-bottom: 0.5rem; - } - - .solution { - color: #6f6f6f; margin-bottom: 1rem; - } + .header { + display: inline; + float: left; + margin-right: 0.5rem; + color: #5e6062; + font-weight: bold; + flex-basis: 0; + } + .solution { + color: #6f6f6f; + } } img { diff --git a/shared/src/components/question/index.jsx b/shared/src/components/question/index.jsx index e11bc10533..c43d6815cc 100644 --- a/shared/src/components/question/index.jsx +++ b/shared/src/components/question/index.jsx @@ -135,7 +135,7 @@ class Question extends React.Component { solution = <div className="detailed-solution"> <div className="header"> - Detailed solution + Detailed solution: </div> <ArbitraryHtmlAndMath {...htmlAndMathProps} @@ -153,8 +153,8 @@ class Question extends React.Component { <QuestionHtml type="stem" html={stem_html} hidden={hidePreambles} questionNumber={questionNumber} /> {this.props.children} <AnswersTable {...this.props} hasCorrectAnswer={hasCorrectAnswer} /> - {this.props.displayFormats ? <FormatsListing formats={formats} /> : undefined} {solution} + {this.props.displayFormats ? <FormatsListing formats={formats} /> : undefined} {exerciseUid} </div> ); From e166d9586c3954e1448a7f5648001b4b580cc1c7 Mon Sep 17 00:00:00 2001 From: Nathan Stitt <nathan@stitt.org> Date: Thu, 10 Jun 2021 12:48:06 -0500 Subject: [PATCH 05/18] condense margins --- exercises/resources/styles/print.scss | 39 ++++++++++++++++++++++++--- 1 file changed, 35 insertions(+), 4 deletions(-) diff --git a/exercises/resources/styles/print.scss b/exercises/resources/styles/print.scss index 06ce5a4beb..88b1179d60 100644 --- a/exercises/resources/styles/print.scss +++ b/exercises/resources/styles/print.scss @@ -8,15 +8,46 @@ display: none; } - .openstax-exercise-preview { - page-break-inside: avoid; - border: 1px solid black; - } .search-title { position: fixed; top: 0; display: none; } + + .openstax-exercise-preview { + page-break-inside: avoid; + border: 1px solid black; + + .card-body { + .answers-table { + margin-bottom: 0.5rem; + line-height: inherit; + label { + margin-bottom: 0; + } + .question-feedback-content { + line-height: 1rem; + margin: 0; + } + } + .question-stem { + margin-bottom: 0; + } + } + + + .openstax-question { + .detailed-solution { + margin-bottom: 0; + .solution, + .header { + margin-bottom: 0; + } + + } + } + } } } + From d447f4ac5b7e5de78827979bbe1012bfa6bb4b62 Mon Sep 17 00:00:00 2001 From: Nathan Stitt <nathan@stitt.org> Date: Thu, 10 Jun 2021 12:48:14 -0500 Subject: [PATCH 06/18] exercises styles aren't used --- .../styles/components/exercises.scss | 245 ------------------ 1 file changed, 245 deletions(-) delete mode 100644 tutor/resources/styles/components/exercises.scss diff --git a/tutor/resources/styles/components/exercises.scss b/tutor/resources/styles/components/exercises.scss deleted file mode 100644 index ef835da2d7..0000000000 --- a/tutor/resources/styles/components/exercises.scss +++ /dev/null @@ -1,245 +0,0 @@ -$exercise-details-body-height: calc(100vh - 250px); - -.exercise-details { - min-height: $exercise-details-body-height; - position: relative; - display: flex; - flex-direction: column; - - & .action-controls { - cursor: pointer; - position: absolute; - top: 35px; - left: 7%; - z-index: 2; - .message { - border: 1px solid #d5d5d5; - width: 49px; - .action { - height: 43px; - width: 47px; - background: #fff; - color: #818181; - display: flex; - align-items: center; - justify-content: center; - - &:hover { - box-shadow: none; - } - - svg { - display: block; - font-size: 1.6rem; - } - &:before { - display: none; - } - .label-message { - display: none; - position: absolute; - left: 48px; - background: inherit; - height: 45px; - width: 150px; - border: 1px solid #d5d5d5; - border-width: 1px 1px 1px 0; - font-size: 1.4rem; - font-weight: normal; - } - &:hover { - &:not(.exclude):not(.include) { - color: #424242; - } - .label-message { - display: flex; - align-items: center; - } - } - - &.exclude { - background-color: $openstax-primary; - color: #fff; - } - &.include { - background-color: #0DC0DC; - color: #fff; - } - } - } - } - - .paged-content { - display: flex; - flex-direction: column; - justify-content: flex-start; - } - - .tutor-paging-navigation { - flex: 1; - } - - .controls { - position: absolute; - top: 20px; - left: 50%; - z-index: 1; - text-align: right; - width: calc(50% - 60px); - - .pinned-shy & { - top: 130px; - @include transition('top 0.2s ease-out'); - } - } - - a:not([href]).show-cards { - color: $tutor-blue-control; - cursor: pointer; - align-self: flex-end; - margin-bottom: 1.2rem; - } - - .exercise-card { - width: 800px; - min-height: 450px; - &:focus { outline: none; } - - .panel-body { - min-height: 600px; - padding: 70px 70px 70px 100px !important; - - } - - .controls-overlay { - transition: none; - } - &.has-interactive { - .controls-overlay { - left: 400px - $tutor-interactive-iframe-width/2; - } - } - } - - .paging-control { - .icon.arrow { position: absolute; } - } - - .detailed-solution, .exercise-footer { - color: #6f6f6f; - line-height: 2rem; - .author { - margin-top: 1.2rem; - } - a .chapter-section { - color: inherit; - } - } - - .detailed-solution { - margin-bottom: 3.5rem; - } - - .exercise-footer { - margin-top: 2.5rem; - } - -} - -.exercise-controls-bar { - $padding-horizontal: 16px; - - display: flex; - justify-content: space-between; - align-items: center; - padding: 0 $padding-horizontal; - min-height: 5.5rem; - - .sectionizer { - $size: 40px; - flex-wrap: nowrap; - display: flex; - align-items: center; - text-align: center; - div { - cursor: pointer; - display: inline-block; - font-size: 12px; - width: $size; - line-height: $size; - - &.section { - border: 1px solid $default-border; - height: $size; - position: relative; - margin-right: 0; - margin-left: -1px; - - &:first-child { - margin-left: 0; - } - } - &.active { - background-color: $tutor-neutral-light; - z-index: 10; - } - &.disabled { - cursor: default; - color: $tutor-neutral-light; - } - } - } - - .btn-group { - box-shadow: none; - button:not(.btn-link):not(.btn-flat):not(.btn-fab).btn-default { - font-size: 12px; - background-size: $icon-size-lg $icon-size-lg; - background-repeat: no-repeat; - border-radius: 0; - box-shadow: none !important; - border: 1px solid $tutor-neutral; - height: 40px; - margin-right: 0; - margin-left: -1px; - - &:first-child { - margin-left: 0; - } - &.active { - background-color: $tutor-neutral-light; - z-index: 10; - } - .ox-icon { - margin: 0; - } - } - &.filters { - button { - width: 90px; - } - } - &.display-types { - margin-left: 10px; - button { - width: 40px; - } - } - } - .save-cancel { - width: 200px; - display: flex; - justify-content: space-around; - } -} - -.exercise-cards { - max-width: 1200px; - margin: auto; - - .exercise-card { - &:focus { - @include tab-focus(); - } - } -} From ef572a5a9d8015d58c63a5e4ba11b7207b7876ab Mon Sep 17 00:00:00 2001 From: Nathan Stitt <nathan@stitt.org> Date: Thu, 10 Jun 2021 13:05:04 -0500 Subject: [PATCH 07/18] fix infinite recursion error --- shared/src/helpers/string.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/shared/src/helpers/string.ts b/shared/src/helpers/string.ts index 219b25e364..6bb4b8ef3f 100644 --- a/shared/src/helpers/string.ts +++ b/shared/src/helpers/string.ts @@ -1,4 +1,4 @@ -import { isNaN, isString, isNumber, trimStart, trimEnd } from 'lodash'; +import { isNaN, isString, isNumber, isEmpty as _isEmpty, trimStart, trimEnd } from 'lodash'; const SMALL_WORDS = /^(a|an|and|as|at|but|by|en|for|if|in|nor|of|on|or|per|the|to|vs?\.?|via)$/i; const UUID_REGEX = /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/; @@ -86,7 +86,7 @@ export function toSentence(arry: string | string[], join = 'and') { export function isEmpty(s: string | null | undefined): boolean { return Boolean( - isEmpty(s) || (isString(s) && !s.match(/\S/)) + _isEmpty(s) || (isString(s) && !s.match(/\S/)) ); } From 5f035fec1605344ff1dc69670692da5f2f5fb12b Mon Sep 17 00:00:00 2001 From: Nathan Stitt <nathan@stitt.org> Date: Thu, 10 Jun 2021 13:05:21 -0500 Subject: [PATCH 08/18] export default for modules that use it that way --- tutor/src/helpers/string.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tutor/src/helpers/string.ts b/tutor/src/helpers/string.ts index 72a823fddb..12ff37082c 100644 --- a/tutor/src/helpers/string.ts +++ b/tutor/src/helpers/string.ts @@ -2,5 +2,7 @@ export function assignmentHeaderText(type: string) { if(type === 'external') return 'external assignment'; return type; } +import S from 'shared/helpers/string' export * from 'shared/helpers/string' +export default S From 9de673ed11421c53214976a7f7c905675fa53939 Mon Sep 17 00:00:00 2001 From: Nathan Stitt <nathan@stitt.org> Date: Thu, 10 Jun 2021 13:05:44 -0500 Subject: [PATCH 09/18] mark as action --- tutor/src/components/notes/summary-page.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tutor/src/components/notes/summary-page.jsx b/tutor/src/components/notes/summary-page.jsx index 9d312828cf..d914bea62d 100644 --- a/tutor/src/components/notes/summary-page.jsx +++ b/tutor/src/components/notes/summary-page.jsx @@ -51,7 +51,7 @@ class NoteSummaryPage extends React.Component { modelize(this); } - resetCurrentPage() { + @action resetCurrentPage() { this.selectedPages.clear(); this.props.notes.ensurePageExists(this.props.page); const summary = this.props.notes.summary.forPage(this.props.page); From e46f77d841eeb414afdb141e21bdf9bd2a760d44 Mon Sep 17 00:00:00 2001 From: Nathan Stitt <nathan@stitt.org> Date: Thu, 10 Jun 2021 13:05:54 -0500 Subject: [PATCH 10/18] update snapshots to match new detailed solution position --- .../__snapshots__/exercise.spec.js.snap | 144 +++++++++--------- .../__snapshots__/index.spec.js.snap | 44 +++--- .../question/__snapshots__/index.spec.js.snap | 2 +- .../__snapshots__/preview.spec.js.snap | 2 +- 4 files changed, 96 insertions(+), 96 deletions(-) diff --git a/exercises/specs/components/__snapshots__/exercise.spec.js.snap b/exercises/specs/components/__snapshots__/exercise.spec.js.snap index b731598574..9a7be84623 100644 --- a/exercises/specs/components/__snapshots__/exercise.spec.js.snap +++ b/exercises/specs/components/__snapshots__/exercise.spec.js.snap @@ -1507,28 +1507,13 @@ exports[`Exercises component renders and matches snapshot 1`] = ` </section> </div> </div> - <div - className="formats-listing" - > - <div - className="header" - > - Formats: - </div> - <span> - free-response - </span> - <span> - multiple-choice - </span> - </div> <div className="detailed-solution" > <div className="header" > - Detailed solution + Detailed solution: </div> <div dangerouslySetInnerHTML={ @@ -1538,6 +1523,21 @@ exports[`Exercises component renders and matches snapshot 1`] = ` } /> </div> + <div + className="formats-listing" + > + <div + className="header" + > + Formats: + </div> + <span> + free-response + </span> + <span> + multiple-choice + </span> + </div> </div> </div> </div> @@ -4444,28 +4444,13 @@ exports[`Exercises component renders with intro and a multiple questions when ex </section> </div> </div> - <div - className="formats-listing" - > - <div - className="header" - > - Formats: - </div> - <span> - free-response - </span> - <span> - multiple-choice - </span> - </div> <div className="detailed-solution" > <div className="header" > - Detailed solution + Detailed solution: </div> <div dangerouslySetInnerHTML={ @@ -4475,6 +4460,21 @@ exports[`Exercises component renders with intro and a multiple questions when ex } /> </div> + <div + className="formats-listing" + > + <div + className="header" + > + Formats: + </div> + <span> + free-response + </span> + <span> + multiple-choice + </span> + </div> </div> </div> <div> @@ -4742,28 +4742,13 @@ exports[`Exercises component renders with intro and a multiple questions when ex </section> </div> </div> - <div - className="formats-listing" - > - <div - className="header" - > - Formats: - </div> - <span> - free-response - </span> - <span> - multiple-choice - </span> - </div> <div className="detailed-solution" > <div className="header" > - Detailed solution + Detailed solution: </div> <div dangerouslySetInnerHTML={ @@ -4773,6 +4758,21 @@ exports[`Exercises component renders with intro and a multiple questions when ex } /> </div> + <div + className="formats-listing" + > + <div + className="header" + > + Formats: + </div> + <span> + free-response + </span> + <span> + multiple-choice + </span> + </div> </div> </div> <div> @@ -4900,28 +4900,13 @@ exports[`Exercises component renders with intro and a multiple questions when ex </section> </div> </div> - <div - className="formats-listing" - > - <div - className="header" - > - Formats: - </div> - <span> - free-response - </span> - <span> - multiple-choice - </span> - </div> <div className="detailed-solution" > <div className="header" > - Detailed solution + Detailed solution: </div> <div dangerouslySetInnerHTML={ @@ -4931,6 +4916,21 @@ exports[`Exercises component renders with intro and a multiple questions when ex } /> </div> + <div + className="formats-listing" + > + <div + className="header" + > + Formats: + </div> + <span> + free-response + </span> + <span> + multiple-choice + </span> + </div> </div> </div> </div> @@ -6205,6 +6205,14 @@ exports[`Exercises component resets fields when model is new 1`] = ` </Answer> </div> </AnswersTable> + <div className=\\"detailed-solution\\"> + <div className=\\"header\\"> + Detailed solution: + </div> + <Component className=\\"solution\\" block={true} html=\\"four\\"> + <div dangerouslySetInnerHTML={{...}} /> + </Component> + </div> <FormatsListing formats={{...}}> <div className=\\"formats-listing\\"> <div className=\\"header\\"> @@ -6218,14 +6226,6 @@ exports[`Exercises component resets fields when model is new 1`] = ` </span> </div> </FormatsListing> - <div className=\\"detailed-solution\\"> - <div className=\\"header\\"> - Detailed solution - </div> - <Component className=\\"solution\\" block={true} html=\\"four\\"> - <div dangerouslySetInnerHTML={{...}} /> - </Component> - </div> </div> </Question> </div> diff --git a/shared/specs/components/exercise-preview/__snapshots__/index.spec.js.snap b/shared/specs/components/exercise-preview/__snapshots__/index.spec.js.snap index 707c0ed91b..c8ca36fc08 100644 --- a/shared/specs/components/exercise-preview/__snapshots__/index.spec.js.snap +++ b/shared/specs/components/exercise-preview/__snapshots__/index.spec.js.snap @@ -159,7 +159,7 @@ exports[`Exercise Preview Component callbacks are called when overlay and action <div className="header" > - Detailed solution + Detailed solution: </div> <div dangerouslySetInnerHTML={ @@ -253,7 +253,7 @@ exports[`Exercise Preview Component can hide the answers 1`] = ` <div className="header" > - Detailed solution + Detailed solution: </div> <div dangerouslySetInnerHTML={ @@ -420,28 +420,13 @@ exports[`Exercise Preview Component can render question formats 1`] = ` </section> </div> </div> - <div - className="formats-listing" - > - <div - className="header" - > - Formats: - </div> - <span> - free-response - </span> - <span> - multiple-choice - </span> - </div> <div className="detailed-solution" > <div className="header" > - Detailed solution + Detailed solution: </div> <div dangerouslySetInnerHTML={ @@ -451,6 +436,21 @@ exports[`Exercise Preview Component can render question formats 1`] = ` } /> </div> + <div + className="formats-listing" + > + <div + className="header" + > + Formats: + </div> + <span> + free-response + </span> + <span> + multiple-choice + </span> + </div> </div> </div> </div> @@ -703,7 +703,7 @@ exports[`Exercise Preview Component hides context if missing 1`] = ` <div className="header" > - Detailed solution + Detailed solution: </div> <div dangerouslySetInnerHTML={ @@ -876,7 +876,7 @@ exports[`Exercise Preview Component limits tags 1`] = ` <div className="header" > - Detailed solution + Detailed solution: </div> <div dangerouslySetInnerHTML={ @@ -1101,7 +1101,7 @@ exports[`Exercise Preview Component renders and matches snapshot 1`] = ` <div className="header" > - Detailed solution + Detailed solution: </div> <div dangerouslySetInnerHTML={ @@ -1315,7 +1315,7 @@ exports[`Exercise Preview Component sets the className when displaying feedback <div className="header" > - Detailed solution + Detailed solution: </div> <div dangerouslySetInnerHTML={ diff --git a/shared/specs/components/question/__snapshots__/index.spec.js.snap b/shared/specs/components/question/__snapshots__/index.spec.js.snap index 4f576da3a3..ab6f1d314a 100644 --- a/shared/specs/components/question/__snapshots__/index.spec.js.snap +++ b/shared/specs/components/question/__snapshots__/index.spec.js.snap @@ -116,7 +116,7 @@ exports[`Question Component renders and matches snapshot 1`] = ` <div className="header" > - Detailed solution + Detailed solution: </div> <div className="solution" diff --git a/tutor/specs/components/exercises/__snapshots__/preview.spec.js.snap b/tutor/specs/components/exercises/__snapshots__/preview.spec.js.snap index 8ba0bb541e..8eea8b943a 100644 --- a/tutor/specs/components/exercises/__snapshots__/preview.spec.js.snap +++ b/tutor/specs/components/exercises/__snapshots__/preview.spec.js.snap @@ -226,7 +226,7 @@ exports[`Exercise Preview Wrapper Component renders and matches snapshot 1`] = ` <div className="header" > - Detailed solution + Detailed solution: </div> <div className="solution" From 8c073dabeeb6c9116d50f988b181d1d82ca4da7e Mon Sep 17 00:00:00 2001 From: Nathan Stitt <nathan@stitt.org> Date: Thu, 10 Jun 2021 13:13:19 -0500 Subject: [PATCH 11/18] fix type errors --- exercises/src/components/search.tsx | 14 +++++++++++--- exercises/src/{ux.js => ux.ts} | 0 tutor/src/models/student-tasks/task.ts | 2 +- 3 files changed, 12 insertions(+), 4 deletions(-) rename exercises/src/{ux.js => ux.ts} (100%) diff --git a/exercises/src/components/search.tsx b/exercises/src/components/search.tsx index 63a2d1d991..d288675383 100644 --- a/exercises/src/components/search.tsx +++ b/exercises/src/components/search.tsx @@ -5,6 +5,7 @@ import Preview from './exercise/preview'; import Clause from './search/clause'; import Controls from './search/controls'; import { observer, inject } from 'mobx-react'; +import type { IReactionDisposer } from 'mobx'; import BSPagination from 'shared/components/pagination'; import Loading from 'shared/components/loading-animation'; import { modelize, action, autorun } from 'shared/model'; @@ -16,9 +17,14 @@ const Pagination = styled(BSPagination)` margin-top: 2rem; `; +interface SearchProps { + ux: UX + history: any +} + @inject('ux') @observer -class Search extends React.Component { +class Search extends React.Component<SearchProps> { static Controls = Controls; static propTypes = { @@ -28,7 +34,9 @@ class Search extends React.Component { }).isRequired, }; - constructor(props) { + titleChangeDisposer: IReactionDisposer + + constructor(props: SearchProps) { super(props); modelize(this); this.titleChangeDisposer = autorun(() => { @@ -45,7 +53,7 @@ class Search extends React.Component { return this.props.ux.search; } - @action.bound onEdit(ev) { + @action.bound onEdit(ev: React.MouseEvent<HTMLAnchorElement>) { ev.preventDefault(); this.props.history.push(ev.currentTarget.pathname); } diff --git a/exercises/src/ux.js b/exercises/src/ux.ts similarity index 100% rename from exercises/src/ux.js rename to exercises/src/ux.ts diff --git a/tutor/src/models/student-tasks/task.ts b/tutor/src/models/student-tasks/task.ts index 95ccc1ef91..f58cc4e48a 100644 --- a/tutor/src/models/student-tasks/task.ts +++ b/tutor/src/models/student-tasks/task.ts @@ -75,7 +75,7 @@ export class StudentTask extends BaseModel { } @computed get humanLateWorkPenalty() { - const amount = this.late_work_penalty_applied !== 'not_accepted' ? this.late_work_penalty_per_period : 1; + const amount = this.late_work_penalty_applied !== 'not_accepted' ? this.late_work_penalty_per_period || 0 : 1; return `${S.asPercent(amount)}%`; } From f9558a6897e23b01e2b480933871474ab213fc0c Mon Sep 17 00:00:00 2001 From: Nathan Stitt <nathan@stitt.org> Date: Thu, 10 Jun 2021 13:26:42 -0500 Subject: [PATCH 12/18] revert to main --- tutor/src/components/dropped-question.tsx | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/tutor/src/components/dropped-question.tsx b/tutor/src/components/dropped-question.tsx index 802204610e..472172b317 100644 --- a/tutor/src/components/dropped-question.tsx +++ b/tutor/src/components/dropped-question.tsx @@ -91,20 +91,18 @@ const DroppedQuestionHeadingIndicator: React.FC<DroppedQuestionHeadingIndicatorP }) => { if (!heading.someQuestionsDropped) return null - const isHomework = heading.tasking.scores.type == 'homework' - let tooltip - if (heading.droppedQuestion.drop_method == 'full_credit') { - tooltip = 'Full credit given ' - } else { - tooltip = 'Points changed to 0 ' - } + const isHomework = heading.tasking.scores.type == 'homework' - if (isHomework && heading.isCore) { - tooltip += 'to all students' + if (isHomework) { + if (heading.everyQuestionFullCredit) { + tooltip = 'Full credit given to all students' + } else { + tooltip = 'Points changed to 0 for all students' + } } else { - tooltip += `for ${pluralize('student', responseCount, true)}` + tooltip = `Question dropped for ${pluralize('student', responseCount, true)}` } return ( From 8b6e31d57477b6e8582d522e9c255550c3d281e3 Mon Sep 17 00:00:00 2001 From: Nathan Stitt <nathan@stitt.org> Date: Thu, 10 Jun 2021 13:40:48 -0500 Subject: [PATCH 13/18] restore file --- .../styles/components/exercises.scss | 245 ++++++++++++++++++ 1 file changed, 245 insertions(+) create mode 100644 tutor/resources/styles/components/exercises.scss diff --git a/tutor/resources/styles/components/exercises.scss b/tutor/resources/styles/components/exercises.scss new file mode 100644 index 0000000000..ef835da2d7 --- /dev/null +++ b/tutor/resources/styles/components/exercises.scss @@ -0,0 +1,245 @@ +$exercise-details-body-height: calc(100vh - 250px); + +.exercise-details { + min-height: $exercise-details-body-height; + position: relative; + display: flex; + flex-direction: column; + + & .action-controls { + cursor: pointer; + position: absolute; + top: 35px; + left: 7%; + z-index: 2; + .message { + border: 1px solid #d5d5d5; + width: 49px; + .action { + height: 43px; + width: 47px; + background: #fff; + color: #818181; + display: flex; + align-items: center; + justify-content: center; + + &:hover { + box-shadow: none; + } + + svg { + display: block; + font-size: 1.6rem; + } + &:before { + display: none; + } + .label-message { + display: none; + position: absolute; + left: 48px; + background: inherit; + height: 45px; + width: 150px; + border: 1px solid #d5d5d5; + border-width: 1px 1px 1px 0; + font-size: 1.4rem; + font-weight: normal; + } + &:hover { + &:not(.exclude):not(.include) { + color: #424242; + } + .label-message { + display: flex; + align-items: center; + } + } + + &.exclude { + background-color: $openstax-primary; + color: #fff; + } + &.include { + background-color: #0DC0DC; + color: #fff; + } + } + } + } + + .paged-content { + display: flex; + flex-direction: column; + justify-content: flex-start; + } + + .tutor-paging-navigation { + flex: 1; + } + + .controls { + position: absolute; + top: 20px; + left: 50%; + z-index: 1; + text-align: right; + width: calc(50% - 60px); + + .pinned-shy & { + top: 130px; + @include transition('top 0.2s ease-out'); + } + } + + a:not([href]).show-cards { + color: $tutor-blue-control; + cursor: pointer; + align-self: flex-end; + margin-bottom: 1.2rem; + } + + .exercise-card { + width: 800px; + min-height: 450px; + &:focus { outline: none; } + + .panel-body { + min-height: 600px; + padding: 70px 70px 70px 100px !important; + + } + + .controls-overlay { + transition: none; + } + &.has-interactive { + .controls-overlay { + left: 400px - $tutor-interactive-iframe-width/2; + } + } + } + + .paging-control { + .icon.arrow { position: absolute; } + } + + .detailed-solution, .exercise-footer { + color: #6f6f6f; + line-height: 2rem; + .author { + margin-top: 1.2rem; + } + a .chapter-section { + color: inherit; + } + } + + .detailed-solution { + margin-bottom: 3.5rem; + } + + .exercise-footer { + margin-top: 2.5rem; + } + +} + +.exercise-controls-bar { + $padding-horizontal: 16px; + + display: flex; + justify-content: space-between; + align-items: center; + padding: 0 $padding-horizontal; + min-height: 5.5rem; + + .sectionizer { + $size: 40px; + flex-wrap: nowrap; + display: flex; + align-items: center; + text-align: center; + div { + cursor: pointer; + display: inline-block; + font-size: 12px; + width: $size; + line-height: $size; + + &.section { + border: 1px solid $default-border; + height: $size; + position: relative; + margin-right: 0; + margin-left: -1px; + + &:first-child { + margin-left: 0; + } + } + &.active { + background-color: $tutor-neutral-light; + z-index: 10; + } + &.disabled { + cursor: default; + color: $tutor-neutral-light; + } + } + } + + .btn-group { + box-shadow: none; + button:not(.btn-link):not(.btn-flat):not(.btn-fab).btn-default { + font-size: 12px; + background-size: $icon-size-lg $icon-size-lg; + background-repeat: no-repeat; + border-radius: 0; + box-shadow: none !important; + border: 1px solid $tutor-neutral; + height: 40px; + margin-right: 0; + margin-left: -1px; + + &:first-child { + margin-left: 0; + } + &.active { + background-color: $tutor-neutral-light; + z-index: 10; + } + .ox-icon { + margin: 0; + } + } + &.filters { + button { + width: 90px; + } + } + &.display-types { + margin-left: 10px; + button { + width: 40px; + } + } + } + .save-cancel { + width: 200px; + display: flex; + justify-content: space-around; + } +} + +.exercise-cards { + max-width: 1200px; + margin: auto; + + .exercise-card { + &:focus { + @include tab-focus(); + } + } +} From 80cf2f7e8f56ce93817ed02f73cf50e53e67b6a6 Mon Sep 17 00:00:00 2001 From: Dante Soares <dante.m.soares@rice.edu> Date: Mon, 7 Jun 2021 15:33:39 -0500 Subject: [PATCH 14/18] Added checkbox toggle for public solutions to Exercises --- exercises/src/components/exercise/tags.jsx | 2 + .../src/components/tags/public-solutions.jsx | 38 +++++++++++++++++++ 2 files changed, 40 insertions(+) create mode 100644 exercises/src/components/tags/public-solutions.jsx diff --git a/exercises/src/components/exercise/tags.jsx b/exercises/src/components/exercise/tags.jsx index c9ca670415..e2101ad340 100644 --- a/exercises/src/components/exercise/tags.jsx +++ b/exercises/src/components/exercise/tags.jsx @@ -14,6 +14,7 @@ import HistoricalThinking from '../tags/historical-thinking'; import ReasoningProcess from '../tags/reasoning-process'; import ApLo from '../tags/aplo'; import SciencePractice from '../tags/science-practice'; +import PublicSolutions from '../tags/public-solutions'; import Exercise from '../../models/exercises/exercise'; function ExerciseTags({ exercise }) { @@ -36,6 +37,7 @@ function ExerciseTags({ exercise }) { <Dok {...tagProps} /> <Blooms {...tagProps} /> <Time {...tagProps} /> + <PublicSolutions {...tagProps} /> </div> </div> ); diff --git a/exercises/src/components/tags/public-solutions.jsx b/exercises/src/components/tags/public-solutions.jsx new file mode 100644 index 0000000000..45ec94ed13 --- /dev/null +++ b/exercises/src/components/tags/public-solutions.jsx @@ -0,0 +1,38 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import { observer } from 'mobx-react'; +import { action, modelize } from 'shared/model'; +import Exercise from '../../models/exercises/exercise'; +import Wrapper from './wrapper'; + +@observer +class PublicSolutions extends React.Component { + static propTypes = { + exercise: PropTypes.instanceOf(Exercise).isRequired, + }; + + constructor(props) { + super(props); + modelize(this); + } + + @action.bound updateValue(ev) { + this.props.exercise.solutions_are_public = ev.target.checked; + } + + render() { + return ( + <Wrapper label="Public Solutions?"> + <div className="tag"> + <input + type="checkbox" + label="" + onChange={this.updateValue} + checked={this.props.exercise.solutions_are_public} /> + </div> + </Wrapper> + ); + } +} + +export default PublicSolutions; From 9ca1aa63617d53016b8afe0105cff90a0e920b16 Mon Sep 17 00:00:00 2001 From: Dante Soares <dante.m.soares@rice.edu> Date: Mon, 7 Jun 2021 16:00:02 -0500 Subject: [PATCH 15/18] Added solutions_are_public field to Exercise --- exercises/src/models/exercises/exercise.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/exercises/src/models/exercises/exercise.ts b/exercises/src/models/exercises/exercise.ts index dac430bef2..7c51d646c4 100644 --- a/exercises/src/models/exercises/exercise.ts +++ b/exercises/src/models/exercises/exercise.ts @@ -1,6 +1,6 @@ import { action } from 'mobx'; import { merge, find, isEmpty, isObject, map } from 'lodash'; -import { modelize, model, hydrateModel, observable, array } from 'shared/model'; +import { field, modelize, model, hydrateModel, observable, array } from 'shared/model'; import Image from './image'; import Delegation from './delegation'; import SharedExercise from 'shared/model/exercise'; @@ -9,6 +9,7 @@ import CurrentUser from '../user'; export default class Exercise extends SharedExercise { + @field solutions_are_public = false static build(attrs: any) { return hydrateModel(Exercise, merge(attrs, { From 6f30308a50eb3522ed32dcaec8a032f7169191a3 Mon Sep 17 00:00:00 2001 From: Dante Soares <dante.m.soares@rice.edu> Date: Wed, 9 Jun 2021 10:35:42 -0500 Subject: [PATCH 16/18] Updated snapshots to add Public Solutions checkbox --- .../__snapshots__/exercise.spec.js.snap | 67 +++++++++++++++++++ 1 file changed, 67 insertions(+) diff --git a/exercises/specs/components/__snapshots__/exercise.spec.js.snap b/exercises/specs/components/__snapshots__/exercise.spec.js.snap index 9a7be84623..37e4c5b15e 100644 --- a/exercises/specs/components/__snapshots__/exercise.spec.js.snap +++ b/exercises/specs/components/__snapshots__/exercise.spec.js.snap @@ -1316,6 +1316,32 @@ exports[`Exercises component renders and matches snapshot 1`] = ` </select> </div> </div> + <div + className="tag-type publicSolutions" + > + <div + className="heading" + > + <span + className="label" + > + Public Solutions? + </span> + <div + className="controls" + /> + </div> + <div + className="tag" + > + <input + checked={false} + label="" + onChange={[Function]} + type="checkbox" + /> + </div> + </div> </div> </div> </div> @@ -4263,6 +4289,32 @@ exports[`Exercises component renders with intro and a multiple questions when ex </select> </div> </div> + <div + className="tag-type publicSolutions" + > + <div + className="heading" + > + <span + className="label" + > + Public Solutions? + </span> + <div + className="controls" + /> + </div> + <div + className="tag" + > + <input + checked={false} + label="" + onChange={[Function]} + type="checkbox" + /> + </div> + </div> </div> </div> </div> @@ -6000,6 +6052,21 @@ exports[`Exercises component resets fields when model is new 1`] = ` </TagWrapper> </SingleDropdown> </TimeTag> + <PublicSolutions exercise={{...}}> + <TagWrapper label=\\"Public Solutions?\\"> + <div className=\\"tag-type publicSolutions\\"> + <div className=\\"heading\\"> + <span className=\\"label\\"> + Public Solutions? + </span> + <div className=\\"controls\\" /> + </div> + <div className=\\"tag\\"> + <input type=\\"checkbox\\" label=\\"\\" onChange={[Function: updateValue] { isMobxAction: true }} checked={false} /> + </div> + </div> + </TagWrapper> + </PublicSolutions> </div> </div> </ExerciseTags> From e303c6810aa8a8e31ad405fd570c20d5ac6cee32 Mon Sep 17 00:00:00 2001 From: Nathan Stitt <nathan@stitt.org> Date: Tue, 15 Jun 2021 13:05:31 -0500 Subject: [PATCH 17/18] update wording, don't nest inside full-width tag --- exercises/src/components/tags/public-solutions.jsx | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/exercises/src/components/tags/public-solutions.jsx b/exercises/src/components/tags/public-solutions.jsx index 45ec94ed13..13e95155db 100644 --- a/exercises/src/components/tags/public-solutions.jsx +++ b/exercises/src/components/tags/public-solutions.jsx @@ -22,14 +22,12 @@ class PublicSolutions extends React.Component { render() { return ( - <Wrapper label="Public Solutions?"> - <div className="tag"> - <input - type="checkbox" - label="" - onChange={this.updateValue} - checked={this.props.exercise.solutions_are_public} /> - </div> + <Wrapper label="Solution is public"> + <input + type="checkbox" + label="" + onChange={this.updateValue} + checked={this.props.exercise.solutions_are_public} /> </Wrapper> ); } From d2e0e4d6f51ad1fb7472b7d8cee7c2dee8a60c68 Mon Sep 17 00:00:00 2001 From: Nathan Stitt <nathan@stitt.org> Date: Wed, 16 Jun 2021 12:00:08 -0500 Subject: [PATCH 18/18] update snapshot --- .../__snapshots__/exercise.spec.js.snap | 50 ++++++++----------- 1 file changed, 20 insertions(+), 30 deletions(-) diff --git a/exercises/specs/components/__snapshots__/exercise.spec.js.snap b/exercises/specs/components/__snapshots__/exercise.spec.js.snap index 37e4c5b15e..96504746bc 100644 --- a/exercises/specs/components/__snapshots__/exercise.spec.js.snap +++ b/exercises/specs/components/__snapshots__/exercise.spec.js.snap @@ -1317,7 +1317,7 @@ exports[`Exercises component renders and matches snapshot 1`] = ` </div> </div> <div - className="tag-type publicSolutions" + className="tag-type solutionIsPublic" > <div className="heading" @@ -1325,22 +1325,18 @@ exports[`Exercises component renders and matches snapshot 1`] = ` <span className="label" > - Public Solutions? + Solution is public </span> <div className="controls" /> </div> - <div - className="tag" - > - <input - checked={false} - label="" - onChange={[Function]} - type="checkbox" - /> - </div> + <input + checked={false} + label="" + onChange={[Function]} + type="checkbox" + /> </div> </div> </div> @@ -4290,7 +4286,7 @@ exports[`Exercises component renders with intro and a multiple questions when ex </div> </div> <div - className="tag-type publicSolutions" + className="tag-type solutionIsPublic" > <div className="heading" @@ -4298,22 +4294,18 @@ exports[`Exercises component renders with intro and a multiple questions when ex <span className="label" > - Public Solutions? + Solution is public </span> <div className="controls" /> </div> - <div - className="tag" - > - <input - checked={false} - label="" - onChange={[Function]} - type="checkbox" - /> - </div> + <input + checked={false} + label="" + onChange={[Function]} + type="checkbox" + /> </div> </div> </div> @@ -6053,17 +6045,15 @@ exports[`Exercises component resets fields when model is new 1`] = ` </SingleDropdown> </TimeTag> <PublicSolutions exercise={{...}}> - <TagWrapper label=\\"Public Solutions?\\"> - <div className=\\"tag-type publicSolutions\\"> + <TagWrapper label=\\"Solution is public\\"> + <div className=\\"tag-type solutionIsPublic\\"> <div className=\\"heading\\"> <span className=\\"label\\"> - Public Solutions? + Solution is public </span> <div className=\\"controls\\" /> </div> - <div className=\\"tag\\"> - <input type=\\"checkbox\\" label=\\"\\" onChange={[Function: updateValue] { isMobxAction: true }} checked={false} /> - </div> + <input type=\\"checkbox\\" label=\\"\\" onChange={[Function: updateValue] { isMobxAction: true }} checked={false} /> </div> </TagWrapper> </PublicSolutions>