Skip to content

Commit

Permalink
Self review (#1270)
Browse files Browse the repository at this point in the history
* WIP

* Split exercise block into more blocks

* Add new fields to the database

* Include only the latest completion in completions export

* Update document schema processor

* WIP

* Fix focus handling

* State handling

* Saving fixes

* Add self review checkbox

* WIP

* Change hardcoded instructions to a label in peer review view

* Update state_deriver

* Fix for reviews received

* Rename database tables and columns

* Fixes on the typescript side

* Fixes

* Fixes

* Fix

* WIP tests

* Bug fix

* Bug fix

* Update snapshots

* Test fixes

* Test fixes

* System test fixes

* Clippy fixes

* Peer reviews in the seed should require to receive one peer review

* Test fixes

* Fix AutomaticallyGradeOrManualReviewByAverage test

* Fix ManualReviewEverything test

* Remove not needed screenshots

* Eslint fixes

* Update snapshots

* Finalize peer and self review spec

* Finalize more self review tests

* Label self reviews in the teacher's view

* Show count of answers requiring attention in the top bar
  • Loading branch information
nygrenh authored May 14, 2024
1 parent bf52dc4 commit ce7bf9e
Show file tree
Hide file tree
Showing 282 changed files with 5,421 additions and 3,703 deletions.
1 change: 1 addition & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,7 @@ module.exports = {
"weight",
"action",
"tagName",
"templateLock",
],
},
words: {
Expand Down
170 changes: 43 additions & 127 deletions services/cms/src/blocks/Exercise/ExerciseEditor.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,16 @@
import { css } from "@emotion/css"
import styled from "@emotion/styled"
import { InnerBlocks } from "@wordpress/block-editor"
import { BlockEditProps } from "@wordpress/blocks"
import { BlockEditProps, TemplateArray } from "@wordpress/blocks"
import { useContext } from "react"
import { useTranslation } from "react-i18next"

import PeerReviewEditor from "../../components/PeerReviewEditor"
import { EditorContentDispatch } from "../../contexts/EditorContentContext"
import PageContext from "../../contexts/PageContext"
import ExerciseBlockContext from "../../contexts/ExerciseBlockContext"
import Button from "../../shared-module/components/Button"
import BreakFromCentered from "../../shared-module/components/Centering/BreakFromCentered"
import Centered from "../../shared-module/components/Centering/Centered"
import CheckBox from "../../shared-module/components/InputFields/CheckBox"
import TextField from "../../shared-module/components/InputFields/TextField"
import { baseTheme, primaryFont, typography } from "../../shared-module/styles"
import { respondToOrLarger } from "../../shared-module/styles/respond"
import { gutenbergControlsHidden } from "../../styles/EditorStyles"
import breakFromCenteredProps from "../../utils/breakfromCenteredProps"

import { ExerciseAttributes } from "."
Expand All @@ -30,6 +25,11 @@ const ExerciseEditorCard = styled.div`
margin-right: 0;
`

const INNER_BLOCKS_TEMPLATE: TemplateArray = [
["moocfi/exercise-settings", {}],
["moocfi/exercise-slides", {}],
]

const ExerciseEditor: React.FC<React.PropsWithChildren<BlockEditProps<ExerciseAttributes>>> = ({
attributes,
clientId,
Expand All @@ -43,133 +43,49 @@ const ExerciseEditor: React.FC<React.PropsWithChildren<BlockEditProps<ExerciseAt
dispatch({ type: "addExerciseSlide", payload: { clientId } })
}

const courseId = useContext(PageContext)?.page.course_id

return (
<BreakFromCentered {...breakFromCenteredProps}>
<div
className={css`
background-color: ${baseTheme.colors.clear[100]};
`}
>
<Centered variant="narrow">
<ExerciseEditorCard id={attributes.id}>
<div
className={css`
font-family: ${primaryFont};
font-size: ${typography.h4};
color: ${baseTheme.colors.gray[500]};
font-weight: bold;
margin-bottom: 1.5rem;
`}
>
{t("exercise-title")}
</div>
<div
className={css`
background-color: white;
border: 1px solid ${baseTheme.colors.clear[100]};
border-radius: 2px;
padding: 1rem 2rem;
margin-bottom: 1rem;
`}
>
<TextField
label={t("exercise-name")}
placeholder={t("exercise-name")}
value={attributes.name}
onChangeByValue={(value) => setAttributes({ name: value })}
className={css`
margin-bottom: 1rem !important;
`}
/>
<TextField
label={t("exercise-max-points")}
placeholder={t("exercise-max-points")}
value={attributes.score_maximum?.toString() ?? ""}
type="number"
onChangeByValue={(value) => {
const parsed = parseInt(value)
if (isNaN(parsed)) {
// empty
setAttributes({ score_maximum: undefined })
return
}
setAttributes({ score_maximum: parsed })
}}
<ExerciseBlockContext.Provider value={{ attributes, setAttributes }}>
<BreakFromCentered {...breakFromCenteredProps}>
<div
className={css`
background-color: ${baseTheme.colors.clear[100]};
`}
>
<Centered variant="narrow">
<ExerciseEditorCard id={attributes.id}>
<div
className={css`
margin-bottom: 1rem !important;
font-family: ${primaryFont};
font-size: ${typography.h4};
color: ${baseTheme.colors.gray[500]};
font-weight: bold;
margin-bottom: 1.5rem;
`}
>
{t("exercise-title")}
</div>

<InnerBlocks
allowedBlocks={ALLOWED_NESTED_BLOCKS}
template={INNER_BLOCKS_TEMPLATE}
templateLock="all"
/>

<div>
<Button variant="primary" size="medium" onClick={handleAddNewSlide}>
{t("add-slide")}
</Button>
</div>
<div
className={css`
display: flex;
flex-direction: column;
margin-bottom: 1rem;
${respondToOrLarger.md} {
align-items: center;
flex-direction: row;
}
margin-top: 1rem;
`}
>
<CheckBox
label={t("limit-number-of-tries")}
checked={attributes.limit_number_of_tries}
onChangeByValue={function (checked: boolean): void {
setAttributes({ limit_number_of_tries: checked })
}}
className={css`
flex: 1;
padding-top: 1.3rem;
`}
/>
<TextField
label={t("tries-per-slide")}
placeholder={t("tries-per-slide")}
value={attributes.max_tries_per_slide?.toString() ?? ""}
disabled={!attributes.limit_number_of_tries}
type="number"
onChangeByValue={(value) => {
const parsed = parseInt(value)
if (isNaN(parsed)) {
// empty
setAttributes({ max_tries_per_slide: undefined })
return
}
setAttributes({ max_tries_per_slide: parsed })
}}
className={css`
flex: 1;
`}
/>
</div>
{courseId && (
<PeerReviewEditor
attributes={attributes}
setAttributes={setAttributes}
exerciseId={attributes.id}
courseId={courseId}
courseGlobalEditor={false}
/>
)}
</div>
<div className={gutenbergControlsHidden}>
<InnerBlocks allowedBlocks={ALLOWED_NESTED_BLOCKS} />
</div>
<div>
<Button variant="primary" size="medium" onClick={handleAddNewSlide}>
{t("add-slide")}
</Button>
</div>
<div
className={css`
margin-top: 1rem;
`}
></div>
</ExerciseEditorCard>
</Centered>
</div>
</BreakFromCentered>
></div>
</ExerciseEditorCard>
</Centered>
</div>
</BreakFromCentered>
</ExerciseBlockContext.Provider>
)
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
import { css } from "@emotion/css"
import { InnerBlocks } from "@wordpress/block-editor"
import { useContext } from "react"
import { useTranslation } from "react-i18next"

import PeerReviewEditor from "../../../components/PeerReviewEditor"
import ExerciseBlockContext from "../../../contexts/ExerciseBlockContext"
import PageContext from "../../../contexts/PageContext"
import Accordion from "../../../shared-module/components/Accordion"
import CheckBox from "../../../shared-module/components/InputFields/CheckBox"
import TextField from "../../../shared-module/components/InputFields/TextField"
import { baseTheme } from "../../../shared-module/styles"
import { respondToOrLarger } from "../../../shared-module/styles/respond"

const ALLOWED_NESTED_BLOCKS = ["core/image", "core/paragraph", "core/list", "moocfi/latex"]

const ExerciseSettingsEditor = () => {
const { t } = useTranslation()
const courseId = useContext(PageContext)?.page.course_id
const { attributes, setAttributes } = useContext(ExerciseBlockContext)

if (!attributes) {
return null
}

return (
<div
className={css`
background-color: white;
border: 1px solid ${baseTheme.colors.clear[100]};
border-radius: 2px;
padding: 1rem 2rem;
margin-bottom: 1rem;
`}
>
<TextField
label={t("exercise-name")}
placeholder={t("exercise-name")}
value={attributes.name}
onChangeByValue={(value) => setAttributes({ name: value })}
className={css`
margin-bottom: 1rem !important;
`}
/>
<TextField
label={t("exercise-max-points")}
placeholder={t("exercise-max-points")}
value={attributes.score_maximum?.toString() ?? ""}
type="number"
onChangeByValue={(value) => {
const parsed = parseInt(value)
if (isNaN(parsed)) {
// empty
setAttributes({ score_maximum: undefined })
return
}
setAttributes({ score_maximum: parsed })
}}
className={css`
margin-bottom: 1rem !important;
`}
/>
<div
className={css`
display: flex;
flex-direction: column;
margin-bottom: 1rem;
${respondToOrLarger.md} {
align-items: center;
flex-direction: row;
}
`}
>
<CheckBox
label={t("limit-number-of-tries")}
checked={attributes.limit_number_of_tries}
onChangeByValue={function (checked: boolean): void {
setAttributes({ limit_number_of_tries: checked })
}}
className={css`
flex: 1;
padding-top: 1.3rem;
`}
/>
<TextField
label={t("tries-per-slide")}
placeholder={t("tries-per-slide")}
value={attributes.max_tries_per_slide?.toString() ?? ""}
disabled={!attributes.limit_number_of_tries}
type="number"
onChangeByValue={(value) => {
const parsed = parseInt(value)
if (isNaN(parsed)) {
// empty
setAttributes({ max_tries_per_slide: undefined })
return
}
setAttributes({ max_tries_per_slide: parsed })
}}
className={css`
flex: 1;
`}
/>
</div>
{courseId && (
<Accordion variant="detail">
<details>
<summary>{t("peer-and-self-review-configuration")}</summary>
<PeerReviewEditor
attributes={attributes}
setAttributes={setAttributes}
exerciseId={attributes.id}
courseId={courseId}
courseGlobalEditor={false}
instructionsEditor={
<div
className={css`
border: 1px solid ${baseTheme.colors.gray[100]};
padding: 1rem;
`}
>
<InnerBlocks allowedBlocks={ALLOWED_NESTED_BLOCKS} templateLock={false} />
</div>
}
/>
</details>
</Accordion>
)}
</div>
)
}

export default ExerciseSettingsEditor
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { InnerBlocks } from "@wordpress/block-editor"

const ExerciseSettingsSave: React.FC<unknown> = () => {
return (
<div>
<InnerBlocks.Content />
</div>
)
}

export default ExerciseSettingsSave
19 changes: 19 additions & 0 deletions services/cms/src/blocks/Exercise/ExerciseSettings/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/* eslint-disable i18next/no-literal-string */
import { BlockConfiguration } from "@wordpress/blocks"

import { MOOCFI_CATEGORY_SLUG } from "../../../utils/Gutenberg/modifyGutenbergCategories"

import ExerciseSettingsEditor from "./ExerciseSettingsEditor"
import ExerciseSettingsSave from "./ExerciseSettingsSave"

const ExerciseSettingsConfiguration: BlockConfiguration<Record<string, never>> = {
title: "ExerciseSettings",
description: "Wrapper block for exercise settings, required for the exercise block to work",
category: MOOCFI_CATEGORY_SLUG,
parent: ["moocfi/exercise"],
attributes: {},
edit: ExerciseSettingsEditor,
save: ExerciseSettingsSave,
}

export default ExerciseSettingsConfiguration
Loading

0 comments on commit ce7bf9e

Please sign in to comment.