diff --git a/.eslintrc.js b/.eslintrc.js index 28f06cbd61ab..b3dc967f1b04 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -166,6 +166,7 @@ module.exports = { "weight", "action", "tagName", + "templateLock", ], }, words: { diff --git a/services/cms/src/blocks/Exercise/ExerciseEditor.tsx b/services/cms/src/blocks/Exercise/ExerciseEditor.tsx index db82ef55c796..a53aa38ae401 100644 --- a/services/cms/src/blocks/Exercise/ExerciseEditor.tsx +++ b/services/cms/src/blocks/Exercise/ExerciseEditor.tsx @@ -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 "." @@ -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>> = ({ attributes, clientId, @@ -43,133 +43,49 @@ const ExerciseEditor: React.FC -
- - -
- {t("exercise-title")} -
-
- setAttributes({ name: value })} - className={css` - margin-bottom: 1rem !important; - `} - /> - { - const parsed = parseInt(value) - if (isNaN(parsed)) { - // empty - setAttributes({ score_maximum: undefined }) - return - } - setAttributes({ score_maximum: parsed }) - }} + + +
+ + +
+ {t("exercise-title")} +
+ + + +
+ +
- - { - 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; - `} - /> -
- {courseId && ( - - )} -
-
- -
-
- -
-
- - -
- + >
+ + + + + ) } diff --git a/services/cms/src/blocks/Exercise/ExerciseSettings/ExerciseSettingsEditor.tsx b/services/cms/src/blocks/Exercise/ExerciseSettings/ExerciseSettingsEditor.tsx new file mode 100644 index 000000000000..b35bd0c67e43 --- /dev/null +++ b/services/cms/src/blocks/Exercise/ExerciseSettings/ExerciseSettingsEditor.tsx @@ -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 ( +
+ setAttributes({ name: value })} + className={css` + margin-bottom: 1rem !important; + `} + /> + { + const parsed = parseInt(value) + if (isNaN(parsed)) { + // empty + setAttributes({ score_maximum: undefined }) + return + } + setAttributes({ score_maximum: parsed }) + }} + className={css` + margin-bottom: 1rem !important; + `} + /> +
+ + { + 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; + `} + /> +
+ {courseId && ( + +
+ {t("peer-and-self-review-configuration")} + + +
+ } + /> + + + )} + + ) +} + +export default ExerciseSettingsEditor diff --git a/services/cms/src/blocks/Exercise/ExerciseSettings/ExerciseSettingsSave.tsx b/services/cms/src/blocks/Exercise/ExerciseSettings/ExerciseSettingsSave.tsx new file mode 100644 index 000000000000..7108c76d9bed --- /dev/null +++ b/services/cms/src/blocks/Exercise/ExerciseSettings/ExerciseSettingsSave.tsx @@ -0,0 +1,11 @@ +import { InnerBlocks } from "@wordpress/block-editor" + +const ExerciseSettingsSave: React.FC = () => { + return ( +
+ +
+ ) +} + +export default ExerciseSettingsSave diff --git a/services/cms/src/blocks/Exercise/ExerciseSettings/index.tsx b/services/cms/src/blocks/Exercise/ExerciseSettings/index.tsx new file mode 100644 index 000000000000..afba2ff7507b --- /dev/null +++ b/services/cms/src/blocks/Exercise/ExerciseSettings/index.tsx @@ -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> = { + 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 diff --git a/services/cms/src/blocks/ExerciseSlide/ExerciseSlideEditor.tsx b/services/cms/src/blocks/Exercise/ExerciseSlide/ExerciseSlideEditor.tsx similarity index 70% rename from services/cms/src/blocks/ExerciseSlide/ExerciseSlideEditor.tsx rename to services/cms/src/blocks/Exercise/ExerciseSlide/ExerciseSlideEditor.tsx index 69fc4aad76e1..52ed0d36d81e 100644 --- a/services/cms/src/blocks/ExerciseSlide/ExerciseSlideEditor.tsx +++ b/services/cms/src/blocks/Exercise/ExerciseSlide/ExerciseSlideEditor.tsx @@ -1,16 +1,17 @@ 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 React, { useContext } from "react" import { useTranslation } from "react-i18next" -import { EditorContentDispatch } from "../../contexts/EditorContentContext" -import Button from "../../shared-module/components/Button" -import { primaryFont, typography } from "../../shared-module/styles" -import { gutenbergControlsHidden } from "../../styles/EditorStyles" +import { EditorContentDispatch } from "../../../contexts/EditorContentContext" +import Button from "../../../shared-module/components/Button" +import { primaryFont, typography } from "../../../shared-module/styles" +import { gutenbergControlsHidden } from "../../../styles/EditorStyles" const ALLOWED_NESTED_BLOCKS = ["moocfi/exercise-task"] +const INNER_BLOCKS_TEMPLATE: TemplateArray = [["moocfi/exercise-task", {}]] const ExerciseSlideEditorCard = styled.div` padding: 2rem 2rem; @@ -45,7 +46,11 @@ const ExerciseSlideEditor: React.FC< {t("slide-title", { number: attributes.order_number + 1 })}
- +
@@ -284,4 +309,4 @@ const PeerReviewViewImpl: React.FC> ) } -export default PeerReviewViewImpl +export default PeerOrSelfReviewViewImpl diff --git a/services/course-material/src/components/ContentRenderer/moocfi/ExerciseBlock/PeerReviewView/PeerReviewsReceivedComponent/Essay.tsx b/services/course-material/src/components/ContentRenderer/moocfi/ExerciseBlock/PeerOrSelfReviewView/PeerOrSelfReviewsReceivedComponent/Essay.tsx similarity index 100% rename from services/course-material/src/components/ContentRenderer/moocfi/ExerciseBlock/PeerReviewView/PeerReviewsReceivedComponent/Essay.tsx rename to services/course-material/src/components/ContentRenderer/moocfi/ExerciseBlock/PeerOrSelfReviewView/PeerOrSelfReviewsReceivedComponent/Essay.tsx diff --git a/services/course-material/src/components/ContentRenderer/moocfi/ExerciseBlock/PeerReviewView/PeerReviewsReceivedComponent/Likert.tsx b/services/course-material/src/components/ContentRenderer/moocfi/ExerciseBlock/PeerOrSelfReviewView/PeerOrSelfReviewsReceivedComponent/Likert.tsx similarity index 100% rename from services/course-material/src/components/ContentRenderer/moocfi/ExerciseBlock/PeerReviewView/PeerReviewsReceivedComponent/Likert.tsx rename to services/course-material/src/components/ContentRenderer/moocfi/ExerciseBlock/PeerOrSelfReviewView/PeerOrSelfReviewsReceivedComponent/Likert.tsx diff --git a/services/course-material/src/components/ContentRenderer/moocfi/ExerciseBlock/PeerReviewView/PeerReviewsReceivedComponent/LikertSvgs.tsx b/services/course-material/src/components/ContentRenderer/moocfi/ExerciseBlock/PeerOrSelfReviewView/PeerOrSelfReviewsReceivedComponent/LikertSvgs.tsx similarity index 100% rename from services/course-material/src/components/ContentRenderer/moocfi/ExerciseBlock/PeerReviewView/PeerReviewsReceivedComponent/LikertSvgs.tsx rename to services/course-material/src/components/ContentRenderer/moocfi/ExerciseBlock/PeerOrSelfReviewView/PeerOrSelfReviewsReceivedComponent/LikertSvgs.tsx diff --git a/services/course-material/src/components/ContentRenderer/moocfi/ExerciseBlock/PeerOrSelfReviewView/PeerOrSelfReviewsReceivedComponent/ReceivedPeerOrSelfReview.tsx b/services/course-material/src/components/ContentRenderer/moocfi/ExerciseBlock/PeerOrSelfReviewView/PeerOrSelfReviewsReceivedComponent/ReceivedPeerOrSelfReview.tsx new file mode 100644 index 000000000000..663a137639f0 --- /dev/null +++ b/services/course-material/src/components/ContentRenderer/moocfi/ExerciseBlock/PeerOrSelfReviewView/PeerOrSelfReviewsReceivedComponent/ReceivedPeerOrSelfReview.tsx @@ -0,0 +1,85 @@ +import styled from "@emotion/styled" +import * as React from "react" +import { useMemo } from "react" +import { useTranslation } from "react-i18next" + +import { + PeerOrSelfReviewQuestion, + PeerOrSelfReviewQuestionSubmission, +} from "../../../../../../shared-module/bindings" + +import Essay from "./Essay" +import Likert from "./Likert" +interface ReviewProps { + orderNumber: number + reviews: PeerOrSelfReviewQuestionSubmission[] + questions: PeerOrSelfReviewQuestion[] + selfReview: boolean +} + +const Wrapper = styled.div` + background: #f5f6f7; + padding: 0 !important; + + &:not(:last-child) { + margin-bottom: 10px; + } +` +const Heading = styled.div` + padding: 1rem; + border-bottom: 2px solid #ebedee; +` + +const PeerOrSelfReviewsReceived: React.FunctionComponent = ({ + orderNumber, + reviews, + questions, + selfReview, +}) => { + const { t } = useTranslation() + + const sortedReviews = useMemo( + () => + reviews.sort((o1, o2) => { + const o1Question = questions.find((q) => q.id === o1.peer_or_self_review_question_id) + const o2Question = questions.find((q) => q.id === o2.peer_or_self_review_question_id) + if (!o1Question) { + return 1 + } + if (!o2Question) { + return -1 + } + return o1Question.order_number - o2Question.order_number + }), + [questions, reviews], + ) + + return ( + + + {selfReview ? t("title-self-review") : `${t("peer-review")} #${orderNumber + 1}`} + + {sortedReviews.map( + ({ id, number_data, text_data, peer_or_self_review_question_id }, index) => { + const questionIndex = questions.findIndex((q) => q.id === peer_or_self_review_question_id) + if (questionIndex === -1) { + return null + } + const question = questions[questionIndex].question + return ( + <> + {text_data && ( + + )} + {number_data !== null && ( + + )} + + ) + }, + )} + + ) +} + +export default PeerOrSelfReviewsReceived diff --git a/services/course-material/src/components/ContentRenderer/moocfi/ExerciseBlock/PeerReviewView/PeerReviewsReceivedComponent/index.tsx b/services/course-material/src/components/ContentRenderer/moocfi/ExerciseBlock/PeerOrSelfReviewView/PeerOrSelfReviewsReceivedComponent/index.tsx similarity index 52% rename from services/course-material/src/components/ContentRenderer/moocfi/ExerciseBlock/PeerReviewView/PeerReviewsReceivedComponent/index.tsx rename to services/course-material/src/components/ContentRenderer/moocfi/ExerciseBlock/PeerOrSelfReviewView/PeerOrSelfReviewsReceivedComponent/index.tsx index ac2c20827804..f66040f96779 100644 --- a/services/course-material/src/components/ContentRenderer/moocfi/ExerciseBlock/PeerReviewView/PeerReviewsReceivedComponent/index.tsx +++ b/services/course-material/src/components/ContentRenderer/moocfi/ExerciseBlock/PeerOrSelfReviewView/PeerOrSelfReviewsReceivedComponent/index.tsx @@ -10,9 +10,10 @@ import { useTranslation } from "react-i18next" import { fetchPeerReviewDataReceivedByExerciseId } from "../../../../../../services/backend" import ErrorBanner from "../../../../../../shared-module/components/ErrorBanner" import Spinner from "../../../../../../shared-module/components/Spinner" +import useUserInfo from "../../../../../../shared-module/hooks/useUserInfo" import { baseTheme, headingFont } from "../../../../../../shared-module/styles" -import ReceivedPeerReview from "./ReceivedPeerReview" +import ReceivedPeerOrSelfReview from "./ReceivedPeerOrSelfReview" const openAnimation = keyframes` 0% { opacity: 0; } @@ -105,25 +106,30 @@ interface PeerReviewProps { submissionId: string } -const PeerReviewsReceived: React.FunctionComponent = ({ id, submissionId }) => { +const PeerOrSelfReviewsReceived: React.FunctionComponent = ({ + id, + submissionId, +}) => { const { t } = useTranslation() + const userInfo = useUserInfo() - const getPeerReviewReceived = useQuery({ + const peerOrSelfReviewsReceivedQuery = useQuery({ queryKey: [`exercise-${id}-exercise-slide-submission-${submissionId}-peer-reviews-received`], queryFn: () => fetchPeerReviewDataReceivedByExerciseId(id, submissionId), }) const data = useMemo(() => { - const ordered = getPeerReviewReceived.data?.peer_review_question_submissions.sort( - (a, b) => parseISO(b.created_at).getTime() - parseISO(a.created_at).getTime(), - ) + const ordered = + peerOrSelfReviewsReceivedQuery.data?.peer_or_self_review_question_submissions.sort( + (a, b) => parseISO(b.created_at).getTime() - parseISO(a.created_at).getTime(), + ) - const groupByPeerReviewSubmissionId = groupBy( + const groupByPeerOrSelfReviewSubmissionId = groupBy( ordered, - (review) => review.peer_review_submission_id, + (review) => review.peer_or_self_review_submission_id, ) - let res = Object.values(groupByPeerReviewSubmissionId) + let res = Object.values(groupByPeerOrSelfReviewSubmissionId) res = res.sort((a, b) => { if (a.length === 0) { return 1 @@ -133,35 +139,78 @@ const PeerReviewsReceived: React.FunctionComponent = ({ id, sub } return parseISO(b[0].created_at).getTime() - parseISO(a[0].created_at).getTime() }) - return res - }, [getPeerReviewReceived.data?.peer_review_question_submissions]) - if (getPeerReviewReceived.isPending) { + // group by whether the review is self review or peer review + const res2 = groupBy(res, (questionSubmisssions) => { + if (questionSubmisssions.length === 0) { + // eslint-disable-next-line i18next/no-literal-string + return "peer" + } + const peerOrSelfReviewSubmission = + peerOrSelfReviewsReceivedQuery.data?.peer_or_self_review_submissions.find( + (pr) => pr.id === questionSubmisssions[0].peer_or_self_review_submission_id, + ) + if ( + peerOrSelfReviewSubmission && + peerOrSelfReviewSubmission.user_id === userInfo.data?.user_id + ) { + // eslint-disable-next-line i18next/no-literal-string + return "self" + } + // eslint-disable-next-line i18next/no-literal-string + return "peer" + }) + return res2 + }, [ + peerOrSelfReviewsReceivedQuery.data?.peer_or_self_review_question_submissions, + peerOrSelfReviewsReceivedQuery.data?.peer_or_self_review_submissions, + userInfo.data?.user_id, + ]) + + if (peerOrSelfReviewsReceivedQuery.isPending) { return } - if (getPeerReviewReceived.isError) { - return + if (peerOrSelfReviewsReceivedQuery.isError) { + return } + const numReceivedReviews = (data["peer"]?.length ?? 0) + (data["self"]?.length ?? 0) + return (
- {t("peer-reviews-received-from-other-students")} - {data.length ?? "0"} + {t("received-reviews")} + {numReceivedReviews} - {data.map((item, index) => ( - - ))} + + {(data["self"] ?? []).map((items, index) => { + return ( + + ) + })} + + {(data["peer"] ?? []).map((items, index) => { + return ( + + ) + })}
) } -export default PeerReviewsReceived +export default PeerOrSelfReviewsReceived diff --git a/services/course-material/src/components/ContentRenderer/moocfi/ExerciseBlock/PeerReviewView/WaitingForPeerReviews.tsx b/services/course-material/src/components/ContentRenderer/moocfi/ExerciseBlock/PeerOrSelfReviewView/WaitingForPeerReviews.tsx similarity index 100% rename from services/course-material/src/components/ContentRenderer/moocfi/ExerciseBlock/PeerReviewView/WaitingForPeerReviews.tsx rename to services/course-material/src/components/ContentRenderer/moocfi/ExerciseBlock/PeerOrSelfReviewView/WaitingForPeerReviews.tsx diff --git a/services/course-material/src/components/ContentRenderer/moocfi/ExerciseBlock/PeerReviewView/index.tsx b/services/course-material/src/components/ContentRenderer/moocfi/ExerciseBlock/PeerOrSelfReviewView/index.tsx similarity index 68% rename from services/course-material/src/components/ContentRenderer/moocfi/ExerciseBlock/PeerReviewView/index.tsx rename to services/course-material/src/components/ContentRenderer/moocfi/ExerciseBlock/PeerOrSelfReviewView/index.tsx index 97b9e1786db3..ca193f496ea5 100644 --- a/services/course-material/src/components/ContentRenderer/moocfi/ExerciseBlock/PeerReviewView/index.tsx +++ b/services/course-material/src/components/ContentRenderer/moocfi/ExerciseBlock/PeerOrSelfReviewView/index.tsx @@ -5,19 +5,22 @@ import { useTranslation } from "react-i18next" import { baseTheme } from "../../../../../shared-module/styles" -import PeerReviewViewImpl from "./PeerReviewViewImpl" +import PeerOrSelfReviewViewImpl from "./PeerOrSelfReviewViewImpl" -export interface PeerReviewViewProps { +export interface PeerOrSelfReviewViewProps { exerciseNumber: number exerciseId: string parentExerciseQuery: UseQueryResult + selfReview?: boolean } export const getPeerReviewBeginningScrollingId = (exerciseId: string) => // eslint-disable-next-line i18next/no-literal-string `start-of-peer-review-${exerciseId}` -const PeerReviewView: React.FC> = (props) => { +const PeerOrSelfReviewView: React.FC> = ( + props, +) => { const { t } = useTranslation() return ( @@ -33,11 +36,11 @@ const PeerReviewView: React.FC> = ( color: ${baseTheme.colors.gray[700]}; `} > - {t("title-peer-review")} + {props.selfReview ? t("title-self-review") : t("title-peer-review")} - +
) } -export default PeerReviewView +export default PeerOrSelfReviewView diff --git a/services/course-material/src/components/ContentRenderer/moocfi/ExerciseBlock/PeerReviewView/PeerReviewQuestion/ScalePeerReviewQuestion.tsx b/services/course-material/src/components/ContentRenderer/moocfi/ExerciseBlock/PeerReviewView/PeerReviewQuestion/ScalePeerReviewQuestion.tsx deleted file mode 100644 index a895966d457e..000000000000 --- a/services/course-material/src/components/ContentRenderer/moocfi/ExerciseBlock/PeerReviewView/PeerReviewQuestion/ScalePeerReviewQuestion.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import LikertScale from "../../../../../../shared-module/components/PeerReview/LikertScale" - -import { PeerReviewQuestionProps } from "." - -const ScalePeerReviewQuestion: React.FC> = ({ - question, - setPeerReviewQuestionAnswer, - peerReviewQuestionAnswer, -}) => { - return ( -
- - setPeerReviewQuestionAnswer({ - text_data: null, - number_data: value, - }) - } - /> -
- ) -} - -export default ScalePeerReviewQuestion diff --git a/services/course-material/src/components/ContentRenderer/moocfi/ExerciseBlock/PeerReviewView/PeerReviewQuestion/index.tsx b/services/course-material/src/components/ContentRenderer/moocfi/ExerciseBlock/PeerReviewView/PeerReviewQuestion/index.tsx deleted file mode 100644 index 88f93dec02e8..000000000000 --- a/services/course-material/src/components/ContentRenderer/moocfi/ExerciseBlock/PeerReviewView/PeerReviewQuestion/index.tsx +++ /dev/null @@ -1,43 +0,0 @@ -import { - CourseMaterialPeerReviewQuestionAnswer, - PeerReviewQuestion as PeerReviewQuestionType, -} from "../../../../../../shared-module/bindings" - -import EssayPeerReviewQuestion from "./EssayPeerReviewQuestion" -import ScalePeerReviewQuestion from "./ScalePeerReviewQuestion" - -export interface PeerReviewQuestionProps { - question: PeerReviewQuestionType - setPeerReviewQuestionAnswer: ( - answer: Omit, - ) => void - peerReviewQuestionAnswer: CourseMaterialPeerReviewQuestionAnswer | null -} - -const PeerReviewQuestion: React.FC> = ({ - question, - setPeerReviewQuestionAnswer, - peerReviewQuestionAnswer, -}) => { - if (question.question_type === "Scale") { - return ( - - ) - } - if (question.question_type === "Essay") { - return ( - - ) - } - return null -} - -export default PeerReviewQuestion diff --git a/services/course-material/src/components/ContentRenderer/moocfi/ExerciseBlock/PeerReviewView/PeerReviewsReceivedComponent/ReceivedPeerReview.tsx b/services/course-material/src/components/ContentRenderer/moocfi/ExerciseBlock/PeerReviewView/PeerReviewsReceivedComponent/ReceivedPeerReview.tsx deleted file mode 100644 index 3c9916a7effa..000000000000 --- a/services/course-material/src/components/ContentRenderer/moocfi/ExerciseBlock/PeerReviewView/PeerReviewsReceivedComponent/ReceivedPeerReview.tsx +++ /dev/null @@ -1,77 +0,0 @@ -import styled from "@emotion/styled" -import * as React from "react" -import { useMemo } from "react" -import { useTranslation } from "react-i18next" - -import { - PeerReviewQuestion, - PeerReviewQuestionSubmission, -} from "../../../../../../shared-module/bindings" - -import Essay from "./Essay" -import Likert from "./Likert" -interface ReviewProps { - orderNumber: number - review: PeerReviewQuestionSubmission[] - questions: PeerReviewQuestion[] -} - -const Wrapper = styled.div` - background: #f5f6f7; - padding: 0 !important; - - &:not(:last-child) { - margin-bottom: 10px; - } -` -const Heading = styled.div` - padding: 1rem; - border-bottom: 2px solid #ebedee; -` - -const ReceivedPeerReview: React.FunctionComponent = ({ - orderNumber, - review, - questions, -}) => { - const { t } = useTranslation() - - const sortedReview = useMemo( - () => - review.sort((o1, o2) => { - const o1Question = questions.find((q) => q.id === o1.peer_review_question_id) - const o2Question = questions.find((q) => q.id === o2.peer_review_question_id) - if (!o1Question) { - return 1 - } - if (!o2Question) { - return -1 - } - return o1Question.order_number - o2Question.order_number - }), - [questions, review], - ) - - return ( - - {`${t("peer-review")} #${orderNumber + 1}`} - {sortedReview.map(({ id, number_data, text_data, peer_review_question_id }, index) => { - const questionIndex = questions.findIndex((q) => q.id === peer_review_question_id) - if (questionIndex === -1) { - return null - } - const question = questions[questionIndex].question - return ( - <> - {text_data && } - {number_data !== null && ( - - )} - - ) - })} - - ) -} - -export default ReceivedPeerReview diff --git a/services/course-material/src/components/ContentRenderer/moocfi/ExerciseBlock/index.tsx b/services/course-material/src/components/ContentRenderer/moocfi/ExerciseBlock/index.tsx index a2d1326da486..af5ce7a82fa3 100644 --- a/services/course-material/src/components/ContentRenderer/moocfi/ExerciseBlock/index.tsx +++ b/services/course-material/src/components/ContentRenderer/moocfi/ExerciseBlock/index.tsx @@ -13,7 +13,7 @@ import useCourseMaterialExerciseQuery, { courseMaterialExerciseQueryKey, } from "../../../../hooks/useCourseMaterialExerciseQuery" import exerciseBlockPostThisStateToIFrameReducer from "../../../../reducers/exerciseBlockPostThisStateToIFrameReducer" -import { postStartPeerReview, postSubmission } from "../../../../services/backend" +import { postStartPeerOrSelfReview, postSubmission } from "../../../../services/backend" import { CourseMaterialExercise, StudentExerciseSlideSubmission, @@ -33,9 +33,11 @@ import YellowBox from "../../../YellowBox" import ExerciseTask from "./ExerciseTask" import GradingState from "./GradingState" -import PeerReviewView from "./PeerReviewView" -import PeerReviewsReceived from "./PeerReviewView/PeerReviewsReceivedComponent/index" -import WaitingForPeerReviews from "./PeerReviewView/WaitingForPeerReviews" +import PeerOrSelfReviewView from "./PeerOrSelfReviewView" +import PeerOrSelfReviewsReceived from "./PeerOrSelfReviewView/PeerOrSelfReviewsReceivedComponent/index" +import WaitingForPeerReviews from "./PeerOrSelfReviewView/WaitingForPeerReviews" + +const FORWARD_SLASH = "/" interface ExerciseBlockAttributes { id: string @@ -236,8 +238,8 @@ const ExerciseBlock: React.FC< getCourseMaterialExercise.data?.exercise.deadline, ) - const startPeerReviewMutation = useToastMutation( - () => postStartPeerReview(id), + const startPeerOrSelfReviewMutation = useToastMutation( + () => postStartPeerOrSelfReview(id), { notify: false }, { onSuccess: async () => { @@ -312,6 +314,7 @@ const ExerciseBlock: React.FC< const inSubmissionView = postThisStateToIFrame?.every((x) => x.view_type === "view-submission") ?? false const needsPeerReview = getCourseMaterialExercise.data.exercise.needs_peer_review + const needsSelfReview = getCourseMaterialExercise.data.exercise.needs_self_review const reviewingStage = getCourseMaterialExercise.data.exercise_status?.reviewing_stage const gradingState = getCourseMaterialExercise.data.exercise_status?.grading_progress @@ -463,9 +466,10 @@ const ExerciseBlock: React.FC< {t("points-label")}
- + {/* eslint-disable-next-line i18next/no-literal-string */} - {points ?? 0}⁄ + {points ?? 0} + {FORWARD_SLASH} {getCourseMaterialExercise.data.exercise.score_maximum}
@@ -525,14 +529,16 @@ const ExerciseBlock: React.FC< {t("Deadline-passed-n-days-ago", { days: dateDiffInDays(exerciseDeadline) })} ))} - - {getCourseMaterialExercise.data.peer_review_config && gradingState && reviewingStage && ( - - )} + {getCourseMaterialExercise.data.peer_or_self_review_config && + gradingState && + reviewingStage && ( + + )} {/* Reviewing stage seems to be undefined at least for exams */} {reviewingStage !== "PeerReview" && reviewingStage !== "SelfReview" && @@ -558,10 +564,18 @@ const ExerciseBlock: React.FC< /> ))} {reviewingStage === "PeerReview" && ( - + )} + {reviewingStage === "SelfReview" && ( + )} {(reviewingStage === "WaitingForPeerReviews" || @@ -581,7 +595,7 @@ const ExerciseBlock: React.FC< exerciseSlideSubmissionId && (reviewingStage === "WaitingForPeerReviews" || reviewingStage === "ReviewedAndLocked") && ( - + )} )} @@ -708,12 +722,21 @@ const ExerciseBlock: React.FC< {needsPeerReview && ( )} + {!needsPeerReview && needsSelfReview && ( + + )} )} diff --git a/services/course-material/src/pages/[organizationSlug]/exams/[id].tsx b/services/course-material/src/pages/[organizationSlug]/exams/[id].tsx index 553f6f454c6a..4327e1fdde78 100644 --- a/services/course-material/src/pages/[organizationSlug]/exams/[id].tsx +++ b/services/course-material/src/pages/[organizationSlug]/exams/[id].tsx @@ -36,7 +36,7 @@ const Exam: React.FC> = ({ query }) => { const examId = query.id const [pageState, pageStateDispatch] = useReducer( pageStateReducer, - // We don't pass a refetch function here on purpose because refetching during an exam is risky because we don't want to accidentally lose unsubitted answers + // We don't pass a refetch function here on purpose because refetching during an exam is risky because we don't want to accidentally lose unsubmitted answers getDefaultPageState(undefined), ) const now = useTime(5000) diff --git a/services/course-material/src/services/backend.ts b/services/course-material/src/services/backend.ts index b15d1f12cbe1..67c9e5ab7121 100644 --- a/services/course-material/src/services/backend.ts +++ b/services/course-material/src/services/backend.ts @@ -7,8 +7,8 @@ import { CourseBackgroundQuestionsAndAnswers, CourseInstance, CourseMaterialExercise, - CourseMaterialPeerReviewDataWithToken, - CourseMaterialPeerReviewSubmission, + CourseMaterialPeerOrSelfReviewDataWithToken, + CourseMaterialPeerOrSelfReviewSubmission, CourseModuleCompletion, CoursePageWithUserData, CustomViewExerciseSubmissions, @@ -27,7 +27,7 @@ import { PageNavigationInformation, PageSearchResult, PageWithExercises, - PeerReviewsRecieved, + PeerOrSelfReviewsReceived, ResearchForm, ResearchFormQuestion, ResearchFormQuestionAnswer, @@ -50,7 +50,7 @@ import { isCourseBackgroundQuestionsAndAnswers, isCourseInstance, isCourseMaterialExercise, - isCourseMaterialPeerReviewDataWithToken, + isCourseMaterialPeerOrSelfReviewDataWithToken, isCourseModuleCompletion, isCoursePageWithUserData, isCustomViewExerciseSubmissions, @@ -64,7 +64,7 @@ import { isPageNavigationInformation, isPageSearchResult, isPageWithExercises, - isPeerReviewsRecieved, + isPeerOrSelfReviewsReceived, isResearchForm, isResearchFormQuestion, isResearchFormQuestionAnswer, @@ -261,26 +261,26 @@ export const fetchExerciseById = async (id: string): Promise => { +): Promise => { const response = await courseMaterialClient.get(`/exercises/${id}/peer-review`, { responseType: "json", }) - return validateResponse(response, isCourseMaterialPeerReviewDataWithToken) + return validateResponse(response, isCourseMaterialPeerOrSelfReviewDataWithToken) } export const fetchPeerReviewDataReceivedByExerciseId = async ( id: string, submissionId: string, -): Promise => { +): Promise => { const response = await courseMaterialClient.get( - `/exercises/${id}/exercise-slide-submission/${submissionId}/peer-reviews-received`, + `/exercises/${id}/exercise-slide-submission/${submissionId}/peer-or-self-reviews-received`, { responseType: "json", }, ) - return validateResponse(response, isPeerReviewsRecieved) + return validateResponse(response, isPeerOrSelfReviewsReceived) } export const fetchChaptersPagesWithExercises = async ( @@ -382,17 +382,21 @@ export const postProposedEdits = async ( await courseMaterialClient.post(`/proposed-edits/${courseId}`, newProposedEdits) } -export const postPeerReviewSubmission = async ( +export const postPeerOrSelfReviewSubmission = async ( exerciseId: string, - peerReviewSubmission: CourseMaterialPeerReviewSubmission, + peerOrSelfReviewSubmission: CourseMaterialPeerOrSelfReviewSubmission, ): Promise => { - await courseMaterialClient.post(`/exercises/${exerciseId}/peer-reviews`, peerReviewSubmission, { - responseType: "json", - }) + await courseMaterialClient.post( + `/exercises/${exerciseId}/peer-or-self-reviews`, + peerOrSelfReviewSubmission, + { + responseType: "json", + }, + ) } -export const postStartPeerReview = async (exerciseId: string): Promise => { - await courseMaterialClient.post(`/exercises/${exerciseId}/peer-reviews/start`) +export const postStartPeerOrSelfReview = async (exerciseId: string): Promise => { + await courseMaterialClient.post(`/exercises/${exerciseId}/peer-or-self-reviews/start`) } export const fetchExamEnrollment = async (examId: string): Promise => { diff --git a/services/headless-lms/migrations/20240219083701_add_self_review.down.sql b/services/headless-lms/migrations/20240219083701_add_self_review.down.sql new file mode 100644 index 000000000000..7aef7c7c9706 --- /dev/null +++ b/services/headless-lms/migrations/20240219083701_add_self_review.down.sql @@ -0,0 +1,23 @@ +-- Undo renaming columns +ALTER TABLE peer_or_self_review_question_submissions + RENAME COLUMN peer_or_self_review_submission_id TO peer_review_submission_id; +ALTER TABLE peer_or_self_review_question_submissions + RENAME COLUMN peer_or_self_review_question_id TO peer_review_question_id; +ALTER TABLE peer_or_self_review_questions + RENAME COLUMN peer_or_self_review_config_id TO peer_review_config_id; +ALTER TABLE exercises + RENAME COLUMN use_course_default_peer_or_self_review_config TO use_course_default_peer_review_config; +ALTER TABLE peer_or_self_review_submissions + RENAME COLUMN peer_or_self_review_config_id TO peer_review_config_id; +-- Undo renaming tables +ALTER TABLE peer_or_self_review_questions + RENAME TO peer_review_questions; +ALTER TABLE peer_or_self_review_submissions + RENAME TO peer_review_submissions; +ALTER TABLE peer_or_self_review_question_submissions + RENAME TO peer_review_question_submissions; +ALTER TABLE peer_or_self_review_configs + RENAME TO peer_review_configs; +-- Drop added columns +ALTER TABLE peer_review_configs DROP COLUMN review_instructions; +ALTER TABLE exercises DROP COLUMN needs_self_review; diff --git a/services/headless-lms/migrations/20240219083701_add_self_review.up.sql b/services/headless-lms/migrations/20240219083701_add_self_review.up.sql new file mode 100644 index 000000000000..08b9afc0cf20 --- /dev/null +++ b/services/headless-lms/migrations/20240219083701_add_self_review.up.sql @@ -0,0 +1,24 @@ +ALTER TABLE exercises +ADD COLUMN needs_self_review BOOLEAN NOT NULL DEFAULT FALSE; +COMMENT ON COLUMN exercises.needs_self_review IS 'If true, students are required to review their own submissions before getting any points.'; +ALTER TABLE peer_review_configs +ADD COLUMN review_instructions JSONB; +COMMENT ON COLUMN peer_review_configs.review_instructions IS 'Content of additional instructions shown when self of peer review starts. The content is in an abstract format, the same as pages.content.'; +ALTER TABLE peer_review_configs + RENAME TO peer_or_self_review_configs; +ALTER TABLE peer_review_question_submissions + RENAME TO peer_or_self_review_question_submissions; +ALTER TABLE peer_review_submissions + RENAME TO peer_or_self_review_submissions; +ALTER TABLE peer_review_questions + RENAME TO peer_or_self_review_questions; +ALTER TABLE exercises + RENAME COLUMN use_course_default_peer_review_config TO use_course_default_peer_or_self_review_config; +ALTER TABLE peer_or_self_review_questions + RENAME COLUMN peer_review_config_id TO peer_or_self_review_config_id; +ALTER TABLE peer_or_self_review_question_submissions + RENAME COLUMN peer_review_question_id TO peer_or_self_review_question_id; +ALTER TABLE peer_or_self_review_submissions + RENAME COLUMN peer_review_config_id TO peer_or_self_review_config_id; +ALTER TABLE peer_or_self_review_question_submissions + RENAME COLUMN peer_review_submission_id TO peer_or_self_review_submission_id; diff --git a/services/headless-lms/models/.sqlx/query-02338a196bb57a6142b16723681fba35a8369b3487ed2464480540471c0eb9b5.json b/services/headless-lms/models/.sqlx/query-02338a196bb57a6142b16723681fba35a8369b3487ed2464480540471c0eb9b5.json index 2531ed584a81..d2b12f641848 100644 --- a/services/headless-lms/models/.sqlx/query-02338a196bb57a6142b16723681fba35a8369b3487ed2464480540471c0eb9b5.json +++ b/services/headless-lms/models/.sqlx/query-02338a196bb57a6142b16723681fba35a8369b3487ed2464480540471c0eb9b5.json @@ -85,13 +85,18 @@ }, { "ordinal": 16, - "name": "use_course_default_peer_review_config", + "name": "use_course_default_peer_or_self_review_config", "type_info": "Bool" }, { "ordinal": 17, "name": "exercise_language_group_id", "type_info": "Uuid" + }, + { + "ordinal": 18, + "name": "needs_self_review", + "type_info": "Bool" } ], "parameters": { @@ -115,7 +120,8 @@ false, false, false, - true + true, + false ] }, "hash": "02338a196bb57a6142b16723681fba35a8369b3487ed2464480540471c0eb9b5" diff --git a/services/headless-lms/models/.sqlx/query-2287acace1485c0a48de22f8d85eb2ea26349ab882eb27b0bba65c2ae4b7a039.json b/services/headless-lms/models/.sqlx/query-0534c60f0cbbceb4edee56e89900433aca9afe9236eedc6bfff6843cb45e1ac0.json similarity index 70% rename from services/headless-lms/models/.sqlx/query-2287acace1485c0a48de22f8d85eb2ea26349ab882eb27b0bba65c2ae4b7a039.json rename to services/headless-lms/models/.sqlx/query-0534c60f0cbbceb4edee56e89900433aca9afe9236eedc6bfff6843cb45e1ac0.json index 251ef48af106..b5541a0e8999 100644 --- a/services/headless-lms/models/.sqlx/query-2287acace1485c0a48de22f8d85eb2ea26349ab882eb27b0bba65c2ae4b7a039.json +++ b/services/headless-lms/models/.sqlx/query-0534c60f0cbbceb4edee56e89900433aca9afe9236eedc6bfff6843cb45e1ac0.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\nINSERT INTO exercises(\n id,\n course_id,\n name,\n order_number,\n page_id,\n chapter_id,\n exam_id,\n score_maximum,\n max_tries_per_slide,\n limit_number_of_tries,\n deadline,\n needs_peer_review,\n use_course_default_peer_review_config,\n exercise_language_group_id\n )\nVALUES (\n $1,\n $2,\n $3,\n $4,\n $5,\n $6,\n $7,\n $8,\n $9,\n $10,\n $11,\n $12,\n $13,\n $14\n ) ON CONFLICT (id) DO\nUPDATE\nSET course_id = $2,\n name = $3,\n order_number = $4,\n page_id = $5,\n chapter_id = $6,\n exam_id = $7,\n score_maximum = $8,\n max_tries_per_slide = $9,\n limit_number_of_tries = $10,\n deadline = $11,\n needs_peer_review = $12,\n use_course_default_peer_review_config = $13,\n exercise_language_group_id = $14,\n deleted_at = NULL\nRETURNING *;\n ", + "query": "\nINSERT INTO exercises(\n id,\n course_id,\n name,\n order_number,\n page_id,\n chapter_id,\n exam_id,\n score_maximum,\n max_tries_per_slide,\n limit_number_of_tries,\n deadline,\n needs_peer_review,\n needs_self_review,\n use_course_default_peer_or_self_review_config,\n exercise_language_group_id\n )\nVALUES (\n $1,\n $2,\n $3,\n $4,\n $5,\n $6,\n $7,\n $8,\n $9,\n $10,\n $11,\n $12,\n $13,\n $14,\n $15\n ) ON CONFLICT (id) DO\nUPDATE\nSET course_id = $2,\n name = $3,\n order_number = $4,\n page_id = $5,\n chapter_id = $6,\n exam_id = $7,\n score_maximum = $8,\n max_tries_per_slide = $9,\n limit_number_of_tries = $10,\n deadline = $11,\n needs_peer_review = $12,\n needs_self_review = $13,\n use_course_default_peer_or_self_review_config = $14,\n exercise_language_group_id = $15,\n deleted_at = NULL\nRETURNING *;\n ", "describe": { "columns": [ { @@ -85,13 +85,18 @@ }, { "ordinal": 16, - "name": "use_course_default_peer_review_config", + "name": "use_course_default_peer_or_self_review_config", "type_info": "Bool" }, { "ordinal": 17, "name": "exercise_language_group_id", "type_info": "Uuid" + }, + { + "ordinal": 18, + "name": "needs_self_review", + "type_info": "Bool" } ], "parameters": { @@ -109,6 +114,7 @@ "Timestamptz", "Bool", "Bool", + "Bool", "Uuid" ] }, @@ -130,8 +136,9 @@ false, false, false, - true + true, + false ] }, - "hash": "2287acace1485c0a48de22f8d85eb2ea26349ab882eb27b0bba65c2ae4b7a039" + "hash": "0534c60f0cbbceb4edee56e89900433aca9afe9236eedc6bfff6843cb45e1ac0" } diff --git a/services/headless-lms/models/.sqlx/query-001fda3c6f4ce795ee4dab0d4081e96c9e44db1f2ab67248c7f012aa3e96e0e1.json b/services/headless-lms/models/.sqlx/query-080280dfc4de35b435239a601fa041dcbbf736488a03456614dd110dba22004d.json similarity index 55% rename from services/headless-lms/models/.sqlx/query-001fda3c6f4ce795ee4dab0d4081e96c9e44db1f2ab67248c7f012aa3e96e0e1.json rename to services/headless-lms/models/.sqlx/query-080280dfc4de35b435239a601fa041dcbbf736488a03456614dd110dba22004d.json index a86860254576..b61a7b0973d5 100644 --- a/services/headless-lms/models/.sqlx/query-001fda3c6f4ce795ee4dab0d4081e96c9e44db1f2ab67248c7f012aa3e96e0e1.json +++ b/services/headless-lms/models/.sqlx/query-080280dfc4de35b435239a601fa041dcbbf736488a03456614dd110dba22004d.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\n SELECT qs.id,\n qs.created_at,\n qs.updated_at,\n qs.deleted_at,\n qs.peer_review_question_id,\n qs.peer_review_submission_id,\n qs.text_data,\n qs.number_data\n FROM peer_review_question_submissions qs\n JOIN peer_review_submissions s ON (qs.peer_review_submission_id = s.id)\n JOIN exercise_slide_submissions es ON (s.exercise_slide_submission_id = es.id)\n WHERE peer_review_question_id IN (\n SELECT UNNEST($1::uuid [])\n )\n AND s.exercise_slide_submission_id = $3\n AND es.user_id = $2\n AND qs.deleted_at IS NULL;\n ", + "query": "\n SELECT qs.id,\n qs.created_at,\n qs.updated_at,\n qs.deleted_at,\n qs.peer_or_self_review_question_id,\n qs.peer_or_self_review_submission_id,\n qs.text_data,\n qs.number_data\n FROM peer_or_self_review_question_submissions qs\n JOIN peer_or_self_review_submissions s ON (qs.peer_or_self_review_submission_id = s.id)\n JOIN exercise_slide_submissions es ON (s.exercise_slide_submission_id = es.id)\n WHERE peer_or_self_review_question_id IN (\n SELECT UNNEST($1::uuid [])\n )\n AND s.exercise_slide_submission_id = $3\n AND es.user_id = $2\n AND qs.deleted_at IS NULL;\n ", "describe": { "columns": [ { @@ -25,12 +25,12 @@ }, { "ordinal": 4, - "name": "peer_review_question_id", + "name": "peer_or_self_review_question_id", "type_info": "Uuid" }, { "ordinal": 5, - "name": "peer_review_submission_id", + "name": "peer_or_self_review_submission_id", "type_info": "Uuid" }, { @@ -49,5 +49,5 @@ }, "nullable": [false, false, false, true, false, false, true, true] }, - "hash": "001fda3c6f4ce795ee4dab0d4081e96c9e44db1f2ab67248c7f012aa3e96e0e1" + "hash": "080280dfc4de35b435239a601fa041dcbbf736488a03456614dd110dba22004d" } diff --git a/services/headless-lms/models/.sqlx/query-f5f6f6ee7f41b785b8b8b4bdd59d6d82338f119d2099cdb332bc2c89a7778721.json b/services/headless-lms/models/.sqlx/query-0c5ecaba29bb357054511e90eaaee3f3cd36b02d73fe30af1baf7f253a02595d.json similarity index 50% rename from services/headless-lms/models/.sqlx/query-f5f6f6ee7f41b785b8b8b4bdd59d6d82338f119d2099cdb332bc2c89a7778721.json rename to services/headless-lms/models/.sqlx/query-0c5ecaba29bb357054511e90eaaee3f3cd36b02d73fe30af1baf7f253a02595d.json index d0e9a08caaac..806d0a4013dd 100644 --- a/services/headless-lms/models/.sqlx/query-f5f6f6ee7f41b785b8b8b4bdd59d6d82338f119d2099cdb332bc2c89a7778721.json +++ b/services/headless-lms/models/.sqlx/query-0c5ecaba29bb357054511e90eaaee3f3cd36b02d73fe30af1baf7f253a02595d.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\nSELECT COUNT(*)\nFROM peer_review_submissions\nWHERE user_id = $1\n AND exercise_id = $3\n AND course_instance_id = $2\n AND deleted_at IS NULL\n ", + "query": "\nSELECT COUNT(*)\nFROM peer_or_self_review_submissions\nWHERE user_id = $1\n AND exercise_id = $3\n AND course_instance_id = $2\n AND deleted_at IS NULL\n ", "describe": { "columns": [ { @@ -14,5 +14,5 @@ }, "nullable": [null] }, - "hash": "f5f6f6ee7f41b785b8b8b4bdd59d6d82338f119d2099cdb332bc2c89a7778721" + "hash": "0c5ecaba29bb357054511e90eaaee3f3cd36b02d73fe30af1baf7f253a02595d" } diff --git a/services/headless-lms/models/.sqlx/query-0d27fed835b74d0406a456ef50fdda617627befb3309e387b27d932928426142.json b/services/headless-lms/models/.sqlx/query-0d27fed835b74d0406a456ef50fdda617627befb3309e387b27d932928426142.json new file mode 100644 index 000000000000..e42b4f8a3132 --- /dev/null +++ b/services/headless-lms/models/.sqlx/query-0d27fed835b74d0406a456ef50fdda617627befb3309e387b27d932928426142.json @@ -0,0 +1,23 @@ +{ + "db_name": "PostgreSQL", + "query": "\nINSERT INTO exercises (\n id,\n course_id,\n name,\n deadline,\n page_id,\n score_maximum,\n order_number,\n chapter_id,\n copied_from,\n exercise_language_group_id,\n max_tries_per_slide,\n limit_number_of_tries,\n needs_peer_review,\n use_course_default_peer_or_self_review_config\n )\nSELECT uuid_generate_v5($1, id::text),\n $1,\n name,\n deadline,\n uuid_generate_v5($1, page_id::text),\n score_maximum,\n order_number,\n uuid_generate_v5($1, chapter_id::text),\n id,\n exercise_language_group_id,\n max_tries_per_slide,\n limit_number_of_tries,\n needs_peer_review,\n use_course_default_peer_or_self_review_config\nFROM exercises\nWHERE course_id = $2\n AND deleted_at IS NULL\nRETURNING id,\n copied_from;\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Uuid" + }, + { + "ordinal": 1, + "name": "copied_from", + "type_info": "Uuid" + } + ], + "parameters": { + "Left": ["Uuid", "Uuid"] + }, + "nullable": [false, true] + }, + "hash": "0d27fed835b74d0406a456ef50fdda617627befb3309e387b27d932928426142" +} diff --git a/services/headless-lms/models/.sqlx/query-2a4db8bf5f10ee2ac5d5a08d2da9aeca85a403b3b873e3678846982f1ea0210e.json b/services/headless-lms/models/.sqlx/query-0d353d5b070ffc575805e7e1a5fe3b7197543c9bebc0f657780cdf428bac46d5.json similarity index 57% rename from services/headless-lms/models/.sqlx/query-2a4db8bf5f10ee2ac5d5a08d2da9aeca85a403b3b873e3678846982f1ea0210e.json rename to services/headless-lms/models/.sqlx/query-0d353d5b070ffc575805e7e1a5fe3b7197543c9bebc0f657780cdf428bac46d5.json index c9ada68199f5..ba04d25a9d7b 100644 --- a/services/headless-lms/models/.sqlx/query-2a4db8bf5f10ee2ac5d5a08d2da9aeca85a403b3b873e3678846982f1ea0210e.json +++ b/services/headless-lms/models/.sqlx/query-0d353d5b070ffc575805e7e1a5fe3b7197543c9bebc0f657780cdf428bac46d5.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\nSELECT prq.id as id,\n prq.peer_review_config_id as peer_review_config_id,\n prq.order_number as order_number,\n prq.question as question,\n prq.question_type AS \"question_type: _\",\n prq.answer_required as answer_required,\n prq.weight\nfrom pages p\n join exercises e on p.id = e.page_id\n join peer_review_configs pr on e.id = pr.exercise_id\n join peer_review_questions prq on pr.id = prq.peer_review_config_id\nwhere p.id = $1\n AND p.deleted_at IS NULL\n AND e.deleted_at IS NULL\n AND pr.deleted_at IS NULL\n AND prq.deleted_at IS NULL;\n ", + "query": "\nSELECT prq.id as id,\n prq.peer_or_self_review_config_id as peer_or_self_review_config_id,\n prq.order_number as order_number,\n prq.question as question,\n prq.question_type AS \"question_type: _\",\n prq.answer_required as answer_required,\n prq.weight\nfrom pages p\n join exercises e on p.id = e.page_id\n join peer_or_self_review_configs pr on e.id = pr.exercise_id\n join peer_or_self_review_questions prq on pr.id = prq.peer_or_self_review_config_id\nwhere p.id = $1\n AND p.deleted_at IS NULL\n AND e.deleted_at IS NULL\n AND pr.deleted_at IS NULL\n AND prq.deleted_at IS NULL;\n ", "describe": { "columns": [ { @@ -10,7 +10,7 @@ }, { "ordinal": 1, - "name": "peer_review_config_id", + "name": "peer_or_self_review_config_id", "type_info": "Uuid" }, { @@ -51,5 +51,5 @@ }, "nullable": [false, false, false, false, false, false, false] }, - "hash": "2a4db8bf5f10ee2ac5d5a08d2da9aeca85a403b3b873e3678846982f1ea0210e" + "hash": "0d353d5b070ffc575805e7e1a5fe3b7197543c9bebc0f657780cdf428bac46d5" } diff --git a/services/headless-lms/models/.sqlx/query-10deeca44ae988c99c174419db183aab355815caff6189a9f405cbef739b6b48.json b/services/headless-lms/models/.sqlx/query-10deeca44ae988c99c174419db183aab355815caff6189a9f405cbef739b6b48.json new file mode 100644 index 000000000000..0b56df689ce1 --- /dev/null +++ b/services/headless-lms/models/.sqlx/query-10deeca44ae988c99c174419db183aab355815caff6189a9f405cbef739b6b48.json @@ -0,0 +1,18 @@ +{ + "db_name": "PostgreSQL", + "query": "\nUPDATE exercises\nSET use_course_default_peer_or_self_review_config = $1,\n needs_peer_review = $2,\n needs_self_review = $3\nWHERE id = $4\nRETURNING id;\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Uuid" + } + ], + "parameters": { + "Left": ["Bool", "Bool", "Bool", "Uuid"] + }, + "nullable": [false] + }, + "hash": "10deeca44ae988c99c174419db183aab355815caff6189a9f405cbef739b6b48" +} diff --git a/services/headless-lms/models/.sqlx/query-2747949af670b45066518efe972a7a5a01580cf78d465361330c818aa6970788.json b/services/headless-lms/models/.sqlx/query-1419b92dec6050f80bdedffe32234599273e4eae26bb9984444ef93e46c9b033.json similarity index 50% rename from services/headless-lms/models/.sqlx/query-2747949af670b45066518efe972a7a5a01580cf78d465361330c818aa6970788.json rename to services/headless-lms/models/.sqlx/query-1419b92dec6050f80bdedffe32234599273e4eae26bb9984444ef93e46c9b033.json index 49be2001cfd1..ebbba67ed963 100644 --- a/services/headless-lms/models/.sqlx/query-2747949af670b45066518efe972a7a5a01580cf78d465361330c818aa6970788.json +++ b/services/headless-lms/models/.sqlx/query-1419b92dec6050f80bdedffe32234599273e4eae26bb9984444ef93e46c9b033.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\nSELECT exercise_slide_submission_id\nFROM peer_review_submissions\nWHERE user_id = $1\n AND exercise_id = $2\n AND course_instance_id = $3\n AND deleted_at IS NULL\n ", + "query": "\nSELECT exercise_slide_submission_id\nFROM peer_or_self_review_submissions\nWHERE user_id = $1\n AND exercise_id = $2\n AND course_instance_id = $3\n AND deleted_at IS NULL\n ", "describe": { "columns": [ { @@ -14,5 +14,5 @@ }, "nullable": [false] }, - "hash": "2747949af670b45066518efe972a7a5a01580cf78d465361330c818aa6970788" + "hash": "1419b92dec6050f80bdedffe32234599273e4eae26bb9984444ef93e46c9b033" } diff --git a/services/headless-lms/models/.sqlx/query-18cad5d0cf2a854a36655738b9a05c4fe5376c77174239b17cdf7e21bc739b1f.json b/services/headless-lms/models/.sqlx/query-18cad5d0cf2a854a36655738b9a05c4fe5376c77174239b17cdf7e21bc739b1f.json index 5f5e76e3ec54..159011efc68d 100644 --- a/services/headless-lms/models/.sqlx/query-18cad5d0cf2a854a36655738b9a05c4fe5376c77174239b17cdf7e21bc739b1f.json +++ b/services/headless-lms/models/.sqlx/query-18cad5d0cf2a854a36655738b9a05c4fe5376c77174239b17cdf7e21bc739b1f.json @@ -85,13 +85,18 @@ }, { "ordinal": 16, - "name": "use_course_default_peer_review_config", + "name": "use_course_default_peer_or_self_review_config", "type_info": "Bool" }, { "ordinal": 17, "name": "exercise_language_group_id", "type_info": "Uuid" + }, + { + "ordinal": 18, + "name": "needs_self_review", + "type_info": "Bool" } ], "parameters": { @@ -115,7 +120,8 @@ false, false, false, - true + true, + false ] }, "hash": "18cad5d0cf2a854a36655738b9a05c4fe5376c77174239b17cdf7e21bc739b1f" diff --git a/services/headless-lms/models/.sqlx/query-85e75fc324178be2da3671efdfa3145a7cfbb7ef16aa52539fc9fd71b955a461.json b/services/headless-lms/models/.sqlx/query-21cf57babb132e5550a3cbfb60127eba0f10063189ec1642a8bd64961417b7af.json similarity index 51% rename from services/headless-lms/models/.sqlx/query-85e75fc324178be2da3671efdfa3145a7cfbb7ef16aa52539fc9fd71b955a461.json rename to services/headless-lms/models/.sqlx/query-21cf57babb132e5550a3cbfb60127eba0f10063189ec1642a8bd64961417b7af.json index 17baf6003691..30bd9e6d3206 100644 --- a/services/headless-lms/models/.sqlx/query-85e75fc324178be2da3671efdfa3145a7cfbb7ef16aa52539fc9fd71b955a461.json +++ b/services/headless-lms/models/.sqlx/query-21cf57babb132e5550a3cbfb60127eba0f10063189ec1642a8bd64961417b7af.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\nUPDATE peer_review_configs\nSET deleted_at = now()\nWHERE exercise_id = ANY ($1)\nAND deleted_at IS NULL\nRETURNING id;\n ", + "query": "\nUPDATE peer_or_self_review_configs\nSET deleted_at = now()\nWHERE exercise_id = ANY ($1)\nAND deleted_at IS NULL\nRETURNING id;\n ", "describe": { "columns": [ { @@ -14,5 +14,5 @@ }, "nullable": [false] }, - "hash": "85e75fc324178be2da3671efdfa3145a7cfbb7ef16aa52539fc9fd71b955a461" + "hash": "21cf57babb132e5550a3cbfb60127eba0f10063189ec1642a8bd64961417b7af" } diff --git a/services/headless-lms/models/.sqlx/query-2311d15c76557b16045c59de4e7554173054e3e22f97ed17d0f26bace954da74.json b/services/headless-lms/models/.sqlx/query-2311d15c76557b16045c59de4e7554173054e3e22f97ed17d0f26bace954da74.json deleted file mode 100644 index 43a4251699ba..000000000000 --- a/services/headless-lms/models/.sqlx/query-2311d15c76557b16045c59de4e7554173054e3e22f97ed17d0f26bace954da74.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "\nINSERT INTO peer_review_question_submissions (\n id,\n peer_review_question_id,\n peer_review_submission_id,\n text_data,\n number_data\n )\nVALUES ($1, $2, $3, $4, $5)\nRETURNING id\n ", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "id", - "type_info": "Uuid" - } - ], - "parameters": { - "Left": ["Uuid", "Uuid", "Uuid", "Varchar", "Float4"] - }, - "nullable": [false] - }, - "hash": "2311d15c76557b16045c59de4e7554173054e3e22f97ed17d0f26bace954da74" -} diff --git a/services/headless-lms/models/.sqlx/query-2708cf9f25dfbc37ee92a97f41292ff4edd72d7a6f70ff35fb7cfad510268e8d.json b/services/headless-lms/models/.sqlx/query-2708cf9f25dfbc37ee92a97f41292ff4edd72d7a6f70ff35fb7cfad510268e8d.json new file mode 100644 index 000000000000..78774570070b --- /dev/null +++ b/services/headless-lms/models/.sqlx/query-2708cf9f25dfbc37ee92a97f41292ff4edd72d7a6f70ff35fb7cfad510268e8d.json @@ -0,0 +1,12 @@ +{ + "db_name": "PostgreSQL", + "query": "\nUPDATE peer_or_self_review_question_submissions\nSET deleted_at = now()\nWHERE peer_or_self_review_submission_id IN (\n SELECT id\n FROM peer_or_self_review_submissions\n WHERE user_id = $1\n AND course_instance_id = $2\n )\n AND deleted_at IS NULL\n", + "describe": { + "columns": [], + "parameters": { + "Left": ["Uuid", "Uuid"] + }, + "nullable": [] + }, + "hash": "2708cf9f25dfbc37ee92a97f41292ff4edd72d7a6f70ff35fb7cfad510268e8d" +} diff --git a/services/headless-lms/models/.sqlx/query-306820247b9533af5d464aa15a58f9fcde6a59b1666a3709b32bc1823ad2e970.json b/services/headless-lms/models/.sqlx/query-306820247b9533af5d464aa15a58f9fcde6a59b1666a3709b32bc1823ad2e970.json index ab0d24b049aa..2f1b4abff615 100644 --- a/services/headless-lms/models/.sqlx/query-306820247b9533af5d464aa15a58f9fcde6a59b1666a3709b32bc1823ad2e970.json +++ b/services/headless-lms/models/.sqlx/query-306820247b9533af5d464aa15a58f9fcde6a59b1666a3709b32bc1823ad2e970.json @@ -85,13 +85,18 @@ }, { "ordinal": 16, - "name": "use_course_default_peer_review_config", + "name": "use_course_default_peer_or_self_review_config", "type_info": "Bool" }, { "ordinal": 17, "name": "exercise_language_group_id", "type_info": "Uuid" + }, + { + "ordinal": 18, + "name": "needs_self_review", + "type_info": "Bool" } ], "parameters": { @@ -115,7 +120,8 @@ false, false, false, - true + true, + false ] }, "hash": "306820247b9533af5d464aa15a58f9fcde6a59b1666a3709b32bc1823ad2e970" diff --git a/services/headless-lms/models/.sqlx/query-3095a7dcf831b27cc6fe6333b8769f49bd20ac07ad521721b3514dc706c98b6a.json b/services/headless-lms/models/.sqlx/query-3095a7dcf831b27cc6fe6333b8769f49bd20ac07ad521721b3514dc706c98b6a.json deleted file mode 100644 index d282c797e1c1..000000000000 --- a/services/headless-lms/models/.sqlx/query-3095a7dcf831b27cc6fe6333b8769f49bd20ac07ad521721b3514dc706c98b6a.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "\nINSERT INTO peer_review_submissions (\n id,\n user_id,\n exercise_id,\n course_instance_id,\n peer_review_config_id,\n exercise_slide_submission_id\n )\nVALUES ($1, $2, $3, $4, $5, $6)\nRETURNING id\n ", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "id", - "type_info": "Uuid" - } - ], - "parameters": { - "Left": ["Uuid", "Uuid", "Uuid", "Uuid", "Uuid", "Uuid"] - }, - "nullable": [false] - }, - "hash": "3095a7dcf831b27cc6fe6333b8769f49bd20ac07ad521721b3514dc706c98b6a" -} diff --git a/services/headless-lms/models/.sqlx/query-32c5c53d0e1e9057ce0db003d89296d727634c9d3508f66f5bd38a1e396c219e.json b/services/headless-lms/models/.sqlx/query-32c5c53d0e1e9057ce0db003d89296d727634c9d3508f66f5bd38a1e396c219e.json new file mode 100644 index 000000000000..00fe37bbf9fb --- /dev/null +++ b/services/headless-lms/models/.sqlx/query-32c5c53d0e1e9057ce0db003d89296d727634c9d3508f66f5bd38a1e396c219e.json @@ -0,0 +1,18 @@ +{ + "db_name": "PostgreSQL", + "query": "\nUPDATE peer_or_self_review_questions\nSET deleted_at = now()\nWHERE peer_or_self_review_config_id = ANY ($1)\nAND deleted_at IS NULL\nRETURNING id;\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Uuid" + } + ], + "parameters": { + "Left": ["UuidArray"] + }, + "nullable": [false] + }, + "hash": "32c5c53d0e1e9057ce0db003d89296d727634c9d3508f66f5bd38a1e396c219e" +} diff --git a/services/headless-lms/models/.sqlx/query-d99d37a38c18d917538b9192859efaf22ed8604e94447cc20040a265330428fb.json b/services/headless-lms/models/.sqlx/query-362ba0c40f0eb627382d2a7374a130623de3cdb5d436b567cc51c32037f6704c.json similarity index 76% rename from services/headless-lms/models/.sqlx/query-d99d37a38c18d917538b9192859efaf22ed8604e94447cc20040a265330428fb.json rename to services/headless-lms/models/.sqlx/query-362ba0c40f0eb627382d2a7374a130623de3cdb5d436b567cc51c32037f6704c.json index 80134e04ba67..632bec5b2b3f 100644 --- a/services/headless-lms/models/.sqlx/query-d99d37a38c18d917538b9192859efaf22ed8604e94447cc20040a265330428fb.json +++ b/services/headless-lms/models/.sqlx/query-362ba0c40f0eb627382d2a7374a130623de3cdb5d436b567cc51c32037f6704c.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\nSELECT prs.*\nFROM exercise_slide_submissions ess\nINNER JOIN peer_review_submissions prs ON (ess.id = prs.exercise_slide_submission_id)\nWHERE ess.user_id = $1\n AND ess.course_instance_id = $2\n AND ess.deleted_at IS NULL\n AND prs.deleted_at IS NULL\n ", + "query": "\nSELECT prs.*\nFROM exercise_slide_submissions ess\nINNER JOIN peer_or_self_review_submissions prs ON (ess.id = prs.exercise_slide_submission_id)\nWHERE ess.user_id = $1\n AND ess.course_instance_id = $2\n AND ess.deleted_at IS NULL\n AND prs.deleted_at IS NULL\n ", "describe": { "columns": [ { @@ -40,7 +40,7 @@ }, { "ordinal": 7, - "name": "peer_review_config_id", + "name": "peer_or_self_review_config_id", "type_info": "Uuid" }, { @@ -54,5 +54,5 @@ }, "nullable": [false, false, false, true, false, false, false, false, false] }, - "hash": "d99d37a38c18d917538b9192859efaf22ed8604e94447cc20040a265330428fb" + "hash": "362ba0c40f0eb627382d2a7374a130623de3cdb5d436b567cc51c32037f6704c" } diff --git a/services/headless-lms/models/.sqlx/query-367d1bc000b761155e64b28572e0409d3c2942097ec46aaabf63b0bf5e527a64.json b/services/headless-lms/models/.sqlx/query-367d1bc000b761155e64b28572e0409d3c2942097ec46aaabf63b0bf5e527a64.json new file mode 100644 index 000000000000..0c1e42a99e7a --- /dev/null +++ b/services/headless-lms/models/.sqlx/query-367d1bc000b761155e64b28572e0409d3c2942097ec46aaabf63b0bf5e527a64.json @@ -0,0 +1,12 @@ +{ + "db_name": "PostgreSQL", + "query": "\nUPDATE peer_or_self_review_submissions\nSET deleted_at = now()\nWHERE user_id = $1\n AND course_instance_id = $2\n AND deleted_at IS NULL\n", + "describe": { + "columns": [], + "parameters": { + "Left": ["Uuid", "Uuid"] + }, + "nullable": [] + }, + "hash": "367d1bc000b761155e64b28572e0409d3c2942097ec46aaabf63b0bf5e527a64" +} diff --git a/services/headless-lms/models/.sqlx/query-377c6a8e44a2d72d5f81a88b84e7945679fea9ae58cc456c840726c2ac1fa1ac.json b/services/headless-lms/models/.sqlx/query-377c6a8e44a2d72d5f81a88b84e7945679fea9ae58cc456c840726c2ac1fa1ac.json new file mode 100644 index 000000000000..ea2bd1699294 --- /dev/null +++ b/services/headless-lms/models/.sqlx/query-377c6a8e44a2d72d5f81a88b84e7945679fea9ae58cc456c840726c2ac1fa1ac.json @@ -0,0 +1,58 @@ +{ + "db_name": "PostgreSQL", + "query": "\nSELECT *\nFROM peer_or_self_review_submissions\nWHERE id = ANY($1)\n AND deleted_at IS NULL\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Uuid" + }, + { + "ordinal": 1, + "name": "created_at", + "type_info": "Timestamptz" + }, + { + "ordinal": 2, + "name": "updated_at", + "type_info": "Timestamptz" + }, + { + "ordinal": 3, + "name": "deleted_at", + "type_info": "Timestamptz" + }, + { + "ordinal": 4, + "name": "user_id", + "type_info": "Uuid" + }, + { + "ordinal": 5, + "name": "exercise_id", + "type_info": "Uuid" + }, + { + "ordinal": 6, + "name": "course_instance_id", + "type_info": "Uuid" + }, + { + "ordinal": 7, + "name": "peer_or_self_review_config_id", + "type_info": "Uuid" + }, + { + "ordinal": 8, + "name": "exercise_slide_submission_id", + "type_info": "Uuid" + } + ], + "parameters": { + "Left": ["UuidArray"] + }, + "nullable": [false, false, false, true, false, false, false, false, false] + }, + "hash": "377c6a8e44a2d72d5f81a88b84e7945679fea9ae58cc456c840726c2ac1fa1ac" +} diff --git a/services/headless-lms/models/.sqlx/query-5ed164d4da522f655120cbdf4ab79f589d4d7cebfdfa4aeb09f8742f52322f07.json b/services/headless-lms/models/.sqlx/query-3e4aff0245e7d0a2cd08c79bf44a5df4032b551f192032948dcb90909acbf577.json similarity index 71% rename from services/headless-lms/models/.sqlx/query-5ed164d4da522f655120cbdf4ab79f589d4d7cebfdfa4aeb09f8742f52322f07.json rename to services/headless-lms/models/.sqlx/query-3e4aff0245e7d0a2cd08c79bf44a5df4032b551f192032948dcb90909acbf577.json index 40a0de58d9db..d1709363bacc 100644 --- a/services/headless-lms/models/.sqlx/query-5ed164d4da522f655120cbdf4ab79f589d4d7cebfdfa4aeb09f8742f52322f07.json +++ b/services/headless-lms/models/.sqlx/query-3e4aff0245e7d0a2cd08c79bf44a5df4032b551f192032948dcb90909acbf577.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\nSELECT *\nFROM peer_review_question_submissions\nWHERE peer_review_submission_id IN (\n SELECT UNNEST($1::uuid [])\n )\n AND deleted_at IS NULL\n ", + "query": "\nSELECT *\nFROM peer_or_self_review_question_submissions\nWHERE peer_or_self_review_submission_id IN (\n SELECT UNNEST($1::uuid [])\n )\n AND deleted_at IS NULL\n ", "describe": { "columns": [ { @@ -25,12 +25,12 @@ }, { "ordinal": 4, - "name": "peer_review_question_id", + "name": "peer_or_self_review_question_id", "type_info": "Uuid" }, { "ordinal": 5, - "name": "peer_review_submission_id", + "name": "peer_or_self_review_submission_id", "type_info": "Uuid" }, { @@ -49,5 +49,5 @@ }, "nullable": [false, false, false, true, false, false, true, true] }, - "hash": "5ed164d4da522f655120cbdf4ab79f589d4d7cebfdfa4aeb09f8742f52322f07" + "hash": "3e4aff0245e7d0a2cd08c79bf44a5df4032b551f192032948dcb90909acbf577" } diff --git a/services/headless-lms/models/.sqlx/query-46baab67e600d6ee84312a6c53a493d7520957e27a76e9bf8188b0ae0844b1a0.json b/services/headless-lms/models/.sqlx/query-46baab67e600d6ee84312a6c53a493d7520957e27a76e9bf8188b0ae0844b1a0.json deleted file mode 100644 index 75c697144bce..000000000000 --- a/services/headless-lms/models/.sqlx/query-46baab67e600d6ee84312a6c53a493d7520957e27a76e9bf8188b0ae0844b1a0.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "\nUPDATE peer_review_question_submissions\nSET deleted_at = now()\nWHERE peer_review_submission_id IN (\n SELECT id\n FROM peer_review_submissions\n WHERE user_id = $1\n AND course_instance_id = $2\n )\n AND deleted_at IS NULL\n", - "describe": { - "columns": [], - "parameters": { - "Left": ["Uuid", "Uuid"] - }, - "nullable": [] - }, - "hash": "46baab67e600d6ee84312a6c53a493d7520957e27a76e9bf8188b0ae0844b1a0" -} diff --git a/services/headless-lms/models/.sqlx/query-46d0b51b38b5edcd222cf363239dd63ff78bf68f954b1f71bb5896601bb86a37.json b/services/headless-lms/models/.sqlx/query-46d0b51b38b5edcd222cf363239dd63ff78bf68f954b1f71bb5896601bb86a37.json new file mode 100644 index 000000000000..2a52f9af7260 --- /dev/null +++ b/services/headless-lms/models/.sqlx/query-46d0b51b38b5edcd222cf363239dd63ff78bf68f954b1f71bb5896601bb86a37.json @@ -0,0 +1,58 @@ +{ + "db_name": "PostgreSQL", + "query": "\nSELECT prs.*\nFROM peer_or_self_review_submissions prs\n JOIN exercise_slide_submissions ess ON (ess.id = prs.exercise_slide_submission_id)\nWHERE ess.user_id = $1\n AND ess.id = $2\n AND prs.peer_or_self_review_config_id = $3\n AND prs.deleted_at IS NULL\n AND ess.deleted_at IS NULL\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Uuid" + }, + { + "ordinal": 1, + "name": "created_at", + "type_info": "Timestamptz" + }, + { + "ordinal": 2, + "name": "updated_at", + "type_info": "Timestamptz" + }, + { + "ordinal": 3, + "name": "deleted_at", + "type_info": "Timestamptz" + }, + { + "ordinal": 4, + "name": "user_id", + "type_info": "Uuid" + }, + { + "ordinal": 5, + "name": "exercise_id", + "type_info": "Uuid" + }, + { + "ordinal": 6, + "name": "course_instance_id", + "type_info": "Uuid" + }, + { + "ordinal": 7, + "name": "peer_or_self_review_config_id", + "type_info": "Uuid" + }, + { + "ordinal": 8, + "name": "exercise_slide_submission_id", + "type_info": "Uuid" + } + ], + "parameters": { + "Left": ["Uuid", "Uuid", "Uuid"] + }, + "nullable": [false, false, false, true, false, false, false, false, false] + }, + "hash": "46d0b51b38b5edcd222cf363239dd63ff78bf68f954b1f71bb5896601bb86a37" +} diff --git a/services/headless-lms/models/.sqlx/query-1725a72bc66a1d386efb868433b98525cc543f46c649705d5de175e07be8867a.json b/services/headless-lms/models/.sqlx/query-471e8bb953a98c2d5a58efb12317b1924d18bd38c82433a55b042dec9eb1928c.json similarity index 52% rename from services/headless-lms/models/.sqlx/query-1725a72bc66a1d386efb868433b98525cc543f46c649705d5de175e07be8867a.json rename to services/headless-lms/models/.sqlx/query-471e8bb953a98c2d5a58efb12317b1924d18bd38c82433a55b042dec9eb1928c.json index e1ee3652733e..b23441d48c41 100644 --- a/services/headless-lms/models/.sqlx/query-1725a72bc66a1d386efb868433b98525cc543f46c649705d5de175e07be8867a.json +++ b/services/headless-lms/models/.sqlx/query-471e8bb953a98c2d5a58efb12317b1924d18bd38c82433a55b042dec9eb1928c.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\nINSERT INTO exercises (\n id,\n exam_id,\n name,\n deadline,\n page_id,\n score_maximum,\n order_number,\n chapter_id,\n copied_from,\n max_tries_per_slide,\n limit_number_of_tries,\n needs_peer_review,\n use_course_default_peer_review_config\n )\nSELECT uuid_generate_v5($1, id::text),\n $1,\n name,\n deadline,\n uuid_generate_v5($1, page_id::text),\n score_maximum,\n order_number,\n uuid_generate_v5($1, chapter_id::text),\n id,\n max_tries_per_slide,\n limit_number_of_tries,\n needs_peer_review,\n use_course_default_peer_review_config\nFROM exercises\nWHERE exam_id = $2\n AND deleted_at IS NULL\nRETURNING id,\n copied_from;\n ", + "query": "\nINSERT INTO exercises (\n id,\n exam_id,\n name,\n deadline,\n page_id,\n score_maximum,\n order_number,\n chapter_id,\n copied_from,\n max_tries_per_slide,\n limit_number_of_tries,\n needs_peer_review,\n use_course_default_peer_or_self_review_config\n )\nSELECT uuid_generate_v5($1, id::text),\n $1,\n name,\n deadline,\n uuid_generate_v5($1, page_id::text),\n score_maximum,\n order_number,\n uuid_generate_v5($1, chapter_id::text),\n id,\n max_tries_per_slide,\n limit_number_of_tries,\n needs_peer_review,\n use_course_default_peer_or_self_review_config\nFROM exercises\nWHERE exam_id = $2\n AND deleted_at IS NULL\nRETURNING id,\n copied_from;\n ", "describe": { "columns": [ { @@ -19,5 +19,5 @@ }, "nullable": [false, true] }, - "hash": "1725a72bc66a1d386efb868433b98525cc543f46c649705d5de175e07be8867a" + "hash": "471e8bb953a98c2d5a58efb12317b1924d18bd38c82433a55b042dec9eb1928c" } diff --git a/services/headless-lms/models/.sqlx/query-9183665dc5a29003e2d95c3107ef9b051d8ea686d681a662a8f0091a8de4cf63.json b/services/headless-lms/models/.sqlx/query-55508b24395116c527428508b4d43a6ee9081c1d96ccc764689977dec6be45fc.json similarity index 83% rename from services/headless-lms/models/.sqlx/query-9183665dc5a29003e2d95c3107ef9b051d8ea686d681a662a8f0091a8de4cf63.json rename to services/headless-lms/models/.sqlx/query-55508b24395116c527428508b4d43a6ee9081c1d96ccc764689977dec6be45fc.json index ddda52e88759..2de68b174d81 100644 --- a/services/headless-lms/models/.sqlx/query-9183665dc5a29003e2d95c3107ef9b051d8ea686d681a662a8f0091a8de4cf63.json +++ b/services/headless-lms/models/.sqlx/query-55508b24395116c527428508b4d43a6ee9081c1d96ccc764689977dec6be45fc.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\nSELECT id,\n course_id,\n exercise_id,\n peer_reviews_to_give,\n peer_reviews_to_receive,\n accepting_threshold,\n processing_strategy AS \"processing_strategy:_\",\n points_are_all_or_nothing\nFROM peer_review_configs\nWHERE id = $1;\n ", + "query": "\nSELECT id,\n course_id,\n exercise_id,\n peer_reviews_to_give,\n peer_reviews_to_receive,\n accepting_threshold,\n processing_strategy AS \"processing_strategy:_\",\n points_are_all_or_nothing,\n review_instructions\nFROM peer_or_self_review_configs\nWHERE id = $1;\n ", "describe": { "columns": [ { @@ -53,12 +53,17 @@ "ordinal": 7, "name": "points_are_all_or_nothing", "type_info": "Bool" + }, + { + "ordinal": 8, + "name": "review_instructions", + "type_info": "Jsonb" } ], "parameters": { "Left": ["Uuid"] }, - "nullable": [false, false, true, false, false, false, false, false] + "nullable": [false, false, true, false, false, false, false, false, true] }, - "hash": "9183665dc5a29003e2d95c3107ef9b051d8ea686d681a662a8f0091a8de4cf63" + "hash": "55508b24395116c527428508b4d43a6ee9081c1d96ccc764689977dec6be45fc" } diff --git a/services/headless-lms/models/.sqlx/query-58e5aac7861f558385ecf507e49afa25c340c5c83d6710b80b98436a6cc4c608.json b/services/headless-lms/models/.sqlx/query-58e5aac7861f558385ecf507e49afa25c340c5c83d6710b80b98436a6cc4c608.json index 97376626f9a8..31f969cdd738 100644 --- a/services/headless-lms/models/.sqlx/query-58e5aac7861f558385ecf507e49afa25c340c5c83d6710b80b98436a6cc4c608.json +++ b/services/headless-lms/models/.sqlx/query-58e5aac7861f558385ecf507e49afa25c340c5c83d6710b80b98436a6cc4c608.json @@ -85,13 +85,18 @@ }, { "ordinal": 16, - "name": "use_course_default_peer_review_config", + "name": "use_course_default_peer_or_self_review_config", "type_info": "Bool" }, { "ordinal": 17, "name": "exercise_language_group_id", "type_info": "Uuid" + }, + { + "ordinal": 18, + "name": "needs_self_review", + "type_info": "Bool" } ], "parameters": { @@ -115,7 +120,8 @@ false, false, false, - true + true, + false ] }, "hash": "58e5aac7861f558385ecf507e49afa25c340c5c83d6710b80b98436a6cc4c608" diff --git a/services/headless-lms/models/.sqlx/query-56698aa8b7cfcdf704ad7ad23bce16841652aed343aa9f69f22e8fdd52f1b2c9.json b/services/headless-lms/models/.sqlx/query-6032c6b6895479657eda7c7e4c244bfc294ae6275f9c64786909e25ac60479cd.json similarity index 78% rename from services/headless-lms/models/.sqlx/query-56698aa8b7cfcdf704ad7ad23bce16841652aed343aa9f69f22e8fdd52f1b2c9.json rename to services/headless-lms/models/.sqlx/query-6032c6b6895479657eda7c7e4c244bfc294ae6275f9c64786909e25ac60479cd.json index c27b759ad694..5e60a2344497 100644 --- a/services/headless-lms/models/.sqlx/query-56698aa8b7cfcdf704ad7ad23bce16841652aed343aa9f69f22e8fdd52f1b2c9.json +++ b/services/headless-lms/models/.sqlx/query-6032c6b6895479657eda7c7e4c244bfc294ae6275f9c64786909e25ac60479cd.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\nSELECT id,\n created_at,\n updated_at,\n deleted_at,\n peer_review_config_id,\n order_number,\n question,\n question_type AS \"question_type: _\",\n answer_required,\n weight\nFROM peer_review_questions\nWHERE id IN (\n SELECT UNNEST($1::uuid [])\n )\n AND deleted_at IS NULL;\n ", + "query": "\nSELECT id,\n created_at,\n updated_at,\n deleted_at,\n peer_or_self_review_config_id,\n order_number,\n question,\n question_type AS \"question_type: _\",\n answer_required,\n weight\nFROM peer_or_self_review_questions\nWHERE id IN (\n SELECT UNNEST($1::uuid [])\n )\n AND deleted_at IS NULL;\n ", "describe": { "columns": [ { @@ -25,7 +25,7 @@ }, { "ordinal": 4, - "name": "peer_review_config_id", + "name": "peer_or_self_review_config_id", "type_info": "Uuid" }, { @@ -66,5 +66,5 @@ }, "nullable": [false, false, false, true, false, false, false, false, false, false] }, - "hash": "56698aa8b7cfcdf704ad7ad23bce16841652aed343aa9f69f22e8fdd52f1b2c9" + "hash": "6032c6b6895479657eda7c7e4c244bfc294ae6275f9c64786909e25ac60479cd" } diff --git a/services/headless-lms/models/.sqlx/query-680c5eed3c49d67d404af7d0a2df7dc8f9391db14848e8aff877a8a117335be2.json b/services/headless-lms/models/.sqlx/query-680c5eed3c49d67d404af7d0a2df7dc8f9391db14848e8aff877a8a117335be2.json index 7623091e4fa7..f901bdb3f885 100644 --- a/services/headless-lms/models/.sqlx/query-680c5eed3c49d67d404af7d0a2df7dc8f9391db14848e8aff877a8a117335be2.json +++ b/services/headless-lms/models/.sqlx/query-680c5eed3c49d67d404af7d0a2df7dc8f9391db14848e8aff877a8a117335be2.json @@ -85,13 +85,18 @@ }, { "ordinal": 16, - "name": "use_course_default_peer_review_config", + "name": "use_course_default_peer_or_self_review_config", "type_info": "Bool" }, { "ordinal": 17, "name": "exercise_language_group_id", "type_info": "Uuid" + }, + { + "ordinal": 18, + "name": "needs_self_review", + "type_info": "Bool" } ], "parameters": { @@ -115,7 +120,8 @@ false, false, false, - true + true, + false ] }, "hash": "680c5eed3c49d67d404af7d0a2df7dc8f9391db14848e8aff877a8a117335be2" diff --git a/services/headless-lms/models/.sqlx/query-c6f8015b29670a51b64ea5effe14e0f78ae70ef2196b3e1c1a66a4a4439ee269.json b/services/headless-lms/models/.sqlx/query-6a5b4f427f43f3db0872c385bdb4968cecc785c48cdf36bec10167d0ec382215.json similarity index 60% rename from services/headless-lms/models/.sqlx/query-c6f8015b29670a51b64ea5effe14e0f78ae70ef2196b3e1c1a66a4a4439ee269.json rename to services/headless-lms/models/.sqlx/query-6a5b4f427f43f3db0872c385bdb4968cecc785c48cdf36bec10167d0ec382215.json index bb15148caf5f..7667afbae01b 100644 --- a/services/headless-lms/models/.sqlx/query-c6f8015b29670a51b64ea5effe14e0f78ae70ef2196b3e1c1a66a4a4439ee269.json +++ b/services/headless-lms/models/.sqlx/query-6a5b4f427f43f3db0872c385bdb4968cecc785c48cdf36bec10167d0ec382215.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\nSELECT MAX(created_at) as latest_submission_time\nFROM peer_review_submissions\nWHERE user_id = $1\n AND exercise_id = $2\n AND course_instance_id = $3\n AND deleted_at IS NULL\n ", + "query": "\nSELECT MAX(created_at) as latest_submission_time\nFROM peer_or_self_review_submissions\nWHERE user_id = $1\n AND exercise_id = $2\n AND course_instance_id = $3\n AND deleted_at IS NULL\n ", "describe": { "columns": [ { @@ -14,5 +14,5 @@ }, "nullable": [null] }, - "hash": "c6f8015b29670a51b64ea5effe14e0f78ae70ef2196b3e1c1a66a4a4439ee269" + "hash": "6a5b4f427f43f3db0872c385bdb4968cecc785c48cdf36bec10167d0ec382215" } diff --git a/services/headless-lms/models/.sqlx/query-d5b1aebe43e86588069f46ececdc7b5c0533de406f3204a56f0ec4e5cf250a0d.json b/services/headless-lms/models/.sqlx/query-6d7f9ab21f6db841df6fd5ebda135ccc9e4e6a9b0f1fa949f7c500c0098fc426.json similarity index 77% rename from services/headless-lms/models/.sqlx/query-d5b1aebe43e86588069f46ececdc7b5c0533de406f3204a56f0ec4e5cf250a0d.json rename to services/headless-lms/models/.sqlx/query-6d7f9ab21f6db841df6fd5ebda135ccc9e4e6a9b0f1fa949f7c500c0098fc426.json index e8a2c90b2b6b..01c60a9970ba 100644 --- a/services/headless-lms/models/.sqlx/query-d5b1aebe43e86588069f46ececdc7b5c0533de406f3204a56f0ec4e5cf250a0d.json +++ b/services/headless-lms/models/.sqlx/query-6d7f9ab21f6db841df6fd5ebda135ccc9e4e6a9b0f1fa949f7c500c0098fc426.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\nSELECT id,\n created_at,\n updated_at,\n deleted_at,\n peer_review_config_id,\n order_number,\n question,\n question_type AS \"question_type: _\",\n answer_required,\n weight\nFROM peer_review_questions\nWHERE peer_review_config_id = $1\n AND deleted_at IS NULL;\n ", + "query": "\nSELECT id,\n created_at,\n updated_at,\n deleted_at,\n peer_or_self_review_config_id,\n order_number,\n question,\n question_type AS \"question_type: _\",\n answer_required,\n weight\nFROM peer_or_self_review_questions\nWHERE peer_or_self_review_config_id = $1\n AND deleted_at IS NULL;\n ", "describe": { "columns": [ { @@ -25,7 +25,7 @@ }, { "ordinal": 4, - "name": "peer_review_config_id", + "name": "peer_or_self_review_config_id", "type_info": "Uuid" }, { @@ -66,5 +66,5 @@ }, "nullable": [false, false, false, true, false, false, false, false, false, false] }, - "hash": "d5b1aebe43e86588069f46ececdc7b5c0533de406f3204a56f0ec4e5cf250a0d" + "hash": "6d7f9ab21f6db841df6fd5ebda135ccc9e4e6a9b0f1fa949f7c500c0098fc426" } diff --git a/services/headless-lms/models/.sqlx/query-6f2e58a27e907e090727b45fb17e8e2658ea8d2d08ae97a2d21361aab63b40e5.json b/services/headless-lms/models/.sqlx/query-6f2e58a27e907e090727b45fb17e8e2658ea8d2d08ae97a2d21361aab63b40e5.json index fdcd196658fd..a394fcc7e8b4 100644 --- a/services/headless-lms/models/.sqlx/query-6f2e58a27e907e090727b45fb17e8e2658ea8d2d08ae97a2d21361aab63b40e5.json +++ b/services/headless-lms/models/.sqlx/query-6f2e58a27e907e090727b45fb17e8e2658ea8d2d08ae97a2d21361aab63b40e5.json @@ -85,13 +85,18 @@ }, { "ordinal": 16, - "name": "use_course_default_peer_review_config", + "name": "use_course_default_peer_or_self_review_config", "type_info": "Bool" }, { "ordinal": 17, "name": "exercise_language_group_id", "type_info": "Uuid" + }, + { + "ordinal": 18, + "name": "needs_self_review", + "type_info": "Bool" } ], "parameters": { @@ -115,7 +120,8 @@ false, false, false, - true + true, + false ] }, "hash": "6f2e58a27e907e090727b45fb17e8e2658ea8d2d08ae97a2d21361aab63b40e5" diff --git a/services/headless-lms/models/.sqlx/query-9a2fbd058bf3930b63b07c39047e2182868fc2917f019d7fe73db7e6ed337d76.json b/services/headless-lms/models/.sqlx/query-7153ac13761f68a25169f54f61ae6c9bacb5c847578bf19496ae008523ab87ac.json similarity index 71% rename from services/headless-lms/models/.sqlx/query-9a2fbd058bf3930b63b07c39047e2182868fc2917f019d7fe73db7e6ed337d76.json rename to services/headless-lms/models/.sqlx/query-7153ac13761f68a25169f54f61ae6c9bacb5c847578bf19496ae008523ab87ac.json index 79cb476f5b8c..3fb52c7f282d 100644 --- a/services/headless-lms/models/.sqlx/query-9a2fbd058bf3930b63b07c39047e2182868fc2917f019d7fe73db7e6ed337d76.json +++ b/services/headless-lms/models/.sqlx/query-7153ac13761f68a25169f54f61ae6c9bacb5c847578bf19496ae008523ab87ac.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\nSELECT id,\n peer_review_config_id,\n order_number,\n question,\n question_type AS \"question_type: _\",\n answer_required,\n weight\nfrom peer_review_questions\nWHERE id IN (\n SELECT UNNEST($1::uuid [])\n )\n AND deleted_at IS NULL;\n ", + "query": "\nSELECT id,\n peer_or_self_review_config_id,\n order_number,\n question,\n question_type AS \"question_type: _\",\n answer_required,\n weight\nfrom peer_or_self_review_questions\nWHERE id IN (\n SELECT UNNEST($1::uuid [])\n )\n AND deleted_at IS NULL;\n ", "describe": { "columns": [ { @@ -10,7 +10,7 @@ }, { "ordinal": 1, - "name": "peer_review_config_id", + "name": "peer_or_self_review_config_id", "type_info": "Uuid" }, { @@ -51,5 +51,5 @@ }, "nullable": [false, false, false, false, false, false, false] }, - "hash": "9a2fbd058bf3930b63b07c39047e2182868fc2917f019d7fe73db7e6ed337d76" + "hash": "7153ac13761f68a25169f54f61ae6c9bacb5c847578bf19496ae008523ab87ac" } diff --git a/services/headless-lms/models/.sqlx/query-736a6168fcbd01710b437fb8f601d34e35e291175681462cbcceb630a1c7bb46.json b/services/headless-lms/models/.sqlx/query-736a6168fcbd01710b437fb8f601d34e35e291175681462cbcceb630a1c7bb46.json new file mode 100644 index 000000000000..5c73d738244b --- /dev/null +++ b/services/headless-lms/models/.sqlx/query-736a6168fcbd01710b437fb8f601d34e35e291175681462cbcceb630a1c7bb46.json @@ -0,0 +1,18 @@ +{ + "db_name": "PostgreSQL", + "query": "\nINSERT INTO peer_or_self_review_submissions (\n id,\n user_id,\n exercise_id,\n course_instance_id,\n peer_or_self_review_config_id,\n exercise_slide_submission_id\n )\nVALUES ($1, $2, $3, $4, $5, $6)\nRETURNING id\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Uuid" + } + ], + "parameters": { + "Left": ["Uuid", "Uuid", "Uuid", "Uuid", "Uuid", "Uuid"] + }, + "nullable": [false] + }, + "hash": "736a6168fcbd01710b437fb8f601d34e35e291175681462cbcceb630a1c7bb46" +} diff --git a/services/headless-lms/models/.sqlx/query-d6142974f6473a58e93a6da9a03004aaaa82bae9a516ff04847c79b93f1693a3.json b/services/headless-lms/models/.sqlx/query-7b900a4b156ae01b052c63fd3721e0f9c7b585057ad84394ebead649e4ef2dfe.json similarity index 81% rename from services/headless-lms/models/.sqlx/query-d6142974f6473a58e93a6da9a03004aaaa82bae9a516ff04847c79b93f1693a3.json rename to services/headless-lms/models/.sqlx/query-7b900a4b156ae01b052c63fd3721e0f9c7b585057ad84394ebead649e4ef2dfe.json index e69d292fcba3..2712eaa98ec5 100644 --- a/services/headless-lms/models/.sqlx/query-d6142974f6473a58e93a6da9a03004aaaa82bae9a516ff04847c79b93f1693a3.json +++ b/services/headless-lms/models/.sqlx/query-7b900a4b156ae01b052c63fd3721e0f9c7b585057ad84394ebead649e4ef2dfe.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\nSELECT id as \"id!\",\n course_id as \"course_id!\",\n exercise_id,\n peer_reviews_to_give as \"peer_reviews_to_give!\",\n peer_reviews_to_receive as \"peer_reviews_to_receive!\",\n processing_strategy AS \"processing_strategy!: _\",\n accepting_threshold \"accepting_threshold!\",\n points_are_all_or_nothing \"points_are_all_or_nothing!\"\nFROM peer_review_configs\nWHERE id IN (\n SELECT UNNEST($1::uuid [])\n )\n AND deleted_at IS NULL;\n ", + "query": "\nSELECT id as \"id!\",\n course_id as \"course_id!\",\n exercise_id,\n peer_reviews_to_give as \"peer_reviews_to_give!\",\n peer_reviews_to_receive as \"peer_reviews_to_receive!\",\n processing_strategy AS \"processing_strategy!: _\",\n accepting_threshold \"accepting_threshold!\",\n points_are_all_or_nothing \"points_are_all_or_nothing!\",\n review_instructions\nFROM peer_or_self_review_configs\nWHERE id IN (\n SELECT UNNEST($1::uuid [])\n )\n AND deleted_at IS NULL;\n ", "describe": { "columns": [ { @@ -53,12 +53,17 @@ "ordinal": 7, "name": "points_are_all_or_nothing!", "type_info": "Bool" + }, + { + "ordinal": 8, + "name": "review_instructions", + "type_info": "Jsonb" } ], "parameters": { "Left": ["UuidArray"] }, - "nullable": [false, false, true, false, false, false, false, false] + "nullable": [false, false, true, false, false, false, false, false, true] }, - "hash": "d6142974f6473a58e93a6da9a03004aaaa82bae9a516ff04847c79b93f1693a3" + "hash": "7b900a4b156ae01b052c63fd3721e0f9c7b585057ad84394ebead649e4ef2dfe" } diff --git a/services/headless-lms/models/.sqlx/query-8077be20d112c948ec70c5f4247692199f313fed03d509a1e5605fc7d2f230c3.json b/services/headless-lms/models/.sqlx/query-8077be20d112c948ec70c5f4247692199f313fed03d509a1e5605fc7d2f230c3.json new file mode 100644 index 000000000000..cf5dd0e4f689 --- /dev/null +++ b/services/headless-lms/models/.sqlx/query-8077be20d112c948ec70c5f4247692199f313fed03d509a1e5605fc7d2f230c3.json @@ -0,0 +1,18 @@ +{ + "db_name": "PostgreSQL", + "query": "\nSELECT COUNT(*) AS count\nFROM peer_or_self_review_submissions\nWHERE user_id = $1\n AND exercise_id = $2\n AND course_instance_id = $3\n AND deleted_at IS NULL\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "count", + "type_info": "Int8" + } + ], + "parameters": { + "Left": ["Uuid", "Uuid", "Uuid"] + }, + "nullable": [null] + }, + "hash": "8077be20d112c948ec70c5f4247692199f313fed03d509a1e5605fc7d2f230c3" +} diff --git a/services/headless-lms/models/.sqlx/query-abcb3513a6184e7a92e117beb6744c9ffa3517930c431aca7f421c0b947cf697.json b/services/headless-lms/models/.sqlx/query-85315f9ee5389a58c56e999d33b74655affd0f3cdb65115a73514c8570ef08e8.json similarity index 80% rename from services/headless-lms/models/.sqlx/query-abcb3513a6184e7a92e117beb6744c9ffa3517930c431aca7f421c0b947cf697.json rename to services/headless-lms/models/.sqlx/query-85315f9ee5389a58c56e999d33b74655affd0f3cdb65115a73514c8570ef08e8.json index 60bd92679393..ba077dfca989 100644 --- a/services/headless-lms/models/.sqlx/query-abcb3513a6184e7a92e117beb6744c9ffa3517930c431aca7f421c0b947cf697.json +++ b/services/headless-lms/models/.sqlx/query-85315f9ee5389a58c56e999d33b74655affd0f3cdb65115a73514c8570ef08e8.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\nSELECT *\nFROM peer_review_submissions\nWHERE user_id = $1\n AND course_instance_id = $2\n AND deleted_at IS NULL\n ", + "query": "\nSELECT *\nFROM peer_or_self_review_submissions\nWHERE user_id = $1\n AND course_instance_id = $2\n AND deleted_at IS NULL\n ", "describe": { "columns": [ { @@ -40,7 +40,7 @@ }, { "ordinal": 7, - "name": "peer_review_config_id", + "name": "peer_or_self_review_config_id", "type_info": "Uuid" }, { @@ -54,5 +54,5 @@ }, "nullable": [false, false, false, true, false, false, false, false, false] }, - "hash": "abcb3513a6184e7a92e117beb6744c9ffa3517930c431aca7f421c0b947cf697" + "hash": "85315f9ee5389a58c56e999d33b74655affd0f3cdb65115a73514c8570ef08e8" } diff --git a/services/headless-lms/models/.sqlx/query-8b86355b3b2df7d09d26531b2b83de805a082b37159798eb089856c3e0c575b9.json b/services/headless-lms/models/.sqlx/query-8b86355b3b2df7d09d26531b2b83de805a082b37159798eb089856c3e0c575b9.json new file mode 100644 index 000000000000..20b13f501408 --- /dev/null +++ b/services/headless-lms/models/.sqlx/query-8b86355b3b2df7d09d26531b2b83de805a082b37159798eb089856c3e0c575b9.json @@ -0,0 +1,18 @@ +{ + "db_name": "PostgreSQL", + "query": "\nINSERT INTO peer_or_self_review_question_submissions (\n id,\n peer_or_self_review_question_id,\n peer_or_self_review_submission_id,\n text_data,\n number_data\n )\nVALUES ($1, $2, $3, $4, $5)\nRETURNING id\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Uuid" + } + ], + "parameters": { + "Left": ["Uuid", "Uuid", "Uuid", "Varchar", "Float4"] + }, + "nullable": [false] + }, + "hash": "8b86355b3b2df7d09d26531b2b83de805a082b37159798eb089856c3e0c575b9" +} diff --git a/services/headless-lms/models/.sqlx/query-32df56efc957577fed1752ea0e95d6c7287f9eebb79fd11726c89901b3eb5982.json b/services/headless-lms/models/.sqlx/query-8b9d41a0e69a7527a78b1ffd44b4ccb83aa1cb5f1facc117d0019bbfcf40ee18.json similarity index 78% rename from services/headless-lms/models/.sqlx/query-32df56efc957577fed1752ea0e95d6c7287f9eebb79fd11726c89901b3eb5982.json rename to services/headless-lms/models/.sqlx/query-8b9d41a0e69a7527a78b1ffd44b4ccb83aa1cb5f1facc117d0019bbfcf40ee18.json index 4ab6be0bb8af..d22d3334e389 100644 --- a/services/headless-lms/models/.sqlx/query-32df56efc957577fed1752ea0e95d6c7287f9eebb79fd11726c89901b3eb5982.json +++ b/services/headless-lms/models/.sqlx/query-8b9d41a0e69a7527a78b1ffd44b4ccb83aa1cb5f1facc117d0019bbfcf40ee18.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\nSELECT *\nFROM peer_review_submissions\nWHERE user_id = $1\n AND exercise_id = $3\n AND course_instance_id = $2\n AND deleted_at IS NULL\n ", + "query": "\nSELECT *\nFROM peer_or_self_review_submissions\nWHERE user_id = $1\n AND exercise_id = $3\n AND course_instance_id = $2\n AND deleted_at IS NULL\n ", "describe": { "columns": [ { @@ -40,7 +40,7 @@ }, { "ordinal": 7, - "name": "peer_review_config_id", + "name": "peer_or_self_review_config_id", "type_info": "Uuid" }, { @@ -54,5 +54,5 @@ }, "nullable": [false, false, false, true, false, false, false, false, false] }, - "hash": "32df56efc957577fed1752ea0e95d6c7287f9eebb79fd11726c89901b3eb5982" + "hash": "8b9d41a0e69a7527a78b1ffd44b4ccb83aa1cb5f1facc117d0019bbfcf40ee18" } diff --git a/services/headless-lms/models/.sqlx/query-9113a9bb47f4075916056074ed25a1138e57765d6020b94687a8fee84934d501.json b/services/headless-lms/models/.sqlx/query-9113a9bb47f4075916056074ed25a1138e57765d6020b94687a8fee84934d501.json deleted file mode 100644 index 7ccba0035ac8..000000000000 --- a/services/headless-lms/models/.sqlx/query-9113a9bb47f4075916056074ed25a1138e57765d6020b94687a8fee84934d501.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "\nSELECT COUNT(*) AS count\nFROM peer_review_submissions\nWHERE user_id = $1\n AND exercise_id = $2\n AND course_instance_id = $3\n AND deleted_at IS NULL\n ", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "count", - "type_info": "Int8" - } - ], - "parameters": { - "Left": ["Uuid", "Uuid", "Uuid"] - }, - "nullable": [null] - }, - "hash": "9113a9bb47f4075916056074ed25a1138e57765d6020b94687a8fee84934d501" -} diff --git a/services/headless-lms/models/.sqlx/query-1ebf7b64a269e1abfec20a6e963258d18e4da50b08361e1e5f3f8b2282718fc3.json b/services/headless-lms/models/.sqlx/query-964325e43dc2816de6ea37d0ab20c3415c5ff5706df461f5d5b24286b21f3263.json similarity index 55% rename from services/headless-lms/models/.sqlx/query-1ebf7b64a269e1abfec20a6e963258d18e4da50b08361e1e5f3f8b2282718fc3.json rename to services/headless-lms/models/.sqlx/query-964325e43dc2816de6ea37d0ab20c3415c5ff5706df461f5d5b24286b21f3263.json index d5d5cc750c3b..6922aed75da0 100644 --- a/services/headless-lms/models/.sqlx/query-1ebf7b64a269e1abfec20a6e963258d18e4da50b08361e1e5f3f8b2282718fc3.json +++ b/services/headless-lms/models/.sqlx/query-964325e43dc2816de6ea37d0ab20c3415c5ff5706df461f5d5b24286b21f3263.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\nINSERT INTO peer_review_configs (id, course_id, exercise_id)\nVALUES ($1, $2, $3)\nRETURNING id\n ", + "query": "\nINSERT INTO peer_or_self_review_configs (id, course_id, exercise_id)\nVALUES ($1, $2, $3)\nRETURNING id\n ", "describe": { "columns": [ { @@ -14,5 +14,5 @@ }, "nullable": [false] }, - "hash": "1ebf7b64a269e1abfec20a6e963258d18e4da50b08361e1e5f3f8b2282718fc3" + "hash": "964325e43dc2816de6ea37d0ab20c3415c5ff5706df461f5d5b24286b21f3263" } diff --git a/services/headless-lms/models/.sqlx/query-9c85b99d223ec35dd6a3d00598117fba6db802ec315cd2d1c38012b577d270bb.json b/services/headless-lms/models/.sqlx/query-9c85b99d223ec35dd6a3d00598117fba6db802ec315cd2d1c38012b577d270bb.json index 53590f17139b..41164b7445fe 100644 --- a/services/headless-lms/models/.sqlx/query-9c85b99d223ec35dd6a3d00598117fba6db802ec315cd2d1c38012b577d270bb.json +++ b/services/headless-lms/models/.sqlx/query-9c85b99d223ec35dd6a3d00598117fba6db802ec315cd2d1c38012b577d270bb.json @@ -85,13 +85,18 @@ }, { "ordinal": 16, - "name": "use_course_default_peer_review_config", + "name": "use_course_default_peer_or_self_review_config", "type_info": "Bool" }, { "ordinal": 17, "name": "exercise_language_group_id", "type_info": "Uuid" + }, + { + "ordinal": 18, + "name": "needs_self_review", + "type_info": "Bool" } ], "parameters": { @@ -115,7 +120,8 @@ false, false, false, - true + true, + false ] }, "hash": "9c85b99d223ec35dd6a3d00598117fba6db802ec315cd2d1c38012b577d270bb" diff --git a/services/headless-lms/models/.sqlx/query-3651de1134b71d12c906908c167535f3c9aae79c268e6b094ebe0bc22b5ee1f9.json b/services/headless-lms/models/.sqlx/query-a6a847fb97b37025b0c95462abf7ce6c57eda7620e0c1c5239e2ac55484aab40.json similarity index 79% rename from services/headless-lms/models/.sqlx/query-3651de1134b71d12c906908c167535f3c9aae79c268e6b094ebe0bc22b5ee1f9.json rename to services/headless-lms/models/.sqlx/query-a6a847fb97b37025b0c95462abf7ce6c57eda7620e0c1c5239e2ac55484aab40.json index 7b51e9957c18..c54c918e31fd 100644 --- a/services/headless-lms/models/.sqlx/query-3651de1134b71d12c906908c167535f3c9aae79c268e6b094ebe0bc22b5ee1f9.json +++ b/services/headless-lms/models/.sqlx/query-a6a847fb97b37025b0c95462abf7ce6c57eda7620e0c1c5239e2ac55484aab40.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\nSELECT id,\n created_at,\n updated_at,\n deleted_at,\n course_id,\n exercise_id,\n peer_reviews_to_give,\n peer_reviews_to_receive,\n accepting_threshold,\n processing_strategy AS \"processing_strategy: _\",\n manual_review_cutoff_in_days,\n points_are_all_or_nothing\nFROM peer_review_configs\nWHERE course_id = $1\n AND exercise_id IS NULL\n AND deleted_at IS NULL;\n ", + "query": "\nSELECT id,\n created_at,\n updated_at,\n deleted_at,\n course_id,\n exercise_id,\n peer_reviews_to_give,\n peer_reviews_to_receive,\n accepting_threshold,\n processing_strategy AS \"processing_strategy: _\",\n manual_review_cutoff_in_days,\n points_are_all_or_nothing,\n review_instructions\nFROM peer_or_self_review_configs\nWHERE id = $1\n AND deleted_at IS NULL\n ", "describe": { "columns": [ { @@ -73,12 +73,31 @@ "ordinal": 11, "name": "points_are_all_or_nothing", "type_info": "Bool" + }, + { + "ordinal": 12, + "name": "review_instructions", + "type_info": "Jsonb" } ], "parameters": { "Left": ["Uuid"] }, - "nullable": [false, false, false, true, false, true, false, false, false, false, false, false] + "nullable": [ + false, + false, + false, + true, + false, + true, + false, + false, + false, + false, + false, + false, + true + ] }, - "hash": "3651de1134b71d12c906908c167535f3c9aae79c268e6b094ebe0bc22b5ee1f9" + "hash": "a6a847fb97b37025b0c95462abf7ce6c57eda7620e0c1c5239e2ac55484aab40" } diff --git a/services/headless-lms/models/.sqlx/query-b57648f4d24a296f9fe2491db7162c90974400f3d523df699448dadde9c660d7.json b/services/headless-lms/models/.sqlx/query-a90d50d9733ae03e37b1deed5d98ead44cf534ec69f417e1c480e52245514d07.json similarity index 50% rename from services/headless-lms/models/.sqlx/query-b57648f4d24a296f9fe2491db7162c90974400f3d523df699448dadde9c660d7.json rename to services/headless-lms/models/.sqlx/query-a90d50d9733ae03e37b1deed5d98ead44cf534ec69f417e1c480e52245514d07.json index cce6c3d9fbbc..c227d0db2ca4 100644 --- a/services/headless-lms/models/.sqlx/query-b57648f4d24a296f9fe2491db7162c90974400f3d523df699448dadde9c660d7.json +++ b/services/headless-lms/models/.sqlx/query-a90d50d9733ae03e37b1deed5d98ead44cf534ec69f417e1c480e52245514d07.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\nSELECT COUNT(*) AS count\nFROM peer_review_submissions\nWHERE exercise_slide_submission_id = $1\n AND deleted_at IS NULL\n ", + "query": "\nSELECT COUNT(*) AS count\nFROM peer_or_self_review_submissions\nWHERE exercise_slide_submission_id = $1\n AND deleted_at IS NULL\n ", "describe": { "columns": [ { @@ -14,5 +14,5 @@ }, "nullable": [null] }, - "hash": "b57648f4d24a296f9fe2491db7162c90974400f3d523df699448dadde9c660d7" + "hash": "a90d50d9733ae03e37b1deed5d98ead44cf534ec69f417e1c480e52245514d07" } diff --git a/services/headless-lms/models/.sqlx/query-6e839a249fbc2dbf1ff7dc7bc4c46bf6b93f0f0ff5f1ca8488e177c46490d389.json b/services/headless-lms/models/.sqlx/query-aabcd9ce6a839e3b5a5e2eb216b7cf6f88d40d08ad59d85becb09122e3d625b4.json similarity index 66% rename from services/headless-lms/models/.sqlx/query-6e839a249fbc2dbf1ff7dc7bc4c46bf6b93f0f0ff5f1ca8488e177c46490d389.json rename to services/headless-lms/models/.sqlx/query-aabcd9ce6a839e3b5a5e2eb216b7cf6f88d40d08ad59d85becb09122e3d625b4.json index 47402bddb8bf..7d4a6b2ecc49 100644 --- a/services/headless-lms/models/.sqlx/query-6e839a249fbc2dbf1ff7dc7bc4c46bf6b93f0f0ff5f1ca8488e177c46490d389.json +++ b/services/headless-lms/models/.sqlx/query-aabcd9ce6a839e3b5a5e2eb216b7cf6f88d40d08ad59d85becb09122e3d625b4.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\nSELECT prqs.*\nFROM peer_review_submissions prs\n JOIN peer_review_question_submissions prqs on prs.id = prqs.peer_review_submission_id\nWHERE prs.exercise_slide_submission_id = $1\n AND prs.deleted_at IS NULL\n AND prqs.deleted_at IS NULL\n ", + "query": "\nSELECT prqs.*\nFROM peer_or_self_review_submissions prs\n JOIN peer_or_self_review_question_submissions prqs on prs.id = prqs.peer_or_self_review_submission_id\nWHERE prs.exercise_slide_submission_id = $1\n AND prs.deleted_at IS NULL\n AND prqs.deleted_at IS NULL\n ", "describe": { "columns": [ { @@ -25,12 +25,12 @@ }, { "ordinal": 4, - "name": "peer_review_question_id", + "name": "peer_or_self_review_question_id", "type_info": "Uuid" }, { "ordinal": 5, - "name": "peer_review_submission_id", + "name": "peer_or_self_review_submission_id", "type_info": "Uuid" }, { @@ -49,5 +49,5 @@ }, "nullable": [false, false, false, true, false, false, true, true] }, - "hash": "6e839a249fbc2dbf1ff7dc7bc4c46bf6b93f0f0ff5f1ca8488e177c46490d389" + "hash": "aabcd9ce6a839e3b5a5e2eb216b7cf6f88d40d08ad59d85becb09122e3d625b4" } diff --git a/services/headless-lms/models/.sqlx/query-acc90e3b064af111f148c11c47b6a5cace02d177c95e0945139398530efe4299.json b/services/headless-lms/models/.sqlx/query-acc90e3b064af111f148c11c47b6a5cace02d177c95e0945139398530efe4299.json deleted file mode 100644 index ed669d952a83..000000000000 --- a/services/headless-lms/models/.sqlx/query-acc90e3b064af111f148c11c47b6a5cace02d177c95e0945139398530efe4299.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "\nUPDATE peer_review_submissions\nSET deleted_at = now()\nWHERE user_id = $1\n AND course_instance_id = $2\n AND deleted_at IS NULL\n", - "describe": { - "columns": [], - "parameters": { - "Left": ["Uuid", "Uuid"] - }, - "nullable": [] - }, - "hash": "acc90e3b064af111f148c11c47b6a5cace02d177c95e0945139398530efe4299" -} diff --git a/services/headless-lms/models/.sqlx/query-b1ebe31e7187448b50b90ec1093d9c54276d3202e292b9d3ec54c15298b0bb44.json b/services/headless-lms/models/.sqlx/query-b1ebe31e7187448b50b90ec1093d9c54276d3202e292b9d3ec54c15298b0bb44.json deleted file mode 100644 index cd87fb49b4b9..000000000000 --- a/services/headless-lms/models/.sqlx/query-b1ebe31e7187448b50b90ec1093d9c54276d3202e292b9d3ec54c15298b0bb44.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "\nINSERT INTO peer_review_configs (\n id,\n course_id,\n exercise_id,\n peer_reviews_to_give,\n peer_reviews_to_receive,\n processing_strategy,\n accepting_threshold\n )\nSELECT uuid_generate_v5($1, id::text),\n $1,\n uuid_generate_v5($1, exercise_id::text),\n peer_reviews_to_give,\n peer_reviews_to_receive,\n processing_strategy,\n accepting_threshold\nFROM peer_review_configs\nWHERE course_id = $2\nAND deleted_at IS NULL;\n ", - "describe": { - "columns": [], - "parameters": { - "Left": ["Uuid", "Uuid"] - }, - "nullable": [] - }, - "hash": "b1ebe31e7187448b50b90ec1093d9c54276d3202e292b9d3ec54c15298b0bb44" -} diff --git a/services/headless-lms/models/.sqlx/query-492184ce49d70f908d34e7cceaf9f471f4d0ea752d79214800dfe0937cf13ed2.json b/services/headless-lms/models/.sqlx/query-b4068938e8568324a7c3e7c304b88044d9388117a6bf01aa7fbdbd731f82c761.json similarity index 69% rename from services/headless-lms/models/.sqlx/query-492184ce49d70f908d34e7cceaf9f471f4d0ea752d79214800dfe0937cf13ed2.json rename to services/headless-lms/models/.sqlx/query-b4068938e8568324a7c3e7c304b88044d9388117a6bf01aa7fbdbd731f82c761.json index 5944d1908389..7723c78e103d 100644 --- a/services/headless-lms/models/.sqlx/query-492184ce49d70f908d34e7cceaf9f471f4d0ea752d79214800dfe0937cf13ed2.json +++ b/services/headless-lms/models/.sqlx/query-b4068938e8568324a7c3e7c304b88044d9388117a6bf01aa7fbdbd731f82c761.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\nSELECT id AS \"id!\",\n answer_required AS \"answer_required!\",\n order_number AS \"order_number!\",\n peer_review_config_id AS \"peer_review_config_id!\",\n question AS \"question!\",\n question_type AS \"question_type!: _\",\n weight AS \"weight!\"\nFROM peer_review_questions\nWHERE id IN (\n SELECT UNNEST($1::uuid [])\n )\n AND deleted_at is null;\n ", + "query": "\nSELECT id AS \"id!\",\n answer_required AS \"answer_required!\",\n order_number AS \"order_number!\",\n peer_or_self_review_config_id AS \"peer_or_self_review_config_id!\",\n question AS \"question!\",\n question_type AS \"question_type!: _\",\n weight AS \"weight!\"\nFROM peer_or_self_review_questions\nWHERE id IN (\n SELECT UNNEST($1::uuid [])\n )\n AND deleted_at is null;\n ", "describe": { "columns": [ { @@ -20,7 +20,7 @@ }, { "ordinal": 3, - "name": "peer_review_config_id!", + "name": "peer_or_self_review_config_id!", "type_info": "Uuid" }, { @@ -51,5 +51,5 @@ }, "nullable": [false, false, false, false, false, false, false] }, - "hash": "492184ce49d70f908d34e7cceaf9f471f4d0ea752d79214800dfe0937cf13ed2" + "hash": "b4068938e8568324a7c3e7c304b88044d9388117a6bf01aa7fbdbd731f82c761" } diff --git a/services/headless-lms/models/.sqlx/query-b6def96f6973719237ce93811fd8a9215bf24529cab71d262f22f11d13ddf526.json b/services/headless-lms/models/.sqlx/query-b6def96f6973719237ce93811fd8a9215bf24529cab71d262f22f11d13ddf526.json index 7025d5c6783d..d0093c16d13c 100644 --- a/services/headless-lms/models/.sqlx/query-b6def96f6973719237ce93811fd8a9215bf24529cab71d262f22f11d13ddf526.json +++ b/services/headless-lms/models/.sqlx/query-b6def96f6973719237ce93811fd8a9215bf24529cab71d262f22f11d13ddf526.json @@ -85,13 +85,18 @@ }, { "ordinal": 16, - "name": "use_course_default_peer_review_config", + "name": "use_course_default_peer_or_self_review_config", "type_info": "Bool" }, { "ordinal": 17, "name": "exercise_language_group_id", "type_info": "Uuid" + }, + { + "ordinal": 18, + "name": "needs_self_review", + "type_info": "Bool" } ], "parameters": { @@ -115,7 +120,8 @@ false, false, false, - true + true, + false ] }, "hash": "b6def96f6973719237ce93811fd8a9215bf24529cab71d262f22f11d13ddf526" diff --git a/services/headless-lms/models/.sqlx/query-b265441f6d457ba57ffe8095657ec04097434e104bc187d4dade61b37023a9d5.json b/services/headless-lms/models/.sqlx/query-bc6df11146cda9ecf56ccaedd4b92a59ce4c62ea2290b1b953016388f6c609b7.json similarity index 81% rename from services/headless-lms/models/.sqlx/query-b265441f6d457ba57ffe8095657ec04097434e104bc187d4dade61b37023a9d5.json rename to services/headless-lms/models/.sqlx/query-bc6df11146cda9ecf56ccaedd4b92a59ce4c62ea2290b1b953016388f6c609b7.json index fd2164263f4c..71ff20399d93 100644 --- a/services/headless-lms/models/.sqlx/query-b265441f6d457ba57ffe8095657ec04097434e104bc187d4dade61b37023a9d5.json +++ b/services/headless-lms/models/.sqlx/query-bc6df11146cda9ecf56ccaedd4b92a59ce4c62ea2290b1b953016388f6c609b7.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\nSELECT id,\n course_id,\n exercise_id,\n peer_reviews_to_give,\n peer_reviews_to_receive,\n accepting_threshold,\n processing_strategy AS \"processing_strategy: _\",\n points_are_all_or_nothing\nFROM peer_review_configs\nWHERE course_id = $1\n AND exercise_id IS NULL\n AND deleted_at IS NULL;\n", + "query": "\nSELECT id,\n course_id,\n exercise_id,\n peer_reviews_to_give,\n peer_reviews_to_receive,\n accepting_threshold,\n processing_strategy AS \"processing_strategy: _\",\n points_are_all_or_nothing,\n review_instructions\nFROM peer_or_self_review_configs\nWHERE course_id = $1\n AND exercise_id IS NULL\n AND deleted_at IS NULL;\n", "describe": { "columns": [ { @@ -53,12 +53,17 @@ "ordinal": 7, "name": "points_are_all_or_nothing", "type_info": "Bool" + }, + { + "ordinal": 8, + "name": "review_instructions", + "type_info": "Jsonb" } ], "parameters": { "Left": ["Uuid"] }, - "nullable": [false, false, true, false, false, false, false, false] + "nullable": [false, false, true, false, false, false, false, false, true] }, - "hash": "b265441f6d457ba57ffe8095657ec04097434e104bc187d4dade61b37023a9d5" + "hash": "bc6df11146cda9ecf56ccaedd4b92a59ce4c62ea2290b1b953016388f6c609b7" } diff --git a/services/headless-lms/models/.sqlx/query-bfa02fde6602f70bb732851aaa2e194cdff03a3562341959cc4b2791bc5176fb.json b/services/headless-lms/models/.sqlx/query-bfa02fde6602f70bb732851aaa2e194cdff03a3562341959cc4b2791bc5176fb.json index 4c1ea552aa2e..e24da97283e0 100644 --- a/services/headless-lms/models/.sqlx/query-bfa02fde6602f70bb732851aaa2e194cdff03a3562341959cc4b2791bc5176fb.json +++ b/services/headless-lms/models/.sqlx/query-bfa02fde6602f70bb732851aaa2e194cdff03a3562341959cc4b2791bc5176fb.json @@ -85,13 +85,18 @@ }, { "ordinal": 16, - "name": "use_course_default_peer_review_config", + "name": "use_course_default_peer_or_self_review_config", "type_info": "Bool" }, { "ordinal": 17, "name": "exercise_language_group_id", "type_info": "Uuid" + }, + { + "ordinal": 18, + "name": "needs_self_review", + "type_info": "Bool" } ], "parameters": { @@ -115,7 +120,8 @@ false, false, false, - true + true, + false ] }, "hash": "bfa02fde6602f70bb732851aaa2e194cdff03a3562341959cc4b2791bc5176fb" diff --git a/services/headless-lms/models/.sqlx/query-c44fe486649e0ad6b4829ec7a5db6e6624bd368f7815c9a28c14b8437e16a249.json b/services/headless-lms/models/.sqlx/query-c44fe486649e0ad6b4829ec7a5db6e6624bd368f7815c9a28c14b8437e16a249.json deleted file mode 100644 index b1a1f3c79a65..000000000000 --- a/services/headless-lms/models/.sqlx/query-c44fe486649e0ad6b4829ec7a5db6e6624bd368f7815c9a28c14b8437e16a249.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "\nINSERT INTO exercises (\n id,\n course_id,\n name,\n deadline,\n page_id,\n score_maximum,\n order_number,\n chapter_id,\n copied_from,\n exercise_language_group_id,\n max_tries_per_slide,\n limit_number_of_tries,\n needs_peer_review,\n use_course_default_peer_review_config\n )\nSELECT uuid_generate_v5($1, id::text),\n $1,\n name,\n deadline,\n uuid_generate_v5($1, page_id::text),\n score_maximum,\n order_number,\n uuid_generate_v5($1, chapter_id::text),\n id,\n exercise_language_group_id,\n max_tries_per_slide,\n limit_number_of_tries,\n needs_peer_review,\n use_course_default_peer_review_config\nFROM exercises\nWHERE course_id = $2\n AND deleted_at IS NULL\nRETURNING id,\n copied_from;\n ", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "id", - "type_info": "Uuid" - }, - { - "ordinal": 1, - "name": "copied_from", - "type_info": "Uuid" - } - ], - "parameters": { - "Left": ["Uuid", "Uuid"] - }, - "nullable": [false, true] - }, - "hash": "c44fe486649e0ad6b4829ec7a5db6e6624bd368f7815c9a28c14b8437e16a249" -} diff --git a/services/headless-lms/models/.sqlx/query-4a0bf7c5a7ca85edec6c14a7c37a494f45735df286c1bba1a83af363e4c2ef34.json b/services/headless-lms/models/.sqlx/query-c823a54750dd455d41df5c5efc4bf57e20266d1f91a1ec6315291b2087041580.json similarity index 56% rename from services/headless-lms/models/.sqlx/query-4a0bf7c5a7ca85edec6c14a7c37a494f45735df286c1bba1a83af363e4c2ef34.json rename to services/headless-lms/models/.sqlx/query-c823a54750dd455d41df5c5efc4bf57e20266d1f91a1ec6315291b2087041580.json index 5cb9b9eee466..fcc2fb195da1 100644 --- a/services/headless-lms/models/.sqlx/query-4a0bf7c5a7ca85edec6c14a7c37a494f45735df286c1bba1a83af363e4c2ef34.json +++ b/services/headless-lms/models/.sqlx/query-c823a54750dd455d41df5c5efc4bf57e20266d1f91a1ec6315291b2087041580.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\nSELECT answers.id AS peer_review_question_submission_id,\n answers.text_data,\n answers.number_data,\n questions.peer_review_config_id,\n questions.id AS peer_review_question_id,\n questions.order_number,\n questions.question,\n questions.question_type AS \"question_type: PeerReviewQuestionType\",\n questions.answer_required,\n submissions.id AS peer_review_submission_id\nFROM peer_review_question_submissions answers\n JOIN peer_review_questions questions ON (\n answers.peer_review_question_id = questions.id\n )\n JOIN peer_review_submissions submissions ON (\n answers.peer_review_submission_id = submissions.id\n )\nWHERE submissions.exercise_slide_submission_id = $1\n AND questions.deleted_at IS NULL\n AND answers.deleted_at IS NULL\n AND submissions.deleted_at IS NULL\n ", + "query": "\nSELECT answers.id AS peer_review_question_submission_id,\n answers.text_data,\n answers.number_data,\n questions.peer_or_self_review_config_id,\n questions.id AS peer_or_self_review_question_id,\n questions.order_number,\n questions.question,\n questions.question_type AS \"question_type: PeerOrSelfReviewQuestionType\",\n questions.answer_required,\n submissions.id AS peer_or_self_review_submission_id\nFROM peer_or_self_review_question_submissions answers\n JOIN peer_or_self_review_questions questions ON (\n answers.peer_or_self_review_question_id = questions.id\n )\n JOIN peer_or_self_review_submissions submissions ON (\n answers.peer_or_self_review_submission_id = submissions.id\n )\nWHERE submissions.exercise_slide_submission_id = $1\n AND questions.deleted_at IS NULL\n AND answers.deleted_at IS NULL\n AND submissions.deleted_at IS NULL\n ", "describe": { "columns": [ { @@ -20,12 +20,12 @@ }, { "ordinal": 3, - "name": "peer_review_config_id", + "name": "peer_or_self_review_config_id", "type_info": "Uuid" }, { "ordinal": 4, - "name": "peer_review_question_id", + "name": "peer_or_self_review_question_id", "type_info": "Uuid" }, { @@ -40,7 +40,7 @@ }, { "ordinal": 7, - "name": "question_type: PeerReviewQuestionType", + "name": "question_type: PeerOrSelfReviewQuestionType", "type_info": { "Custom": { "name": "peer_review_question_type", @@ -57,7 +57,7 @@ }, { "ordinal": 9, - "name": "peer_review_submission_id", + "name": "peer_or_self_review_submission_id", "type_info": "Uuid" } ], @@ -66,5 +66,5 @@ }, "nullable": [false, true, true, false, false, false, false, false, false, false] }, - "hash": "4a0bf7c5a7ca85edec6c14a7c37a494f45735df286c1bba1a83af363e4c2ef34" + "hash": "c823a54750dd455d41df5c5efc4bf57e20266d1f91a1ec6315291b2087041580" } diff --git a/services/headless-lms/models/.sqlx/query-c94f61a635ad062e122b9cf0ed8085858924769294d0c5d6df7b29634b8485d0.json b/services/headless-lms/models/.sqlx/query-c94f61a635ad062e122b9cf0ed8085858924769294d0c5d6df7b29634b8485d0.json new file mode 100644 index 000000000000..b18706a46223 --- /dev/null +++ b/services/headless-lms/models/.sqlx/query-c94f61a635ad062e122b9cf0ed8085858924769294d0c5d6df7b29634b8485d0.json @@ -0,0 +1,12 @@ +{ + "db_name": "PostgreSQL", + "query": "\nINSERT INTO peer_or_self_review_configs (\n id,\n course_id,\n exercise_id,\n peer_reviews_to_give,\n peer_reviews_to_receive,\n processing_strategy,\n accepting_threshold\n )\nSELECT uuid_generate_v5($1, id::text),\n $1,\n uuid_generate_v5($1, exercise_id::text),\n peer_reviews_to_give,\n peer_reviews_to_receive,\n processing_strategy,\n accepting_threshold\nFROM peer_or_self_review_configs\nWHERE course_id = $2\nAND deleted_at IS NULL;\n ", + "describe": { + "columns": [], + "parameters": { + "Left": ["Uuid", "Uuid"] + }, + "nullable": [] + }, + "hash": "c94f61a635ad062e122b9cf0ed8085858924769294d0c5d6df7b29634b8485d0" +} diff --git a/services/headless-lms/models/.sqlx/query-1ea91e201957ee005ad92567c1e9f36210b69a279e6435b747bec1d6580d5a5d.json b/services/headless-lms/models/.sqlx/query-cb16faf7d003f59c41d1419e959f7d9d837fe813173a5b42b88209fea8a72580.json similarity index 81% rename from services/headless-lms/models/.sqlx/query-1ea91e201957ee005ad92567c1e9f36210b69a279e6435b747bec1d6580d5a5d.json rename to services/headless-lms/models/.sqlx/query-cb16faf7d003f59c41d1419e959f7d9d837fe813173a5b42b88209fea8a72580.json index 66705812bae7..7cc187ebb2dc 100644 --- a/services/headless-lms/models/.sqlx/query-1ea91e201957ee005ad92567c1e9f36210b69a279e6435b747bec1d6580d5a5d.json +++ b/services/headless-lms/models/.sqlx/query-cb16faf7d003f59c41d1419e959f7d9d837fe813173a5b42b88209fea8a72580.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\nSELECT *\nFROM peer_review_submissions\nWHERE id = $1\n AND deleted_at IS NULL\n ", + "query": "\nSELECT *\nFROM peer_or_self_review_submissions\nWHERE id = $1\n AND deleted_at IS NULL\n ", "describe": { "columns": [ { @@ -40,7 +40,7 @@ }, { "ordinal": 7, - "name": "peer_review_config_id", + "name": "peer_or_self_review_config_id", "type_info": "Uuid" }, { @@ -54,5 +54,5 @@ }, "nullable": [false, false, false, true, false, false, false, false, false] }, - "hash": "1ea91e201957ee005ad92567c1e9f36210b69a279e6435b747bec1d6580d5a5d" + "hash": "cb16faf7d003f59c41d1419e959f7d9d837fe813173a5b42b88209fea8a72580" } diff --git a/services/headless-lms/models/.sqlx/query-cba4bfc5b328ead58a3c14aa30439ef9daf47a9d25c72f6343d53b8f233965e1.json b/services/headless-lms/models/.sqlx/query-cba4bfc5b328ead58a3c14aa30439ef9daf47a9d25c72f6343d53b8f233965e1.json deleted file mode 100644 index e8944d471cc4..000000000000 --- a/services/headless-lms/models/.sqlx/query-cba4bfc5b328ead58a3c14aa30439ef9daf47a9d25c72f6343d53b8f233965e1.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "\nUPDATE exercises\nSET use_course_default_peer_review_config = $1,\n needs_peer_review = $2\nWHERE id = $3\nRETURNING id;\n ", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "id", - "type_info": "Uuid" - } - ], - "parameters": { - "Left": ["Bool", "Bool", "Uuid"] - }, - "nullable": [false] - }, - "hash": "cba4bfc5b328ead58a3c14aa30439ef9daf47a9d25c72f6343d53b8f233965e1" -} diff --git a/services/headless-lms/models/.sqlx/query-ca2fa41b38eb327686f0890e70145358800c117d275078819886a220d6fb2ff5.json b/services/headless-lms/models/.sqlx/query-d0671a919fe00b66dcfbe3c8df13420b22de7d6703066caf2fca5286e8f163a1.json similarity index 79% rename from services/headless-lms/models/.sqlx/query-ca2fa41b38eb327686f0890e70145358800c117d275078819886a220d6fb2ff5.json rename to services/headless-lms/models/.sqlx/query-d0671a919fe00b66dcfbe3c8df13420b22de7d6703066caf2fca5286e8f163a1.json index 0fc0a628f2d4..c4a5441c201c 100644 --- a/services/headless-lms/models/.sqlx/query-ca2fa41b38eb327686f0890e70145358800c117d275078819886a220d6fb2ff5.json +++ b/services/headless-lms/models/.sqlx/query-d0671a919fe00b66dcfbe3c8df13420b22de7d6703066caf2fca5286e8f163a1.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\nSELECT id,\n created_at,\n updated_at,\n deleted_at,\n peer_review_config_id,\n order_number,\n question,\n question_type AS \"question_type: _\",\n answer_required,\n weight\nFROM peer_review_questions\nWHERE id = $1\n AND deleted_at IS NULL;\n ", + "query": "\nSELECT id,\n created_at,\n updated_at,\n deleted_at,\n peer_or_self_review_config_id,\n order_number,\n question,\n question_type AS \"question_type: _\",\n answer_required,\n weight\nFROM peer_or_self_review_questions\nWHERE id = $1\n AND deleted_at IS NULL;\n ", "describe": { "columns": [ { @@ -25,7 +25,7 @@ }, { "ordinal": 4, - "name": "peer_review_config_id", + "name": "peer_or_self_review_config_id", "type_info": "Uuid" }, { @@ -66,5 +66,5 @@ }, "nullable": [false, false, false, true, false, false, false, false, false, false] }, - "hash": "ca2fa41b38eb327686f0890e70145358800c117d275078819886a220d6fb2ff5" + "hash": "d0671a919fe00b66dcfbe3c8df13420b22de7d6703066caf2fca5286e8f163a1" } diff --git a/services/headless-lms/models/.sqlx/query-bb73fd455c67f662d19807aa949cb02bdfc0f416e55e0688f5b3b271a7b1e4bd.json b/services/headless-lms/models/.sqlx/query-d175019dc54708cfb5a60b4ccbe661fef6dfa77348ecda2e476d2be9fd899b4c.json similarity index 77% rename from services/headless-lms/models/.sqlx/query-bb73fd455c67f662d19807aa949cb02bdfc0f416e55e0688f5b3b271a7b1e4bd.json rename to services/headless-lms/models/.sqlx/query-d175019dc54708cfb5a60b4ccbe661fef6dfa77348ecda2e476d2be9fd899b4c.json index 3ddb2ed3fa25..3f9fdf3d107d 100644 --- a/services/headless-lms/models/.sqlx/query-bb73fd455c67f662d19807aa949cb02bdfc0f416e55e0688f5b3b271a7b1e4bd.json +++ b/services/headless-lms/models/.sqlx/query-d175019dc54708cfb5a60b4ccbe661fef6dfa77348ecda2e476d2be9fd899b4c.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\nSELECT id,\n created_at,\n updated_at,\n deleted_at,\n peer_review_config_id,\n order_number,\n question,\n question_type AS \"question_type: _\",\n answer_required,\n weight\nFROM peer_review_questions\nWHERE peer_review_config_id = $1\n AND deleted_at IS NULL;\n ", + "query": "\nSELECT id,\n created_at,\n updated_at,\n deleted_at,\n peer_or_self_review_config_id,\n order_number,\n question,\n question_type AS \"question_type: _\",\n answer_required,\n weight\nFROM peer_or_self_review_questions\nWHERE peer_or_self_review_config_id = $1\n AND deleted_at IS NULL;\n ", "describe": { "columns": [ { @@ -25,7 +25,7 @@ }, { "ordinal": 4, - "name": "peer_review_config_id", + "name": "peer_or_self_review_config_id", "type_info": "Uuid" }, { @@ -66,5 +66,5 @@ }, "nullable": [false, false, false, true, false, false, false, false, false, false] }, - "hash": "bb73fd455c67f662d19807aa949cb02bdfc0f416e55e0688f5b3b271a7b1e4bd" + "hash": "d175019dc54708cfb5a60b4ccbe661fef6dfa77348ecda2e476d2be9fd899b4c" } diff --git a/services/headless-lms/models/.sqlx/query-41be4f4cee17aa5c37896ab21d8b8578ca5474fb4dfbcdde8a797d63d1a51855.json b/services/headless-lms/models/.sqlx/query-d1ba494cd325829f292ed0d229e9c41d82a18ac7d9d169380f421ce1de5a7180.json similarity index 77% rename from services/headless-lms/models/.sqlx/query-41be4f4cee17aa5c37896ab21d8b8578ca5474fb4dfbcdde8a797d63d1a51855.json rename to services/headless-lms/models/.sqlx/query-d1ba494cd325829f292ed0d229e9c41d82a18ac7d9d169380f421ce1de5a7180.json index 4cb38e54396f..76f20c14d796 100644 --- a/services/headless-lms/models/.sqlx/query-41be4f4cee17aa5c37896ab21d8b8578ca5474fb4dfbcdde8a797d63d1a51855.json +++ b/services/headless-lms/models/.sqlx/query-d1ba494cd325829f292ed0d229e9c41d82a18ac7d9d169380f421ce1de5a7180.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\nSELECT pr.id as id,\n pr.course_id as course_id,\n pr.exercise_id as exercise_id,\n pr.peer_reviews_to_give as peer_reviews_to_give,\n pr.peer_reviews_to_receive as peer_reviews_to_receive,\n pr.accepting_threshold as accepting_threshold,\n pr.processing_strategy AS \"processing_strategy: _\",\n points_are_all_or_nothing\nfrom pages p\n join exercises e on p.id = e.page_id\n join peer_review_configs pr on e.id = pr.exercise_id\nwhere p.id = $1\n AND p.deleted_at IS NULL\n AND e.deleted_at IS NULL\n AND pr.deleted_at IS NULL;\n ", + "query": "\nSELECT pr.id as id,\n pr.course_id as course_id,\n pr.exercise_id as exercise_id,\n pr.peer_reviews_to_give as peer_reviews_to_give,\n pr.peer_reviews_to_receive as peer_reviews_to_receive,\n pr.accepting_threshold as accepting_threshold,\n pr.processing_strategy AS \"processing_strategy: _\",\n points_are_all_or_nothing,\n pr.review_instructions\nfrom pages p\n join exercises e on p.id = e.page_id\n join peer_or_self_review_configs pr on e.id = pr.exercise_id\nwhere p.id = $1\n AND p.deleted_at IS NULL\n AND e.deleted_at IS NULL\n AND pr.deleted_at IS NULL;\n ", "describe": { "columns": [ { @@ -53,12 +53,17 @@ "ordinal": 7, "name": "points_are_all_or_nothing", "type_info": "Bool" + }, + { + "ordinal": 8, + "name": "review_instructions", + "type_info": "Jsonb" } ], "parameters": { "Left": ["Uuid"] }, - "nullable": [false, false, true, false, false, false, false, false] + "nullable": [false, false, true, false, false, false, false, false, true] }, - "hash": "41be4f4cee17aa5c37896ab21d8b8578ca5474fb4dfbcdde8a797d63d1a51855" + "hash": "d1ba494cd325829f292ed0d229e9c41d82a18ac7d9d169380f421ce1de5a7180" } diff --git a/services/headless-lms/models/.sqlx/query-b93dd0d1fc364bd30512ffeb2a8467fc2aac096dfd7a06cea12711d095afd98d.json b/services/headless-lms/models/.sqlx/query-d698c6417b86c08f3aed67d6de15f7765060e595a891843a25e6a397f6fefb1a.json similarity index 52% rename from services/headless-lms/models/.sqlx/query-b93dd0d1fc364bd30512ffeb2a8467fc2aac096dfd7a06cea12711d095afd98d.json rename to services/headless-lms/models/.sqlx/query-d698c6417b86c08f3aed67d6de15f7765060e595a891843a25e6a397f6fefb1a.json index a41edd7eb8b9..bb090803c484 100644 --- a/services/headless-lms/models/.sqlx/query-b93dd0d1fc364bd30512ffeb2a8467fc2aac096dfd7a06cea12711d095afd98d.json +++ b/services/headless-lms/models/.sqlx/query-d698c6417b86c08f3aed67d6de15f7765060e595a891843a25e6a397f6fefb1a.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\nSELECT answers.id AS peer_review_question_submission_id,\n answers.text_data,\n answers.number_data,\n questions.peer_review_config_id,\n questions.id AS peer_review_question_id,\n questions.order_number,\n questions.question,\n questions.question_type AS \"question_type: PeerReviewQuestionType\",\n questions.answer_required,\n submissions.id AS peer_review_submission_id\nFROM peer_review_question_submissions answers\n JOIN peer_review_questions questions ON (\n answers.peer_review_question_id = questions.id\n )\n JOIN peer_review_submissions submissions ON (\n answers.peer_review_submission_id = submissions.id\n )\nWHERE submissions.user_id = $1\n AND submissions.exercise_id = $2\n AND submissions.course_instance_id = $3\n AND questions.deleted_at IS NULL\n AND answers.deleted_at IS NULL\n AND submissions.deleted_at IS NULL\n ", + "query": "\nSELECT answers.id AS peer_review_question_submission_id,\n answers.text_data,\n answers.number_data,\n questions.peer_or_self_review_config_id,\n questions.id AS peer_or_self_review_question_id,\n questions.order_number,\n questions.question,\n questions.question_type AS \"question_type: PeerOrSelfReviewQuestionType\",\n questions.answer_required,\n submissions.id AS peer_or_self_review_submission_id\nFROM peer_or_self_review_question_submissions answers\n JOIN peer_or_self_review_questions questions ON (\n answers.peer_or_self_review_question_id = questions.id\n )\n JOIN peer_or_self_review_submissions submissions ON (\n answers.peer_or_self_review_submission_id = submissions.id\n )\n JOIN exercise_slide_submissions ess ON (\n submissions.exercise_slide_submission_id = ess.id\n )\nWHERE submissions.user_id = $1\n AND submissions.exercise_id = $2\n AND submissions.course_instance_id = $3\n AND questions.deleted_at IS NULL\n AND answers.deleted_at IS NULL\n AND submissions.deleted_at IS NULL\n AND ess.user_id != $1\n ", "describe": { "columns": [ { @@ -20,12 +20,12 @@ }, { "ordinal": 3, - "name": "peer_review_config_id", + "name": "peer_or_self_review_config_id", "type_info": "Uuid" }, { "ordinal": 4, - "name": "peer_review_question_id", + "name": "peer_or_self_review_question_id", "type_info": "Uuid" }, { @@ -40,7 +40,7 @@ }, { "ordinal": 7, - "name": "question_type: PeerReviewQuestionType", + "name": "question_type: PeerOrSelfReviewQuestionType", "type_info": { "Custom": { "name": "peer_review_question_type", @@ -57,7 +57,7 @@ }, { "ordinal": 9, - "name": "peer_review_submission_id", + "name": "peer_or_self_review_submission_id", "type_info": "Uuid" } ], @@ -66,5 +66,5 @@ }, "nullable": [false, true, true, false, false, false, false, false, false, false] }, - "hash": "b93dd0d1fc364bd30512ffeb2a8467fc2aac096dfd7a06cea12711d095afd98d" + "hash": "d698c6417b86c08f3aed67d6de15f7765060e595a891843a25e6a397f6fefb1a" } diff --git a/services/headless-lms/models/.sqlx/query-fb235a44fd752f62c6638cfbeb180dd2a78ea583ff2eb77b43892991273b3cd0.json b/services/headless-lms/models/.sqlx/query-db452fe203f60d2ac53f96a2534189bf8a2045771daef30ebe40ad00c7d4c712.json similarity index 78% rename from services/headless-lms/models/.sqlx/query-fb235a44fd752f62c6638cfbeb180dd2a78ea583ff2eb77b43892991273b3cd0.json rename to services/headless-lms/models/.sqlx/query-db452fe203f60d2ac53f96a2534189bf8a2045771daef30ebe40ad00c7d4c712.json index 53d7df48ce19..f49b455d6765 100644 --- a/services/headless-lms/models/.sqlx/query-fb235a44fd752f62c6638cfbeb180dd2a78ea583ff2eb77b43892991273b3cd0.json +++ b/services/headless-lms/models/.sqlx/query-db452fe203f60d2ac53f96a2534189bf8a2045771daef30ebe40ad00c7d4c712.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\nSELECT id,\n created_at,\n updated_at,\n deleted_at,\n course_id,\n exercise_id,\n peer_reviews_to_give,\n peer_reviews_to_receive,\n accepting_threshold,\n processing_strategy AS \"processing_strategy: _\",\n manual_review_cutoff_in_days,\n points_are_all_or_nothing\nFROM peer_review_configs\nWHERE exercise_id = $1\n AND deleted_at IS NULL\n ", + "query": "\nSELECT id,\n created_at,\n updated_at,\n deleted_at,\n course_id,\n exercise_id,\n peer_reviews_to_give,\n peer_reviews_to_receive,\n accepting_threshold,\n processing_strategy AS \"processing_strategy: _\",\n manual_review_cutoff_in_days,\n points_are_all_or_nothing,\n review_instructions\nFROM peer_or_self_review_configs\nWHERE exercise_id = $1\n AND deleted_at IS NULL\n ", "describe": { "columns": [ { @@ -73,12 +73,31 @@ "ordinal": 11, "name": "points_are_all_or_nothing", "type_info": "Bool" + }, + { + "ordinal": 12, + "name": "review_instructions", + "type_info": "Jsonb" } ], "parameters": { "Left": ["Uuid"] }, - "nullable": [false, false, false, true, false, true, false, false, false, false, false, false] + "nullable": [ + false, + false, + false, + true, + false, + true, + false, + false, + false, + false, + false, + false, + true + ] }, - "hash": "fb235a44fd752f62c6638cfbeb180dd2a78ea583ff2eb77b43892991273b3cd0" + "hash": "db452fe203f60d2ac53f96a2534189bf8a2045771daef30ebe40ad00c7d4c712" } diff --git a/services/headless-lms/models/.sqlx/query-e33938dcc21416adf06acb53f7d94fd179f095c8fc605c1bd3dfa6037497e04d.json b/services/headless-lms/models/.sqlx/query-e33938dcc21416adf06acb53f7d94fd179f095c8fc605c1bd3dfa6037497e04d.json deleted file mode 100644 index 0e764270630f..000000000000 --- a/services/headless-lms/models/.sqlx/query-e33938dcc21416adf06acb53f7d94fd179f095c8fc605c1bd3dfa6037497e04d.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "\nUPDATE peer_review_questions\nSET deleted_at = now()\nWHERE peer_review_config_id = ANY ($1)\nAND deleted_at IS NULL\nRETURNING id;\n ", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "id", - "type_info": "Uuid" - } - ], - "parameters": { - "Left": ["UuidArray"] - }, - "nullable": [false] - }, - "hash": "e33938dcc21416adf06acb53f7d94fd179f095c8fc605c1bd3dfa6037497e04d" -} diff --git a/services/headless-lms/models/.sqlx/query-be08e7992f9a64d794d4206a684839d3a801f21e5d425ad472d36cea47c8e0a3.json b/services/headless-lms/models/.sqlx/query-e410ef993eeeb7c28e4f268a5e1c86cc0b2cf0469e84764bc1219da28320dc53.json similarity index 59% rename from services/headless-lms/models/.sqlx/query-be08e7992f9a64d794d4206a684839d3a801f21e5d425ad472d36cea47c8e0a3.json rename to services/headless-lms/models/.sqlx/query-e410ef993eeeb7c28e4f268a5e1c86cc0b2cf0469e84764bc1219da28320dc53.json index e9599a14d10b..0925cfdc2e05 100644 --- a/services/headless-lms/models/.sqlx/query-be08e7992f9a64d794d4206a684839d3a801f21e5d425ad472d36cea47c8e0a3.json +++ b/services/headless-lms/models/.sqlx/query-e410ef993eeeb7c28e4f268a5e1c86cc0b2cf0469e84764bc1219da28320dc53.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\n INSERT INTO peer_review_configs (\n id,\n course_id,\n exercise_id,\n peer_reviews_to_give,\n peer_reviews_to_receive,\n accepting_threshold,\n processing_strategy,\n points_are_all_or_nothing\n )\nVALUES ($1, $2, $3, $4, $5, $6, $7, $8) ON CONFLICT (id) DO\nUPDATE\nSET course_id = excluded.course_id,\n exercise_id = excluded.exercise_id,\n peer_reviews_to_give = excluded.peer_reviews_to_give,\n peer_reviews_to_receive = excluded.peer_reviews_to_receive,\n accepting_threshold = excluded.accepting_threshold,\n processing_strategy = excluded.processing_strategy,\n points_are_all_or_nothing = excluded.points_are_all_or_nothing\nRETURNING id,\n course_id,\n exercise_id,\n peer_reviews_to_give,\n peer_reviews_to_receive,\n accepting_threshold,\n processing_strategy AS \"processing_strategy:_\",\n points_are_all_or_nothing\n", + "query": "\n INSERT INTO peer_or_self_review_configs (\n id,\n course_id,\n exercise_id,\n peer_reviews_to_give,\n peer_reviews_to_receive,\n accepting_threshold,\n processing_strategy,\n points_are_all_or_nothing,\n review_instructions\n )\nVALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9) ON CONFLICT (id) DO\nUPDATE\nSET course_id = excluded.course_id,\n exercise_id = excluded.exercise_id,\n peer_reviews_to_give = excluded.peer_reviews_to_give,\n peer_reviews_to_receive = excluded.peer_reviews_to_receive,\n accepting_threshold = excluded.accepting_threshold,\n processing_strategy = excluded.processing_strategy,\n points_are_all_or_nothing = excluded.points_are_all_or_nothing,\n review_instructions = excluded.review_instructions\nRETURNING id,\n course_id,\n exercise_id,\n peer_reviews_to_give,\n peer_reviews_to_receive,\n accepting_threshold,\n processing_strategy AS \"processing_strategy:_\",\n points_are_all_or_nothing,\n review_instructions\n", "describe": { "columns": [ { @@ -53,6 +53,11 @@ "ordinal": 7, "name": "points_are_all_or_nothing", "type_info": "Bool" + }, + { + "ordinal": 8, + "name": "review_instructions", + "type_info": "Jsonb" } ], "parameters": { @@ -75,10 +80,11 @@ } } }, - "Bool" + "Bool", + "Jsonb" ] }, - "nullable": [false, false, true, false, false, false, false, false] + "nullable": [false, false, true, false, false, false, false, false, true] }, - "hash": "be08e7992f9a64d794d4206a684839d3a801f21e5d425ad472d36cea47c8e0a3" + "hash": "e410ef993eeeb7c28e4f268a5e1c86cc0b2cf0469e84764bc1219da28320dc53" } diff --git a/services/headless-lms/models/.sqlx/query-63d58cdbacf38184f86b054d840c4d9a5dead359588dd2c6f028003211e0184e.json b/services/headless-lms/models/.sqlx/query-e8b0f5ea7d7294abc656ec6bba523fa4046538d19f6927e8bb28626fceb27a23.json similarity index 62% rename from services/headless-lms/models/.sqlx/query-63d58cdbacf38184f86b054d840c4d9a5dead359588dd2c6f028003211e0184e.json rename to services/headless-lms/models/.sqlx/query-e8b0f5ea7d7294abc656ec6bba523fa4046538d19f6927e8bb28626fceb27a23.json index b4de98a86ec4..c1eff9592c34 100644 --- a/services/headless-lms/models/.sqlx/query-63d58cdbacf38184f86b054d840c4d9a5dead359588dd2c6f028003211e0184e.json +++ b/services/headless-lms/models/.sqlx/query-e8b0f5ea7d7294abc656ec6bba523fa4046538d19f6927e8bb28626fceb27a23.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\nINSERT INTO peer_review_questions (\n id,\n peer_review_config_id,\n order_number,\n question,\n question_type\n )\nVALUES ($1, $2, $3, $4, $5)\nRETURNING id\n ", + "query": "\nINSERT INTO peer_or_self_review_questions (\n id,\n peer_or_self_review_config_id,\n order_number,\n question,\n question_type\n )\nVALUES ($1, $2, $3, $4, $5)\nRETURNING id\n ", "describe": { "columns": [ { @@ -27,5 +27,5 @@ }, "nullable": [false] }, - "hash": "63d58cdbacf38184f86b054d840c4d9a5dead359588dd2c6f028003211e0184e" + "hash": "e8b0f5ea7d7294abc656ec6bba523fa4046538d19f6927e8bb28626fceb27a23" } diff --git a/services/headless-lms/models/.sqlx/query-e16b5381eb2fe049dd9ec702a117114ff11db906f5e7beb4b6961eb3bf8033fb.json b/services/headless-lms/models/.sqlx/query-f4b0c47d2a06ae02e9ff2174d67969b06cd246783b4c1a885b2ce706648a089d.json similarity index 77% rename from services/headless-lms/models/.sqlx/query-e16b5381eb2fe049dd9ec702a117114ff11db906f5e7beb4b6961eb3bf8033fb.json rename to services/headless-lms/models/.sqlx/query-f4b0c47d2a06ae02e9ff2174d67969b06cd246783b4c1a885b2ce706648a089d.json index b860374d2761..1bf0acbf6731 100644 --- a/services/headless-lms/models/.sqlx/query-e16b5381eb2fe049dd9ec702a117114ff11db906f5e7beb4b6961eb3bf8033fb.json +++ b/services/headless-lms/models/.sqlx/query-f4b0c47d2a06ae02e9ff2174d67969b06cd246783b4c1a885b2ce706648a089d.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\nSELECT id,\n created_at,\n updated_at,\n deleted_at,\n course_id,\n exercise_id,\n peer_reviews_to_give,\n peer_reviews_to_receive,\n accepting_threshold,\n processing_strategy AS \"processing_strategy: _\",\n manual_review_cutoff_in_days,\n points_are_all_or_nothing\nFROM peer_review_configs\nWHERE id = $1\n AND deleted_at IS NULL\n ", + "query": "\nSELECT id,\n created_at,\n updated_at,\n deleted_at,\n course_id,\n exercise_id,\n peer_reviews_to_give,\n peer_reviews_to_receive,\n accepting_threshold,\n processing_strategy AS \"processing_strategy: _\",\n manual_review_cutoff_in_days,\n points_are_all_or_nothing,\n review_instructions\nFROM peer_or_self_review_configs\nWHERE course_id = $1\n AND exercise_id IS NULL\n AND deleted_at IS NULL;\n ", "describe": { "columns": [ { @@ -73,12 +73,31 @@ "ordinal": 11, "name": "points_are_all_or_nothing", "type_info": "Bool" + }, + { + "ordinal": 12, + "name": "review_instructions", + "type_info": "Jsonb" } ], "parameters": { "Left": ["Uuid"] }, - "nullable": [false, false, false, true, false, true, false, false, false, false, false, false] + "nullable": [ + false, + false, + false, + true, + false, + true, + false, + false, + false, + false, + false, + false, + true + ] }, - "hash": "e16b5381eb2fe049dd9ec702a117114ff11db906f5e7beb4b6961eb3bf8033fb" + "hash": "f4b0c47d2a06ae02e9ff2174d67969b06cd246783b4c1a885b2ce706648a089d" } diff --git a/services/headless-lms/models/.sqlx/query-e046182cc942742134b0ed817a6094b03ae7a76696e5052d7b17fbf432adc205.json b/services/headless-lms/models/.sqlx/query-fab4cf475d4a0c303a0e481e6aac367795398a494bab15ea968837fc63ce4e00.json similarity index 56% rename from services/headless-lms/models/.sqlx/query-e046182cc942742134b0ed817a6094b03ae7a76696e5052d7b17fbf432adc205.json rename to services/headless-lms/models/.sqlx/query-fab4cf475d4a0c303a0e481e6aac367795398a494bab15ea968837fc63ce4e00.json index 29dab207d7f2..2c8535450727 100644 --- a/services/headless-lms/models/.sqlx/query-e046182cc942742134b0ed817a6094b03ae7a76696e5052d7b17fbf432adc205.json +++ b/services/headless-lms/models/.sqlx/query-fab4cf475d4a0c303a0e481e6aac367795398a494bab15ea968837fc63ce4e00.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\nUPDATE peer_review_configs\nSET deleted_at = now()\nWHERE id = $1\nRETURNING id\n ", + "query": "\nUPDATE peer_or_self_review_configs\nSET deleted_at = now()\nWHERE id = $1\nRETURNING id\n ", "describe": { "columns": [ { @@ -14,5 +14,5 @@ }, "nullable": [false] }, - "hash": "e046182cc942742134b0ed817a6094b03ae7a76696e5052d7b17fbf432adc205" + "hash": "fab4cf475d4a0c303a0e481e6aac367795398a494bab15ea968837fc63ce4e00" } diff --git a/services/headless-lms/models/.sqlx/query-3a3948719f24eb9fdf35167a3863826861af3fa43f1441cfe5fea5ab939a8cf7.json b/services/headless-lms/models/.sqlx/query-fba0d3cdf5549862a9a8195a8427271b573ac84ab1fe2f8956d02e8f96551625.json similarity index 71% rename from services/headless-lms/models/.sqlx/query-3a3948719f24eb9fdf35167a3863826861af3fa43f1441cfe5fea5ab939a8cf7.json rename to services/headless-lms/models/.sqlx/query-fba0d3cdf5549862a9a8195a8427271b573ac84ab1fe2f8956d02e8f96551625.json index d4cae6509126..fb1b7167e6b3 100644 --- a/services/headless-lms/models/.sqlx/query-3a3948719f24eb9fdf35167a3863826861af3fa43f1441cfe5fea5ab939a8cf7.json +++ b/services/headless-lms/models/.sqlx/query-fba0d3cdf5549862a9a8195a8427271b573ac84ab1fe2f8956d02e8f96551625.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\nSELECT id,\n peer_review_config_id,\n order_number,\n question_type AS \"question_type: _\",\n question,\n answer_required,\n weight\nFROM peer_review_questions\nwhere peer_review_config_id = $1\n AND deleted_at IS NULL;\n ", + "query": "\nSELECT id,\n peer_or_self_review_config_id,\n order_number,\n question_type AS \"question_type: _\",\n question,\n answer_required,\n weight\nFROM peer_or_self_review_questions\nwhere peer_or_self_review_config_id = $1\n AND deleted_at IS NULL;\n ", "describe": { "columns": [ { @@ -10,7 +10,7 @@ }, { "ordinal": 1, - "name": "peer_review_config_id", + "name": "peer_or_self_review_config_id", "type_info": "Uuid" }, { @@ -51,5 +51,5 @@ }, "nullable": [false, false, false, false, false, false, false] }, - "hash": "3a3948719f24eb9fdf35167a3863826861af3fa43f1441cfe5fea5ab939a8cf7" + "hash": "fba0d3cdf5549862a9a8195a8427271b573ac84ab1fe2f8956d02e8f96551625" } diff --git a/services/headless-lms/models/.sqlx/query-fbd8ef8a5c023482f68a66ab62a655033ffb347b33756eaa8573e3c122ea1e92.json b/services/headless-lms/models/.sqlx/query-fbd8ef8a5c023482f68a66ab62a655033ffb347b33756eaa8573e3c122ea1e92.json new file mode 100644 index 000000000000..23b97e884ad6 --- /dev/null +++ b/services/headless-lms/models/.sqlx/query-fbd8ef8a5c023482f68a66ab62a655033ffb347b33756eaa8573e3c122ea1e92.json @@ -0,0 +1,58 @@ +{ + "db_name": "PostgreSQL", + "query": "\nSELECT prs.*\nFROM peer_or_self_review_submissions prs\nJOIN exercise_slide_submissions ess ON (ess.id = prs.exercise_slide_submission_id)\nWHERE ess.user_id = $1\n AND prs.exercise_id = $2\n AND prs.course_instance_id = $3\n AND prs.deleted_at IS NULL\n AND ess.deleted_at IS NULL\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Uuid" + }, + { + "ordinal": 1, + "name": "created_at", + "type_info": "Timestamptz" + }, + { + "ordinal": 2, + "name": "updated_at", + "type_info": "Timestamptz" + }, + { + "ordinal": 3, + "name": "deleted_at", + "type_info": "Timestamptz" + }, + { + "ordinal": 4, + "name": "user_id", + "type_info": "Uuid" + }, + { + "ordinal": 5, + "name": "exercise_id", + "type_info": "Uuid" + }, + { + "ordinal": 6, + "name": "course_instance_id", + "type_info": "Uuid" + }, + { + "ordinal": 7, + "name": "peer_or_self_review_config_id", + "type_info": "Uuid" + }, + { + "ordinal": 8, + "name": "exercise_slide_submission_id", + "type_info": "Uuid" + } + ], + "parameters": { + "Left": ["Uuid", "Uuid", "Uuid"] + }, + "nullable": [false, false, false, true, false, false, false, false, false] + }, + "hash": "fbd8ef8a5c023482f68a66ab62a655033ffb347b33756eaa8573e3c122ea1e92" +} diff --git a/services/headless-lms/models/src/course_instances.rs b/services/headless-lms/models/src/course_instances.rs index 8d2c043c54b1..43a18d6fb2b0 100644 --- a/services/headless-lms/models/src/course_instances.rs +++ b/services/headless-lms/models/src/course_instances.rs @@ -632,7 +632,7 @@ WHERE user_id = $1 .await?; sqlx::query!( " -UPDATE peer_review_submissions +UPDATE peer_or_self_review_submissions SET deleted_at = now() WHERE user_id = $1 AND course_instance_id = $2 @@ -645,11 +645,11 @@ WHERE user_id = $1 .await?; sqlx::query!( " -UPDATE peer_review_question_submissions +UPDATE peer_or_self_review_question_submissions SET deleted_at = now() -WHERE peer_review_submission_id IN ( +WHERE peer_or_self_review_submission_id IN ( SELECT id - FROM peer_review_submissions + FROM peer_or_self_review_submissions WHERE user_id = $1 AND course_instance_id = $2 ) diff --git a/services/headless-lms/models/src/exercise_task_submissions.rs b/services/headless-lms/models/src/exercise_task_submissions.rs index 3159cad63935..19a6eec63fc3 100644 --- a/services/headless-lms/models/src/exercise_task_submissions.rs +++ b/services/headless-lms/models/src/exercise_task_submissions.rs @@ -9,8 +9,9 @@ use crate::{ exercise_services, exercise_slide_submissions, exercise_tasks::{CourseMaterialExerciseTask, ExerciseTask}, library::custom_view_exercises::{CustomViewExerciseTaskSubmission, CustomViewExerciseTasks}, - peer_review_question_submissions::PeerReviewQuestionSubmission, - peer_review_questions::PeerReviewQuestion, + peer_or_self_review_question_submissions::PeerOrSelfReviewQuestionSubmission, + peer_or_self_review_questions::PeerOrSelfReviewQuestion, + peer_or_self_review_submissions::PeerOrSelfReviewSubmission, prelude::*, CourseOrExamId, }; @@ -32,9 +33,10 @@ pub struct ExerciseTaskSubmission { #[derive(Debug, Serialize, Deserialize, PartialEq, Clone)] #[cfg_attr(feature = "ts_rs", derive(TS))] -pub struct PeerReviewsRecieved { - pub peer_review_questions: Vec, - pub peer_review_question_submissions: Vec, +pub struct PeerOrSelfReviewsReceived { + pub peer_or_self_review_questions: Vec, + pub peer_or_self_review_question_submissions: Vec, + pub peer_or_self_review_submissions: Vec, } #[derive(Debug, Serialize, Deserialize, PartialEq, Clone)] @@ -245,41 +247,56 @@ pub async fn get_peer_reviews_received( exercise_id: Uuid, exercise_slide_submission_id: Uuid, user_id: Uuid, -) -> ModelResult { +) -> ModelResult { let exercise = crate::exercises::get_by_id(&mut *conn, exercise_id).await?; - let peer_review_config = crate::peer_review_configs::get_by_exercise_or_course_id( - &mut *conn, - &exercise, - exercise.course_id.ok_or_else(|| { - ModelError::new( - ModelErrorType::InvalidRequest, - "Peer reviews work only on courses (and not, for example, on exams)".to_string(), - None, - ) - })?, - ) - .await?; - let peer_review_questions = crate::peer_review_questions::get_by_peer_review_configs_id( - &mut *conn, - peer_review_config.id, - ) - .await?; + let peer_or_self_review_config = + crate::peer_or_self_review_configs::get_by_exercise_or_course_id( + &mut *conn, + &exercise, + exercise.course_id.ok_or_else(|| { + ModelError::new( + ModelErrorType::InvalidRequest, + "Peer reviews work only on courses (and not, for example, on exams)" + .to_string(), + None, + ) + })?, + ) + .await?; + let peer_or_self_review_questions = + crate::peer_or_self_review_questions::get_by_peer_or_self_review_configs_id( + &mut *conn, + peer_or_self_review_config.id, + ) + .await?; + + let peer_or_self_review_question_ids = peer_or_self_review_questions + .iter() + .map(|x| (x.id)) + .collect::>(); + + let peer_or_self_review_submissions = + crate::peer_or_self_review_submissions::get_received_peer_or_self_review_submissions_for_user_by_peer_or_self_review_config_id_and_exercise_slide_submission( + &mut *conn, + user_id, + exercise_slide_submission_id, + peer_or_self_review_config.id, + ) + .await?; - let peer_review_question_submissions = - crate::peer_review_question_submissions::get_by_peer_reviews_question_ids( + let peer_or_self_review_question_submissions = + crate::peer_or_self_review_question_submissions::get_by_peer_reviews_question_ids( &mut *conn, - &peer_review_questions - .iter() - .map(|x| (x.id)) - .collect::>(), + &peer_or_self_review_question_ids, user_id, exercise_slide_submission_id, ) .await?; - Ok(PeerReviewsRecieved { - peer_review_questions, - peer_review_question_submissions, + Ok(PeerOrSelfReviewsReceived { + peer_or_self_review_questions, + peer_or_self_review_question_submissions, + peer_or_self_review_submissions, }) } diff --git a/services/headless-lms/models/src/exercises.rs b/services/headless-lms/models/src/exercises.rs index 984f5038d1ff..fda8754d974f 100644 --- a/services/headless-lms/models/src/exercises.rs +++ b/services/headless-lms/models/src/exercises.rs @@ -10,11 +10,11 @@ use crate::{ }, exercise_slides::{self, CourseMaterialExerciseSlide}, exercise_tasks, - peer_review_configs::CourseMaterialPeerReviewConfig, - peer_review_question_submissions::PeerReviewQuestionSubmission, - peer_review_questions::PeerReviewQuestion, + peer_or_self_review_configs::CourseMaterialPeerOrSelfReviewConfig, + peer_or_self_review_question_submissions::PeerOrSelfReviewQuestionSubmission, + peer_or_self_review_questions::PeerOrSelfReviewQuestion, + peer_or_self_review_submissions::PeerOrSelfReviewSubmission, peer_review_queue_entries::PeerReviewQueueEntry, - peer_review_submissions::PeerReviewSubmission, prelude::*, teacher_grading_decisions::{TeacherDecisionType, TeacherGradingDecision}, user_course_instance_exercise_service_variables::UserCourseInstanceExerciseServiceVariable, @@ -43,7 +43,8 @@ pub struct Exercise { pub max_tries_per_slide: Option, pub limit_number_of_tries: bool, pub needs_peer_review: bool, - pub use_course_default_peer_review_config: bool, + pub needs_self_review: bool, + pub use_course_default_peer_or_self_review_config: bool, pub exercise_language_group_id: Option, } @@ -77,13 +78,13 @@ pub struct ExerciseStatusSummaryForUser { pub exercise: Exercise, pub user_exercise_state: Option, pub exercise_slide_submissions: Vec, - pub given_peer_review_submissions: Vec, - pub given_peer_review_question_submissions: Vec, - pub received_peer_review_submissions: Vec, - pub received_peer_review_question_submissions: Vec, + pub given_peer_or_self_review_submissions: Vec, + pub given_peer_or_self_review_question_submissions: Vec, + pub received_peer_or_self_review_submissions: Vec, + pub received_peer_or_self_review_question_submissions: Vec, pub peer_review_queue_entry: Option, pub teacher_grading_decision: Option, - pub peer_review_questions: Vec, + pub peer_or_self_review_questions: Vec, } #[derive(Debug, Serialize, Deserialize)] @@ -96,7 +97,7 @@ pub struct CourseMaterialExercise { pub exercise_status: Option, #[cfg_attr(feature = "ts_rs", ts(type = "Record"))] pub exercise_slide_submission_counts: HashMap, - pub peer_review_config: Option, + pub peer_or_self_review_config: Option, pub previous_exercise_slide_submission: Option, pub user_course_instance_exercise_service_variables: Vec, @@ -474,22 +475,25 @@ pub async fn get_course_material_exercise( HashMap::new() }; - let peer_review_config = match (exercise.needs_peer_review, exercise.course_id) { - (true, Some(course_id)) => { - let prc = crate::peer_review_configs::get_by_exercise_or_course_id( + let peer_or_self_review_config = if let Some(course_id) = exercise.course_id { + if exercise.needs_peer_review || exercise.needs_self_review { + let prc = crate::peer_or_self_review_configs::get_by_exercise_or_course_id( conn, &exercise, course_id, ) .await .optional()?; - prc.map(|prc| CourseMaterialPeerReviewConfig { + prc.map(|prc| CourseMaterialPeerOrSelfReviewConfig { id: prc.id, course_id: prc.course_id, exercise_id: prc.exercise_id, peer_reviews_to_give: prc.peer_reviews_to_give, peer_reviews_to_receive: prc.peer_reviews_to_receive, }) + } else { + None } - _ => None, + } else { + None }; let user_course_instance_exercise_service_variables = match (user_id, instance_or_exam_id) { @@ -505,7 +509,7 @@ pub async fn get_course_material_exercise( current_exercise_slide, exercise_status, exercise_slide_submission_counts, - peer_review_config, + peer_or_self_review_config, user_course_instance_exercise_service_variables, previous_exercise_slide_submission, }) @@ -716,22 +720,25 @@ RETURNING id; Ok(deleted_ids) } -pub async fn set_exercise_to_use_exercise_specific_peer_review_config( +pub async fn set_exercise_to_use_exercise_specific_peer_or_self_review_config( conn: &mut PgConnection, exercise_id: Uuid, needs_peer_review: bool, - use_course_default_peer_review_config: bool, + needs_self_review: bool, + use_course_default_peer_or_self_review_config: bool, ) -> ModelResult { let id = sqlx::query!( " UPDATE exercises -SET use_course_default_peer_review_config = $1, - needs_peer_review = $2 -WHERE id = $3 +SET use_course_default_peer_or_self_review_config = $1, + needs_peer_review = $2, + needs_self_review = $3 +WHERE id = $4 RETURNING id; ", - use_course_default_peer_review_config, + use_course_default_peer_or_self_review_config, needs_peer_review, + needs_self_review, exercise_id ) .fetch_one(conn) @@ -769,32 +776,32 @@ pub async fn get_all_exercise_statuses_by_user_id_and_course_instance_id( .await? .into_iter() .into_group_map_by(|o| o.exercise_id); - let mut given_peer_review_submissions = crate::peer_review_submissions::get_all_given_peer_review_submissions_for_user_and_course_instance(&mut *conn, user_id, course_instance_id).await?.into_iter() + let mut given_peer_or_self_review_submissions = crate::peer_or_self_review_submissions::get_all_given_peer_or_self_review_submissions_for_user_and_course_instance(&mut *conn, user_id, course_instance_id).await?.into_iter() .into_group_map_by(|o| o.exercise_id); - let mut received_peer_review_submissions = crate::peer_review_submissions::get_all_received_peer_review_submissions_for_user_and_course_instance(&mut *conn, user_id, course_instance_id).await?.into_iter() + let mut received_peer_or_self_review_submissions = crate::peer_or_self_review_submissions::get_all_received_peer_or_self_review_submissions_for_user_and_course_instance(&mut *conn, user_id, course_instance_id).await?.into_iter() .into_group_map_by(|o| o.exercise_id); - let given_peer_review_submission_ids = given_peer_review_submissions + let given_peer_or_self_review_submission_ids = given_peer_or_self_review_submissions .values() .flatten() .map(|x| x.id) .collect::>(); - let mut given_peer_review_question_submissions = crate::peer_review_question_submissions::get_question_submissions_from_from_peer_review_submission_ids(&mut *conn, &given_peer_review_submission_ids).await? + let mut given_peer_or_self_review_question_submissions = crate::peer_or_self_review_question_submissions::get_question_submissions_from_from_peer_or_self_review_submission_ids(&mut *conn, &given_peer_or_self_review_submission_ids).await? .into_iter() .into_group_map_by(|o| { - let peer_review_submission = given_peer_review_submissions.clone().into_iter() - .find(|(_exercise_id, prs)| prs.iter().any(|p| p.id == o.peer_review_submission_id)) + let peer_review_submission = given_peer_or_self_review_submissions.clone().into_iter() + .find(|(_exercise_id, prs)| prs.iter().any(|p| p.id == o.peer_or_self_review_submission_id)) .unwrap_or_else(|| (Uuid::nil(), vec![])); peer_review_submission.0 }); - let received_peer_review_submission_ids = received_peer_review_submissions + let received_peer_or_self_review_submission_ids = received_peer_or_self_review_submissions .values() .flatten() .map(|x| x.id) .collect::>(); - let mut received_peer_review_question_submissions = crate::peer_review_question_submissions::get_question_submissions_from_from_peer_review_submission_ids(&mut *conn, &received_peer_review_submission_ids).await?.into_iter() + let mut received_peer_or_self_review_question_submissions = crate::peer_or_self_review_question_submissions::get_question_submissions_from_from_peer_or_self_review_submission_ids(&mut *conn, &received_peer_or_self_review_submission_ids).await?.into_iter() .into_group_map_by(|o| { - let peer_review_submission = received_peer_review_submissions.clone().into_iter() - .find(|(_exercise_id, prs)| prs.iter().any(|p| p.id == o.peer_review_submission_id)) + let peer_review_submission = received_peer_or_self_review_submissions.clone().into_iter() + .find(|(_exercise_id, prs)| prs.iter().any(|p| p.id == o.peer_or_self_review_submission_id)) .unwrap_or_else(|| (Uuid::nil(), vec![])); peer_review_submission.0 }); @@ -814,13 +821,16 @@ pub async fn get_all_exercise_statuses_by_user_id_and_course_instance_id( .find(|(_exercise_id, ues)| ues.id == tgd.user_exercise_state_id)?; Some((user_exercise_state.0, tgd)) }).collect::>(); - let all_peer_review_question_ids = given_peer_review_question_submissions + let all_peer_or_self_review_question_ids = given_peer_or_self_review_question_submissions .iter() - .chain(received_peer_review_question_submissions.iter()) - .flat_map(|(_exercise_id, prqs)| prqs.iter().map(|p| p.peer_review_question_id)) + .chain(received_peer_or_self_review_question_submissions.iter()) + .flat_map(|(_exercise_id, prqs)| prqs.iter().map(|p| p.peer_or_self_review_question_id)) .collect::>(); - let all_peer_review_questions = - crate::peer_review_questions::get_by_ids(&mut *conn, &all_peer_review_question_ids).await?; + let all_peer_or_self_review_questions = crate::peer_or_self_review_questions::get_by_ids( + &mut *conn, + &all_peer_or_self_review_question_ids, + ) + .await?; // Map all the data for all the exercises to be summaries of the data for each exercise. // @@ -834,43 +844,44 @@ pub async fn get_all_exercise_statuses_by_user_id_and_course_instance_id( let exercise_slide_submissions = exercise_slide_submissions .remove(&exercise.id) .unwrap_or_default(); - let given_peer_review_submissions = given_peer_review_submissions + let given_peer_or_self_review_submissions = given_peer_or_self_review_submissions .remove(&exercise.id) .unwrap_or_default(); - let received_peer_review_submissions = received_peer_review_submissions + let received_peer_or_self_review_submissions = received_peer_or_self_review_submissions .remove(&exercise.id) .unwrap_or_default(); - let given_peer_review_question_submissions = given_peer_review_question_submissions - .remove(&exercise.id) - .unwrap_or_default(); - let received_peer_review_question_submissions = - received_peer_review_question_submissions + let given_peer_or_self_review_question_submissions = + given_peer_or_self_review_question_submissions + .remove(&exercise.id) + .unwrap_or_default(); + let received_peer_or_self_review_question_submissions = + received_peer_or_self_review_question_submissions .remove(&exercise.id) .unwrap_or_default(); let peer_review_queue_entry = peer_review_queue_entries.remove(&exercise.id); let teacher_grading_decision = teacher_grading_decisions.remove(&exercise.id); - let peer_review_question_ids = given_peer_review_question_submissions + let peer_or_self_review_question_ids = given_peer_or_self_review_question_submissions .iter() - .chain(received_peer_review_question_submissions.iter()) - .map(|prqs| prqs.peer_review_question_id) + .chain(received_peer_or_self_review_question_submissions.iter()) + .map(|prqs| prqs.peer_or_self_review_question_id) .unique() .collect::>(); - let peer_review_questions = all_peer_review_questions + let peer_or_self_review_questions = all_peer_or_self_review_questions .iter() - .filter(|prq| peer_review_question_ids.contains(&prq.id)) + .filter(|prq| peer_or_self_review_question_ids.contains(&prq.id)) .cloned() .collect::>(); ExerciseStatusSummaryForUser { exercise, user_exercise_state, exercise_slide_submissions, - given_peer_review_submissions, - received_peer_review_submissions, - given_peer_review_question_submissions, - received_peer_review_question_submissions, + given_peer_or_self_review_submissions, + received_peer_or_self_review_submissions, + given_peer_or_self_review_question_submissions, + received_peer_or_self_review_question_submissions, peer_review_queue_entry, teacher_grading_decision, - peer_review_questions, + peer_or_self_review_questions, } }) .collect::>(); diff --git a/services/headless-lms/models/src/lib.rs b/services/headless-lms/models/src/lib.rs index de1697f28657..c3fdd0bd7be7 100644 --- a/services/headless-lms/models/src/lib.rs +++ b/services/headless-lms/models/src/lib.rs @@ -54,11 +54,11 @@ pub mod page_visit_datum_summary_by_courses_countries; pub mod page_visit_datum_summary_by_courses_device_types; pub mod page_visit_datum_summary_by_pages; pub mod pages; -pub mod peer_review_configs; -pub mod peer_review_question_submissions; -pub mod peer_review_questions; +pub mod peer_or_self_review_configs; +pub mod peer_or_self_review_question_submissions; +pub mod peer_or_self_review_questions; +pub mod peer_or_self_review_submissions; pub mod peer_review_queue_entries; -pub mod peer_review_submissions; pub mod pending_roles; pub mod playground_examples; pub mod proposed_block_edits; diff --git a/services/headless-lms/models/src/library/content_management.rs b/services/headless-lms/models/src/library/content_management.rs index 797b97a44352..511a1c81331d 100644 --- a/services/headless-lms/models/src/library/content_management.rs +++ b/services/headless-lms/models/src/library/content_management.rs @@ -10,7 +10,7 @@ use crate::{ courses::{self, Course, NewCourse}, exercise_service_info::ExerciseServiceInfoApi, pages::{self, NewPage, Page}, - peer_review_questions::CmsPeerReviewQuestion, + peer_or_self_review_questions::CmsPeerOrSelfReviewQuestion, prelude::*, SpecFetcher, }; @@ -102,37 +102,41 @@ pub async fn create_new_course( .await?; // Create course default peer review config - let peer_review_config_id = - crate::peer_review_configs::insert(&mut tx, PKeyPolicy::Generate, course.id, None).await?; + let peer_or_self_review_config_id = + crate::peer_or_self_review_configs::insert(&mut tx, PKeyPolicy::Generate, course.id, None) + .await?; // Create peer review questions for default peer review config - crate::peer_review_questions::upsert_multiple_peer_review_questions( + crate::peer_or_self_review_questions::upsert_multiple_peer_or_self_review_questions( &mut tx, &[ - CmsPeerReviewQuestion { + CmsPeerOrSelfReviewQuestion { id: Uuid::new_v4(), - peer_review_config_id, + peer_or_self_review_config_id, order_number: 0, question: "General comments".to_string(), - question_type: crate::peer_review_questions::PeerReviewQuestionType::Essay, + question_type: + crate::peer_or_self_review_questions::PeerOrSelfReviewQuestionType::Essay, answer_required: false, weight: 0.0, }, - CmsPeerReviewQuestion { + CmsPeerOrSelfReviewQuestion { id: Uuid::new_v4(), - peer_review_config_id, + peer_or_self_review_config_id, order_number: 1, question: "The answer was correct".to_string(), - question_type: crate::peer_review_questions::PeerReviewQuestionType::Scale, + question_type: + crate::peer_or_self_review_questions::PeerOrSelfReviewQuestionType::Scale, answer_required: true, weight: 0.0, }, - CmsPeerReviewQuestion { + CmsPeerOrSelfReviewQuestion { id: Uuid::new_v4(), - peer_review_config_id, + peer_or_self_review_config_id, order_number: 2, question: "The answer was easy to read".to_string(), - question_type: crate::peer_review_questions::PeerReviewQuestionType::Scale, + question_type: + crate::peer_or_self_review_questions::PeerOrSelfReviewQuestionType::Scale, answer_required: true, weight: 0.0, }, diff --git a/services/headless-lms/models/src/library/copying.rs b/services/headless-lms/models/src/library/copying.rs index 767a33180b9b..60e87c65913d 100644 --- a/services/headless-lms/models/src/library/copying.rs +++ b/services/headless-lms/models/src/library/copying.rs @@ -192,7 +192,7 @@ WHERE id = $2; ) .await?; - copy_peer_review_configs(&mut tx, copied_course.id, course_id).await?; + copy_peer_or_self_review_configs(&mut tx, copied_course.id, course_id).await?; copy_material_references(&mut tx, copied_course.id, course_id).await?; @@ -522,7 +522,7 @@ INSERT INTO exercises ( max_tries_per_slide, limit_number_of_tries, needs_peer_review, - use_course_default_peer_review_config + use_course_default_peer_or_self_review_config ) SELECT uuid_generate_v5($1, id::text), $1, @@ -537,7 +537,7 @@ SELECT uuid_generate_v5($1, id::text), max_tries_per_slide, limit_number_of_tries, needs_peer_review, - use_course_default_peer_review_config + use_course_default_peer_or_self_review_config FROM exercises WHERE course_id = $2 AND deleted_at IS NULL @@ -590,7 +590,7 @@ INSERT INTO exercises ( max_tries_per_slide, limit_number_of_tries, needs_peer_review, - use_course_default_peer_review_config + use_course_default_peer_or_self_review_config ) SELECT uuid_generate_v5($1, id::text), $1, @@ -604,7 +604,7 @@ SELECT uuid_generate_v5($1, id::text), max_tries_per_slide, limit_number_of_tries, needs_peer_review, - use_course_default_peer_review_config + use_course_default_peer_or_self_review_config FROM exercises WHERE exam_id = $2 AND deleted_at IS NULL @@ -745,7 +745,7 @@ AND deleted_at IS NULL; Ok(()) } -async fn copy_peer_review_configs( +async fn copy_peer_or_self_review_configs( tx: &mut PgConnection, namespace_id: Uuid, parent_id: Uuid, @@ -753,7 +753,7 @@ async fn copy_peer_review_configs( // Copy exercise tasks sqlx::query!( " -INSERT INTO peer_review_configs ( +INSERT INTO peer_or_self_review_configs ( id, course_id, exercise_id, @@ -769,7 +769,7 @@ SELECT uuid_generate_v5($1, id::text), peer_reviews_to_receive, processing_strategy, accepting_threshold -FROM peer_review_configs +FROM peer_or_self_review_configs WHERE course_id = $2 AND deleted_at IS NULL; ", @@ -1108,8 +1108,8 @@ mod tests { original_exercise.needs_peer_review ); assert_eq!( - copied_exercise.use_course_default_peer_review_config, - original_exercise.use_course_default_peer_review_config + copied_exercise.use_course_default_peer_or_self_review_config, + original_exercise.use_course_default_peer_or_self_review_config ); let copied_slides = crate::exercise_slides::get_exercise_slides_by_exercise_id( tx.as_mut(), diff --git a/services/headless-lms/models/src/library/grading.rs b/services/headless-lms/models/src/library/grading.rs index 705e43dd4903..0863b3295d10 100644 --- a/services/headless-lms/models/src/library/grading.rs +++ b/services/headless-lms/models/src/library/grading.rs @@ -14,9 +14,9 @@ use crate::{ exercise_task_submissions::{self, ExerciseTaskSubmission}, exercise_tasks::{self, CourseMaterialExerciseTask, ExerciseTask}, exercises::{self, Exercise, ExerciseStatus, GradingProgress}, - peer_review_configs::PeerReviewProcessingStrategy, - peer_review_question_submissions::{ - self, PeerReviewQuestionSubmission, PeerReviewWithQuestionsAndAnswers, + peer_or_self_review_configs::PeerReviewProcessingStrategy, + peer_or_self_review_question_submissions::{ + self, PeerOrSelfReviewQuestionSubmission, PeerReviewWithQuestionsAndAnswers, }, prelude::*, regradings, @@ -97,7 +97,7 @@ pub struct ExerciseStateUpdateNeedToUpdatePeerReviewStatusWithThis { pub peer_review_processing_strategy: PeerReviewProcessingStrategy, pub peer_review_accepting_threshold: f32, /// Used to for calculating averages when acting on PeerReviewProcessingStrategy - pub received_peer_review_question_submissions: Vec, + pub received_peer_or_self_review_question_submissions: Vec, } /// Inserts user submission to database. Tasks within submission are validated to make sure that @@ -525,7 +525,7 @@ pub struct AnswerRequiringAttentionWithTasks { pub exercise_id: Uuid, pub tasks: Vec, pub given_peer_reviews: Vec, - pub received_peer_reviews: Vec, + pub received_peer_or_self_reviews: Vec, } /// Gets submissions that require input from the teacher to continue processing. @@ -555,7 +555,7 @@ pub async fn get_paginated_answers_requiring_attention_for_exercise( ) .await?; let given_peer_reviews = if let Some(course_instance_id) = answer.course_instance_id { - peer_review_question_submissions::get_questions_and_answers_by_user_exercise_instance( + peer_or_self_review_question_submissions::get_given_peer_reviews( conn, answer.user_id, answer.exercise_id, @@ -565,8 +565,8 @@ pub async fn get_paginated_answers_requiring_attention_for_exercise( } else { vec![] }; - let received_peer_reviews = - peer_review_question_submissions::get_questions_and_answers_by_submission_id( + let received_peer_or_self_reviews = + peer_or_self_review_question_submissions::get_questions_and_answers_by_submission_id( conn, answer.submission_id, ) @@ -584,7 +584,7 @@ pub async fn get_paginated_answers_requiring_attention_for_exercise( exercise_id: answer.exercise_id, tasks, given_peer_reviews, - received_peer_reviews, + received_peer_or_self_reviews, }; answers.push(new_answer); } diff --git a/services/headless-lms/models/src/library/mod.rs b/services/headless-lms/models/src/library/mod.rs index 0ff3e0b4f9f9..8301b4e0bcdd 100644 --- a/services/headless-lms/models/src/library/mod.rs +++ b/services/headless-lms/models/src/library/mod.rs @@ -5,7 +5,7 @@ pub mod custom_view_exercises; pub mod global_stats; pub mod grading; pub mod page_visit_stats; -pub mod peer_reviewing; +pub mod peer_or_self_reviewing; pub mod progressing; pub mod regrading; pub mod user_exercise_state_updater; diff --git a/services/headless-lms/models/src/library/peer_reviewing.rs b/services/headless-lms/models/src/library/peer_or_self_reviewing.rs similarity index 64% rename from services/headless-lms/models/src/library/peer_reviewing.rs rename to services/headless-lms/models/src/library/peer_or_self_reviewing.rs index 9cb05ddcff2a..c6a83571731c 100644 --- a/services/headless-lms/models/src/library/peer_reviewing.rs +++ b/services/headless-lms/models/src/library/peer_or_self_reviewing.rs @@ -11,11 +11,11 @@ use crate::{ exercise_task_submissions, exercise_tasks::CourseMaterialExerciseTask, exercises::Exercise, - peer_review_configs::{self, PeerReviewConfig}, - peer_review_question_submissions, - peer_review_questions::{self, PeerReviewQuestion}, + peer_or_self_review_configs::{self, PeerOrSelfReviewConfig}, + peer_or_self_review_question_submissions, + peer_or_self_review_questions::{self, PeerOrSelfReviewQuestion}, + peer_or_self_review_submissions, peer_review_queue_entries::{self, PeerReviewQueueEntry}, - peer_review_submissions, prelude::*, user_exercise_states::{self, CourseInstanceOrExamId, ReviewingStage, UserExerciseState}, }; @@ -28,21 +28,35 @@ use super::user_exercise_state_updater::{ const MAX_PEER_REVIEW_CANDIDATES: i64 = 10; /// Starts peer review state for the student for this exercise. -pub async fn start_peer_review_for_user( +pub async fn start_peer_or_self_review_for_user( conn: &mut PgConnection, user_exercise_state: UserExerciseState, + exercise: &Exercise, ) -> ModelResult<()> { if user_exercise_state.reviewing_stage != ReviewingStage::NotStarted { return Err(ModelError::new( ModelErrorType::PreconditionFailed, - "Cannot start peer review anymore.".to_string(), + "Cannot start peer or self review anymore.".to_string(), + None, + )); + } + if !exercise.needs_peer_review && !exercise.needs_self_review { + return Err(ModelError::new( + ModelErrorType::PreconditionFailed, + "Exercise does not need peer or self review.".to_string(), None, )); } + let new_reviewing_stage = if exercise.needs_peer_review { + ReviewingStage::PeerReview + } else { + ReviewingStage::SelfReview + }; + let _user_exercise_state = user_exercise_states::update_exercise_progress( conn, user_exercise_state.id, - ReviewingStage::PeerReview, + new_reviewing_stage, ) .await?; Ok(()) @@ -50,43 +64,70 @@ pub async fn start_peer_review_for_user( #[derive(Debug, Serialize, Deserialize, PartialEq, Clone)] #[cfg_attr(feature = "ts_rs", derive(TS))] -pub struct CourseMaterialPeerReviewSubmission { +pub struct CourseMaterialPeerOrSelfReviewSubmission { pub exercise_slide_submission_id: Uuid, - pub peer_review_config_id: Uuid, - pub peer_review_question_answers: Vec, + pub peer_or_self_review_config_id: Uuid, + pub peer_review_question_answers: Vec, pub token: String, } #[derive(Debug, Serialize, Deserialize, PartialEq, Clone)] #[cfg_attr(feature = "ts_rs", derive(TS))] -pub struct CourseMaterialPeerReviewQuestionAnswer { - pub peer_review_question_id: Uuid, +pub struct CourseMaterialPeerOrSelfReviewQuestionAnswer { + pub peer_or_self_review_question_id: Uuid, pub text_data: Option, pub number_data: Option, } -pub async fn create_peer_review_submission_for_user( +pub async fn create_peer_or_self_review_submission_for_user( conn: &mut PgConnection, exercise: &Exercise, giver_exercise_state: UserExerciseState, - peer_review_submission: CourseMaterialPeerReviewSubmission, + receiver_exercise_state: UserExerciseState, + peer_review_submission: CourseMaterialPeerOrSelfReviewSubmission, ) -> ModelResult { - let peer_review = peer_review_configs::get_by_exercise_or_course_id( + let is_self_review = giver_exercise_state.user_id == receiver_exercise_state.user_id; + + if is_self_review + && (!exercise.needs_self_review + || giver_exercise_state.reviewing_stage != ReviewingStage::SelfReview) + { + return Err(ModelError::new( + ModelErrorType::PreconditionFailed, + "Self review not allowed.".to_string(), + None, + )); + } + if !is_self_review + && (!exercise.needs_peer_review + || giver_exercise_state.reviewing_stage == ReviewingStage::NotStarted) + { + return Err(ModelError::new( + ModelErrorType::PreconditionFailed, + "Peer review not allowed.".to_string(), + None, + )); + } + + let peer_or_self_review_config = peer_or_self_review_configs::get_by_exercise_or_course_id( conn, exercise, exercise.get_course_id()?, ) .await?; let sanitized_answers = validate_and_sanitize_peer_review_submission_answers( - peer_review_questions::get_all_by_peer_review_config_id_as_map(conn, peer_review.id) - .await?, + peer_or_self_review_questions::get_all_by_peer_or_self_review_config_id_as_map( + conn, + peer_or_self_review_config.id, + ) + .await?, peer_review_submission.peer_review_question_answers, )?; let mut tx = conn.begin().await?; let peer_reviews_given_before_this_review: i32 = - peer_review_submissions::get_users_submission_count_for_exercise_and_course_instance( + peer_or_self_review_submissions::get_users_submission_count_for_exercise_and_course_instance( &mut tx, giver_exercise_state.user_id, giver_exercise_state.exercise_id, @@ -96,77 +137,101 @@ pub async fn create_peer_review_submission_for_user( .try_into()?; let peer_reviews_given = peer_reviews_given_before_this_review + 1; - let unacceptable_amount_of_peer_reviews = - std::cmp::max(peer_review.peer_reviews_to_give, 1) * 15; - let suspicious_amount_of_peer_reviews = - std::cmp::max(std::cmp::max(peer_review.peer_reviews_to_give, 1) * 2, 4); - // To prevent someone from spamming peer reviews - if peer_reviews_given > unacceptable_amount_of_peer_reviews { - return Err(ModelError::new( - ModelErrorType::PreconditionFailed, - "You have given too many peer reviews to this exercise".to_string(), - None, - )); - } - // If someone has created more peer reviews than usual, apply rate limiting - if peer_reviews_given > suspicious_amount_of_peer_reviews { - // This is purposefully getting submission time to any peer reviewed exercise to prevent the user from spamming multiple exercises at the same time. - let last_submission_time = - peer_review_submissions::get_last_time_user_submitted_peer_review( - &mut tx, - giver_exercise_state.user_id, - giver_exercise_state.exercise_id, - giver_exercise_state.get_course_instance_id()?, - ) - .await?; + if !is_self_review { + let unacceptable_amount_of_peer_reviews = + std::cmp::max(peer_or_self_review_config.peer_reviews_to_give, 1) * 15; + let suspicious_amount_of_peer_reviews = std::cmp::max( + std::cmp::max(peer_or_self_review_config.peer_reviews_to_give, 1) * 2, + 4, + ); + // To prevent someone from spamming peer reviews + if peer_reviews_given > unacceptable_amount_of_peer_reviews { + return Err(ModelError::new( + ModelErrorType::PreconditionFailed, + "You have given too many peer reviews to this exercise".to_string(), + None, + )); + } + // If someone has created more peer reviews than usual, apply rate limiting + if peer_reviews_given > suspicious_amount_of_peer_reviews { + // This is purposefully getting submission time to any peer reviewed exercise to prevent the user from spamming multiple exercises at the same time. + let last_submission_time = + peer_or_self_review_submissions::get_last_time_user_submitted_peer_review( + &mut tx, + giver_exercise_state.user_id, + giver_exercise_state.exercise_id, + giver_exercise_state.get_course_instance_id()?, + ) + .await?; - if let Some(last_submission_time) = last_submission_time { - let diff = peer_reviews_given - suspicious_amount_of_peer_reviews; - let coefficient = std::cmp::min(std::cmp::max(diff, 1), 10); - // Between 30 seconds and 5 minutes - if Utc::now() - Duration::seconds(30 * coefficient as i64) < last_submission_time { - return Err(ModelError::new( - ModelErrorType::InvalidRequest, - "You are submitting too fast. Try again later.".to_string(), - None, - )); + if let Some(last_submission_time) = last_submission_time { + let diff = peer_reviews_given - suspicious_amount_of_peer_reviews; + let coefficient = std::cmp::min(std::cmp::max(diff, 1), 10); + // Between 30 seconds and 5 minutes + if Utc::now() - Duration::seconds(30 * coefficient as i64) < last_submission_time { + return Err(ModelError::new( + ModelErrorType::InvalidRequest, + "You are submitting too fast. Try again later.".to_string(), + None, + )); + } } } } - let peer_review_submission_id = peer_review_submissions::insert( + let peer_or_self_review_submission_id = peer_or_self_review_submissions::insert( &mut tx, PKeyPolicy::Generate, giver_exercise_state.user_id, giver_exercise_state.exercise_id, giver_exercise_state.get_course_instance_id()?, - peer_review.id, + peer_or_self_review_config.id, peer_review_submission.exercise_slide_submission_id, ) .await?; for answer in sanitized_answers { - peer_review_question_submissions::insert( + peer_or_self_review_question_submissions::insert( &mut tx, PKeyPolicy::Generate, - answer.peer_review_question_id, - peer_review_submission_id, + answer.peer_or_self_review_question_id, + peer_or_self_review_submission_id, answer.text_data, answer.number_data, ) .await?; } - let giver_exercise_state = if peer_reviews_given >= peer_review.peer_reviews_to_give { - update_peer_review_giver_exercise_progress( + if !is_self_review && peer_reviews_given >= peer_or_self_review_config.peer_reviews_to_give { + // Update peer review queue entry + let users_latest_submission = + exercise_slide_submissions::get_users_latest_exercise_slide_submission( + &mut tx, + giver_exercise_state.get_selected_exercise_slide_id()?, + giver_exercise_state.user_id, + ) + .await?; + let peer_reviews_received: i32 = + peer_or_self_review_submissions::count_peer_or_self_review_submissions_for_exercise_slide_submission( &mut tx, - exercise, - giver_exercise_state, - peer_reviews_given, - peer_review.clone(), + users_latest_submission.id, ) .await? - } else { - giver_exercise_state - }; + .try_into()?; + let _peer_review_queue_entry = peer_review_queue_entries::upsert_peer_review_priority( + &mut tx, + giver_exercise_state.user_id, + giver_exercise_state.exercise_id, + giver_exercise_state.get_course_instance_id()?, + peer_reviews_given, + users_latest_submission.id, + peer_reviews_received >= peer_or_self_review_config.peer_reviews_to_receive, + ) + .await?; + } + + let giver_exercise_state = + user_exercise_state_updater::update_user_exercise_state(&mut tx, giver_exercise_state.id) + .await?; + let exercise_slide_submission = exercise_slide_submissions::get_by_id( &mut tx, peer_review_submission.exercise_slide_submission_id, @@ -188,7 +253,16 @@ pub async fn create_peer_review_submission_for_user( ) .await?; if let Some(entry) = receiver_peer_review_queue_entry { - update_peer_review_receiver_exercise_status(&mut tx, exercise, &peer_review, entry).await?; + // No need to update the user exercise state again if this is a self review + if entry.user_id != giver_exercise_state.user_id { + update_peer_review_receiver_exercise_status( + &mut tx, + exercise, + &peer_or_self_review_config, + entry, + ) + .await?; + } } // Make it possible for the user to receive a new submission to review crate::offered_answers_to_peer_review_temporary::delete_saved_submissions_for_user( @@ -205,18 +279,18 @@ pub async fn create_peer_review_submission_for_user( /// Filters submitted peer review answers to those that are part of the peer review. fn validate_and_sanitize_peer_review_submission_answers( - mut peer_review_questions: HashMap, - peer_review_submission_question_answers: Vec, -) -> ModelResult> { + mut peer_or_self_review_questions: HashMap, + peer_review_submission_question_answers: Vec, +) -> ModelResult> { let valid_peer_review_question_answers = peer_review_submission_question_answers .into_iter() .filter(|answer| { - peer_review_questions - .remove(&answer.peer_review_question_id) + peer_or_self_review_questions + .remove(&answer.peer_or_self_review_question_id) .is_some() }) .collect(); - if peer_review_questions + if peer_or_self_review_questions .into_iter() .all(|question| !question.1.answer_required) { @@ -231,69 +305,14 @@ fn validate_and_sanitize_peer_review_submission_answers( } } -/// Creates or updates submitter's exercise state and peer review queue entry. -async fn update_peer_review_giver_exercise_progress( - conn: &mut PgConnection, - exercise: &Exercise, - user_exercise_state: UserExerciseState, - peer_reviews_given: i32, - peer_review: PeerReviewConfig, -) -> ModelResult { - let users_latest_submission = - exercise_slide_submissions::get_users_latest_exercise_slide_submission( - conn, - user_exercise_state.get_selected_exercise_slide_id()?, - user_exercise_state.user_id, - ) - .await?; - let peer_reviews_received: i32 = - peer_review_submissions::count_peer_review_submissions_for_exercise_slide_submission( - conn, - users_latest_submission.id, - ) - .await? - .try_into()?; - let peer_review_queue_entry = peer_review_queue_entries::upsert_peer_review_priority( - conn, - user_exercise_state.user_id, - user_exercise_state.exercise_id, - user_exercise_state.get_course_instance_id()?, - peer_reviews_given, - users_latest_submission.id, - peer_reviews_received >= peer_review.peer_reviews_to_receive, - ) - .await?; - let received_peer_review_question_submissions = crate::peer_review_question_submissions::get_received_question_submissions_for_exercise_slide_submission(conn, users_latest_submission.id).await?; - let updated_user_exercise_state = - user_exercise_state_updater::update_user_exercise_state_with_some_already_loaded_data( - conn, - user_exercise_state.id, - UserExerciseStateUpdateAlreadyLoadedRequiredData { - current_user_exercise_state: Some(user_exercise_state), - exercise: Some(exercise.clone()), - peer_review_information: Some(UserExerciseStateUpdateAlreadyLoadedRequiredDataPeerReviewInformation { - peer_review_queue_entry: Some(Some(peer_review_queue_entry)), - latest_exercise_slide_submission_received_peer_review_question_submissions: - Some(received_peer_review_question_submissions), - latest_exercise_slide_submission: Some(users_latest_submission), - ..Default::default() - }), - ..Default::default() - }, - ) - .await?; - - Ok(updated_user_exercise_state) -} - async fn update_peer_review_receiver_exercise_status( conn: &mut PgConnection, exercise: &Exercise, - peer_review: &PeerReviewConfig, + peer_review: &PeerOrSelfReviewConfig, peer_review_queue_entry: PeerReviewQueueEntry, ) -> ModelResult<()> { let peer_reviews_received = - peer_review_submissions::count_peer_review_submissions_for_exercise_slide_submission( + peer_or_self_review_submissions::count_peer_or_self_review_submissions_for_exercise_slide_submission( conn, peer_review_queue_entry.receiving_peer_reviews_exercise_slide_submission_id, ) @@ -315,7 +334,7 @@ async fn update_peer_review_receiver_exercise_status( ) .await?; if let Some(user_exercise_state) = user_exercise_state { - let received_peer_review_question_submissions = crate::peer_review_question_submissions::get_received_question_submissions_for_exercise_slide_submission(conn, peer_review_queue_entry.receiving_peer_reviews_exercise_slide_submission_id).await?; + let received_peer_or_self_review_question_submissions = crate::peer_or_self_review_question_submissions::get_received_question_submissions_for_exercise_slide_submission(conn, peer_review_queue_entry.receiving_peer_reviews_exercise_slide_submission_id).await?; let _updated_user_exercise_state = user_exercise_state_updater::update_user_exercise_state_with_some_already_loaded_data( conn, @@ -323,10 +342,10 @@ async fn update_peer_review_receiver_exercise_status( UserExerciseStateUpdateAlreadyLoadedRequiredData { current_user_exercise_state: Some(user_exercise_state), exercise: Some(exercise.clone()), - peer_review_information: Some(UserExerciseStateUpdateAlreadyLoadedRequiredDataPeerReviewInformation { + peer_or_self_review_information: Some(UserExerciseStateUpdateAlreadyLoadedRequiredDataPeerReviewInformation { peer_review_queue_entry: Some(Some(peer_review_queue_entry)), - latest_exercise_slide_submission_received_peer_review_question_submissions: - Some(received_peer_review_question_submissions), + latest_exercise_slide_submission_received_peer_or_self_review_question_submissions: + Some(received_peer_or_self_review_question_submissions), ..Default::default() }), ..Default::default() @@ -340,18 +359,18 @@ async fn update_peer_review_receiver_exercise_status( #[derive(Debug, Serialize, Deserialize, PartialEq, Clone)] #[cfg_attr(feature = "ts_rs", derive(TS))] -pub struct CourseMaterialPeerReviewData { +pub struct CourseMaterialPeerOrSelfReviewData { /// If none, no answer was available for review. - pub answer_to_review: Option, - pub peer_review_config: PeerReviewConfig, - pub peer_review_questions: Vec, + pub answer_to_review: Option, + pub peer_or_self_review_config: PeerOrSelfReviewConfig, + pub peer_or_self_review_questions: Vec, #[cfg_attr(feature = "ts_rs", ts(type = "number"))] pub num_peer_reviews_given: i64, } #[derive(Debug, Serialize, Deserialize, PartialEq, Clone)] #[cfg_attr(feature = "ts_rs", derive(TS))] -pub struct CourseMaterialPeerReviewDataAnswerToReview { +pub struct CourseMaterialPeerOrSelfReviewDataAnswerToReview { pub exercise_slide_submission_id: Uuid, /// Uses the same type as we use when we render and exercise in course material. Allows us to reuse existing logic for getting all the necessary information for rendering the submission. pub course_material_exercise_tasks: Vec, @@ -368,8 +387,8 @@ pub async fn try_to_select_exercise_slide_submission_for_peer_review( exercise: &Exercise, reviewer_user_exercise_state: &UserExerciseState, fetch_service_info: impl Fn(Url) -> BoxFuture<'static, ModelResult>, -) -> ModelResult { - let peer_review_config = peer_review_configs::get_by_exercise_or_course_id( +) -> ModelResult { + let peer_or_self_review_config = peer_or_self_review_configs::get_by_exercise_or_course_id( conn, exercise, exercise.get_course_id()?, @@ -379,9 +398,9 @@ pub async fn try_to_select_exercise_slide_submission_for_peer_review( // If an answer has been given within 1 hour to be reviewed and it still needs peer review, return the same one if let Some(saved_exercise_slide_submission_to_review) = crate::offered_answers_to_peer_review_temporary::try_to_restore_previously_given_exercise_slide_submission(&mut *conn, exercise.id, reviewer_user_exercise_state.user_id, course_instance_id).await? { - let data = get_course_material_peer_review_data( + let data = get_course_material_peer_or_self_review_data( conn, - &peer_review_config, + &peer_or_self_review_config, &Some(saved_exercise_slide_submission_to_review), reviewer_user_exercise_state.user_id, course_instance_id, @@ -394,7 +413,7 @@ pub async fn try_to_select_exercise_slide_submission_for_peer_review( } let excluded_exercise_slide_submission_ids = - peer_review_submissions::get_users_submission_ids_for_exercise_and_course_instance( + peer_or_self_review_submissions::get_users_submission_ids_for_exercise_and_course_instance( conn, reviewer_user_exercise_state.user_id, reviewer_user_exercise_state.exercise_id, @@ -432,9 +451,9 @@ pub async fn try_to_select_exercise_slide_submission_for_peer_review( .await? } }; - let data = get_course_material_peer_review_data( + let data = get_course_material_peer_or_self_review_data( conn, - &peer_review_config, + &peer_or_self_review_config, &exercise_slide_submission_to_review, reviewer_user_exercise_state.user_id, course_instance_id, @@ -446,6 +465,41 @@ pub async fn try_to_select_exercise_slide_submission_for_peer_review( Ok(data) } +/// Selects a user's own submission to be self-reviewed. Works similarly to `try_to_select_exercise_slide_submission_for_peer_review` but selects the user's latest submission. +pub async fn select_own_submission_for_self_review( + conn: &mut PgConnection, + exercise: &Exercise, + reviewer_user_exercise_state: &UserExerciseState, + fetch_service_info: impl Fn(Url) -> BoxFuture<'static, ModelResult>, +) -> ModelResult { + let peer_or_self_review_config = peer_or_self_review_configs::get_by_exercise_or_course_id( + conn, + exercise, + exercise.get_course_id()?, + ) + .await?; + let course_instance_id = reviewer_user_exercise_state.get_course_instance_id()?; + let exercise_slide_submission = + exercise_slide_submissions::get_users_latest_exercise_slide_submission( + conn, + reviewer_user_exercise_state.get_selected_exercise_slide_id()?, + reviewer_user_exercise_state.user_id, + ) + .await?; + let data = get_course_material_peer_or_self_review_data( + conn, + &peer_or_self_review_config, + &Some(exercise_slide_submission), + reviewer_user_exercise_state.user_id, + course_instance_id, + exercise.id, + fetch_service_info, + ) + .await?; + + Ok(data) +} + async fn try_to_select_peer_review_candidate_from_queue( conn: &mut PgConnection, exercise_id: Uuid, @@ -524,20 +578,23 @@ async fn try_to_select_peer_review_candidate_from_queue_impl( } } -async fn get_course_material_peer_review_data( +async fn get_course_material_peer_or_self_review_data( conn: &mut PgConnection, - peer_review_config: &PeerReviewConfig, + peer_or_self_review_config: &PeerOrSelfReviewConfig, exercise_slide_submission: &Option, reviewer_user_id: Uuid, reviewer_course_instance_id: Uuid, exercise_id: Uuid, fetch_service_info: impl Fn(Url) -> BoxFuture<'static, ModelResult>, -) -> ModelResult { - let peer_review_questions = - peer_review_questions::get_all_by_peer_review_config_id(conn, peer_review_config.id) - .await?; +) -> ModelResult { + let peer_or_self_review_questions = + peer_or_self_review_questions::get_all_by_peer_or_self_review_config_id( + conn, + peer_or_self_review_config.id, + ) + .await?; let num_peer_reviews_given = - peer_review_submissions::get_num_peer_reviews_given_by_user_and_course_instance_and_exercise( + peer_or_self_review_submissions::get_num_peer_reviews_given_by_user_and_course_instance_and_exercise( conn, reviewer_user_id, reviewer_course_instance_id, @@ -554,7 +611,7 @@ async fn get_course_material_peer_review_data( reviewer_user_id, fetch_service_info ).await?; - Some(CourseMaterialPeerReviewDataAnswerToReview { + Some(CourseMaterialPeerOrSelfReviewDataAnswerToReview { exercise_slide_submission_id, course_material_exercise_tasks, }) @@ -562,10 +619,10 @@ async fn get_course_material_peer_review_data( None => None, }; - Ok(CourseMaterialPeerReviewData { + Ok(CourseMaterialPeerOrSelfReviewData { answer_to_review, - peer_review_config: peer_review_config.clone(), - peer_review_questions, + peer_or_self_review_config: peer_or_self_review_config.clone(), + peer_or_self_review_questions, num_peer_reviews_given, }) } @@ -584,9 +641,10 @@ pub async fn update_peer_review_queue_reviews_received( .collect::>(); for exercise in exercises { info!("Processing exercise {:?}", exercise.id); - let peer_review_config = - peer_review_configs::get_by_exercise_or_course_id(&mut tx, &exercise, course_id) - .await?; + let peer_or_self_review_config = peer_or_self_review_configs::get_by_exercise_or_course_id( + &mut tx, &exercise, course_id, + ) + .await?; let peer_review_queue_entries = crate::peer_review_queue_entries::get_all_that_need_peer_reviews_by_exercise_id( &mut tx, @@ -601,7 +659,7 @@ pub async fn update_peer_review_queue_reviews_received( update_peer_review_receiver_exercise_status( &mut tx, &exercise, - &peer_review_config, + &peer_or_self_review_config, peer_review_queue_entry, ) .await?; @@ -616,21 +674,22 @@ pub async fn update_peer_review_queue_reviews_received( mod tests { use super::*; - mod validate_peer_review_submissions_answers { + mod validate_peer_or_self_review_submissions_answers { use chrono::TimeZone; - use crate::peer_review_questions::PeerReviewQuestionType; + use crate::peer_or_self_review_questions::PeerOrSelfReviewQuestionType; use super::*; #[test] fn accepts_valid_answers() { - let peer_review_config_id = + let peer_or_self_review_config_id = Uuid::parse_str("5f464818-1e68-4839-ae86-850b310f508c").unwrap(); let question_id = Uuid::parse_str("68d5cda3-6ad8-464b-9af1-bd1692fcbee1").unwrap(); let questions = HashMap::from([( question_id, - create_peer_review_question(question_id, peer_review_config_id, true).unwrap(), + create_peer_review_question(question_id, peer_or_self_review_config_id, true) + .unwrap(), )]); let answers = vec![create_peer_review_answer(question_id)]; assert_eq!( @@ -643,10 +702,10 @@ mod tests { #[test] fn filters_illegal_answers() { - let peer_review_config_id = + let peer_or_self_review_config_id = Uuid::parse_str("5f464818-1e68-4839-ae86-850b310f508c").unwrap(); let questions = HashMap::new(); - let answers = vec![create_peer_review_answer(peer_review_config_id)]; + let answers = vec![create_peer_review_answer(peer_or_self_review_config_id)]; assert_eq!( validate_and_sanitize_peer_review_submission_answers(questions, answers) .unwrap() @@ -657,12 +716,13 @@ mod tests { #[test] fn errors_on_missing_required_answers() { - let peer_review_config_id = + let peer_or_self_review_config_id = Uuid::parse_str("5f464818-1e68-4839-ae86-850b310f508c").unwrap(); let question_id = Uuid::parse_str("68d5cda3-6ad8-464b-9af1-bd1692fcbee1").unwrap(); let questions = HashMap::from([( question_id, - create_peer_review_question(question_id, peer_review_config_id, true).unwrap(), + create_peer_review_question(question_id, peer_or_self_review_config_id, true) + .unwrap(), )]); assert!( validate_and_sanitize_peer_review_submission_answers(questions, vec![]).is_err() @@ -671,28 +731,28 @@ mod tests { fn create_peer_review_question( id: Uuid, - peer_review_config_id: Uuid, + peer_or_self_review_config_id: Uuid, answer_required: bool, - ) -> ModelResult { - Ok(PeerReviewQuestion { + ) -> ModelResult { + Ok(PeerOrSelfReviewQuestion { id, created_at: Utc.with_ymd_and_hms(2022, 1, 1, 0, 0, 0).unwrap(), updated_at: Utc.with_ymd_and_hms(2022, 1, 1, 0, 0, 0).unwrap(), deleted_at: None, - peer_review_config_id, + peer_or_self_review_config_id, order_number: 0, question: "".to_string(), - question_type: PeerReviewQuestionType::Essay, + question_type: PeerOrSelfReviewQuestionType::Essay, answer_required, weight: 0.0, }) } fn create_peer_review_answer( - peer_review_question_id: Uuid, - ) -> CourseMaterialPeerReviewQuestionAnswer { - CourseMaterialPeerReviewQuestionAnswer { - peer_review_question_id, + peer_or_self_review_question_id: Uuid, + ) -> CourseMaterialPeerOrSelfReviewQuestionAnswer { + CourseMaterialPeerOrSelfReviewQuestionAnswer { + peer_or_self_review_question_id, text_data: Some("".to_string()), number_data: None, } diff --git a/services/headless-lms/models/src/library/user_exercise_state_updater/data_loader.rs b/services/headless-lms/models/src/library/user_exercise_state_updater/data_loader.rs index c00868ae5cb1..fb311cc41b0a 100644 --- a/services/headless-lms/models/src/library/user_exercise_state_updater/data_loader.rs +++ b/services/headless-lms/models/src/library/user_exercise_state_updater/data_loader.rs @@ -1,11 +1,11 @@ use crate::{ exercise_slide_submissions::ExerciseSlideSubmission, exercises::Exercise, - peer_review_configs::{self, PeerReviewConfig}, - peer_review_question_submissions::PeerReviewQuestionSubmission, - peer_review_questions::{self, PeerReviewQuestion, PeerReviewQuestionType}, + peer_or_self_review_configs::{self, PeerOrSelfReviewConfig}, + peer_or_self_review_question_submissions::PeerOrSelfReviewQuestionSubmission, + peer_or_self_review_questions::{self, PeerOrSelfReviewQuestion, PeerOrSelfReviewQuestionType}, + peer_or_self_review_submissions::{self, PeerOrSelfReviewSubmission}, peer_review_queue_entries::PeerReviewQueueEntry, - peer_review_submissions::{self, PeerReviewSubmission}, prelude::*, teacher_grading_decisions::{self, TeacherGradingDecision}, user_exercise_slide_states::{self, UserExerciseSlideStateGradingSummary}, @@ -28,7 +28,7 @@ pub(super) async fn load_required_data( let UserExerciseStateUpdateAlreadyLoadedRequiredData { exercise, current_user_exercise_state, - peer_review_information, + peer_or_self_review_information, latest_teacher_grading_decision, user_exercise_slide_state_grading_summary, } = already_loaded_internal_dependencies; @@ -39,9 +39,9 @@ pub(super) async fn load_required_data( let loaded_exercise = load_exercise(conn, exercise, &loaded_user_exercise_state).await?; Ok(UserExerciseStateUpdateRequiredData { - peer_review_information: load_peer_review_information( + peer_or_self_review_information: load_peer_or_self_review_information( conn, - peer_review_information, + peer_or_self_review_information, &loaded_user_exercise_state, &loaded_exercise, ) @@ -125,29 +125,36 @@ async fn load_exercise( } } -async fn load_peer_review_information( +async fn load_peer_or_self_review_information( conn: &mut PgConnection, - already_loaded_peer_review_information: Option< + already_loaded_peer_or_self_review_information: Option< UserExerciseStateUpdateAlreadyLoadedRequiredDataPeerReviewInformation, >, loaded_user_exercise_state: &UserExerciseState, loaded_exercise: &Exercise, ) -> ModelResult> { - info!("Loading peer review information"); - if loaded_exercise.needs_peer_review { - info!("Exercise needs peer review"); - // Destruct the contents of already_loaded_peer_review_information so that we can use the fields of the parent struct independently + info!("Loading peer or self review information"); + if loaded_exercise.needs_peer_review || loaded_exercise.needs_self_review { + if loaded_exercise.needs_peer_review { + info!("Exercise needs peer review"); + } + if loaded_exercise.needs_self_review { + info!("Exercise needs self review"); + } + + // Destruct the contents of already_loaded_peer_or_self_review_information so that we can use the fields of the parent struct independently let UserExerciseStateUpdateAlreadyLoadedRequiredDataPeerReviewInformation { - given_peer_review_submissions, + given_peer_or_self_review_submissions, + given_self_review_submission, latest_exercise_slide_submission, - latest_exercise_slide_submission_received_peer_review_question_submissions, + latest_exercise_slide_submission_received_peer_or_self_review_question_submissions, peer_review_queue_entry, - peer_review_config, - peer_review_questions, - } = if let Some(already_loaded_peer_review_information) = - already_loaded_peer_review_information + peer_or_self_review_config, + peer_or_self_review_questions, + } = if let Some(already_loaded_peer_or_self_review_information) = + already_loaded_peer_or_self_review_information { - already_loaded_peer_review_information + already_loaded_peer_or_self_review_information } else { Default::default() }; @@ -159,21 +166,28 @@ async fn load_peer_review_information( ) .await?; - let loaded_peer_review_config = - load_peer_review_config(conn, peer_review_config, loaded_exercise).await?; + let loaded_peer_or_self_review_config = + load_peer_or_self_review_config(conn, peer_or_self_review_config, loaded_exercise) + .await?; Ok(Some( UserExerciseStateUpdateRequiredDataPeerReviewInformation { - given_peer_review_submissions: load_given_peer_review_submissions( + given_peer_or_self_review_submissions: load_given_peer_or_self_review_submissions( + conn, + given_peer_or_self_review_submissions, + loaded_user_exercise_state, + ) + .await?, + given_self_review_submission: load_given_self_review_submission( conn, - given_peer_review_submissions, + given_self_review_submission, loaded_user_exercise_state, ) .await?, - latest_exercise_slide_submission_received_peer_review_question_submissions: - load_latest_exercise_slide_submission_received_peer_review_question_submissions( + latest_exercise_slide_submission_received_peer_or_self_review_question_submissions: + load_latest_exercise_slide_submission_received_peer_or_self_review_question_submissions( conn, - latest_exercise_slide_submission_received_peer_review_question_submissions, + latest_exercise_slide_submission_received_peer_or_self_review_question_submissions, loaded_latest_exercise_slide_submission.id, ) .await?, @@ -184,33 +198,35 @@ async fn load_peer_review_information( loaded_user_exercise_state, ) .await?, - peer_review_config: loaded_peer_review_config.clone(), - peer_review_questions: load_peer_review_questions( + peer_or_self_review_config: loaded_peer_or_self_review_config.clone(), + peer_or_self_review_questions: load_peer_or_self_review_questions( conn, - peer_review_questions, - &loaded_peer_review_config, + peer_or_self_review_questions, + &loaded_peer_or_self_review_config, ) .await?, }, )) } else { - info!("Exercise does not need peer review"); + info!("Exercise does not need peer or self review"); // Peer review disabled for the exercise, no need to load any information related to peer reviews. Ok(None) } } -async fn load_peer_review_config( +async fn load_peer_or_self_review_config( conn: &mut PgConnection, - already_loaded_peer_review_config: Option, + already_loaded_peer_or_self_review_config: Option< + crate::peer_or_self_review_configs::PeerOrSelfReviewConfig, + >, loaded_exercise: &Exercise, -) -> ModelResult { - if let Some(prc) = already_loaded_peer_review_config { +) -> ModelResult { + if let Some(prc) = already_loaded_peer_or_self_review_config { info!("Using already loaded peer review config"); Ok(prc) } else { info!("Loading peer review config"); - Ok(peer_review_configs::get_by_exercise_or_course_id( + Ok(peer_or_self_review_configs::get_by_exercise_or_course_id( conn, loaded_exercise, loaded_exercise.course_id.ok_or_else(|| { @@ -227,30 +243,33 @@ async fn load_peer_review_config( } /** Loads peer review config and normalizes weights, if necessary */ -async fn load_peer_review_questions( +async fn load_peer_or_self_review_questions( conn: &mut PgConnection, - already_loaded_peer_review_questions: Option>, - loaded_peer_review_config: &PeerReviewConfig, -) -> ModelResult> { - if let Some(prq) = already_loaded_peer_review_questions { + already_loaded_peer_or_self_review_questions: Option>, + loaded_peer_or_self_review_config: &PeerOrSelfReviewConfig, +) -> ModelResult> { + if let Some(prq) = already_loaded_peer_or_self_review_questions { info!("Using already loaded peer review questions"); Ok(prq) } else { info!("Loading peer review questions"); - let mut questions = peer_review_questions::get_all_by_peer_review_config_id( - conn, - loaded_peer_review_config.id, - ) - .await?; + let mut questions = + peer_or_self_review_questions::get_all_by_peer_or_self_review_config_id( + conn, + loaded_peer_or_self_review_config.id, + ) + .await?; - if !loaded_peer_review_config.points_are_all_or_nothing { + if !loaded_peer_or_self_review_config.points_are_all_or_nothing { questions = normalize_weights(questions); } Ok(questions) } } -fn normalize_weights(mut questions: Vec) -> Vec { +fn normalize_weights( + mut questions: Vec, +) -> Vec { info!("Normalizing peer review question weights to sum to 1"); questions.sort_by(|a, b| a.order_number.cmp(&b.order_number)); info!( @@ -259,7 +278,7 @@ fn normalize_weights(mut questions: Vec) -> Vec, _>(|q| q.question_type == PeerReviewQuestionType::Scale); + .partition::, _>(|q| q.question_type == PeerOrSelfReviewQuestionType::Scale); let number_of_questions = allowed_to_have_weight.len(); let sum_of_weights = allowed_to_have_weight.iter().map(|q| q.weight).sum::(); if sum_of_weights < 0.000001 { @@ -313,21 +332,22 @@ async fn load_peer_review_queue_entry( } } -async fn load_latest_exercise_slide_submission_received_peer_review_question_submissions( +async fn load_latest_exercise_slide_submission_received_peer_or_self_review_question_submissions( conn: &mut PgConnection, - latest_exercise_slide_submission_received_peer_review_question_submissions: Option< - Vec, + latest_exercise_slide_submission_received_peer_or_self_review_question_submissions: Option< + Vec, >, latest_exercise_slide_submission_id: Uuid, -) -> ModelResult> { - if let Some(latest_exercise_slide_submission_received_peer_review_question_submissions) = - latest_exercise_slide_submission_received_peer_review_question_submissions +) -> ModelResult> { + if let Some( + latest_exercise_slide_submission_received_peer_or_self_review_question_submissions, + ) = latest_exercise_slide_submission_received_peer_or_self_review_question_submissions { info!("Using already loaded latest exercise slide submission received peer review question submissions"); - Ok(latest_exercise_slide_submission_received_peer_review_question_submissions) + Ok(latest_exercise_slide_submission_received_peer_or_self_review_question_submissions) } else { info!("Loading latest exercise slide submission received peer review question submissions"); - Ok(crate::peer_review_question_submissions::get_received_question_submissions_for_exercise_slide_submission(conn, latest_exercise_slide_submission_id).await?) + Ok(crate::peer_or_self_review_question_submissions::get_received_question_submissions_for_exercise_slide_submission(conn, latest_exercise_slide_submission_id).await?) } } @@ -355,14 +375,16 @@ async fn load_latest_exercise_slide_submission( } } -async fn load_given_peer_review_submissions( +async fn load_given_peer_or_self_review_submissions( conn: &mut PgConnection, - already_loaded_given_peer_review_submissions: Option>, + already_loaded_given_peer_or_self_review_submissions: Option>, loaded_user_exercise_state: &UserExerciseState, -) -> ModelResult> { - if let Some(given_peer_review_submissions) = already_loaded_given_peer_review_submissions { +) -> ModelResult> { + if let Some(given_peer_or_self_review_submissions) = + already_loaded_given_peer_or_self_review_submissions + { info!("Using already loaded given peer review submissions"); - Ok(given_peer_review_submissions) + Ok(given_peer_or_self_review_submissions) } else { info!("Loading given peer review submissions"); let course_instance_id = @@ -376,7 +398,35 @@ async fn load_given_peer_review_submissions( None, ) })?; - Ok(peer_review_submissions::get_peer_reviews_given_by_user_and_course_instance_and_exercise(conn, loaded_user_exercise_state.user_id, course_instance_id, loaded_user_exercise_state.exercise_id).await?) + Ok(peer_or_self_review_submissions::get_peer_reviews_given_by_user_and_course_instance_and_exercise(conn, loaded_user_exercise_state.user_id, course_instance_id, loaded_user_exercise_state.exercise_id).await?) + } +} + +async fn load_given_self_review_submission( + conn: &mut PgConnection, + already_loaded_given_self_review_submission: Option>, + loaded_user_exercise_state: &UserExerciseState, +) -> ModelResult> { + if let Some(given_self_review_submission) = already_loaded_given_self_review_submission { + info!("Using already loaded given self review submission"); + Ok(given_self_review_submission) + } else if let Some(course_instance_id) = loaded_user_exercise_state.course_instance_id { + info!("Loading given self review submission"); + Ok( + peer_or_self_review_submissions::get_self_review_submission_by_user_and_exercise( + conn, + loaded_user_exercise_state.user_id, + loaded_user_exercise_state.exercise_id, + course_instance_id, + ) + .await?, + ) + } else { + Err(ModelError::new( + ModelErrorType::PreconditionFailed, + "No course instance found: self review is only possible on courses".to_string(), + None, + )) } } @@ -389,11 +439,11 @@ mod tests { #[test] fn test_normalize_weights() { - let peer_review_questions = vec![ - PeerReviewQuestion { + let peer_or_self_review_questions = vec![ + PeerOrSelfReviewQuestion { id: Uuid::new_v4(), - peer_review_config_id: Uuid::new_v4(), - question_type: PeerReviewQuestionType::Scale, + peer_or_self_review_config_id: Uuid::new_v4(), + question_type: PeerOrSelfReviewQuestionType::Scale, order_number: 1, weight: 0.3, question: "Scale question".to_string(), @@ -402,10 +452,10 @@ mod tests { deleted_at: None, answer_required: true, }, - PeerReviewQuestion { + PeerOrSelfReviewQuestion { id: Uuid::new_v4(), - peer_review_config_id: Uuid::new_v4(), - question_type: PeerReviewQuestionType::Essay, + peer_or_self_review_config_id: Uuid::new_v4(), + question_type: PeerOrSelfReviewQuestionType::Essay, order_number: 2, weight: 0.1, question: "Text question".to_string(), @@ -414,10 +464,10 @@ mod tests { deleted_at: None, answer_required: true, }, - PeerReviewQuestion { + PeerOrSelfReviewQuestion { id: Uuid::new_v4(), - peer_review_config_id: Uuid::new_v4(), - question_type: PeerReviewQuestionType::Scale, + peer_or_self_review_config_id: Uuid::new_v4(), + question_type: PeerOrSelfReviewQuestionType::Scale, order_number: 3, weight: 0.1, question: "Scale question".to_string(), @@ -427,10 +477,11 @@ mod tests { answer_required: true, }, ]; - let normalized_peer_review_questions = normalize_weights(peer_review_questions); - assert_eq!(normalized_peer_review_questions[0].weight, 0.75); - assert_eq!(normalized_peer_review_questions[1].weight, 0.0); - assert_eq!(normalized_peer_review_questions[2].weight, 0.25); + let normalized_peer_or_self_review_questions = + normalize_weights(peer_or_self_review_questions); + assert_eq!(normalized_peer_or_self_review_questions[0].weight, 0.75); + assert_eq!(normalized_peer_or_self_review_questions[1].weight, 0.0); + assert_eq!(normalized_peer_or_self_review_questions[2].weight, 0.25); } } } diff --git a/services/headless-lms/models/src/library/user_exercise_state_updater/mod.rs b/services/headless-lms/models/src/library/user_exercise_state_updater/mod.rs index 3b6741c86c52..7510ada5866d 100644 --- a/services/headless-lms/models/src/library/user_exercise_state_updater/mod.rs +++ b/services/headless-lms/models/src/library/user_exercise_state_updater/mod.rs @@ -10,11 +10,11 @@ use crate::{ course_modules, exercise_slide_submissions::ExerciseSlideSubmission, exercises::Exercise, - peer_review_configs::PeerReviewConfig, - peer_review_question_submissions::PeerReviewQuestionSubmission, - peer_review_questions::PeerReviewQuestion, + peer_or_self_review_configs::PeerOrSelfReviewConfig, + peer_or_self_review_question_submissions::PeerOrSelfReviewQuestionSubmission, + peer_or_self_review_questions::PeerOrSelfReviewQuestion, + peer_or_self_review_submissions::PeerOrSelfReviewSubmission, peer_review_queue_entries::PeerReviewQueueEntry, - peer_review_submissions::PeerReviewSubmission, prelude::*, teacher_grading_decisions::TeacherGradingDecision, user_exercise_slide_states::UserExerciseSlideStateGradingSummary, @@ -24,11 +24,13 @@ use crate::{ use std::default::Default; /// Visible only in the current module (and submodules) to prevent misuse. +#[derive(Debug)] pub struct UserExerciseStateUpdateRequiredData { pub exercise: Exercise, pub current_user_exercise_state: UserExerciseState, /// None if peer review is not enabled for the exercise - pub peer_review_information: Option, + pub peer_or_self_review_information: + Option, /// None if a teacher has not made a grading decision yet. pub latest_teacher_grading_decision: Option, /// The grades summed up from all the user exercise slide states. Note that multiple slides can give points, and they are all aggregated here. @@ -36,13 +38,15 @@ pub struct UserExerciseStateUpdateRequiredData { } /// Visible only in the current module (and submodules) to prevent misuse. +#[derive(Debug)] pub struct UserExerciseStateUpdateRequiredDataPeerReviewInformation { - pub given_peer_review_submissions: Vec, - pub latest_exercise_slide_submission_received_peer_review_question_submissions: - Vec, + pub given_peer_or_self_review_submissions: Vec, + pub given_self_review_submission: Option, + pub latest_exercise_slide_submission_received_peer_or_self_review_question_submissions: + Vec, pub peer_review_queue_entry: Option, - pub peer_review_config: PeerReviewConfig, - pub peer_review_questions: Vec, + pub peer_or_self_review_config: PeerOrSelfReviewConfig, + pub peer_or_self_review_questions: Vec, } /** @@ -52,7 +56,7 @@ Same as `UserExerciseStateUpdateRequiredData` but public and everything is optio pub struct UserExerciseStateUpdateAlreadyLoadedRequiredData { pub exercise: Option, pub current_user_exercise_state: Option, - pub peer_review_information: + pub peer_or_self_review_information: Option, /// The outer option is to indicate whether this cached value is provided or not, and the inner option is to tell whether a teacher has made a grading decision or not. pub latest_teacher_grading_decision: Option>, @@ -64,14 +68,15 @@ Same as `UserExerciseStateUpdateRequiredDataPeerReviewInformation` but public an */ #[derive(Default)] pub struct UserExerciseStateUpdateAlreadyLoadedRequiredDataPeerReviewInformation { - pub given_peer_review_submissions: Option>, + pub given_peer_or_self_review_submissions: Option>, + pub given_self_review_submission: Option>, pub latest_exercise_slide_submission: Option, - pub latest_exercise_slide_submission_received_peer_review_question_submissions: - Option>, + pub latest_exercise_slide_submission_received_peer_or_self_review_question_submissions: + Option>, /// The outer option is to indicate whether this cached value is provided or not, and the inner option is to tell whether the answer has been added to the the peer review queue or not pub peer_review_queue_entry: Option>, - pub peer_review_config: Option, - pub peer_review_questions: Option>, + pub peer_or_self_review_config: Option, + pub peer_or_self_review_questions: Option>, } /// Loads all required data and updates user_exercise_state. Also creates completions if needed. diff --git a/services/headless-lms/models/src/library/user_exercise_state_updater/state_deriver.rs b/services/headless-lms/models/src/library/user_exercise_state_updater/state_deriver.rs index 616578509b75..e02a0e17ae26 100644 --- a/services/headless-lms/models/src/library/user_exercise_state_updater/state_deriver.rs +++ b/services/headless-lms/models/src/library/user_exercise_state_updater/state_deriver.rs @@ -4,9 +4,9 @@ use itertools::Itertools; use crate::{ exercises::{ActivityProgress, GradingProgress}, library::user_exercise_state_updater::validation::validate_input, - peer_review_configs::PeerReviewProcessingStrategy, - peer_review_question_submissions::PeerReviewQuestionSubmission, - peer_review_questions::{PeerReviewQuestion, PeerReviewQuestionType}, + peer_or_self_review_configs::PeerReviewProcessingStrategy, + peer_or_self_review_question_submissions::PeerOrSelfReviewQuestionSubmission, + peer_or_self_review_questions::{PeerOrSelfReviewQuestion, PeerOrSelfReviewQuestionType}, prelude::*, user_exercise_states::{ReviewingStage, UserExerciseStateUpdate}, }; @@ -14,7 +14,8 @@ use crate::{ use super::UserExerciseStateUpdateRequiredData; /// What the peer review thinks the state should be changed to -struct PeerReviewOpinion { +#[derive(Debug)] +struct PeerOrSelfReviewOpinion { score_given: Option, reviewing_stage: ReviewingStage, } @@ -26,8 +27,8 @@ pub(super) fn derive_new_user_exercise_state( validate_input(&input_data)?; - let peer_review_opinion = get_peer_review_opinion(&input_data); - let new_reviewing_stage = derive_new_reviewing_stage(&input_data, &peer_review_opinion); + let peer_or_self_review_opinion = get_peer_or_self_review_opinion(&input_data); + let new_reviewing_stage = derive_new_reviewing_stage(&input_data, &peer_or_self_review_opinion); let reviewing_stage_changed = input_data.current_user_exercise_state.reviewing_stage != new_reviewing_stage; @@ -40,9 +41,12 @@ pub(super) fn derive_new_user_exercise_state( ); } - let new_score_given = - derive_new_score_given(&input_data, &new_reviewing_stage, &peer_review_opinion) - .map(f32_to_two_decimals); + let new_score_given = derive_new_score_given( + &input_data, + &new_reviewing_stage, + &peer_or_self_review_opinion, + ) + .map(f32_to_two_decimals); if input_data.current_user_exercise_state.score_given != new_score_given { info!( @@ -94,9 +98,9 @@ fn derive_new_activity_progress( .user_exercise_slide_state_grading_summary .grading_progress; // If no peer review or no self review are needed, the activity is completed as soon as the user has submitted the exercise - if !input_data.exercise.needs_peer_review { + if !input_data.exercise.needs_peer_review && !input_data.exercise.needs_self_review { if slide_grading_progress == GradingProgress::NotReady { - // The user has not subitted the exercise + // The user has not submitted the exercise return ActivityProgress::Initialized; } // The user has submitted the exercise @@ -105,7 +109,7 @@ fn derive_new_activity_progress( // The exercise needs peer review the activity is complete once the user has done everything they have to do if new_reviewing_stage == &ReviewingStage::NotStarted { if slide_grading_progress == GradingProgress::NotReady { - // The user has not subitted the exercise + // The user has not submitted the exercise return ActivityProgress::Initialized; } // The user has submitted the exercise @@ -124,7 +128,7 @@ fn derive_new_activity_progress( fn derive_new_score_given( input_data: &UserExerciseStateUpdateRequiredData, new_reviewing_stage: &ReviewingStage, - peer_review_opinion: &Option, + peer_or_self_review_opinion: &Option, ) -> Option { // Teacher grading decisions always override everything else if let Some(teacher_grading_decision) = &input_data.latest_teacher_grading_decision { @@ -138,9 +142,9 @@ fn derive_new_score_given( { return input_data.current_user_exercise_state.score_given; } - if let Some(peer_review_opinion) = peer_review_opinion { - if input_data.exercise.needs_peer_review { - return peer_review_opinion.score_given; + if let Some(peer_or_self_review_opinion) = peer_or_self_review_opinion { + if input_data.exercise.needs_peer_review || input_data.exercise.needs_self_review { + return peer_or_self_review_opinion.score_given; } } // Peer reviews are not enabled, we'll just give the points according to the automated grading @@ -152,15 +156,15 @@ fn derive_new_score_given( fn derive_new_reviewing_stage( input_data: &UserExerciseStateUpdateRequiredData, - peer_review_opinion: &Option, + peer_or_self_review_opinion: &Option, ) -> ReviewingStage { // Teacher grading decisions always override everything else if let Some(_teacher_grading_decision) = &input_data.latest_teacher_grading_decision { return ReviewingStage::ReviewedAndLocked; }; let user_exercise_state = &input_data.current_user_exercise_state; - if input_data.exercise.needs_peer_review { - return peer_review_opinion + if input_data.exercise.needs_peer_review || input_data.exercise.needs_self_review { + return peer_or_self_review_opinion .as_ref() .map(|o| o.reviewing_stage) .unwrap_or_else(|| input_data.current_user_exercise_state.reviewing_stage); @@ -180,77 +184,122 @@ fn derive_new_reviewing_stage( } #[instrument(skip(input_data))] -fn get_peer_review_opinion( +fn get_peer_or_self_review_opinion( input_data: &UserExerciseStateUpdateRequiredData, -) -> Option { - if !input_data.exercise.needs_peer_review { - // Peer review is not enabled, no opinion +) -> Option { + if !input_data.exercise.needs_peer_review && !input_data.exercise.needs_self_review { + // Peer review or self review is not enabled, no opinion return None; } + if input_data.current_user_exercise_state.reviewing_stage == ReviewingStage::NotStarted { + // The user has not started a peer review or a self review, so our opinion is that the user should not receive any points yet. + return Some(PeerOrSelfReviewOpinion { + score_given: None, + reviewing_stage: ReviewingStage::NotStarted, + }); + } + let score_maximum = input_data.exercise.score_maximum; - if let Some(info) = &input_data.peer_review_information { - let given_enough_peer_reviews = info.given_peer_review_submissions.len() as i32 - >= info.peer_review_config.peer_reviews_to_give; - // Received enough peer reviews is cached to the queue entry, lets use it here to make sure its value has been kept up-to-date. - let received_enough_peer_reviews = info - .peer_review_queue_entry - .as_ref() - .map(|o| o.received_enough_peer_reviews) - .unwrap_or(false); - - if !given_enough_peer_reviews { - // Keeps the state in Intialized or PeerReview - return Some(PeerReviewOpinion { - score_given: None, - reviewing_stage: input_data.current_user_exercise_state.reviewing_stage, - }); - } else if !received_enough_peer_reviews { - // Has given enough but has not received enough: the student has to wait until others have reviewed their answer more - - // Handle the case where the answer is waiting for manual review but is still receiving peer reviews - if input_data.current_user_exercise_state.reviewing_stage - == ReviewingStage::WaitingForManualGrading - { - return Some(PeerReviewOpinion { + if let Some(info) = &input_data.peer_or_self_review_information { + if input_data.exercise.needs_peer_review { + let given_enough_peer_reviews = info.given_peer_or_self_review_submissions.len() as i32 + >= info.peer_or_self_review_config.peer_reviews_to_give; + // Received enough peer reviews is cached to the queue entry, lets use it here to make sure its value has been kept up-to-date. + let received_enough_peer_reviews = info + .peer_review_queue_entry + .as_ref() + .map(|o| o.received_enough_peer_reviews) + .unwrap_or(false); + + if !given_enough_peer_reviews { + // Keeps the state in Intialized or PeerReview + return Some(PeerOrSelfReviewOpinion { score_given: None, - reviewing_stage: ReviewingStage::WaitingForManualGrading, + reviewing_stage: input_data.current_user_exercise_state.reviewing_stage, + }); + } else if !received_enough_peer_reviews { + // Has given enough but has not received enough: the student has to wait until others have reviewed their answer more + + // Handle the case where the answer is waiting for manual review but is still receiving peer reviews + if input_data.current_user_exercise_state.reviewing_stage + == ReviewingStage::WaitingForManualGrading + { + return Some(PeerOrSelfReviewOpinion { + score_given: None, + reviewing_stage: ReviewingStage::WaitingForManualGrading, + }); + } + + if input_data.exercise.needs_self_review + && info.given_self_review_submission.is_none() + { + // Student has given enough peer reviews but has not self reviewed yet. + return Some(PeerOrSelfReviewOpinion { + score_given: None, + reviewing_stage: ReviewingStage::SelfReview, + }); + } + + return Some(PeerOrSelfReviewOpinion { + score_given: None, + reviewing_stage: ReviewingStage::WaitingForPeerReviews, }); } + } - return Some(PeerReviewOpinion { - score_given: None, - reviewing_stage: ReviewingStage::WaitingForPeerReviews, - }); + // Given and received enough peer reviews + if input_data.exercise.needs_self_review { + if input_data.exercise.needs_peer_review { + if info.given_self_review_submission.is_none() { + // Student has given and received enough peer reviews but has not self reviewed yet. + return Some(PeerOrSelfReviewOpinion { + score_given: None, + reviewing_stage: ReviewingStage::SelfReview, + }); + } + } else if info.given_self_review_submission.is_some() { + // Student has given a self review and there is no peer review. There is no way to determine a score for the student automatically, so we'll give the answer to the teacher to review. + return Some(PeerOrSelfReviewOpinion { + score_given: None, + reviewing_stage: ReviewingStage::WaitingForManualGrading, + }); + } else { + // Student has not given a self review yet. + return Some(PeerOrSelfReviewOpinion { + score_given: None, + reviewing_stage: ReviewingStage::SelfReview, + }); + } } // Users have given and received enough peer reviews, time to consider how we're doing the grading - match info.peer_review_config.processing_strategy { + match info.peer_or_self_review_config.processing_strategy { PeerReviewProcessingStrategy::AutomaticallyGradeByAverage => { let avg = calculate_average_received_peer_review_score( &info - .latest_exercise_slide_submission_received_peer_review_question_submissions, + .latest_exercise_slide_submission_received_peer_or_self_review_question_submissions, ); - if !info.peer_review_config.points_are_all_or_nothing { + if !info.peer_or_self_review_config.points_are_all_or_nothing { let score_given = calculate_peer_review_weighted_points( - &info.peer_review_questions, + &info.peer_or_self_review_questions, &info - .latest_exercise_slide_submission_received_peer_review_question_submissions, + .latest_exercise_slide_submission_received_peer_or_self_review_question_submissions, score_maximum, ); - Some(PeerReviewOpinion { + Some(PeerOrSelfReviewOpinion { score_given: Some(score_given), reviewing_stage: ReviewingStage::ReviewedAndLocked, }) - } else if avg < info.peer_review_config.accepting_threshold { - info!(avg = ?avg, threshold = ?info.peer_review_config.accepting_threshold, peer_review_processing_strategy = ?info.peer_review_config.processing_strategy, "Automatically giving zero points because average is below the threshold"); - Some(PeerReviewOpinion { + } else if avg < info.peer_or_self_review_config.accepting_threshold { + info!(avg = ?avg, threshold = ?info.peer_or_self_review_config.accepting_threshold, peer_review_processing_strategy = ?info.peer_or_self_review_config.processing_strategy, "Automatically giving zero points because average is below the threshold"); + Some(PeerOrSelfReviewOpinion { score_given: Some(0.0), reviewing_stage: ReviewingStage::ReviewedAndLocked, }) } else { - info!(avg = ?avg, threshold = ?info.peer_review_config.accepting_threshold, peer_review_processing_strategy = ?info.peer_review_config.processing_strategy, "Automatically giving the points since the average is above the threshold"); - Some(PeerReviewOpinion { + info!(avg = ?avg, threshold = ?info.peer_or_self_review_config.accepting_threshold, peer_review_processing_strategy = ?info.peer_or_self_review_config.processing_strategy, "Automatically giving the points since the average is above the threshold"); + Some(PeerOrSelfReviewOpinion { score_given: Some(score_maximum as f32), reviewing_stage: ReviewingStage::ReviewedAndLocked, }) @@ -259,36 +308,36 @@ fn get_peer_review_opinion( PeerReviewProcessingStrategy::AutomaticallyGradeOrManualReviewByAverage => { let avg = calculate_average_received_peer_review_score( &info - .latest_exercise_slide_submission_received_peer_review_question_submissions, + .latest_exercise_slide_submission_received_peer_or_self_review_question_submissions, ); - if avg < info.peer_review_config.accepting_threshold { - info!(avg = ?avg, threshold = ?info.peer_review_config.accepting_threshold, peer_review_processing_strategy = ?info.peer_review_config.processing_strategy, "Not giving points because average is below the threshold. The answer should be moved to manual review."); - Some(PeerReviewOpinion { + if avg < info.peer_or_self_review_config.accepting_threshold { + info!(avg = ?avg, threshold = ?info.peer_or_self_review_config.accepting_threshold, peer_review_processing_strategy = ?info.peer_or_self_review_config.processing_strategy, "Not giving points because average is below the threshold. The answer should be moved to manual review."); + Some(PeerOrSelfReviewOpinion { score_given: None, reviewing_stage: ReviewingStage::WaitingForManualGrading, }) - } else if !info.peer_review_config.points_are_all_or_nothing { + } else if !info.peer_or_self_review_config.points_are_all_or_nothing { let score_given = calculate_peer_review_weighted_points( - &info.peer_review_questions, + &info.peer_or_self_review_questions, &info - .latest_exercise_slide_submission_received_peer_review_question_submissions, + .latest_exercise_slide_submission_received_peer_or_self_review_question_submissions, score_maximum, ); - Some(PeerReviewOpinion { + Some(PeerOrSelfReviewOpinion { score_given: Some(score_given), reviewing_stage: ReviewingStage::ReviewedAndLocked, }) } else { - info!(avg = ?avg, threshold = ?info.peer_review_config.accepting_threshold, peer_review_processing_strategy = ?info.peer_review_config.processing_strategy, "Automatically giving the points since the average is above the threshold"); - Some(PeerReviewOpinion { + info!(avg = ?avg, threshold = ?info.peer_or_self_review_config.accepting_threshold, peer_review_processing_strategy = ?info.peer_or_self_review_config.processing_strategy, "Automatically giving the points since the average is above the threshold"); + Some(PeerOrSelfReviewOpinion { score_given: Some(score_maximum as f32), reviewing_stage: ReviewingStage::ReviewedAndLocked, }) } } PeerReviewProcessingStrategy::ManualReviewEverything => { - info!(peer_review_processing_strategy = ?info.peer_review_config.processing_strategy, "Not giving points because the teacher reviews all answers manually"); - Some(PeerReviewOpinion { + info!(peer_review_processing_strategy = ?info.peer_or_self_review_config.processing_strategy, "Not giving points because the teacher reviews all answers manually"); + Some(PeerOrSelfReviewOpinion { score_given: None, reviewing_stage: ReviewingStage::WaitingForManualGrading, }) @@ -296,15 +345,15 @@ fn get_peer_review_opinion( } } else { // Even though the exercise needs peer review, the peer review has not been configured. The safest thing to do here is to consider peer review as not complete - warn!("Peer review is enabled in the exercise but no peer_review_config found"); + warn!("Peer review is enabled in the exercise but no peer_or_self_review_config found"); None } } fn calculate_average_received_peer_review_score( - peer_review_question_submissions: &[PeerReviewQuestionSubmission], + peer_or_self_review_question_submissions: &[PeerOrSelfReviewQuestionSubmission], ) -> f32 { - let answers_considered = peer_review_question_submissions + let answers_considered = peer_or_self_review_question_submissions .iter() .filter_map(|prqs| { if prqs.deleted_at.is_some() { @@ -321,42 +370,42 @@ fn calculate_average_received_peer_review_score( } fn calculate_peer_review_weighted_points( - peer_review_questions: &[PeerReviewQuestion], - received_peer_review_question_submissions: &[PeerReviewQuestionSubmission], + peer_or_self_review_questions: &[PeerOrSelfReviewQuestion], + received_peer_or_self_review_question_submissions: &[PeerOrSelfReviewQuestionSubmission], score_maximum: i32, ) -> f32 { // Weights should be sum to 1. This should be guranteed by the data loader. - let questions_considered_for_weighted_points = peer_review_questions + let questions_considered_for_weighted_points = peer_or_self_review_questions .iter() - .filter(|prq| prq.question_type == PeerReviewQuestionType::Scale) + .filter(|prq| prq.question_type == PeerOrSelfReviewQuestionType::Scale) .collect::>(); let question_submissions_considered_for_weighted_points = - received_peer_review_question_submissions + received_peer_or_self_review_question_submissions .iter() .filter(|prqs| { questions_considered_for_weighted_points .iter() - .any(|prq| prq.id == prqs.peer_review_question_id) + .any(|prq| prq.id == prqs.peer_or_self_review_question_id) }) .collect::>(); let number_of_submissions = question_submissions_considered_for_weighted_points .iter() - .map(|prqs| prqs.peer_review_submission_id) + .map(|prqs| prqs.peer_or_self_review_submission_id) .unique() .count(); let grouped = question_submissions_considered_for_weighted_points .iter() - .group_by(|prqs| prqs.peer_review_submission_id); + .group_by(|prqs| prqs.peer_or_self_review_submission_id); let weighted_score_by_submission = grouped .into_iter() .map( - |(_peer_review_submission_id, peer_review_question_answers)| { + |(_peer_or_self_review_submission_id, peer_review_question_answers)| { peer_review_question_answers .filter_map(|prqs| { questions_considered_for_weighted_points .iter() - .find(|prq| prq.id == prqs.peer_review_question_id) + .find(|prq| prq.id == prqs.peer_or_self_review_question_id) .map(|question| question.weight * prqs.number_data.unwrap_or_default()) }) .sum::() @@ -385,8 +434,9 @@ mod tests { use crate::{ exercises::Exercise, library::user_exercise_state_updater::UserExerciseStateUpdateRequiredDataPeerReviewInformation, - peer_review_configs::PeerReviewConfig, peer_review_queue_entries::PeerReviewQueueEntry, - peer_review_submissions::PeerReviewSubmission, + peer_or_self_review_configs::PeerOrSelfReviewConfig, + peer_or_self_review_submissions::PeerOrSelfReviewSubmission, + peer_review_queue_entries::PeerReviewQueueEntry, user_exercise_slide_states::UserExerciseSlideStateGradingSummary, user_exercise_states::UserExerciseState, }; @@ -396,7 +446,7 @@ mod tests { #[test] fn updates_state_for_normal_exercise() { let id = Uuid::parse_str("5f464818-1e68-4839-ae86-850b310f508c").unwrap(); - let exercise = create_exercise(CourseOrExamId::Course(id), false, false); + let exercise = create_exercise(CourseOrExamId::Course(id), false, false, false); let user_exercise_state = create_user_exercise_state( &exercise, None, @@ -407,7 +457,7 @@ mod tests { derive_new_user_exercise_state(UserExerciseStateUpdateRequiredData { exercise, current_user_exercise_state: user_exercise_state, - peer_review_information: None, + peer_or_self_review_information: None, latest_teacher_grading_decision: None, user_exercise_slide_state_grading_summary: UserExerciseSlideStateGradingSummary { @@ -428,7 +478,7 @@ mod tests { #[test] fn doesnt_update_score_for_exercise_that_needs_to_be_peer_reviewed() { let id = Uuid::parse_str("5f464818-1e68-4839-ae86-850b310f508c").unwrap(); - let exercise = create_exercise(CourseOrExamId::Course(id), true, true); + let exercise = create_exercise(CourseOrExamId::Course(id), true, false, true); let user_exercise_state = create_user_exercise_state( &exercise, None, @@ -439,11 +489,14 @@ mod tests { derive_new_user_exercise_state(UserExerciseStateUpdateRequiredData { exercise, current_user_exercise_state: user_exercise_state, - peer_review_information: Some( + peer_or_self_review_information: Some( UserExerciseStateUpdateRequiredDataPeerReviewInformation { - given_peer_review_submissions: Vec::new(), latest_exercise_slide_submission_received_peer_review_question_submissions: Vec::new(), peer_review_queue_entry: None, - peer_review_config: create_peer_review_config(PeerReviewProcessingStrategy::AutomaticallyGradeByAverage), - peer_review_questions: Vec::new(), + given_peer_or_self_review_submissions: Vec::new(), + given_self_review_submission: None, + latest_exercise_slide_submission_received_peer_or_self_review_question_submissions: Vec::new(), + peer_review_queue_entry: None, + peer_or_self_review_config: create_peer_or_self_review_config(PeerReviewProcessingStrategy::AutomaticallyGradeByAverage), + peer_or_self_review_questions: Vec::new(), }, ), latest_teacher_grading_decision: None, @@ -468,24 +521,25 @@ mod tests { #[test] fn peer_review_automatically_accept_or_reject_by_average_works_gives_full_points() { let id = Uuid::parse_str("5f464818-1e68-4839-ae86-850b310f508c").unwrap(); - let exercise = create_exercise(CourseOrExamId::Course(id), true, true); + let exercise = create_exercise(CourseOrExamId::Course(id), true, false, true); let user_exercise_state = create_user_exercise_state( &exercise, None, ActivityProgress::Initialized, - ReviewingStage::NotStarted, + ReviewingStage::PeerReview, ); let new_user_exercise_state = derive_new_user_exercise_state(UserExerciseStateUpdateRequiredData { exercise, current_user_exercise_state: user_exercise_state, - peer_review_information: Some( + peer_or_self_review_information: Some( UserExerciseStateUpdateRequiredDataPeerReviewInformation { - given_peer_review_submissions: vec![create_peer_review_submission(), create_peer_review_submission(), create_peer_review_submission()], - latest_exercise_slide_submission_received_peer_review_question_submissions: vec![create_peer_review_question_submission(4.0), create_peer_review_question_submission(3.0), create_peer_review_question_submission(4.0)], - peer_review_queue_entry: Some(create_peer_review_queue_entry()), - peer_review_config: create_peer_review_config(PeerReviewProcessingStrategy::AutomaticallyGradeByAverage), - peer_review_questions: Vec::new(), + given_peer_or_self_review_submissions: vec![create_peer_review_submission(), create_peer_review_submission(), create_peer_review_submission()], + given_self_review_submission: None, + latest_exercise_slide_submission_received_peer_or_self_review_question_submissions: vec![create_peer_review_question_submission(4.0), create_peer_review_question_submission(3.0), create_peer_review_question_submission(4.0)], + peer_review_queue_entry: Some(create_peer_review_queue_entry(true)), + peer_or_self_review_config: create_peer_or_self_review_config(PeerReviewProcessingStrategy::AutomaticallyGradeByAverage), + peer_or_self_review_questions: Vec::new(), }, ), latest_teacher_grading_decision: None, @@ -508,25 +562,26 @@ mod tests { #[test] fn peer_review_automatically_accept_or_reject_by_average_works_gives_zero_points() { let id = Uuid::parse_str("5f464818-1e68-4839-ae86-850b310f508c").unwrap(); - let exercise = create_exercise(CourseOrExamId::Course(id), true, true); + let exercise = create_exercise(CourseOrExamId::Course(id), true, false, true); let user_exercise_state = create_user_exercise_state( &exercise, None, ActivityProgress::Initialized, - ReviewingStage::NotStarted, + ReviewingStage::PeerReview, ); let new_user_exercise_state = derive_new_user_exercise_state(UserExerciseStateUpdateRequiredData { exercise, current_user_exercise_state: user_exercise_state, - peer_review_information: Some( + peer_or_self_review_information: Some( UserExerciseStateUpdateRequiredDataPeerReviewInformation { - given_peer_review_submissions: vec![create_peer_review_submission(), create_peer_review_submission(), create_peer_review_submission()], + given_peer_or_self_review_submissions: vec![create_peer_review_submission(), create_peer_review_submission(), create_peer_review_submission()], + given_self_review_submission: None, // Average below 2.1 - latest_exercise_slide_submission_received_peer_review_question_submissions: vec![create_peer_review_question_submission(3.0), create_peer_review_question_submission(1.0), create_peer_review_question_submission(1.0)], - peer_review_queue_entry: Some(create_peer_review_queue_entry()), - peer_review_config: create_peer_review_config(PeerReviewProcessingStrategy::AutomaticallyGradeByAverage), - peer_review_questions: Vec::new(), + latest_exercise_slide_submission_received_peer_or_self_review_question_submissions: vec![create_peer_review_question_submission(3.0), create_peer_review_question_submission(1.0), create_peer_review_question_submission(1.0)], + peer_review_queue_entry: Some(create_peer_review_queue_entry(true)), + peer_or_self_review_config: create_peer_or_self_review_config(PeerReviewProcessingStrategy::AutomaticallyGradeByAverage), + peer_or_self_review_questions: Vec::new(), }, ), latest_teacher_grading_decision: None, @@ -554,24 +609,25 @@ mod tests { fn peer_review_automatically_accept_or_manual_review_by_average_works_gives_full_points( ) { let id = Uuid::parse_str("5f464818-1e68-4839-ae86-850b310f508c").unwrap(); - let exercise = create_exercise(CourseOrExamId::Course(id), true, true); + let exercise = create_exercise(CourseOrExamId::Course(id), true, false, true); let user_exercise_state = create_user_exercise_state( &exercise, None, ActivityProgress::Initialized, - ReviewingStage::NotStarted, + ReviewingStage::PeerReview, ); let new_user_exercise_state = derive_new_user_exercise_state(UserExerciseStateUpdateRequiredData { exercise, current_user_exercise_state: user_exercise_state, - peer_review_information: Some( + peer_or_self_review_information: Some( UserExerciseStateUpdateRequiredDataPeerReviewInformation { - given_peer_review_submissions: vec![create_peer_review_submission(), create_peer_review_submission(), create_peer_review_submission()], - latest_exercise_slide_submission_received_peer_review_question_submissions: vec![create_peer_review_question_submission(4.0), create_peer_review_question_submission(3.0), create_peer_review_question_submission(4.0)], - peer_review_queue_entry: Some(create_peer_review_queue_entry()), - peer_review_config: create_peer_review_config(PeerReviewProcessingStrategy::AutomaticallyGradeOrManualReviewByAverage), - peer_review_questions: Vec::new(), + given_peer_or_self_review_submissions: vec![create_peer_review_submission(), create_peer_review_submission(), create_peer_review_submission()], + given_self_review_submission: None, + latest_exercise_slide_submission_received_peer_or_self_review_question_submissions: vec![create_peer_review_question_submission(4.0), create_peer_review_question_submission(3.0), create_peer_review_question_submission(4.0)], + peer_review_queue_entry: Some(create_peer_review_queue_entry(true)), + peer_or_self_review_config: create_peer_or_self_review_config(PeerReviewProcessingStrategy::AutomaticallyGradeOrManualReviewByAverage), + peer_or_self_review_questions: Vec::new(), }, ), latest_teacher_grading_decision: None, @@ -595,25 +651,26 @@ mod tests { fn peer_review_automatically_accept_or_manual_review_by_average_works_puts_the_answer_to_manual_review( ) { let id = Uuid::parse_str("5f464818-1e68-4839-ae86-850b310f508c").unwrap(); - let exercise = create_exercise(CourseOrExamId::Course(id), true, true); + let exercise = create_exercise(CourseOrExamId::Course(id), true, false, true); let user_exercise_state = create_user_exercise_state( &exercise, None, ActivityProgress::Initialized, - ReviewingStage::NotStarted, + ReviewingStage::PeerReview, ); let new_user_exercise_state = derive_new_user_exercise_state(UserExerciseStateUpdateRequiredData { exercise, current_user_exercise_state: user_exercise_state, - peer_review_information: Some( + peer_or_self_review_information: Some( UserExerciseStateUpdateRequiredDataPeerReviewInformation { - given_peer_review_submissions: vec![create_peer_review_submission(), create_peer_review_submission(), create_peer_review_submission()], + given_peer_or_self_review_submissions: vec![create_peer_review_submission(), create_peer_review_submission(), create_peer_review_submission()], + given_self_review_submission: None, // Average below 2.1 - latest_exercise_slide_submission_received_peer_review_question_submissions: vec![create_peer_review_question_submission(3.0), create_peer_review_question_submission(1.0), create_peer_review_question_submission(1.0)], - peer_review_queue_entry: Some(create_peer_review_queue_entry()), - peer_review_config: create_peer_review_config(PeerReviewProcessingStrategy::AutomaticallyGradeOrManualReviewByAverage), - peer_review_questions: Vec::new(), + latest_exercise_slide_submission_received_peer_or_self_review_question_submissions: vec![create_peer_review_question_submission(3.0), create_peer_review_question_submission(1.0), create_peer_review_question_submission(1.0)], + peer_review_queue_entry: Some(create_peer_review_queue_entry(true)), + peer_or_self_review_config: create_peer_or_self_review_config(PeerReviewProcessingStrategy::AutomaticallyGradeOrManualReviewByAverage), + peer_or_self_review_questions: Vec::new(), }, ), latest_teacher_grading_decision: None, @@ -641,24 +698,25 @@ mod tests { fn peer_review_manual_review_everything_works_does_not_give_full_points_to_passing_answer_and_puts_to_manual_review( ) { let id = Uuid::parse_str("5f464818-1e68-4839-ae86-850b310f508c").unwrap(); - let exercise = create_exercise(CourseOrExamId::Course(id), true, true); + let exercise = create_exercise(CourseOrExamId::Course(id), true, false, true); let user_exercise_state = create_user_exercise_state( &exercise, None, ActivityProgress::Initialized, - ReviewingStage::NotStarted, + ReviewingStage::PeerReview, ); let new_user_exercise_state = derive_new_user_exercise_state(UserExerciseStateUpdateRequiredData { exercise, current_user_exercise_state: user_exercise_state, - peer_review_information: Some( + peer_or_self_review_information: Some( UserExerciseStateUpdateRequiredDataPeerReviewInformation { - given_peer_review_submissions: vec![create_peer_review_submission(), create_peer_review_submission(), create_peer_review_submission()], - latest_exercise_slide_submission_received_peer_review_question_submissions: vec![create_peer_review_question_submission(4.0), create_peer_review_question_submission(3.0), create_peer_review_question_submission(4.0)], - peer_review_queue_entry: Some(create_peer_review_queue_entry()), - peer_review_config: create_peer_review_config(PeerReviewProcessingStrategy::ManualReviewEverything), - peer_review_questions: Vec::new(), + given_peer_or_self_review_submissions: vec![create_peer_review_submission(), create_peer_review_submission(), create_peer_review_submission()], + given_self_review_submission: None, + latest_exercise_slide_submission_received_peer_or_self_review_question_submissions: vec![create_peer_review_question_submission(4.0), create_peer_review_question_submission(3.0), create_peer_review_question_submission(4.0)], + peer_review_queue_entry: Some(create_peer_review_queue_entry(true)), + peer_or_self_review_config: create_peer_or_self_review_config(PeerReviewProcessingStrategy::ManualReviewEverything), + peer_or_self_review_questions: Vec::new(), }, ), latest_teacher_grading_decision: None, @@ -682,25 +740,26 @@ mod tests { fn peer_review_manual_review_everything_works_puts_failing_answer_the_answer_to_manual_review( ) { let id = Uuid::parse_str("5f464818-1e68-4839-ae86-850b310f508c").unwrap(); - let exercise = create_exercise(CourseOrExamId::Course(id), true, true); + let exercise = create_exercise(CourseOrExamId::Course(id), true, false, true); let user_exercise_state = create_user_exercise_state( &exercise, None, ActivityProgress::Initialized, - ReviewingStage::NotStarted, + ReviewingStage::PeerReview, ); let new_user_exercise_state = derive_new_user_exercise_state(UserExerciseStateUpdateRequiredData { exercise, current_user_exercise_state: user_exercise_state, - peer_review_information: Some( + peer_or_self_review_information: Some( UserExerciseStateUpdateRequiredDataPeerReviewInformation { - given_peer_review_submissions: vec![create_peer_review_submission(), create_peer_review_submission(), create_peer_review_submission()], + given_peer_or_self_review_submissions: vec![create_peer_review_submission(), create_peer_review_submission(), create_peer_review_submission()], + given_self_review_submission: None, // Average below 2.1 - latest_exercise_slide_submission_received_peer_review_question_submissions: vec![create_peer_review_question_submission(3.0), create_peer_review_question_submission(1.0), create_peer_review_question_submission(1.0)], - peer_review_queue_entry: Some(create_peer_review_queue_entry()), - peer_review_config: create_peer_review_config(PeerReviewProcessingStrategy::ManualReviewEverything), - peer_review_questions: Vec::new(), + latest_exercise_slide_submission_received_peer_or_self_review_question_submissions: vec![create_peer_review_question_submission(3.0), create_peer_review_question_submission(1.0), create_peer_review_question_submission(1.0)], + peer_review_queue_entry: Some(create_peer_review_queue_entry(true)), + peer_or_self_review_config: create_peer_or_self_review_config(PeerReviewProcessingStrategy::ManualReviewEverything), + peer_or_self_review_questions: Vec::new(), }, ), latest_teacher_grading_decision: None, @@ -778,6 +837,211 @@ mod tests { } } + mod self_review { + use super::*; + + #[test] + fn if_self_review_enabled_does_not_put_answer_automatically_to_self_review() { + let id = Uuid::parse_str("5f464818-1e68-4839-ae86-850b310f508c").unwrap(); + let exercise = create_exercise(CourseOrExamId::Course(id), true, true, true); + let user_exercise_state = create_user_exercise_state( + &exercise, + None, + ActivityProgress::Initialized, + ReviewingStage::NotStarted, + ); + let new_user_exercise_state = + derive_new_user_exercise_state(UserExerciseStateUpdateRequiredData { + exercise, + current_user_exercise_state: user_exercise_state, + peer_or_self_review_information: Some( + UserExerciseStateUpdateRequiredDataPeerReviewInformation { + given_peer_or_self_review_submissions: vec![create_peer_review_submission(), create_peer_review_submission(), create_peer_review_submission()], + given_self_review_submission: None, + latest_exercise_slide_submission_received_peer_or_self_review_question_submissions: vec![create_peer_review_question_submission(4.0), create_peer_review_question_submission(3.0), create_peer_review_question_submission(4.0)], + peer_review_queue_entry: Some(create_peer_review_queue_entry(true)), + peer_or_self_review_config: create_peer_or_self_review_config(PeerReviewProcessingStrategy::AutomaticallyGradeByAverage), + peer_or_self_review_questions: Vec::new(), + }, + ), + latest_teacher_grading_decision: None, + user_exercise_slide_state_grading_summary: + UserExerciseSlideStateGradingSummary { + score_given: Some(1.0), + grading_progress: GradingProgress::FullyGraded, + }, + }) + .unwrap(); + assert_results( + &new_user_exercise_state, + None, + ActivityProgress::InProgress, + ReviewingStage::NotStarted, + ); + } + + #[test] + fn if_peer_and_self_review_enabled_self_review_comes_after_peer_review() { + let id = Uuid::parse_str("5f464818-1e68-4839-ae86-850b310f508c").unwrap(); + let exercise = create_exercise(CourseOrExamId::Course(id), true, true, true); + let user_exercise_state = create_user_exercise_state( + &exercise, + None, + ActivityProgress::Initialized, + ReviewingStage::PeerReview, + ); + let new_user_exercise_state = + derive_new_user_exercise_state(UserExerciseStateUpdateRequiredData { + exercise, + current_user_exercise_state: user_exercise_state, + peer_or_self_review_information: Some( + UserExerciseStateUpdateRequiredDataPeerReviewInformation { + given_peer_or_self_review_submissions: vec![create_peer_review_submission(), create_peer_review_submission(), create_peer_review_submission()], + given_self_review_submission: None, + latest_exercise_slide_submission_received_peer_or_self_review_question_submissions: vec![], + peer_review_queue_entry: Some(create_peer_review_queue_entry(false)), + peer_or_self_review_config: create_peer_or_self_review_config(PeerReviewProcessingStrategy::AutomaticallyGradeByAverage), + peer_or_self_review_questions: Vec::new(), + }, + ), + latest_teacher_grading_decision: None, + user_exercise_slide_state_grading_summary: + UserExerciseSlideStateGradingSummary { + score_given: Some(1.0), + grading_progress: GradingProgress::FullyGraded, + }, + }) + .unwrap(); + assert_results( + &new_user_exercise_state, + None, + ActivityProgress::InProgress, + ReviewingStage::SelfReview, + ); + } + + #[test] + fn moves_out_of_self_review() { + let id = Uuid::parse_str("5f464818-1e68-4839-ae86-850b310f508c").unwrap(); + let exercise = create_exercise(CourseOrExamId::Course(id), true, true, true); + let user_exercise_state = create_user_exercise_state( + &exercise, + None, + ActivityProgress::Initialized, + ReviewingStage::SelfReview, + ); + let new_user_exercise_state = + derive_new_user_exercise_state(UserExerciseStateUpdateRequiredData { + exercise, + current_user_exercise_state: user_exercise_state, + peer_or_self_review_information: Some( + UserExerciseStateUpdateRequiredDataPeerReviewInformation { + given_peer_or_self_review_submissions: vec![create_peer_review_submission(), create_peer_review_submission(), create_peer_review_submission()], + given_self_review_submission: Some(create_peer_review_submission()), + latest_exercise_slide_submission_received_peer_or_self_review_question_submissions: vec![create_peer_review_question_submission(4.0), create_peer_review_question_submission(3.0), create_peer_review_question_submission(4.0)], + peer_review_queue_entry: Some(create_peer_review_queue_entry(true)), + peer_or_self_review_config: create_peer_or_self_review_config(PeerReviewProcessingStrategy::AutomaticallyGradeByAverage), + peer_or_self_review_questions: Vec::new(), + }, + ), + latest_teacher_grading_decision: None, + user_exercise_slide_state_grading_summary: + UserExerciseSlideStateGradingSummary { + score_given: Some(1.0), + grading_progress: GradingProgress::FullyGraded, + }, + }) + .unwrap(); + assert_results( + &new_user_exercise_state, + Some(9000.0), + ActivityProgress::Completed, + ReviewingStage::ReviewedAndLocked, + ); + } + + // User has to start the self review themselves by clicking a button. + #[test] + fn does_not_move_to_self_review_if_self_review_but_no_peer_review() { + let id = Uuid::parse_str("5f464818-1e68-4839-ae86-850b310f508c").unwrap(); + let exercise = create_exercise(CourseOrExamId::Course(id), false, true, true); + let user_exercise_state = create_user_exercise_state( + &exercise, + None, + ActivityProgress::Initialized, + ReviewingStage::NotStarted, + ); + let new_user_exercise_state = + derive_new_user_exercise_state(UserExerciseStateUpdateRequiredData { + exercise, + current_user_exercise_state: user_exercise_state, + peer_or_self_review_information: Some( + UserExerciseStateUpdateRequiredDataPeerReviewInformation { + given_peer_or_self_review_submissions: vec![], + given_self_review_submission: None, + latest_exercise_slide_submission_received_peer_or_self_review_question_submissions: vec![], + peer_review_queue_entry: None, + peer_or_self_review_config: create_peer_or_self_review_config(PeerReviewProcessingStrategy::AutomaticallyGradeByAverage), + peer_or_self_review_questions: Vec::new(), + }, + ), + latest_teacher_grading_decision: None, + user_exercise_slide_state_grading_summary: + UserExerciseSlideStateGradingSummary { + score_given: Some(1.0), + grading_progress: GradingProgress::FullyGraded, + }, + }) + .unwrap(); + assert_results( + &new_user_exercise_state, + None, + ActivityProgress::InProgress, + ReviewingStage::NotStarted, + ); + } + } + + #[test] + fn moves_out_of_self_review_if_self_review_but_no_peer_review() { + let id = Uuid::parse_str("5f464818-1e68-4839-ae86-850b310f508c").unwrap(); + let exercise = create_exercise(CourseOrExamId::Course(id), false, true, true); + let user_exercise_state = create_user_exercise_state( + &exercise, + None, + ActivityProgress::Initialized, + ReviewingStage::SelfReview, + ); + let new_user_exercise_state = + derive_new_user_exercise_state(UserExerciseStateUpdateRequiredData { + exercise, + current_user_exercise_state: user_exercise_state, + peer_or_self_review_information: Some( + UserExerciseStateUpdateRequiredDataPeerReviewInformation { + given_peer_or_self_review_submissions: vec![], + given_self_review_submission: Some(create_peer_review_submission()), + latest_exercise_slide_submission_received_peer_or_self_review_question_submissions: vec![], + peer_review_queue_entry: None, + peer_or_self_review_config: create_peer_or_self_review_config(PeerReviewProcessingStrategy::AutomaticallyGradeByAverage), + peer_or_self_review_questions: Vec::new(), + }, + ), + latest_teacher_grading_decision: None, + user_exercise_slide_state_grading_summary: + UserExerciseSlideStateGradingSummary { + score_given: Some(1.0), + grading_progress: GradingProgress::FullyGraded, + }, + }) + .unwrap(); + assert_results( + &new_user_exercise_state, + None, + ActivityProgress::Completed, + ReviewingStage::WaitingForManualGrading, + ); + } + fn assert_results( update: &UserExerciseStateUpdate, score_given: Option, @@ -792,7 +1056,8 @@ mod tests { fn create_exercise( course_or_exam_id: CourseOrExamId, needs_peer_review: bool, - use_course_default_peer_review_config: bool, + needs_self_review: bool, + use_course_default_peer_or_self_review_config: bool, ) -> Exercise { let id = Uuid::parse_str("5f464818-1e68-4839-ae86-850b310f508c").unwrap(); let (course_id, exam_id) = course_or_exam_id.to_course_and_exam_ids(); @@ -813,16 +1078,17 @@ mod tests { max_tries_per_slide: None, limit_number_of_tries: false, needs_peer_review, - use_course_default_peer_review_config, + use_course_default_peer_or_self_review_config, exercise_language_group_id: None, + needs_self_review, } } - fn create_peer_review_config( + fn create_peer_or_self_review_config( processing_strategy: PeerReviewProcessingStrategy, - ) -> PeerReviewConfig { + ) -> PeerOrSelfReviewConfig { let id = Uuid::parse_str("5f464818-1e68-4839-ae86-850b310f508c").unwrap(); - PeerReviewConfig { + PeerOrSelfReviewConfig { id, created_at: Utc.with_ymd_and_hms(2022, 1, 1, 0, 0, 0).unwrap(), updated_at: Utc.with_ymd_and_hms(2022, 1, 1, 0, 0, 0).unwrap(), @@ -835,21 +1101,26 @@ mod tests { processing_strategy, manual_review_cutoff_in_days: 21, points_are_all_or_nothing: true, + review_instructions: None, } } fn create_peer_review_question_submission( number_data: f32, - ) -> PeerReviewQuestionSubmission { - PeerReviewQuestionSubmission { + ) -> PeerOrSelfReviewQuestionSubmission { + PeerOrSelfReviewQuestionSubmission { id: Uuid::parse_str("bf923ea4-a637-4d97-b78b-6f843d76120a").unwrap(), created_at: Utc::now(), updated_at: Utc::now(), deleted_at: None, - peer_review_question_id: Uuid::parse_str("b853bbd7-feee-4447-ab14-c9622e565ea1") - .unwrap(), - peer_review_submission_id: Uuid::parse_str("be4061b5-b468-4f50-93b0-cf3bf9de9a13") - .unwrap(), + peer_or_self_review_question_id: Uuid::parse_str( + "b853bbd7-feee-4447-ab14-c9622e565ea1", + ) + .unwrap(), + peer_or_self_review_submission_id: Uuid::parse_str( + "be4061b5-b468-4f50-93b0-cf3bf9de9a13", + ) + .unwrap(), text_data: None, number_data: Some(number_data), } @@ -857,56 +1128,60 @@ mod tests { fn create_peer_review_question_submission_with_ids( number_data: f32, - peer_review_question_id: Uuid, - peer_review_submission_id: Uuid, - ) -> PeerReviewQuestionSubmission { - PeerReviewQuestionSubmission { + peer_or_self_review_question_id: Uuid, + peer_or_self_review_submission_id: Uuid, + ) -> PeerOrSelfReviewQuestionSubmission { + PeerOrSelfReviewQuestionSubmission { id: Uuid::parse_str("bf923ea4-a637-4d97-b78b-6f843d76120a").unwrap(), created_at: Utc::now(), updated_at: Utc::now(), deleted_at: None, - peer_review_question_id, - peer_review_submission_id, + peer_or_self_review_question_id, + peer_or_self_review_submission_id, text_data: None, number_data: Some(number_data), } } - fn create_peer_review_question_scale(id: Uuid, weight: f32) -> PeerReviewQuestion { - PeerReviewQuestion { + fn create_peer_review_question_scale(id: Uuid, weight: f32) -> PeerOrSelfReviewQuestion { + PeerOrSelfReviewQuestion { id, weight, created_at: Utc::now(), updated_at: Utc::now(), deleted_at: None, - peer_review_config_id: Uuid::parse_str("bf923ea4-a637-4d97-b78b-6f843d76120a") - .unwrap(), + peer_or_self_review_config_id: Uuid::parse_str( + "bf923ea4-a637-4d97-b78b-6f843d76120a", + ) + .unwrap(), order_number: 1, question: "A question".to_string(), - question_type: PeerReviewQuestionType::Scale, + question_type: PeerOrSelfReviewQuestionType::Scale, answer_required: true, } } - fn create_peer_review_question_essay(id: Uuid, weight: f32) -> PeerReviewQuestion { - PeerReviewQuestion { + fn create_peer_review_question_essay(id: Uuid, weight: f32) -> PeerOrSelfReviewQuestion { + PeerOrSelfReviewQuestion { id, weight, created_at: Utc::now(), updated_at: Utc::now(), deleted_at: None, - peer_review_config_id: Uuid::parse_str("bf923ea4-a637-4d97-b78b-6f843d76120a") - .unwrap(), + peer_or_self_review_config_id: Uuid::parse_str( + "bf923ea4-a637-4d97-b78b-6f843d76120a", + ) + .unwrap(), order_number: 1, question: "A question".to_string(), - question_type: PeerReviewQuestionType::Essay, + question_type: PeerOrSelfReviewQuestionType::Essay, answer_required: true, } } - fn create_peer_review_submission() -> PeerReviewSubmission { + fn create_peer_review_submission() -> PeerOrSelfReviewSubmission { let id = Uuid::parse_str("5f464818-1e68-4839-ae86-850b310f508c").unwrap(); - PeerReviewSubmission { + PeerOrSelfReviewSubmission { id, created_at: Utc::now(), updated_at: Utc::now(), @@ -914,12 +1189,14 @@ mod tests { user_id: id, exercise_id: id, course_instance_id: id, - peer_review_config_id: id, + peer_or_self_review_config_id: id, exercise_slide_submission_id: id, } } - fn create_peer_review_queue_entry() -> PeerReviewQueueEntry { + fn create_peer_review_queue_entry( + received_enough_peer_reviews: bool, + ) -> PeerReviewQueueEntry { let id = Uuid::parse_str("5f464818-1e68-4839-ae86-850b310f508c").unwrap(); PeerReviewQueueEntry { id, @@ -930,7 +1207,7 @@ mod tests { exercise_id: id, course_instance_id: id, receiving_peer_reviews_exercise_slide_submission_id: id, - received_enough_peer_reviews: true, + received_enough_peer_reviews, peer_review_priority: 100, removed_from_queue_for_unusual_reason: false, } diff --git a/services/headless-lms/models/src/library/user_exercise_state_updater/validation.rs b/services/headless-lms/models/src/library/user_exercise_state_updater/validation.rs index 6c30c7517c3f..c6f4cc03c230 100644 --- a/services/headless-lms/models/src/library/user_exercise_state_updater/validation.rs +++ b/services/headless-lms/models/src/library/user_exercise_state_updater/validation.rs @@ -12,9 +12,9 @@ pub(super) fn validate_input(input_data: &UserExerciseStateUpdateRequiredData) - None, )); } - if let Some(peer_review_information) = &input_data.peer_review_information { - if peer_review_information - .given_peer_review_submissions + if let Some(peer_or_self_review_information) = &input_data.peer_or_self_review_information { + if peer_or_self_review_information + .given_peer_or_self_review_submissions .iter() .any(|prs| prs.deleted_at.is_some()) { @@ -26,8 +26,8 @@ pub(super) fn validate_input(input_data: &UserExerciseStateUpdateRequiredData) - )); } - if peer_review_information - .latest_exercise_slide_submission_received_peer_review_question_submissions + if peer_or_self_review_information + .latest_exercise_slide_submission_received_peer_or_self_review_question_submissions .iter() .any(|prqs| prqs.deleted_at.is_some()) { @@ -39,7 +39,9 @@ pub(super) fn validate_input(input_data: &UserExerciseStateUpdateRequiredData) - )); } - if let Some(peer_review_queue_entry) = &peer_review_information.peer_review_queue_entry { + if let Some(peer_review_queue_entry) = + &peer_or_self_review_information.peer_review_queue_entry + { if peer_review_queue_entry.deleted_at.is_some() { return Err(ModelError::new( ModelErrorType::Generic, @@ -50,8 +52,8 @@ pub(super) fn validate_input(input_data: &UserExerciseStateUpdateRequiredData) - } } - if peer_review_information - .peer_review_config + if peer_or_self_review_information + .peer_or_self_review_config .deleted_at .is_some() { diff --git a/services/headless-lms/models/src/page_history.rs b/services/headless-lms/models/src/page_history.rs index c591c98a90bb..9594ed9dc34c 100644 --- a/services/headless-lms/models/src/page_history.rs +++ b/services/headless-lms/models/src/page_history.rs @@ -2,8 +2,8 @@ use serde_json::Value; use crate::{ pages::{CmsPageExercise, CmsPageExerciseSlide, CmsPageExerciseTask}, - peer_review_configs::CmsPeerReviewConfig, - peer_review_questions::CmsPeerReviewQuestion, + peer_or_self_review_configs::CmsPeerOrSelfReviewConfig, + peer_or_self_review_questions::CmsPeerOrSelfReviewQuestion, prelude::*, }; @@ -34,8 +34,8 @@ pub struct PageHistoryContent { pub exercises: Vec, pub exercise_slides: Vec, pub exercise_tasks: Vec, - pub peer_review_configs: Vec, - pub peer_review_questions: Vec, + pub peer_or_self_review_configs: Vec, + pub peer_or_self_review_questions: Vec, } // Batch refactor pushed past the limit diff --git a/services/headless-lms/models/src/pages.rs b/services/headless-lms/models/src/pages.rs index dc4f7be980f7..4b79a80556c2 100644 --- a/services/headless-lms/models/src/pages.rs +++ b/services/headless-lms/models/src/pages.rs @@ -20,8 +20,10 @@ use crate::{ exercise_tasks::ExerciseTask, exercises::Exercise, page_history::{self, HistoryChangeReason, PageHistoryContent}, - peer_review_configs::CmsPeerReviewConfig, - peer_review_questions::{normalize_cms_peer_review_questions, CmsPeerReviewQuestion}, + peer_or_self_review_configs::CmsPeerOrSelfReviewConfig, + peer_or_self_review_questions::{ + normalize_cms_peer_or_self_review_questions, CmsPeerOrSelfReviewQuestion, + }, prelude::*, user_course_settings::{self, UserCourseSettings}, CourseOrExamId, SpecFetcher, @@ -182,8 +184,8 @@ pub struct ContentManagementPage { pub exercises: Vec, pub exercise_slides: Vec, pub exercise_tasks: Vec, - pub peer_review_configs: Vec, - pub peer_review_questions: Vec, + pub peer_or_self_review_configs: Vec, + pub peer_or_self_review_questions: Vec, pub organization_id: Uuid, } @@ -316,8 +318,8 @@ RETURNING id exercises: vec![], exercise_slides: vec![], exercise_tasks: vec![], - peer_review_configs: Vec::new(), - peer_review_questions: Vec::new(), + peer_or_self_review_configs: Vec::new(), + peer_or_self_review_questions: Vec::new(), }, HistoryChangeReason::PageSaved, author, @@ -366,8 +368,8 @@ RETURNING id exercises: vec![], exercise_slides: vec![], exercise_tasks: vec![], - peer_review_configs: Vec::new(), - peer_review_questions: Vec::new(), + peer_or_self_review_configs: Vec::new(), + peer_or_self_review_questions: Vec::new(), }, HistoryChangeReason::PageSaved, author, @@ -777,33 +779,37 @@ pub async fn get_page_with_exercises( ) -> ModelResult { let page = get_page(&mut *conn, page_id).await?; - let peer_review_configs = - crate::peer_review_configs::get_peer_reviews_by_page_id(conn, page.id) + let peer_or_self_review_configs = + crate::peer_or_self_review_configs::get_peer_reviews_by_page_id(conn, page.id) .await? .into_iter() .flat_map(|pr| (pr.exercise_id.map(|id| (id, pr)))) .collect::>(); - let peer_review_questions = crate::peer_review_questions::get_by_page_id(conn, page.id) - .await? - .into_iter() - .into_group_map_by(|prq| prq.peer_review_config_id) - .into_iter() - .collect::>(); + let peer_or_self_review_questions = + crate::peer_or_self_review_questions::get_by_page_id(conn, page.id) + .await? + .into_iter() + .into_group_map_by(|prq| prq.peer_or_self_review_config_id) + .into_iter() + .collect::>(); let exercises = crate::exercises::get_exercises_by_page_id(&mut *conn, page.id) .await? .into_iter() .map(|exercise| { - let (a, b) = - if exercise.needs_peer_review && exercise.use_course_default_peer_review_config { - (None, None) - } else { - let peer_review_config = peer_review_configs.get(&exercise.id).copied(); - let peer_review_questions = peer_review_config - .and_then(|prc| peer_review_questions.get(&prc.id).cloned()); - (peer_review_config, peer_review_questions) - }; + let (a, b) = if exercise.needs_peer_review + && exercise.use_course_default_peer_or_self_review_config + { + (None, None) + } else { + let peer_or_self_review_config = + peer_or_self_review_configs.get(&exercise.id).cloned(); + let peer_or_self_review_questions = peer_or_self_review_config + .as_ref() + .and_then(|prc| peer_or_self_review_questions.get(&prc.id).cloned()); + (peer_or_self_review_config, peer_or_self_review_questions) + }; Ok(CmsPageExercise::from_exercise_and_peer_review_data( exercise, a, b, @@ -837,8 +843,11 @@ pub async fn get_page_with_exercises( exercises, exercise_slides, exercise_tasks, - peer_review_configs: peer_review_configs.into_values().collect(), - peer_review_questions: peer_review_questions.into_values().flatten().collect(), + peer_or_self_review_configs: peer_or_self_review_configs.into_values().collect(), + peer_or_self_review_questions: peer_or_self_review_questions + .into_values() + .flatten() + .collect(), organization_id, }) } @@ -884,16 +893,17 @@ pub struct CmsPageExercise { pub limit_number_of_tries: bool, pub deadline: Option>, pub needs_peer_review: bool, - pub peer_review_config: Option, - pub peer_review_questions: Option>, - pub use_course_default_peer_review_config: bool, + pub needs_self_review: bool, + pub peer_or_self_review_config: Option, + pub peer_or_self_review_questions: Option>, + pub use_course_default_peer_or_self_review_config: bool, } impl CmsPageExercise { fn from_exercise_and_peer_review_data( exercise: Exercise, - peer_review_config: Option, - peer_review_questions: Option>, + peer_or_self_review_config: Option, + peer_or_self_review_questions: Option>, ) -> Self { Self { id: exercise.id, @@ -904,9 +914,11 @@ impl CmsPageExercise { limit_number_of_tries: exercise.limit_number_of_tries, deadline: exercise.deadline, needs_peer_review: exercise.needs_peer_review, - use_course_default_peer_review_config: exercise.use_course_default_peer_review_config, - peer_review_config, - peer_review_questions, + needs_self_review: exercise.needs_self_review, + use_course_default_peer_or_self_review_config: exercise + .use_course_default_peer_or_self_review_config, + peer_or_self_review_config, + peer_or_self_review_questions, } } } @@ -1043,8 +1055,9 @@ pub async fn update_page( cms_page_update.validate_exercise_data()?; for exercise in cms_page_update.exercises.iter_mut() { - if let Some(peer_review_questions) = exercise.peer_review_questions.as_mut() { - normalize_cms_peer_review_questions(peer_review_questions); + if let Some(peer_or_self_review_questions) = exercise.peer_or_self_review_questions.as_mut() + { + normalize_cms_peer_or_self_review_questions(peer_or_self_review_questions); } } @@ -1123,46 +1136,49 @@ RETURNING id, .await?; // Peer reviews - let existing_peer_review_config_ids = - crate::peer_review_configs::delete_peer_reviews_by_exrcise_ids( + let existing_peer_or_self_review_config_ids = + crate::peer_or_self_review_configs::delete_peer_reviews_by_exrcise_ids( &mut tx, &existing_exercise_ids, ) .await?; - let (peer_review_configs, peer_review_questions) = cms_page_update + let (peer_or_self_review_configs, peer_or_self_review_questions) = cms_page_update .exercises .into_iter() - .filter(|e| !e.use_course_default_peer_review_config) - .flat_map(|e| e.peer_review_config.zip(e.peer_review_questions)) + .filter(|e| !e.use_course_default_peer_or_self_review_config) + .flat_map(|e| { + e.peer_or_self_review_config + .zip(e.peer_or_self_review_questions) + }) .fold((vec![], vec![]), |(mut a, mut b), (pr, prq)| { a.push(pr); b.extend(prq); (a, b) }); - let remapped_peer_review_configs = upsert_peer_review_configs( + let remapped_peer_or_self_review_configs = upsert_peer_or_self_review_configs( &mut tx, - &existing_peer_review_config_ids, - &peer_review_configs, + &existing_peer_or_self_review_config_ids, + &peer_or_self_review_configs, &remapped_exercises, page_update.retain_ids, ) .await?; // Peer review questions - let existing_peer_review_questions = - crate::peer_review_questions::delete_peer_review_questions_by_peer_review_config_ids( + let existing_peer_or_self_review_questions = + crate::peer_or_self_review_questions::delete_peer_or_self_review_questions_by_peer_or_self_review_config_ids( &mut tx, - &existing_peer_review_config_ids, + &existing_peer_or_self_review_config_ids, ) .await?; - let remapped_peer_review_questions = upsert_peer_review_questions( + let remapped_peer_or_self_review_questions = upsert_peer_or_self_review_questions( &mut tx, - &existing_peer_review_questions, - &peer_review_questions, - &remapped_peer_review_configs, + &existing_peer_or_self_review_questions, + &peer_or_self_review_questions, + &remapped_peer_or_self_review_configs, page_update.retain_ids, ) .await?; @@ -1235,36 +1251,38 @@ RETURNING id, let final_exercises = x .iter() .map(|e| { - let peer_review_config = remapped_peer_review_configs + let peer_or_self_review_config = remapped_peer_or_self_review_configs .values() .find(|prc| prc.exercise_id == Some(e.id)); - if let Some(prc) = peer_review_config { - let peer_review_questions = remapped_peer_review_questions + if let Some(prc) = peer_or_self_review_config { + let peer_or_self_review_questions = remapped_peer_or_self_review_questions .values() - .filter(|prq| prq.peer_review_config_id == prc.id) + .filter(|prq| prq.peer_or_self_review_config_id == prc.id) .cloned() .collect::>(); return CmsPageExercise::from_exercise_and_peer_review_data( e.clone(), - Some(*prc), - Some(peer_review_questions), + Some(prc.clone()), + Some(peer_or_self_review_questions), ); } CmsPageExercise::from_exercise_and_peer_review_data(e.clone(), None, None) }) .collect(); let final_slides: Vec = remapped_exercise_slides.into_values().collect(); - let final_peer_reviews: Vec = - remapped_peer_review_configs.into_values().collect(); - let final_peer_review_questions: Vec = - remapped_peer_review_questions.into_values().collect(); + let final_peer_reviews: Vec = + remapped_peer_or_self_review_configs.into_values().collect(); + let final_peer_or_self_review_questions: Vec = + remapped_peer_or_self_review_questions + .into_values() + .collect(); let history_content = PageHistoryContent { content: page.content.clone(), exercises: final_exercises, exercise_slides: final_slides, exercise_tasks: final_tasks, - peer_review_configs: final_peer_reviews, - peer_review_questions: final_peer_review_questions, + peer_or_self_review_configs: final_peer_reviews, + peer_or_self_review_questions: final_peer_or_self_review_questions, }; crate::page_history::insert( &mut tx, @@ -1286,8 +1304,8 @@ RETURNING id, exercises: history_content.exercises, exercise_slides: history_content.exercise_slides, exercise_tasks: history_content.exercise_tasks, - peer_review_configs: history_content.peer_review_configs, - peer_review_questions: history_content.peer_review_questions, + peer_or_self_review_configs: history_content.peer_or_self_review_configs, + peer_or_self_review_questions: history_content.peer_or_self_review_questions, organization_id, }) } @@ -1350,7 +1368,8 @@ INSERT INTO exercises( limit_number_of_tries, deadline, needs_peer_review, - use_course_default_peer_review_config, + needs_self_review, + use_course_default_peer_or_self_review_config, exercise_language_group_id ) VALUES ( @@ -1367,7 +1386,8 @@ VALUES ( $11, $12, $13, - $14 + $14, + $15 ) ON CONFLICT (id) DO UPDATE SET course_id = $2, @@ -1381,8 +1401,9 @@ SET course_id = $2, limit_number_of_tries = $10, deadline = $11, needs_peer_review = $12, - use_course_default_peer_review_config = $13, - exercise_language_group_id = $14, + needs_self_review = $13, + use_course_default_peer_or_self_review_config = $14, + exercise_language_group_id = $15, deleted_at = NULL RETURNING *; ", @@ -1398,7 +1419,8 @@ RETURNING *; exercise_update.limit_number_of_tries, exercise_update.deadline, exercise_update.needs_peer_review, - exercise_update.use_course_default_peer_review_config, + exercise_update.needs_self_review, + exercise_update.use_course_default_peer_or_self_review_config, exercise_language_group_id, ) .fetch_one(&mut *conn) @@ -1591,20 +1613,20 @@ RETURNING id, Ok(remapped_exercise_tasks) } -pub async fn upsert_peer_review_configs( +pub async fn upsert_peer_or_self_review_configs( conn: &mut PgConnection, existing_peer_reviews: &[Uuid], - peer_reviews: &[CmsPeerReviewConfig], + peer_reviews: &[CmsPeerOrSelfReviewConfig], remapped_exercises: &HashMap, retain_ids: bool, -) -> ModelResult> { +) -> ModelResult> { if peer_reviews.is_empty() { Ok(HashMap::new()) } else { - let mut new_peer_review_config_id_to_old_id = HashMap::new(); + let mut new_peer_or_self_review_config_id_to_old_id = HashMap::new(); let mut sql: QueryBuilder = QueryBuilder::new( - "INSERT INTO peer_review_configs ( + "INSERT INTO peer_or_self_review_configs ( id, course_id, exercise_id, @@ -1613,6 +1635,7 @@ pub async fn upsert_peer_review_configs( processing_strategy, accepting_threshold, points_are_all_or_nothing, + review_instructions, deleted_at ) ", ); @@ -1622,12 +1645,13 @@ pub async fn upsert_peer_review_configs( sql.push_values(peer_reviews.iter().take(1000), |mut x, pr| { let peer_review_exists = existing_peer_reviews.iter().any(|id| *id == pr.id); - let safe_for_db_peer_review_config_id = if retain_ids || peer_review_exists { + let safe_for_db_peer_or_self_review_config_id = if retain_ids || peer_review_exists { pr.id } else { Uuid::new_v4() }; - new_peer_review_config_id_to_old_id.insert(safe_for_db_peer_review_config_id, pr.id); + new_peer_or_self_review_config_id_to_old_id + .insert(safe_for_db_peer_or_self_review_config_id, pr.id); let safe_for_db_exercise_id = pr.exercise_id.and_then(|id| { let res = remapped_exercises.get(&id).map(|e| e.id); @@ -1638,7 +1662,7 @@ pub async fn upsert_peer_review_configs( res }); - x.push_bind(safe_for_db_peer_review_config_id) + x.push_bind(safe_for_db_peer_or_self_review_config_id) .push_bind(pr.course_id) .push_bind(safe_for_db_exercise_id) .push_bind(pr.peer_reviews_to_give) @@ -1646,6 +1670,7 @@ pub async fn upsert_peer_review_configs( .push_bind(pr.processing_strategy) .push_bind(pr.accepting_threshold) .push_bind(pr.points_are_all_or_nothing) + .push_bind(pr.review_instructions.clone()) .push("NULL"); }); @@ -1667,6 +1692,7 @@ SET course_id = excluded.course_id, processing_strategy = excluded.processing_strategy, accepting_threshold = excluded.accepting_threshold, points_are_all_or_nothing = excluded.points_are_all_or_nothing, + review_instructions = excluded.review_instructions, deleted_at = NULL RETURNING id; ", @@ -1681,7 +1707,7 @@ RETURNING id; .collect::>(); let prs = sqlx::query_as!( - CmsPeerReviewConfig, + CmsPeerOrSelfReviewConfig, r#" SELECT id as "id!", course_id as "course_id!", @@ -1690,8 +1716,9 @@ SELECT id as "id!", peer_reviews_to_receive as "peer_reviews_to_receive!", processing_strategy AS "processing_strategy!: _", accepting_threshold "accepting_threshold!", - points_are_all_or_nothing "points_are_all_or_nothing!" -FROM peer_review_configs + points_are_all_or_nothing "points_are_all_or_nothing!", + review_instructions +FROM peer_or_self_review_configs WHERE id IN ( SELECT UNNEST($1::uuid []) ) @@ -1705,7 +1732,7 @@ WHERE id IN ( let mut remapped_peer_reviews = HashMap::new(); for pr in prs { - let old_id = new_peer_review_config_id_to_old_id + let old_id = new_peer_or_self_review_config_id_to_old_id .get(&pr.id) .ok_or_else(|| { ModelError::new( @@ -1721,22 +1748,22 @@ WHERE id IN ( } } -pub async fn upsert_peer_review_questions( +pub async fn upsert_peer_or_self_review_questions( conn: &mut PgConnection, - existing_peer_review_questions: &[Uuid], - peer_review_questions: &[CmsPeerReviewQuestion], - remapped_peer_review_config_ids: &HashMap, + existing_peer_or_self_review_questions: &[Uuid], + peer_or_self_review_questions: &[CmsPeerOrSelfReviewQuestion], + remapped_peer_or_self_review_config_ids: &HashMap, retain_ids: bool, -) -> ModelResult> { - if peer_review_questions.is_empty() { +) -> ModelResult> { + if peer_or_self_review_questions.is_empty() { Ok(HashMap::new()) } else { - let mut new_peer_review_question_id_to_old_id = HashMap::new(); + let mut new_peer_or_self_review_question_id_to_old_id = HashMap::new(); let mut sql: QueryBuilder = QueryBuilder::new( - "INSERT INTO peer_review_questions ( + "INSERT INTO peer_or_self_review_questions ( id, - peer_review_config_id, + peer_or_self_review_config_id, order_number, question, question_type, @@ -1746,12 +1773,12 @@ pub async fn upsert_peer_review_questions( ) ", ); - let peer_review_questions = peer_review_questions + let peer_or_self_review_questions = peer_or_self_review_questions .iter() .take(1000) .map(|prq| { - remapped_peer_review_config_ids - .get(&prq.peer_review_config_id) + remapped_peer_or_self_review_config_ids + .get(&prq.peer_or_self_review_config_id) .map(|r| (prq, r.id)) .ok_or_else(|| { ModelError::new( @@ -1764,22 +1791,22 @@ pub async fn upsert_peer_review_questions( .collect::, _>>()?; sql.push_values( - peer_review_questions, - |mut x, (prq, peer_review_config_id)| { - let peer_review_question_exists = existing_peer_review_questions + peer_or_self_review_questions, + |mut x, (prq, peer_or_self_review_config_id)| { + let peer_review_question_exists = existing_peer_or_self_review_questions .iter() .any(|id| *id == prq.id); - let safe_for_db_peer_review_question_id = + let safe_for_db_peer_or_self_review_question_id = if retain_ids || peer_review_question_exists { prq.id } else { Uuid::new_v4() }; - new_peer_review_question_id_to_old_id - .insert(safe_for_db_peer_review_question_id, prq.id); + new_peer_or_self_review_question_id_to_old_id + .insert(safe_for_db_peer_or_self_review_question_id, prq.id); - x.push_bind(safe_for_db_peer_review_question_id) - .push_bind(peer_review_config_id) + x.push_bind(safe_for_db_peer_or_self_review_question_id) + .push_bind(peer_or_self_review_config_id) .push_bind(prq.order_number) .push_bind(prq.question.as_str()) .push_bind(prq.question_type) @@ -1792,7 +1819,7 @@ pub async fn upsert_peer_review_questions( sql.push( " ON CONFLICT (id) DO UPDATE -SET peer_review_config_id = excluded.peer_review_config_id, +SET peer_or_self_review_config_id = excluded.peer_or_self_review_config_id, order_number = excluded.order_number, question = excluded.question, question_type = excluded.question_type, @@ -1812,16 +1839,16 @@ RETURNING id; .collect::>(); let prqs = sqlx::query_as!( - CmsPeerReviewQuestion, + CmsPeerOrSelfReviewQuestion, r#" SELECT id AS "id!", answer_required AS "answer_required!", order_number AS "order_number!", - peer_review_config_id AS "peer_review_config_id!", + peer_or_self_review_config_id AS "peer_or_self_review_config_id!", question AS "question!", question_type AS "question_type!: _", weight AS "weight!" -FROM peer_review_questions +FROM peer_or_self_review_questions WHERE id IN ( SELECT UNNEST($1::uuid []) ) @@ -1832,10 +1859,10 @@ WHERE id IN ( .fetch_all(&mut *conn) .await?; - let mut remapped_peer_review_questions = HashMap::new(); + let mut remapped_peer_or_self_review_questions = HashMap::new(); for prq in prqs { - let old_id = new_peer_review_question_id_to_old_id + let old_id = new_peer_or_self_review_question_id_to_old_id .get(&prq.id) .ok_or_else(|| { ModelError::new( @@ -1844,10 +1871,10 @@ WHERE id IN ( None, ) })?; - remapped_peer_review_questions.insert(*old_id, prq); + remapped_peer_or_self_review_questions.insert(*old_id, prq); } - Ok(remapped_peer_review_questions) + Ok(remapped_peer_or_self_review_questions) } } @@ -3297,9 +3324,10 @@ mod test { limit_number_of_tries: false, deadline: Some(Utc.with_ymd_and_hms(2125, 1, 1, 23, 59, 59).unwrap()), needs_peer_review: false, - peer_review_config: None, - peer_review_questions: None, - use_course_default_peer_review_config: false, + needs_self_review: false, + peer_or_self_review_config: None, + peer_or_self_review_questions: None, + use_course_default_peer_or_self_review_config: false, }; let e1_s1 = CmsPageExerciseSlide { id: Uuid::parse_str("43380e81-6ff2-4f46-9f38-af0ac6a8421a").unwrap(), @@ -3365,34 +3393,42 @@ mod test { .await .unwrap(); - let pr1 = CmsPeerReviewConfig { + let pr1 = CmsPeerOrSelfReviewConfig { id:pr_id, exercise_id: Some(exercise_id), course_id: course, - processing_strategy: crate::peer_review_configs::PeerReviewProcessingStrategy::AutomaticallyGradeOrManualReviewByAverage, + processing_strategy: crate::peer_or_self_review_configs::PeerReviewProcessingStrategy::AutomaticallyGradeOrManualReviewByAverage, accepting_threshold: 0.5, peer_reviews_to_give: 2, peer_reviews_to_receive: 1, points_are_all_or_nothing: false, + review_instructions: None, }; - let prq = CmsPeerReviewQuestion { + let prq = CmsPeerOrSelfReviewQuestion { id: prq_id, - peer_review_config_id: pr_id, + peer_or_self_review_config_id: pr_id, answer_required: true, order_number: 0, question: "juu".to_string(), - question_type: crate::peer_review_questions::PeerReviewQuestionType::Essay, + question_type: + crate::peer_or_self_review_questions::PeerOrSelfReviewQuestionType::Essay, weight: 0.31, }; let mut remapped_exercises = HashMap::new(); remapped_exercises.insert(exercise_id, exercise); - let pr_res = - upsert_peer_review_configs(tx.as_mut(), &[], &[pr1], &remapped_exercises, false) + let pr_res = upsert_peer_or_self_review_configs( + tx.as_mut(), + &[], + &[pr1], + &remapped_exercises, + false, + ) + .await + .unwrap(); + let prq_res = + upsert_peer_or_self_review_questions(tx.as_mut(), &[], &[prq], &pr_res, false) .await .unwrap(); - let prq_res = upsert_peer_review_questions(tx.as_mut(), &[], &[prq], &pr_res, false) - .await - .unwrap(); assert!(pr_res.get(&pr_id).unwrap().accepting_threshold == 0.5); @@ -3409,32 +3445,34 @@ mod test { .unwrap(); let pr_id = Uuid::parse_str("9b69dc5e-0eca-4fcd-8fd2-031a3a65da82").unwrap(); let prq_id = Uuid::parse_str("de18fa14-4ac6-4b57-b9f8-4843fa52d948").unwrap(); - let pr1 = CmsPeerReviewConfig { + let pr1 = CmsPeerOrSelfReviewConfig { id:pr_id, exercise_id: Some(exercise_id), course_id: course, - processing_strategy: crate::peer_review_configs::PeerReviewProcessingStrategy::AutomaticallyGradeOrManualReviewByAverage, + processing_strategy: crate::peer_or_self_review_configs::PeerReviewProcessingStrategy::AutomaticallyGradeOrManualReviewByAverage, accepting_threshold: 0.5, peer_reviews_to_give: 2, peer_reviews_to_receive: 1, points_are_all_or_nothing: true, + review_instructions: None, }; - let prq = CmsPeerReviewQuestion { + let prq = CmsPeerOrSelfReviewQuestion { id: prq_id, - peer_review_config_id: pr_id, + peer_or_self_review_config_id: pr_id, answer_required: true, order_number: 0, question: "juu".to_string(), - question_type: crate::peer_review_questions::PeerReviewQuestionType::Essay, + question_type: + crate::peer_or_self_review_questions::PeerOrSelfReviewQuestionType::Essay, weight: 0.0, }; let mut remapped_exercises = HashMap::new(); remapped_exercises.insert(exercise_id, exercise); let pr_res = - upsert_peer_review_configs(tx.as_mut(), &[], &[pr1], &remapped_exercises, true) + upsert_peer_or_self_review_configs(tx.as_mut(), &[], &[pr1], &remapped_exercises, true) .await .unwrap(); - let prq_res = upsert_peer_review_questions(tx.as_mut(), &[], &[prq], &pr_res, true) + let prq_res = upsert_peer_or_self_review_questions(tx.as_mut(), &[], &[prq], &pr_res, true) .await .unwrap(); @@ -3453,10 +3491,11 @@ mod test { .unwrap(); let mut remapped_exercises = HashMap::new(); remapped_exercises.insert(exercise_id, exercise); - let pr_res = upsert_peer_review_configs(tx.as_mut(), &[], &[], &remapped_exercises, true) - .await - .unwrap(); - let prq_res = upsert_peer_review_questions(tx.as_mut(), &[], &[], &pr_res, true) + let pr_res = + upsert_peer_or_self_review_configs(tx.as_mut(), &[], &[], &remapped_exercises, true) + .await + .unwrap(); + let prq_res = upsert_peer_or_self_review_questions(tx.as_mut(), &[], &[], &pr_res, true) .await .unwrap(); diff --git a/services/headless-lms/models/src/peer_review_configs.rs b/services/headless-lms/models/src/peer_or_self_review_configs.rs similarity index 71% rename from services/headless-lms/models/src/peer_review_configs.rs rename to services/headless-lms/models/src/peer_or_self_review_configs.rs index 38998b3ad077..1dca9de7419c 100644 --- a/services/headless-lms/models/src/peer_review_configs.rs +++ b/services/headless-lms/models/src/peer_or_self_review_configs.rs @@ -4,10 +4,10 @@ use url::Url; use crate::{ exercise_service_info::ExerciseServiceInfoApi, exercises::{self, Exercise}, - library::{self, peer_reviewing::CourseMaterialPeerReviewData}, - peer_review_questions::{ - delete_peer_review_questions_by_peer_review_config_ids, - upsert_multiple_peer_review_questions, CmsPeerReviewQuestion, + library::{self, peer_or_self_reviewing::CourseMaterialPeerOrSelfReviewData}, + peer_or_self_review_questions::{ + delete_peer_or_self_review_questions_by_peer_or_self_review_config_ids, + upsert_multiple_peer_or_self_review_questions, CmsPeerOrSelfReviewQuestion, }, prelude::*, user_exercise_states::{self, ReviewingStage}, @@ -15,7 +15,7 @@ use crate::{ #[derive(Debug, Serialize, Deserialize, PartialEq, Clone)] #[cfg_attr(feature = "ts_rs", derive(TS))] -pub struct PeerReviewConfig { +pub struct PeerOrSelfReviewConfig { pub id: Uuid, pub created_at: DateTime, pub updated_at: DateTime, @@ -28,12 +28,13 @@ pub struct PeerReviewConfig { pub processing_strategy: PeerReviewProcessingStrategy, pub manual_review_cutoff_in_days: i32, pub points_are_all_or_nothing: bool, + pub review_instructions: Option, } -/// Like `PeerReviewConfig` but only the fields it's fine to show to all users. +/// Like `PeerOrSelfReviewConfig` but only the fields it's fine to show to all users. #[derive(Debug, Serialize, Deserialize, PartialEq, Clone)] #[cfg_attr(feature = "ts_rs", derive(TS))] -pub struct CourseMaterialPeerReviewConfig { +pub struct CourseMaterialPeerOrSelfReviewConfig { pub id: Uuid, pub course_id: Uuid, pub exercise_id: Option, @@ -41,9 +42,9 @@ pub struct CourseMaterialPeerReviewConfig { pub peer_reviews_to_receive: i32, } -#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, Copy)] +#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)] #[cfg_attr(feature = "ts_rs", derive(TS))] -pub struct CmsPeerReviewConfig { +pub struct CmsPeerOrSelfReviewConfig { pub id: Uuid, pub course_id: Uuid, pub exercise_id: Option, @@ -52,13 +53,14 @@ pub struct CmsPeerReviewConfig { pub accepting_threshold: f32, pub processing_strategy: PeerReviewProcessingStrategy, pub points_are_all_or_nothing: bool, + pub review_instructions: Option, } #[derive(Debug, Serialize, Deserialize, PartialEq, Clone)] #[cfg_attr(feature = "ts_rs", derive(TS))] -pub struct CmsPeerReviewConfiguration { - pub peer_review_config: CmsPeerReviewConfig, - pub peer_review_questions: Vec, +pub struct CmsPeerOrSelfReviewConfiguration { + pub peer_or_self_review_config: CmsPeerOrSelfReviewConfig, + pub peer_or_self_review_questions: Vec, } /** @@ -89,7 +91,7 @@ pub async fn insert( ) -> ModelResult { let res = sqlx::query!( " -INSERT INTO peer_review_configs (id, course_id, exercise_id) +INSERT INTO peer_or_self_review_configs (id, course_id, exercise_id) VALUES ($1, $2, $3) RETURNING id ", @@ -105,12 +107,12 @@ RETURNING id pub async fn upsert_with_id( conn: &mut PgConnection, pkey_policy: PKeyPolicy, - cms_peer_review: &CmsPeerReviewConfig, -) -> ModelResult { + cms_peer_review: &CmsPeerOrSelfReviewConfig, +) -> ModelResult { let res = sqlx::query_as!( - CmsPeerReviewConfig, + CmsPeerOrSelfReviewConfig, r#" - INSERT INTO peer_review_configs ( + INSERT INTO peer_or_self_review_configs ( id, course_id, exercise_id, @@ -118,9 +120,10 @@ pub async fn upsert_with_id( peer_reviews_to_receive, accepting_threshold, processing_strategy, - points_are_all_or_nothing + points_are_all_or_nothing, + review_instructions ) -VALUES ($1, $2, $3, $4, $5, $6, $7, $8) ON CONFLICT (id) DO +VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9) ON CONFLICT (id) DO UPDATE SET course_id = excluded.course_id, exercise_id = excluded.exercise_id, @@ -128,7 +131,8 @@ SET course_id = excluded.course_id, peer_reviews_to_receive = excluded.peer_reviews_to_receive, accepting_threshold = excluded.accepting_threshold, processing_strategy = excluded.processing_strategy, - points_are_all_or_nothing = excluded.points_are_all_or_nothing + points_are_all_or_nothing = excluded.points_are_all_or_nothing, + review_instructions = excluded.review_instructions RETURNING id, course_id, exercise_id, @@ -136,7 +140,8 @@ RETURNING id, peer_reviews_to_receive, accepting_threshold, processing_strategy AS "processing_strategy:_", - points_are_all_or_nothing + points_are_all_or_nothing, + review_instructions "#, pkey_policy.into_uuid(), cms_peer_review.course_id, @@ -146,15 +151,16 @@ RETURNING id, cms_peer_review.accepting_threshold, cms_peer_review.processing_strategy as _, cms_peer_review.points_are_all_or_nothing, + cms_peer_review.review_instructions, ) .fetch_one(conn) .await?; Ok(res) } -pub async fn get_by_id(conn: &mut PgConnection, id: Uuid) -> ModelResult { +pub async fn get_by_id(conn: &mut PgConnection, id: Uuid) -> ModelResult { let res = sqlx::query_as!( - PeerReviewConfig, + PeerOrSelfReviewConfig, r#" SELECT id, created_at, @@ -167,8 +173,9 @@ SELECT id, accepting_threshold, processing_strategy AS "processing_strategy: _", manual_review_cutoff_in_days, - points_are_all_or_nothing -FROM peer_review_configs + points_are_all_or_nothing, + review_instructions +FROM peer_or_self_review_configs WHERE id = $1 AND deleted_at IS NULL "#, @@ -183,9 +190,9 @@ WHERE id = $1 pub async fn get_by_exercise_id( conn: &mut PgConnection, exercise_id: Uuid, -) -> ModelResult { +) -> ModelResult { let res = sqlx::query_as!( - PeerReviewConfig, + PeerOrSelfReviewConfig, r#" SELECT id, created_at, @@ -198,8 +205,9 @@ SELECT id, accepting_threshold, processing_strategy AS "processing_strategy: _", manual_review_cutoff_in_days, - points_are_all_or_nothing -FROM peer_review_configs + points_are_all_or_nothing, + review_instructions +FROM peer_or_self_review_configs WHERE exercise_id = $1 AND deleted_at IS NULL "#, @@ -210,13 +218,13 @@ WHERE exercise_id = $1 Ok(res) } -/// Returns the correct peer review config depending on `exercise.use_course_default_peer_review_config`. +/// Returns the correct peer review config depending on `exercise.use_course_default_peer_or_self_review_config`. pub async fn get_by_exercise_or_course_id( conn: &mut PgConnection, exercise: &Exercise, course_id: Uuid, -) -> ModelResult { - if exercise.use_course_default_peer_review_config { +) -> ModelResult { + if exercise.use_course_default_peer_or_self_review_config { get_default_for_course_by_course_id(conn, course_id).await } else { get_by_exercise_id(conn, exercise.id).await @@ -226,9 +234,9 @@ pub async fn get_by_exercise_or_course_id( pub async fn get_default_for_course_by_course_id( conn: &mut PgConnection, course_id: Uuid, -) -> ModelResult { +) -> ModelResult { let res = sqlx::query_as!( - PeerReviewConfig, + PeerOrSelfReviewConfig, r#" SELECT id, created_at, @@ -241,8 +249,9 @@ SELECT id, accepting_threshold, processing_strategy AS "processing_strategy: _", manual_review_cutoff_in_days, - points_are_all_or_nothing -FROM peer_review_configs + points_are_all_or_nothing, + review_instructions +FROM peer_or_self_review_configs WHERE course_id = $1 AND exercise_id IS NULL AND deleted_at IS NULL; @@ -257,7 +266,7 @@ WHERE course_id = $1 pub async fn delete(conn: &mut PgConnection, id: Uuid) -> ModelResult { let res = sqlx::query!( " -UPDATE peer_review_configs +UPDATE peer_or_self_review_configs SET deleted_at = now() WHERE id = $1 RETURNING id @@ -269,12 +278,12 @@ RETURNING id Ok(res.id) } -pub async fn get_course_material_peer_review_data( +pub async fn get_course_material_peer_or_self_review_data( conn: &mut PgConnection, user_id: Uuid, exercise_id: Uuid, fetch_service_info: impl Fn(Url) -> BoxFuture<'static, ModelResult>, -) -> ModelResult { +) -> ModelResult { let exercise = exercises::get_by_id(conn, exercise_id).await?; let (_current_exercise_slide, instance_or_exam_id) = exercises::get_or_select_exercise_slide( &mut *conn, @@ -305,7 +314,7 @@ pub async fn get_course_material_peer_review_data( ) { // Calling library inside a model function. Maybe should be refactored by moving // complicated logic to own library file? - let res = library::peer_reviewing::try_to_select_exercise_slide_submission_for_peer_review( + let res = library::peer_or_self_reviewing::try_to_select_exercise_slide_submission_for_peer_review( conn, &exercise, user_exercise_state, @@ -313,6 +322,15 @@ pub async fn get_course_material_peer_review_data( ) .await?; Ok(res) + } else if user_exercise_state.reviewing_stage == ReviewingStage::SelfReview { + let res = library::peer_or_self_reviewing::select_own_submission_for_self_review( + conn, + &exercise, + user_exercise_state, + &fetch_service_info, + ) + .await?; + Ok(res) } else { Err(ModelError::new( ModelErrorType::PreconditionFailed, @@ -332,9 +350,9 @@ pub async fn get_course_material_peer_review_data( pub async fn get_peer_reviews_by_page_id( conn: &mut PgConnection, page_id: Uuid, -) -> ModelResult> { +) -> ModelResult> { let res = sqlx::query_as!( - CmsPeerReviewConfig, + CmsPeerOrSelfReviewConfig, r#" SELECT pr.id as id, pr.course_id as course_id, @@ -343,10 +361,11 @@ SELECT pr.id as id, pr.peer_reviews_to_receive as peer_reviews_to_receive, pr.accepting_threshold as accepting_threshold, pr.processing_strategy AS "processing_strategy: _", - points_are_all_or_nothing + points_are_all_or_nothing, + pr.review_instructions from pages p join exercises e on p.id = e.page_id - join peer_review_configs pr on e.id = pr.exercise_id + join peer_or_self_review_configs pr on e.id = pr.exercise_id where p.id = $1 AND p.deleted_at IS NULL AND e.deleted_at IS NULL @@ -366,7 +385,7 @@ pub async fn delete_peer_reviews_by_exrcise_ids( ) -> ModelResult> { let res = sqlx::query!( " -UPDATE peer_review_configs +UPDATE peer_or_self_review_configs SET deleted_at = now() WHERE exercise_id = ANY ($1) AND deleted_at IS NULL @@ -385,9 +404,9 @@ RETURNING id; pub async fn get_course_default_cms_peer_review( conn: &mut PgConnection, course_id: Uuid, -) -> ModelResult { +) -> ModelResult { let res = sqlx::query_as!( - CmsPeerReviewConfig, + CmsPeerOrSelfReviewConfig, r#" SELECT id, course_id, @@ -396,8 +415,9 @@ SELECT id, peer_reviews_to_receive, accepting_threshold, processing_strategy AS "processing_strategy: _", - points_are_all_or_nothing -FROM peer_review_configs + points_are_all_or_nothing, + review_instructions +FROM peer_or_self_review_configs WHERE course_id = $1 AND exercise_id IS NULL AND deleted_at IS NULL; @@ -411,10 +431,10 @@ WHERE course_id = $1 pub async fn get_cms_peer_review_by_id( conn: &mut PgConnection, - peer_review_config_id: Uuid, -) -> ModelResult { + peer_or_self_review_config_id: Uuid, +) -> ModelResult { let res = sqlx::query_as!( - CmsPeerReviewConfig, + CmsPeerOrSelfReviewConfig, r#" SELECT id, course_id, @@ -423,11 +443,12 @@ SELECT id, peer_reviews_to_receive, accepting_threshold, processing_strategy AS "processing_strategy:_", - points_are_all_or_nothing -FROM peer_review_configs + points_are_all_or_nothing, + review_instructions +FROM peer_or_self_review_configs WHERE id = $1; "#, - peer_review_config_id + peer_or_self_review_config_id ) .fetch_one(conn) .await?; @@ -436,40 +457,47 @@ WHERE id = $1; pub async fn upsert_course_default_cms_peer_review_and_questions( conn: &mut PgConnection, - peer_review_configuration: &CmsPeerReviewConfiguration, -) -> ModelResult { + peer_or_self_review_configuration: &CmsPeerOrSelfReviewConfiguration, +) -> ModelResult { // Upsert peer review - let peer_review_config = upsert_with_id( + let peer_or_self_review_config = upsert_with_id( conn, - PKeyPolicy::Fixed(peer_review_configuration.peer_review_config.id), - &peer_review_configuration.peer_review_config, + PKeyPolicy::Fixed( + peer_or_self_review_configuration + .peer_or_self_review_config + .id, + ), + &peer_or_self_review_configuration.peer_or_self_review_config, ) .await?; // Upsert peer review questions - let previous_peer_review_question_ids = - delete_peer_review_questions_by_peer_review_config_ids(conn, &[peer_review_config.id]) - .await?; - let peer_review_questions = upsert_multiple_peer_review_questions( + let previous_peer_or_self_review_question_ids = + delete_peer_or_self_review_questions_by_peer_or_self_review_config_ids( + conn, + &[peer_or_self_review_config.id], + ) + .await?; + let peer_or_self_review_questions = upsert_multiple_peer_or_self_review_questions( conn, - &peer_review_configuration - .peer_review_questions + &peer_or_self_review_configuration + .peer_or_self_review_questions .iter() .map(|prq| { - let id = if previous_peer_review_question_ids.contains(&prq.id) { + let id = if previous_peer_or_self_review_question_ids.contains(&prq.id) { prq.id } else { Uuid::new_v4() }; - CmsPeerReviewQuestion { id, ..prq.clone() } + CmsPeerOrSelfReviewQuestion { id, ..prq.clone() } }) .collect::>(), ) .await?; - Ok(CmsPeerReviewConfiguration { - peer_review_config, - peer_review_questions, + Ok(CmsPeerOrSelfReviewConfiguration { + peer_or_self_review_config, + peer_or_self_review_questions, }) } diff --git a/services/headless-lms/models/src/peer_or_self_review_question_submissions.rs b/services/headless-lms/models/src/peer_or_self_review_question_submissions.rs new file mode 100644 index 000000000000..295943a93b95 --- /dev/null +++ b/services/headless-lms/models/src/peer_or_self_review_question_submissions.rs @@ -0,0 +1,323 @@ +use std::collections::HashMap; + +use crate::peer_or_self_review_questions::PeerOrSelfReviewQuestionType; +use crate::prelude::*; + +#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)] +#[cfg_attr(feature = "ts_rs", derive(TS))] +pub struct PeerOrSelfReviewQuestionSubmission { + pub id: Uuid, + pub created_at: DateTime, + pub updated_at: DateTime, + pub deleted_at: Option>, + pub peer_or_self_review_question_id: Uuid, + pub peer_or_self_review_submission_id: Uuid, + pub text_data: Option, + pub number_data: Option, +} + +pub async fn insert( + conn: &mut PgConnection, + pkey_policy: PKeyPolicy, + peer_or_self_review_question_id: Uuid, + peer_or_self_review_submission_id: Uuid, + text_data: Option, + number_data: Option, +) -> ModelResult { + let res = sqlx::query!( + " +INSERT INTO peer_or_self_review_question_submissions ( + id, + peer_or_self_review_question_id, + peer_or_self_review_submission_id, + text_data, + number_data + ) +VALUES ($1, $2, $3, $4, $5) +RETURNING id + ", + pkey_policy.into_uuid(), + peer_or_self_review_question_id, + peer_or_self_review_submission_id, + text_data, + number_data, + ) + .fetch_one(conn) + .await?; + Ok(res.id) +} + +pub async fn get_by_peer_reviews_question_ids( + conn: &mut PgConnection, + ids: &[Uuid], + user_id: Uuid, + exercise_slide_submission_id: Uuid, +) -> ModelResult> { + let res = sqlx::query_as!( + PeerOrSelfReviewQuestionSubmission, + " + SELECT qs.id, + qs.created_at, + qs.updated_at, + qs.deleted_at, + qs.peer_or_self_review_question_id, + qs.peer_or_self_review_submission_id, + qs.text_data, + qs.number_data + FROM peer_or_self_review_question_submissions qs + JOIN peer_or_self_review_submissions s ON (qs.peer_or_self_review_submission_id = s.id) + JOIN exercise_slide_submissions es ON (s.exercise_slide_submission_id = es.id) + WHERE peer_or_self_review_question_id IN ( + SELECT UNNEST($1::uuid []) + ) + AND s.exercise_slide_submission_id = $3 + AND es.user_id = $2 + AND qs.deleted_at IS NULL; + ", + ids, + user_id, + exercise_slide_submission_id + ) + .fetch_all(conn) + .await?; + Ok(res) +} + +pub async fn get_received_question_submissions_for_exercise_slide_submission( + conn: &mut PgConnection, + exercise_slide_submission_id: Uuid, +) -> ModelResult> { + let res = sqlx::query_as!( + PeerOrSelfReviewQuestionSubmission, + " +SELECT prqs.* +FROM peer_or_self_review_submissions prs + JOIN peer_or_self_review_question_submissions prqs on prs.id = prqs.peer_or_self_review_submission_id +WHERE prs.exercise_slide_submission_id = $1 + AND prs.deleted_at IS NULL + AND prqs.deleted_at IS NULL + ", + exercise_slide_submission_id + ) + .fetch_all(conn) + .await?; + Ok(res) +} + +pub async fn get_question_submissions_from_from_peer_or_self_review_submission_ids( + conn: &mut PgConnection, + peer_or_self_review_submission_ids: &[Uuid], +) -> ModelResult> { + let res = sqlx::query_as!( + PeerOrSelfReviewQuestionSubmission, + " +SELECT * +FROM peer_or_self_review_question_submissions +WHERE peer_or_self_review_submission_id IN ( + SELECT UNNEST($1::uuid []) + ) + AND deleted_at IS NULL + ", + peer_or_self_review_submission_ids + ) + .fetch_all(&mut *conn) + .await?; + + Ok(res) +} + +#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)] +#[cfg_attr(feature = "ts_rs", derive(TS))] +#[serde(tag = "type", rename_all = "kebab-case")] +pub enum PeerOrSelfReviewAnswer { + NoAnswer, + Essay { value: String }, + Scale { value: f32 }, +} + +impl PeerOrSelfReviewAnswer { + fn new( + question_type: PeerOrSelfReviewQuestionType, + text_data: Option, + number_data: Option, + ) -> Self { + match (question_type, text_data, number_data) { + (PeerOrSelfReviewQuestionType::Essay, Some(value), _) => Self::Essay { value }, + (PeerOrSelfReviewQuestionType::Scale, _, Some(value)) => Self::Scale { value }, + _ => Self::NoAnswer, + } + } +} + +#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)] +#[cfg_attr(feature = "ts_rs", derive(TS))] +pub struct PeerOrSelfReviewQuestionAndAnswer { + pub peer_or_self_review_config_id: Uuid, + pub peer_or_self_review_question_id: Uuid, + pub peer_or_self_review_submission_id: Uuid, + pub peer_review_question_submission_id: Uuid, + pub order_number: i32, + pub question: String, + pub answer: PeerOrSelfReviewAnswer, + pub answer_required: bool, +} + +#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)] +#[cfg_attr(feature = "ts_rs", derive(TS))] +pub struct PeerReviewWithQuestionsAndAnswers { + pub peer_or_self_review_submission_id: Uuid, + pub peer_review_giver_user_id: Uuid, + pub questions_and_answers: Vec, +} + +pub async fn get_questions_and_answers_by_submission_id( + conn: &mut PgConnection, + exercise_slide_submission_id: Uuid, +) -> ModelResult> { + let res = sqlx::query!( + r#" +SELECT answers.id AS peer_review_question_submission_id, + answers.text_data, + answers.number_data, + questions.peer_or_self_review_config_id, + questions.id AS peer_or_self_review_question_id, + questions.order_number, + questions.question, + questions.question_type AS "question_type: PeerOrSelfReviewQuestionType", + questions.answer_required, + submissions.id AS peer_or_self_review_submission_id +FROM peer_or_self_review_question_submissions answers + JOIN peer_or_self_review_questions questions ON ( + answers.peer_or_self_review_question_id = questions.id + ) + JOIN peer_or_self_review_submissions submissions ON ( + answers.peer_or_self_review_submission_id = submissions.id + ) +WHERE submissions.exercise_slide_submission_id = $1 + AND questions.deleted_at IS NULL + AND answers.deleted_at IS NULL + AND submissions.deleted_at IS NULL + "#, + exercise_slide_submission_id, + ) + .map(|x| PeerOrSelfReviewQuestionAndAnswer { + peer_or_self_review_config_id: x.peer_or_self_review_config_id, + peer_or_self_review_question_id: x.peer_or_self_review_question_id, + peer_review_question_submission_id: x.peer_review_question_submission_id, + peer_or_self_review_submission_id: x.peer_or_self_review_submission_id, + order_number: x.order_number, + question: x.question, + answer: PeerOrSelfReviewAnswer::new(x.question_type, x.text_data, x.number_data), + answer_required: x.answer_required, + }) + .fetch_all(&mut *conn) + .await?; + let peer_or_self_review_submission_ids = res + .iter() + .map(|x| x.peer_or_self_review_submission_id) + .collect::>(); + let peer_review_submission_id_to_giver_user_id = + crate::peer_or_self_review_submissions::get_mapping_from_peer_or_self_review_submission_ids_to_peer_review_giver_user_ids( + &mut *conn, + &peer_or_self_review_submission_ids, + ) + .await?; + Ok(bundle_peer_or_self_review_questions_and_answers( + res, + peer_review_submission_id_to_giver_user_id, + )) +} + +/** Returns the peer reviews given by this user. Does not return the self reviews given by the user. */ +pub async fn get_given_peer_reviews( + conn: &mut PgConnection, + user_id: Uuid, + exercise_id: Uuid, + course_instance_id: Uuid, +) -> ModelResult> { + let res = sqlx::query!( + r#" +SELECT answers.id AS peer_review_question_submission_id, + answers.text_data, + answers.number_data, + questions.peer_or_self_review_config_id, + questions.id AS peer_or_self_review_question_id, + questions.order_number, + questions.question, + questions.question_type AS "question_type: PeerOrSelfReviewQuestionType", + questions.answer_required, + submissions.id AS peer_or_self_review_submission_id +FROM peer_or_self_review_question_submissions answers + JOIN peer_or_self_review_questions questions ON ( + answers.peer_or_self_review_question_id = questions.id + ) + JOIN peer_or_self_review_submissions submissions ON ( + answers.peer_or_self_review_submission_id = submissions.id + ) + JOIN exercise_slide_submissions ess ON ( + submissions.exercise_slide_submission_id = ess.id + ) +WHERE submissions.user_id = $1 + AND submissions.exercise_id = $2 + AND submissions.course_instance_id = $3 + AND questions.deleted_at IS NULL + AND answers.deleted_at IS NULL + AND submissions.deleted_at IS NULL + AND ess.user_id != $1 + "#, + user_id, + exercise_id, + course_instance_id, + ) + .map(|x| PeerOrSelfReviewQuestionAndAnswer { + peer_or_self_review_config_id: x.peer_or_self_review_config_id, + peer_or_self_review_question_id: x.peer_or_self_review_question_id, + peer_review_question_submission_id: x.peer_review_question_submission_id, + peer_or_self_review_submission_id: x.peer_or_self_review_submission_id, + order_number: x.order_number, + question: x.question, + answer: PeerOrSelfReviewAnswer::new(x.question_type, x.text_data, x.number_data), + answer_required: x.answer_required, + }) + .fetch_all(&mut *conn) + .await?; + let peer_or_self_review_submission_ids = res + .iter() + .map(|x| x.peer_or_self_review_submission_id) + .collect::>(); + let peer_review_submission_id_to_giver_user_id = + crate::peer_or_self_review_submissions::get_mapping_from_peer_or_self_review_submission_ids_to_peer_review_giver_user_ids( + &mut *conn, + &peer_or_self_review_submission_ids, + ) + .await?; + Ok(bundle_peer_or_self_review_questions_and_answers( + res, + peer_review_submission_id_to_giver_user_id, + )) +} + +/// Groups answers to peer reviews by peer review ids. +fn bundle_peer_or_self_review_questions_and_answers( + questions_and_answers: Vec, + peer_review_submission_id_to_giver_user_id: HashMap, +) -> Vec { + let mut mapped: HashMap> = HashMap::new(); + questions_and_answers.into_iter().for_each(|x| { + mapped + .entry(x.peer_or_self_review_submission_id) + .or_default() + .push(x) + }); + + mapped + .into_iter() + .map(|(id, qa)| PeerReviewWithQuestionsAndAnswers { + peer_or_self_review_submission_id: id, + peer_review_giver_user_id: *(peer_review_submission_id_to_giver_user_id + .get(&id) + .unwrap_or(&Uuid::nil())), + questions_and_answers: qa, + }) + .collect() +} diff --git a/services/headless-lms/models/src/peer_review_questions.rs b/services/headless-lms/models/src/peer_or_self_review_questions.rs similarity index 60% rename from services/headless-lms/models/src/peer_review_questions.rs rename to services/headless-lms/models/src/peer_or_self_review_questions.rs index 1f8090f1c242..db851c9091a8 100644 --- a/services/headless-lms/models/src/peer_review_questions.rs +++ b/services/headless-lms/models/src/peer_or_self_review_questions.rs @@ -7,28 +7,28 @@ use crate::prelude::*; #[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone, Copy, Type)] #[sqlx(type_name = "peer_review_question_type", rename_all = "snake_case")] #[cfg_attr(feature = "ts_rs", derive(TS))] -pub enum PeerReviewQuestionType { +pub enum PeerOrSelfReviewQuestionType { Essay, Scale, } #[derive(Debug, Serialize, Deserialize, PartialEq, Clone)] #[cfg_attr(feature = "ts_rs", derive(TS))] -pub struct CmsPeerReviewQuestion { +pub struct CmsPeerOrSelfReviewQuestion { pub id: Uuid, - pub peer_review_config_id: Uuid, + pub peer_or_self_review_config_id: Uuid, pub order_number: i32, pub question: String, - pub question_type: PeerReviewQuestionType, + pub question_type: PeerOrSelfReviewQuestionType, pub answer_required: bool, pub weight: f32, } -impl From for CmsPeerReviewQuestion { - fn from(prq: PeerReviewQuestion) -> Self { +impl From for CmsPeerOrSelfReviewQuestion { + fn from(prq: PeerOrSelfReviewQuestion) -> Self { Self { id: prq.id, - peer_review_config_id: prq.peer_review_config_id, + peer_or_self_review_config_id: prq.peer_or_self_review_config_id, order_number: prq.order_number, question: prq.question, question_type: prq.question_type, @@ -40,15 +40,15 @@ impl From for CmsPeerReviewQuestion { #[derive(Debug, Serialize, Deserialize, PartialEq, Clone)] #[cfg_attr(feature = "ts_rs", derive(TS))] -pub struct PeerReviewQuestion { +pub struct PeerOrSelfReviewQuestion { pub id: Uuid, pub created_at: DateTime, pub updated_at: DateTime, pub deleted_at: Option>, - pub peer_review_config_id: Uuid, + pub peer_or_self_review_config_id: Uuid, pub order_number: i32, pub question: String, - pub question_type: PeerReviewQuestionType, + pub question_type: PeerOrSelfReviewQuestionType, pub answer_required: bool, pub weight: f32, } @@ -56,13 +56,13 @@ pub struct PeerReviewQuestion { pub async fn insert( conn: &mut PgConnection, pkey_policy: PKeyPolicy, - new_peer_review_question: &CmsPeerReviewQuestion, + new_peer_review_question: &CmsPeerOrSelfReviewQuestion, ) -> ModelResult { let res = sqlx::query!( " -INSERT INTO peer_review_questions ( +INSERT INTO peer_or_self_review_questions ( id, - peer_review_config_id, + peer_or_self_review_config_id, order_number, question, question_type @@ -71,31 +71,31 @@ VALUES ($1, $2, $3, $4, $5) RETURNING id ", pkey_policy.into_uuid(), - new_peer_review_question.peer_review_config_id, + new_peer_review_question.peer_or_self_review_config_id, new_peer_review_question.order_number, new_peer_review_question.question, - new_peer_review_question.question_type as PeerReviewQuestionType, + new_peer_review_question.question_type as PeerOrSelfReviewQuestionType, ) .fetch_one(conn) .await?; Ok(res.id) } -pub async fn get_by_id(conn: &mut PgConnection, id: Uuid) -> ModelResult { +pub async fn get_by_id(conn: &mut PgConnection, id: Uuid) -> ModelResult { let res = sqlx::query_as!( - PeerReviewQuestion, + PeerOrSelfReviewQuestion, r#" SELECT id, created_at, updated_at, deleted_at, - peer_review_config_id, + peer_or_self_review_config_id, order_number, question, question_type AS "question_type: _", answer_required, weight -FROM peer_review_questions +FROM peer_or_self_review_questions WHERE id = $1 AND deleted_at IS NULL; "#, @@ -109,21 +109,21 @@ WHERE id = $1 pub async fn get_by_ids( conn: &mut PgConnection, id: &[Uuid], -) -> ModelResult> { +) -> ModelResult> { let res = sqlx::query_as!( - PeerReviewQuestion, + PeerOrSelfReviewQuestion, r#" SELECT id, created_at, updated_at, deleted_at, - peer_review_config_id, + peer_or_self_review_config_id, order_number, question, question_type AS "question_type: _", answer_required, weight -FROM peer_review_questions +FROM peer_or_self_review_questions WHERE id IN ( SELECT UNNEST($1::uuid []) ) @@ -136,25 +136,25 @@ WHERE id IN ( Ok(res) } -pub async fn get_by_peer_review_configs_id( +pub async fn get_by_peer_or_self_review_configs_id( conn: &mut PgConnection, peer_review_id: Uuid, -) -> ModelResult> { +) -> ModelResult> { let res = sqlx::query_as!( - PeerReviewQuestion, + PeerOrSelfReviewQuestion, r#" SELECT id, created_at, updated_at, deleted_at, - peer_review_config_id, + peer_or_self_review_config_id, order_number, question, question_type AS "question_type: _", answer_required, weight -FROM peer_review_questions -WHERE peer_review_config_id = $1 +FROM peer_or_self_review_questions +WHERE peer_or_self_review_config_id = $1 AND deleted_at IS NULL; "#, peer_review_id, @@ -164,39 +164,39 @@ WHERE peer_review_config_id = $1 Ok(res) } -pub async fn get_all_by_peer_review_config_id( +pub async fn get_all_by_peer_or_self_review_config_id( conn: &mut PgConnection, - peer_review_config_id: Uuid, -) -> ModelResult> { + peer_or_self_review_config_id: Uuid, +) -> ModelResult> { let res = sqlx::query_as!( - PeerReviewQuestion, + PeerOrSelfReviewQuestion, r#" SELECT id, created_at, updated_at, deleted_at, - peer_review_config_id, + peer_or_self_review_config_id, order_number, question, question_type AS "question_type: _", answer_required, weight -FROM peer_review_questions -WHERE peer_review_config_id = $1 +FROM peer_or_self_review_questions +WHERE peer_or_self_review_config_id = $1 AND deleted_at IS NULL; "#, - peer_review_config_id + peer_or_self_review_config_id ) .fetch_all(conn) .await?; Ok(res) } -pub async fn get_all_by_peer_review_config_id_as_map( +pub async fn get_all_by_peer_or_self_review_config_id_as_map( conn: &mut PgConnection, - peer_review_config_id: Uuid, -) -> ModelResult> { - let res = get_all_by_peer_review_config_id(conn, peer_review_config_id) + peer_or_self_review_config_id: Uuid, +) -> ModelResult> { + let res = get_all_by_peer_or_self_review_config_id(conn, peer_or_self_review_config_id) .await? .into_iter() .map(|x| (x.id, x)) @@ -207,12 +207,12 @@ pub async fn get_all_by_peer_review_config_id_as_map( pub async fn get_by_page_id( conn: &mut PgConnection, page_id: Uuid, -) -> ModelResult> { +) -> ModelResult> { let res = sqlx::query_as!( - CmsPeerReviewQuestion, + CmsPeerOrSelfReviewQuestion, r#" SELECT prq.id as id, - prq.peer_review_config_id as peer_review_config_id, + prq.peer_or_self_review_config_id as peer_or_self_review_config_id, prq.order_number as order_number, prq.question as question, prq.question_type AS "question_type: _", @@ -220,8 +220,8 @@ SELECT prq.id as id, prq.weight from pages p join exercises e on p.id = e.page_id - join peer_review_configs pr on e.id = pr.exercise_id - join peer_review_questions prq on pr.id = prq.peer_review_config_id + join peer_or_self_review_configs pr on e.id = pr.exercise_id + join peer_or_self_review_questions prq on pr.id = prq.peer_or_self_review_config_id where p.id = $1 AND p.deleted_at IS NULL AND e.deleted_at IS NULL @@ -236,19 +236,19 @@ where p.id = $1 Ok(res) } -pub async fn delete_peer_review_questions_by_peer_review_config_ids( +pub async fn delete_peer_or_self_review_questions_by_peer_or_self_review_config_ids( conn: &mut PgConnection, - peer_review_config_ids: &[Uuid], + peer_or_self_review_config_ids: &[Uuid], ) -> ModelResult> { let res = sqlx::query!( " -UPDATE peer_review_questions +UPDATE peer_or_self_review_questions SET deleted_at = now() -WHERE peer_review_config_id = ANY ($1) +WHERE peer_or_self_review_config_id = ANY ($1) AND deleted_at IS NULL RETURNING id; ", - peer_review_config_ids + peer_or_self_review_config_ids ) .fetch_all(conn) .await? @@ -258,25 +258,25 @@ RETURNING id; Ok(res) } -pub async fn get_course_default_cms_peer_review_questions( +pub async fn get_course_default_cms_peer_or_self_review_questions( conn: &mut PgConnection, - peer_review_config_id: Uuid, -) -> ModelResult> { + peer_or_self_review_config_id: Uuid, +) -> ModelResult> { let res = sqlx::query_as!( - CmsPeerReviewQuestion, + CmsPeerOrSelfReviewQuestion, r#" SELECT id, - peer_review_config_id, + peer_or_self_review_config_id, order_number, question_type AS "question_type: _", question, answer_required, weight -FROM peer_review_questions -where peer_review_config_id = $1 +FROM peer_or_self_review_questions +where peer_or_self_review_config_id = $1 AND deleted_at IS NULL; "#, - peer_review_config_id + peer_or_self_review_config_id ) .fetch_all(conn) .await?; @@ -284,15 +284,15 @@ where peer_review_config_id = $1 Ok(res) } -pub async fn upsert_multiple_peer_review_questions( +pub async fn upsert_multiple_peer_or_self_review_questions( conn: &mut PgConnection, - peer_review_questions: &[CmsPeerReviewQuestion], -) -> ModelResult> { - let mut sql:QueryBuilder = sqlx::QueryBuilder::new("INSERT INTO peer_review_questions (id, peer_review_config_id, order_number, question_type, question, answer_required) "); + peer_or_self_review_questions: &[CmsPeerOrSelfReviewQuestion], +) -> ModelResult> { + let mut sql:QueryBuilder = sqlx::QueryBuilder::new("INSERT INTO peer_or_self_review_questions (id, peer_or_self_review_config_id, order_number, question_type, question, answer_required) "); - sql.push_values(peer_review_questions, |mut x, prq| { + sql.push_values(peer_or_self_review_questions, |mut x, prq| { x.push_bind(prq.id) - .push_bind(prq.peer_review_config_id) + .push_bind(prq.peer_or_self_review_config_id) .push_bind(prq.order_number) .push_bind(prq.question_type) .push_bind(prq.question.as_str()) @@ -301,7 +301,7 @@ pub async fn upsert_multiple_peer_review_questions( sql.push( r#" ON CONFLICT (id) DO UPDATE -SET peer_review_config_id = excluded.peer_review_config_id, +SET peer_or_self_review_config_id = excluded.peer_or_self_review_config_id, order_number = excluded.order_number, question_type = excluded.question_type, question = excluded.question, @@ -320,16 +320,16 @@ RETURNING id; .collect::>(); let res = sqlx::query_as!( - CmsPeerReviewQuestion, + CmsPeerOrSelfReviewQuestion, r#" SELECT id, - peer_review_config_id, + peer_or_self_review_config_id, order_number, question, question_type AS "question_type: _", answer_required, weight -from peer_review_questions +from peer_or_self_review_questions WHERE id IN ( SELECT UNNEST($1::uuid []) ) @@ -343,24 +343,27 @@ WHERE id IN ( } /** Modifies the questions in memory so that the weights sum to either 0 or 1. */ -pub fn normalize_cms_peer_review_questions(peer_review_questions: &mut [CmsPeerReviewQuestion]) { +pub fn normalize_cms_peer_or_self_review_questions( + peer_or_self_review_questions: &mut [CmsPeerOrSelfReviewQuestion], +) { // All scales have to be answered, skipping them does not make sense. - for question in peer_review_questions.iter_mut() { - if question.question_type == PeerReviewQuestionType::Scale { + for question in peer_or_self_review_questions.iter_mut() { + if question.question_type == PeerOrSelfReviewQuestionType::Scale { question.answer_required = true; } } - peer_review_questions.sort_by(|a, b| a.order_number.cmp(&b.order_number)); + peer_or_self_review_questions.sort_by(|a, b| a.order_number.cmp(&b.order_number)); info!( "Peer review question weights before normalization: {:?}", - peer_review_questions + peer_or_self_review_questions .iter() .map(|x| x.weight) .collect::>() ); - let (mut allowed_to_have_weight, mut not_allowed_to_have_weight) = peer_review_questions - .iter_mut() - .partition::, _>(|q| q.question_type == PeerReviewQuestionType::Scale); + let (mut allowed_to_have_weight, mut not_allowed_to_have_weight) = + peer_or_self_review_questions + .iter_mut() + .partition::, _>(|q| q.question_type == PeerOrSelfReviewQuestionType::Scale); let total_weight: f32 = allowed_to_have_weight.iter().map(|x| x.weight).sum(); if total_weight == 0.0 { return; @@ -373,7 +376,7 @@ pub fn normalize_cms_peer_review_questions(peer_review_questions: &mut [CmsPeerR } info!( "Peer review question weights after normalization: {:?}", - peer_review_questions + peer_or_self_review_questions .iter() .map(|x| x.weight) .collect::>() @@ -385,38 +388,38 @@ mod tests { use super::*; #[test] - fn test_normalize_cms_peer_review_questions() { + fn test_normalize_cms_peer_or_self_review_questions() { let mut questions = vec![ - CmsPeerReviewQuestion { + CmsPeerOrSelfReviewQuestion { id: Uuid::new_v4(), - peer_review_config_id: Uuid::new_v4(), + peer_or_self_review_config_id: Uuid::new_v4(), order_number: 1, question: String::from("Question 1"), - question_type: PeerReviewQuestionType::Scale, + question_type: PeerOrSelfReviewQuestionType::Scale, answer_required: true, weight: 2.0, }, - CmsPeerReviewQuestion { + CmsPeerOrSelfReviewQuestion { id: Uuid::new_v4(), - peer_review_config_id: Uuid::new_v4(), + peer_or_self_review_config_id: Uuid::new_v4(), order_number: 2, question: String::from("Question 2"), - question_type: PeerReviewQuestionType::Scale, + question_type: PeerOrSelfReviewQuestionType::Scale, answer_required: true, weight: 3.0, }, - CmsPeerReviewQuestion { + CmsPeerOrSelfReviewQuestion { id: Uuid::new_v4(), - peer_review_config_id: Uuid::new_v4(), + peer_or_self_review_config_id: Uuid::new_v4(), order_number: 3, question: String::from("Question 3"), - question_type: PeerReviewQuestionType::Essay, + question_type: PeerOrSelfReviewQuestionType::Essay, answer_required: true, weight: 1.0, }, ]; - normalize_cms_peer_review_questions(&mut questions); + normalize_cms_peer_or_self_review_questions(&mut questions); assert_eq!(questions[0].weight, 2.0 / 5.0); assert_eq!(questions[1].weight, 3.0 / 5.0); diff --git a/services/headless-lms/models/src/peer_review_submissions.rs b/services/headless-lms/models/src/peer_or_self_review_submissions.rs similarity index 56% rename from services/headless-lms/models/src/peer_review_submissions.rs rename to services/headless-lms/models/src/peer_or_self_review_submissions.rs index 711e7cd9bd60..042ddd7160b5 100644 --- a/services/headless-lms/models/src/peer_review_submissions.rs +++ b/services/headless-lms/models/src/peer_or_self_review_submissions.rs @@ -1,8 +1,10 @@ +use std::collections::HashMap; + use crate::prelude::*; #[derive(Debug, Serialize, Deserialize, PartialEq, Clone, Eq)] #[cfg_attr(feature = "ts_rs", derive(TS))] -pub struct PeerReviewSubmission { +pub struct PeerOrSelfReviewSubmission { pub id: Uuid, pub created_at: DateTime, pub updated_at: DateTime, @@ -10,7 +12,7 @@ pub struct PeerReviewSubmission { pub user_id: Uuid, pub exercise_id: Uuid, pub course_instance_id: Uuid, - pub peer_review_config_id: Uuid, + pub peer_or_self_review_config_id: Uuid, pub exercise_slide_submission_id: Uuid, } @@ -20,17 +22,17 @@ pub async fn insert( user_id: Uuid, exercise_id: Uuid, course_instance_id: Uuid, - peer_review_config_id: Uuid, + peer_or_self_review_config_id: Uuid, exercise_slide_submission_id: Uuid, ) -> ModelResult { let res = sqlx::query!( " -INSERT INTO peer_review_submissions ( +INSERT INTO peer_or_self_review_submissions ( id, user_id, exercise_id, course_instance_id, - peer_review_config_id, + peer_or_self_review_config_id, exercise_slide_submission_id ) VALUES ($1, $2, $3, $4, $5, $6) @@ -40,7 +42,7 @@ RETURNING id user_id, exercise_id, course_instance_id, - peer_review_config_id, + peer_or_self_review_config_id, exercise_slide_submission_id, ) .fetch_one(conn) @@ -48,12 +50,15 @@ RETURNING id Ok(res.id) } -pub async fn get_by_id(conn: &mut PgConnection, id: Uuid) -> ModelResult { +pub async fn get_by_id( + conn: &mut PgConnection, + id: Uuid, +) -> ModelResult { let res = sqlx::query_as!( - PeerReviewSubmission, + PeerOrSelfReviewSubmission, " SELECT * -FROM peer_review_submissions +FROM peer_or_self_review_submissions WHERE id = $1 AND deleted_at IS NULL ", @@ -64,6 +69,25 @@ WHERE id = $1 Ok(res) } +pub async fn get_by_ids( + conn: &mut PgConnection, + ids: &[Uuid], +) -> ModelResult> { + let res = sqlx::query_as!( + PeerOrSelfReviewSubmission, + " +SELECT * +FROM peer_or_self_review_submissions +WHERE id = ANY($1) + AND deleted_at IS NULL + ", + ids + ) + .fetch_all(conn) + .await?; + Ok(res) +} + pub async fn get_users_submission_ids_for_exercise_and_course_instance( conn: &mut PgConnection, user_id: Uuid, @@ -73,7 +97,7 @@ pub async fn get_users_submission_ids_for_exercise_and_course_instance( let res = sqlx::query!( " SELECT exercise_slide_submission_id -FROM peer_review_submissions +FROM peer_or_self_review_submissions WHERE user_id = $1 AND exercise_id = $2 AND course_instance_id = $3 @@ -91,17 +115,17 @@ WHERE user_id = $1 Ok(res) } -pub async fn get_all_received_peer_review_submissions_for_user_and_course_instance( +pub async fn get_all_received_peer_or_self_review_submissions_for_user_and_course_instance( conn: &mut PgConnection, user_id: Uuid, course_instance_id: Uuid, -) -> ModelResult> { +) -> ModelResult> { let res = sqlx::query_as!( - PeerReviewSubmission, + PeerOrSelfReviewSubmission, " SELECT prs.* FROM exercise_slide_submissions ess -INNER JOIN peer_review_submissions prs ON (ess.id = prs.exercise_slide_submission_id) +INNER JOIN peer_or_self_review_submissions prs ON (ess.id = prs.exercise_slide_submission_id) WHERE ess.user_id = $1 AND ess.course_instance_id = $2 AND ess.deleted_at IS NULL @@ -115,16 +139,16 @@ WHERE ess.user_id = $1 Ok(res) } -pub async fn get_all_given_peer_review_submissions_for_user_and_course_instance( +pub async fn get_all_given_peer_or_self_review_submissions_for_user_and_course_instance( conn: &mut PgConnection, user_id: Uuid, course_instance_id: Uuid, -) -> ModelResult> { +) -> ModelResult> { let res = sqlx::query_as!( - PeerReviewSubmission, + PeerOrSelfReviewSubmission, " SELECT * -FROM peer_review_submissions +FROM peer_or_self_review_submissions WHERE user_id = $1 AND course_instance_id = $2 AND deleted_at IS NULL @@ -146,7 +170,7 @@ pub async fn get_num_peer_reviews_given_by_user_and_course_instance_and_exercise let res = sqlx::query!( " SELECT COUNT(*) -FROM peer_review_submissions +FROM peer_or_self_review_submissions WHERE user_id = $1 AND exercise_id = $3 AND course_instance_id = $2 @@ -166,12 +190,12 @@ pub async fn get_peer_reviews_given_by_user_and_course_instance_and_exercise( user_id: Uuid, course_instance_id: Uuid, exercise_id: Uuid, -) -> ModelResult> { +) -> ModelResult> { let res = sqlx::query_as!( - PeerReviewSubmission, + PeerOrSelfReviewSubmission, " SELECT * -FROM peer_review_submissions +FROM peer_or_self_review_submissions WHERE user_id = $1 AND exercise_id = $3 AND course_instance_id = $2 @@ -195,7 +219,7 @@ pub async fn get_users_submission_count_for_exercise_and_course_instance( let res = sqlx::query!( " SELECT COUNT(*) AS count -FROM peer_review_submissions +FROM peer_or_self_review_submissions WHERE user_id = $1 AND exercise_id = $2 AND course_instance_id = $3 @@ -219,7 +243,7 @@ pub async fn get_last_time_user_submitted_peer_review( let res = sqlx::query!( " SELECT MAX(created_at) as latest_submission_time -FROM peer_review_submissions +FROM peer_or_self_review_submissions WHERE user_id = $1 AND exercise_id = $2 AND course_instance_id = $3 @@ -234,14 +258,14 @@ WHERE user_id = $1 Ok(res.and_then(|o| o.latest_submission_time)) } -pub async fn count_peer_review_submissions_for_exercise_slide_submission( +pub async fn count_peer_or_self_review_submissions_for_exercise_slide_submission( conn: &mut PgConnection, exercise_slide_submission_id: Uuid, ) -> ModelResult { let res = sqlx::query!( " SELECT COUNT(*) AS count -FROM peer_review_submissions +FROM peer_or_self_review_submissions WHERE exercise_slide_submission_id = $1 AND deleted_at IS NULL ", @@ -251,3 +275,68 @@ WHERE exercise_slide_submission_id = $1 .await?; Ok(res.count.unwrap_or(0).try_into()?) } + +pub async fn get_self_review_submission_by_user_and_exercise( + conn: &mut PgConnection, + user_id: Uuid, + exercise_id: Uuid, + course_instance_id: Uuid, +) -> ModelResult> { + let res = sqlx::query_as!( + PeerOrSelfReviewSubmission, + " +SELECT prs.* +FROM peer_or_self_review_submissions prs +JOIN exercise_slide_submissions ess ON (ess.id = prs.exercise_slide_submission_id) +WHERE ess.user_id = $1 + AND prs.exercise_id = $2 + AND prs.course_instance_id = $3 + AND prs.deleted_at IS NULL + AND ess.deleted_at IS NULL + ", + user_id, + exercise_id, + course_instance_id + ) + .fetch_optional(conn) + .await?; + Ok(res) +} + +pub async fn get_received_peer_or_self_review_submissions_for_user_by_peer_or_self_review_config_id_and_exercise_slide_submission( + conn: &mut PgConnection, + user_id: Uuid, + exercise_slide_submission_id: Uuid, + peer_or_self_review_config_id: Uuid, +) -> ModelResult> { + let res = sqlx::query_as!( + PeerOrSelfReviewSubmission, + " +SELECT prs.* +FROM peer_or_self_review_submissions prs + JOIN exercise_slide_submissions ess ON (ess.id = prs.exercise_slide_submission_id) +WHERE ess.user_id = $1 + AND ess.id = $2 + AND prs.peer_or_self_review_config_id = $3 + AND prs.deleted_at IS NULL + AND ess.deleted_at IS NULL + ", + user_id, + exercise_slide_submission_id, + peer_or_self_review_config_id + ) + .fetch_all(conn) + .await?; + Ok(res) +} + +pub async fn get_mapping_from_peer_or_self_review_submission_ids_to_peer_review_giver_user_ids( + conn: &mut PgConnection, + peer_or_self_review_submission_ids: &[Uuid], +) -> ModelResult> { + let full = get_by_ids(conn, peer_or_self_review_submission_ids).await?; + Ok(full + .into_iter() + .map(|submission: PeerOrSelfReviewSubmission| (submission.id, submission.user_id)) + .collect()) +} diff --git a/services/headless-lms/models/src/peer_review_question_submissions.rs b/services/headless-lms/models/src/peer_review_question_submissions.rs deleted file mode 100644 index 0dbfdfdff1a5..000000000000 --- a/services/headless-lms/models/src/peer_review_question_submissions.rs +++ /dev/null @@ -1,286 +0,0 @@ -use std::collections::HashMap; - -use crate::peer_review_questions::PeerReviewQuestionType; -use crate::prelude::*; - -#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)] -#[cfg_attr(feature = "ts_rs", derive(TS))] -pub struct PeerReviewQuestionSubmission { - pub id: Uuid, - pub created_at: DateTime, - pub updated_at: DateTime, - pub deleted_at: Option>, - pub peer_review_question_id: Uuid, - pub peer_review_submission_id: Uuid, - pub text_data: Option, - pub number_data: Option, -} - -pub async fn insert( - conn: &mut PgConnection, - pkey_policy: PKeyPolicy, - peer_review_question_id: Uuid, - peer_review_submission_id: Uuid, - text_data: Option, - number_data: Option, -) -> ModelResult { - let res = sqlx::query!( - " -INSERT INTO peer_review_question_submissions ( - id, - peer_review_question_id, - peer_review_submission_id, - text_data, - number_data - ) -VALUES ($1, $2, $3, $4, $5) -RETURNING id - ", - pkey_policy.into_uuid(), - peer_review_question_id, - peer_review_submission_id, - text_data, - number_data, - ) - .fetch_one(conn) - .await?; - Ok(res.id) -} - -pub async fn get_by_peer_reviews_question_ids( - conn: &mut PgConnection, - ids: &[Uuid], - user_id: Uuid, - exercise_slide_submission_id: Uuid, -) -> ModelResult> { - let res = sqlx::query_as!( - PeerReviewQuestionSubmission, - " - SELECT qs.id, - qs.created_at, - qs.updated_at, - qs.deleted_at, - qs.peer_review_question_id, - qs.peer_review_submission_id, - qs.text_data, - qs.number_data - FROM peer_review_question_submissions qs - JOIN peer_review_submissions s ON (qs.peer_review_submission_id = s.id) - JOIN exercise_slide_submissions es ON (s.exercise_slide_submission_id = es.id) - WHERE peer_review_question_id IN ( - SELECT UNNEST($1::uuid []) - ) - AND s.exercise_slide_submission_id = $3 - AND es.user_id = $2 - AND qs.deleted_at IS NULL; - ", - ids, - user_id, - exercise_slide_submission_id - ) - .fetch_all(conn) - .await?; - Ok(res) -} - -pub async fn get_received_question_submissions_for_exercise_slide_submission( - conn: &mut PgConnection, - exercise_slide_submission_id: Uuid, -) -> ModelResult> { - let res = sqlx::query_as!( - PeerReviewQuestionSubmission, - " -SELECT prqs.* -FROM peer_review_submissions prs - JOIN peer_review_question_submissions prqs on prs.id = prqs.peer_review_submission_id -WHERE prs.exercise_slide_submission_id = $1 - AND prs.deleted_at IS NULL - AND prqs.deleted_at IS NULL - ", - exercise_slide_submission_id - ) - .fetch_all(conn) - .await?; - Ok(res) -} - -pub async fn get_question_submissions_from_from_peer_review_submission_ids( - conn: &mut PgConnection, - peer_review_submission_ids: &[Uuid], -) -> ModelResult> { - let res = sqlx::query_as!( - PeerReviewQuestionSubmission, - " -SELECT * -FROM peer_review_question_submissions -WHERE peer_review_submission_id IN ( - SELECT UNNEST($1::uuid []) - ) - AND deleted_at IS NULL - ", - peer_review_submission_ids - ) - .fetch_all(&mut *conn) - .await?; - - Ok(res) -} - -#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)] -#[cfg_attr(feature = "ts_rs", derive(TS))] -#[serde(tag = "type", rename_all = "kebab-case")] -pub enum PeerReviewAnswer { - NoAnswer, - Essay { value: String }, - Scale { value: f32 }, -} - -impl PeerReviewAnswer { - fn new( - question_type: PeerReviewQuestionType, - text_data: Option, - number_data: Option, - ) -> Self { - match (question_type, text_data, number_data) { - (PeerReviewQuestionType::Essay, Some(value), _) => Self::Essay { value }, - (PeerReviewQuestionType::Scale, _, Some(value)) => Self::Scale { value }, - _ => Self::NoAnswer, - } - } -} - -#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)] -#[cfg_attr(feature = "ts_rs", derive(TS))] -pub struct PeerReviewQuestionAndAnswer { - pub peer_review_config_id: Uuid, - pub peer_review_question_id: Uuid, - pub peer_review_submission_id: Uuid, - pub peer_review_question_submission_id: Uuid, - pub order_number: i32, - pub question: String, - pub answer: PeerReviewAnswer, - pub answer_required: bool, -} - -#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)] -#[cfg_attr(feature = "ts_rs", derive(TS))] -pub struct PeerReviewWithQuestionsAndAnswers { - pub peer_review_submission_id: Uuid, - pub questions_and_answers: Vec, -} - -pub async fn get_questions_and_answers_by_submission_id( - conn: &mut PgConnection, - exercise_slide_submission_id: Uuid, -) -> ModelResult> { - let res = sqlx::query!( - r#" -SELECT answers.id AS peer_review_question_submission_id, - answers.text_data, - answers.number_data, - questions.peer_review_config_id, - questions.id AS peer_review_question_id, - questions.order_number, - questions.question, - questions.question_type AS "question_type: PeerReviewQuestionType", - questions.answer_required, - submissions.id AS peer_review_submission_id -FROM peer_review_question_submissions answers - JOIN peer_review_questions questions ON ( - answers.peer_review_question_id = questions.id - ) - JOIN peer_review_submissions submissions ON ( - answers.peer_review_submission_id = submissions.id - ) -WHERE submissions.exercise_slide_submission_id = $1 - AND questions.deleted_at IS NULL - AND answers.deleted_at IS NULL - AND submissions.deleted_at IS NULL - "#, - exercise_slide_submission_id, - ) - .map(|x| PeerReviewQuestionAndAnswer { - peer_review_config_id: x.peer_review_config_id, - peer_review_question_id: x.peer_review_question_id, - peer_review_question_submission_id: x.peer_review_question_submission_id, - peer_review_submission_id: x.peer_review_submission_id, - order_number: x.order_number, - question: x.question, - answer: PeerReviewAnswer::new(x.question_type, x.text_data, x.number_data), - answer_required: x.answer_required, - }) - .fetch_all(conn) - .await?; - Ok(bundle_peer_review_questions_and_answers(res)) -} - -pub async fn get_questions_and_answers_by_user_exercise_instance( - conn: &mut PgConnection, - user_id: Uuid, - exercise_id: Uuid, - course_instance_id: Uuid, -) -> ModelResult> { - let res = sqlx::query!( - r#" -SELECT answers.id AS peer_review_question_submission_id, - answers.text_data, - answers.number_data, - questions.peer_review_config_id, - questions.id AS peer_review_question_id, - questions.order_number, - questions.question, - questions.question_type AS "question_type: PeerReviewQuestionType", - questions.answer_required, - submissions.id AS peer_review_submission_id -FROM peer_review_question_submissions answers - JOIN peer_review_questions questions ON ( - answers.peer_review_question_id = questions.id - ) - JOIN peer_review_submissions submissions ON ( - answers.peer_review_submission_id = submissions.id - ) -WHERE submissions.user_id = $1 - AND submissions.exercise_id = $2 - AND submissions.course_instance_id = $3 - AND questions.deleted_at IS NULL - AND answers.deleted_at IS NULL - AND submissions.deleted_at IS NULL - "#, - user_id, - exercise_id, - course_instance_id, - ) - .map(|x| PeerReviewQuestionAndAnswer { - peer_review_config_id: x.peer_review_config_id, - peer_review_question_id: x.peer_review_question_id, - peer_review_question_submission_id: x.peer_review_question_submission_id, - peer_review_submission_id: x.peer_review_submission_id, - order_number: x.order_number, - question: x.question, - answer: PeerReviewAnswer::new(x.question_type, x.text_data, x.number_data), - answer_required: x.answer_required, - }) - .fetch_all(conn) - .await?; - Ok(bundle_peer_review_questions_and_answers(res)) -} - -/// Groups answers to peer reviews by peer review ids. -fn bundle_peer_review_questions_and_answers( - questions_and_answers: Vec, -) -> Vec { - let mut mapped: HashMap> = HashMap::new(); - questions_and_answers.into_iter().for_each(|x| { - mapped - .entry(x.peer_review_submission_id) - .or_default() - .push(x) - }); - mapped - .into_iter() - .map(|(id, qa)| PeerReviewWithQuestionsAndAnswers { - peer_review_submission_id: id, - questions_and_answers: qa, - }) - .collect() -} diff --git a/services/headless-lms/models/src/user_exercise_slide_states.rs b/services/headless-lms/models/src/user_exercise_slide_states.rs index 1a51976e0948..5242999d49f3 100644 --- a/services/headless-lms/models/src/user_exercise_slide_states.rs +++ b/services/headless-lms/models/src/user_exercise_slide_states.rs @@ -13,6 +13,7 @@ pub struct UserExerciseSlideState { pub grading_progress: GradingProgress, } +#[derive(Debug)] pub struct UserExerciseSlideStateGradingSummary { pub score_given: Option, pub grading_progress: GradingProgress, diff --git a/services/headless-lms/server/src/controllers/cms/courses.rs b/services/headless-lms/server/src/controllers/cms/courses.rs index 10f2a8e1b51d..d7dcde5aad9f 100644 --- a/services/headless-lms/server/src/controllers/cms/courses.rs +++ b/services/headless-lms/server/src/controllers/cms/courses.rs @@ -5,8 +5,8 @@ use crate::prelude::*; use models::{ course_instances::CourseInstance, pages::{Page, PageVisibility}, - peer_review_configs::{self, CmsPeerReviewConfiguration}, - peer_review_questions::normalize_cms_peer_review_questions, + peer_or_self_review_configs::{self, CmsPeerOrSelfReviewConfiguration}, + peer_or_self_review_questions::normalize_cms_peer_or_self_review_questions, }; use crate::prelude::models::course_modules::CourseModule; @@ -58,11 +58,11 @@ async fn add_media( } #[instrument(skip(pool))] -async fn get_course_default_peer_review_configuration( +async fn get_course_default_peer_or_self_review_configuration( course_id: web::Path, user: AuthUser, pool: web::Data, -) -> ControllerResult> { +) -> ControllerResult> { let mut conn = pool.acquire().await?; let token = authorize( &mut conn, @@ -72,30 +72,32 @@ async fn get_course_default_peer_review_configuration( ) .await?; - let peer_review_config = - models::peer_review_configs::get_course_default_cms_peer_review(&mut conn, *course_id) - .await?; + let peer_or_self_review_config = + models::peer_or_self_review_configs::get_course_default_cms_peer_review( + &mut conn, *course_id, + ) + .await?; - let peer_review_questions = - models::peer_review_questions::get_course_default_cms_peer_review_questions( + let peer_or_self_review_questions = + models::peer_or_self_review_questions::get_course_default_cms_peer_or_self_review_questions( &mut conn, - peer_review_config.id, + peer_or_self_review_config.id, ) .await?; - token.authorized_ok(web::Json(CmsPeerReviewConfiguration { - peer_review_config, - peer_review_questions, + token.authorized_ok(web::Json(CmsPeerOrSelfReviewConfiguration { + peer_or_self_review_config, + peer_or_self_review_questions, })) } #[instrument(skip(pool))] -async fn put_course_default_peer_review_configuration( +async fn put_course_default_peer_or_self_review_configuration( course_id: web::Path, user: AuthUser, pool: web::Data, - payload: web::Json, -) -> ControllerResult> { + payload: web::Json, +) -> ControllerResult> { let mut conn = pool.acquire().await?; let token = authorize( &mut conn, @@ -105,13 +107,13 @@ async fn put_course_default_peer_review_configuration( ) .await?; let mut config = payload.0; - normalize_cms_peer_review_questions(&mut config.peer_review_questions); - let cms_peer_review_configuration = - peer_review_configs::upsert_course_default_cms_peer_review_and_questions( + normalize_cms_peer_or_self_review_questions(&mut config.peer_or_self_review_questions); + let cms_peer_or_self_review_configuration = + peer_or_self_review_configs::upsert_course_default_cms_peer_review_and_questions( &mut conn, &config, ) .await?; - token.authorized_ok(web::Json(cms_peer_review_configuration)) + token.authorized_ok(web::Json(cms_peer_or_self_review_configuration)) } /** @@ -243,11 +245,11 @@ pub fn _add_routes(cfg: &mut ServiceConfig) { cfg.route("/{course_id}/upload", web::post().to(add_media)) .route( "/{course_id}/default-peer-review", - web::get().to(get_course_default_peer_review_configuration), + web::get().to(get_course_default_peer_or_self_review_configuration), ) .route( "/{course_id}/default-peer-review", - web::put().to(put_course_default_peer_review_configuration), + web::put().to(put_course_default_peer_or_self_review_configuration), ) .route("/{course_id}/pages", web::get().to(get_all_pages)) .route( diff --git a/services/headless-lms/server/src/controllers/course_material/exercises.rs b/services/headless-lms/server/src/controllers/course_material/exercises.rs index b81b0b51790b..7d2e614a66ca 100644 --- a/services/headless-lms/server/src/controllers/course_material/exercises.rs +++ b/services/headless-lms/server/src/controllers/course_material/exercises.rs @@ -8,19 +8,21 @@ use crate::{ prelude::*, }; use models::{ - exercise_task_submissions::PeerReviewsRecieved, + exercise_task_submissions::PeerOrSelfReviewsReceived, exercises::CourseMaterialExercise, library::{ grading::{StudentExerciseSlideSubmission, StudentExerciseSlideSubmissionResult}, - peer_reviewing::{CourseMaterialPeerReviewData, CourseMaterialPeerReviewSubmission}, + peer_or_self_reviewing::{ + CourseMaterialPeerOrSelfReviewData, CourseMaterialPeerOrSelfReviewSubmission, + }, }, user_exercise_states, }; #[derive(Debug, Serialize, Deserialize, PartialEq, Clone)] #[cfg_attr(feature = "ts_rs", derive(TS))] -pub struct CourseMaterialPeerReviewDataWithToken { - pub course_material_peer_review_data: CourseMaterialPeerReviewData, +pub struct CourseMaterialPeerOrSelfReviewDataWithToken { + pub course_material_peer_or_self_review_data: CourseMaterialPeerOrSelfReviewData, pub token: Option, } @@ -111,10 +113,10 @@ async fn get_peer_review_for_exercise( exercise_id: web::Path, user: AuthUser, jwt_key: web::Data, -) -> ControllerResult> { +) -> ControllerResult> { let mut conn = pool.acquire().await?; - let course_material_peer_review_data = - models::peer_review_configs::get_course_material_peer_review_data( + let course_material_peer_or_self_review_data = + models::peer_or_self_review_configs::get_course_material_peer_or_self_review_data( &mut conn, user.id, *exercise_id, @@ -129,11 +131,13 @@ async fn get_peer_review_for_exercise( ) .await?; let give_peer_review_claim = - if let Some(to_review) = &course_material_peer_review_data.answer_to_review { + if let Some(to_review) = &course_material_peer_or_self_review_data.answer_to_review { Some( GivePeerReviewClaim::expiring_in_1_day( to_review.exercise_slide_submission_id, - course_material_peer_review_data.peer_review_config.id, + course_material_peer_or_self_review_data + .peer_or_self_review_config + .id, ) .sign(&jwt_key), ) @@ -141,8 +145,8 @@ async fn get_peer_review_for_exercise( None }; - let res = CourseMaterialPeerReviewDataWithToken { - course_material_peer_review_data, + let res = CourseMaterialPeerOrSelfReviewDataWithToken { + course_material_peer_or_self_review_data, token: give_peer_review_claim, }; token.authorized_ok(web::Json(res)) @@ -156,7 +160,7 @@ async fn get_peer_reviews_received( pool: web::Data, params: web::Path<(Uuid, Uuid)>, user: AuthUser, -) -> ControllerResult> { +) -> ControllerResult> { let mut conn = pool.acquire().await?; let (exercise_id, exercise_slide_submission_id) = params.into_inner(); let peer_review_data = models::exercise_task_submissions::get_peer_reviews_received( @@ -219,27 +223,23 @@ async fn post_submission( } /** - * POST `/api/v0/course-material/exercises/:exercise_id/peer-reviews/start` - Post a signal indicating that - * the user will start peer reviewing process. + * POST `/api/v0/course-material/exercises/:exercise_id/peer-or-self-reviews/start` - Post a signal indicating that + * the user will start the peer or self reviewing process. * * This operation is only valid for exercises marked for peer reviews. No further submissions will be * accepted after posting to this endpoint. */ #[instrument(skip(pool))] -async fn start_peer_review( +async fn start_peer_or_self_review( pool: web::Data, exercise_id: web::Path, user: AuthUser, ) -> ControllerResult> { let mut conn = pool.acquire().await?; - // Authorization let exercise = models::exercises::get_by_id(&mut conn, *exercise_id).await?; let user_exercise_state = user_exercise_states::get_users_current_by_exercise(&mut conn, user.id, &exercise).await?; - models::library::peer_reviewing::start_peer_review_for_user(&mut conn, user_exercise_state) - .await?; - let token = authorize( &mut conn, Act::View, @@ -247,18 +247,25 @@ async fn start_peer_review( Res::Exercise(*exercise_id), ) .await?; + models::library::peer_or_self_reviewing::start_peer_or_self_review_for_user( + &mut conn, + user_exercise_state, + &exercise, + ) + .await?; + token.authorized_ok(web::Json(true)) } /** - * POST `/api/v0/course-material/exercises/:exercise_id/peer-reviews - Post a peer review for an + * POST `/api/v0/course-material/exercises/:exercise_id/peer-or-self-reviews - Post a peer review or a self review for an * exercise submission. */ #[instrument(skip(pool))] -async fn submit_peer_review( +async fn submit_peer_or_self_review( pool: web::Data, exercise_id: web::Path, - payload: web::Json, + payload: web::Json, user: AuthUser, jwt_key: web::Data, ) -> ControllerResult> { @@ -268,7 +275,7 @@ async fn submit_peer_review( // The validation prevents users from chaging which answer they peer review. let claim = GivePeerReviewClaim::validate(&payload.token, &jwt_key)?; if claim.exercise_slide_submission_id != payload.exercise_slide_submission_id - || claim.peer_review_config_id != payload.peer_review_config_id + || claim.peer_or_self_review_config_id != payload.peer_or_self_review_config_id { return Err(ControllerError::new( ControllerErrorType::BadRequest, @@ -277,12 +284,25 @@ async fn submit_peer_review( )); } - let user_exercise_state = + let giver_user_exercise_state = user_exercise_states::get_users_current_by_exercise(&mut conn, user.id, &exercise).await?; - models::library::peer_reviewing::create_peer_review_submission_for_user( + let exercise_slide_submission: models::exercise_slide_submissions::ExerciseSlideSubmission = + models::exercise_slide_submissions::get_by_id( + &mut conn, + payload.exercise_slide_submission_id, + ) + .await?; + let receiver_user_exercise_state = user_exercise_states::get_users_current_by_exercise( &mut conn, + exercise_slide_submission.user_id, &exercise, - user_exercise_state, + ) + .await?; + models::library::peer_or_self_reviewing::create_peer_or_self_review_submission_for_user( + &mut conn, + &exercise, + giver_user_exercise_state, + receiver_user_exercise_state, payload.0, ) .await?; @@ -300,19 +320,19 @@ We add the routes by calling the route method instead of using the route annotat pub fn _add_routes(cfg: &mut ServiceConfig) { cfg.route("/{exercise_id}", web::get().to(get_exercise)) .route( - "/{exercise_id}/peer-reviews", - web::post().to(submit_peer_review), + "/{exercise_id}/peer-or-self-reviews", + web::post().to(submit_peer_or_self_review), ) .route( - "/{exercise_id}/peer-reviews/start", - web::post().to(start_peer_review), + "/{exercise_id}/peer-or-self-reviews/start", + web::post().to(start_peer_or_self_review), ) .route( "/{exercise_id}/peer-review", web::get().to(get_peer_review_for_exercise), ) .route( - "/{exercise_id}/exercise-slide-submission/{exercise_slide_submission_id}/peer-reviews-received", + "/{exercise_id}/exercise-slide-submission/{exercise_slide_submission_id}/peer-or-self-reviews-received", web::get().to(get_peer_reviews_received), ) .route( diff --git a/services/headless-lms/server/src/controllers/main_frontend/courses.rs b/services/headless-lms/server/src/controllers/main_frontend/courses.rs index 1fa2202456e8..b464953fb01e 100644 --- a/services/headless-lms/server/src/controllers/main_frontend/courses.rs +++ b/services/headless-lms/server/src/controllers/main_frontend/courses.rs @@ -23,8 +23,8 @@ use models::{ page_visit_datum_summary_by_courses_device_types::PageVisitDatumSummaryByCourseDeviceTypes, page_visit_datum_summary_by_pages::PageVisitDatumSummaryByPages, pages::Page, - peer_review_configs::PeerReviewConfig, - peer_review_questions::PeerReviewQuestion, + peer_or_self_review_configs::PeerOrSelfReviewConfig, + peer_or_self_review_questions::PeerOrSelfReviewQuestion, user_exercise_states::ExerciseUserCounts, }; @@ -866,7 +866,7 @@ async fn get_course_default_peer_review( course_id: web::Path, pool: web::Data, user: AuthUser, -) -> ControllerResult)>> { +) -> ControllerResult)>> { let mut conn = pool.acquire().await?; let token = authorize( &mut conn, @@ -876,13 +876,17 @@ async fn get_course_default_peer_review( ) .await?; - let peer_review = - models::peer_review_configs::get_default_for_course_by_course_id(&mut conn, *course_id) - .await?; - let peer_review_questions = - models::peer_review_questions::get_all_by_peer_review_config_id(&mut conn, peer_review.id) - .await?; - token.authorized_ok(web::Json((peer_review, peer_review_questions))) + let peer_review = models::peer_or_self_review_configs::get_default_for_course_by_course_id( + &mut conn, *course_id, + ) + .await?; + let peer_or_self_review_questions = + models::peer_or_self_review_questions::get_all_by_peer_or_self_review_config_id( + &mut conn, + peer_review.id, + ) + .await?; + token.authorized_ok(web::Json((peer_review, peer_or_self_review_questions))) } /** @@ -899,7 +903,7 @@ async fn post_update_peer_review_queue_reviews_received( ) -> ControllerResult> { let mut conn = pool.acquire().await?; let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::GlobalPermissions).await?; - models::library::peer_reviewing::update_peer_review_queue_reviews_received( + models::library::peer_or_self_reviewing::update_peer_review_queue_reviews_received( &mut conn, *course_id, ) .await?; diff --git a/services/headless-lms/server/src/domain/csv_export/course_instance_export.rs b/services/headless-lms/server/src/domain/csv_export/course_instance_export.rs index 68f941595e4f..2a6c66828bbf 100644 --- a/services/headless-lms/server/src/domain/csv_export/course_instance_export.rs +++ b/services/headless-lms/server/src/domain/csv_export/course_instance_export.rs @@ -3,6 +3,7 @@ use bytes::Bytes; use headless_lms_models::course_instances; use async_trait::async_trait; +use itertools::Itertools; use models::library::progressing; use crate::domain::csv_export::CsvWriter; @@ -91,6 +92,8 @@ where let user_completion = user .completed_modules .iter() + // sort by created at, latest timestamp first + .sorted_by(|a, b| b.created_at.cmp(&a.created_at)) .find(|cm| cm.course_module_id == module.id); if user_completion.is_some() { has_completed_some_module = true; diff --git a/services/headless-lms/server/src/domain/models_requests.rs b/services/headless-lms/server/src/domain/models_requests.rs index 7f86ee589f1e..832cddfd0a5e 100644 --- a/services/headless-lms/server/src/domain/models_requests.rs +++ b/services/headless-lms/server/src/domain/models_requests.rs @@ -372,18 +372,18 @@ pub fn make_grading_request_sender( #[derive(Debug, Serialize, Deserialize)] pub struct GivePeerReviewClaim { pub exercise_slide_submission_id: Uuid, - pub peer_review_config_id: Uuid, + pub peer_or_self_review_config_id: Uuid, expiration_time: DateTime, } impl GivePeerReviewClaim { pub fn expiring_in_1_day( exercise_slide_submission_id: Uuid, - peer_review_config_id: Uuid, + peer_or_self_review_config_id: Uuid, ) -> Self { Self { exercise_slide_submission_id, - peer_review_config_id, + peer_or_self_review_config_id, expiration_time: Utc::now() + Duration::days(1), } } @@ -396,14 +396,14 @@ impl GivePeerReviewClaim { let claim: Self = token.verify_with_key(&key.0).map_err(|err| { ControllerError::new( ControllerErrorType::BadRequest, - format!("Invalid jwt key: {}", err), + format!("Invalid claim: {}", err), Some(err.into()), ) })?; if claim.expiration_time < Utc::now() { return Err(ControllerError::new( ControllerErrorType::BadRequest, - "The peer review has expired.".to_string(), + "The review has expired.".to_string(), None, )); } diff --git a/services/headless-lms/server/src/programs/doc_file_generator/mod.rs b/services/headless-lms/server/src/programs/doc_file_generator/mod.rs index d6fe25418ff3..07bbcfd6e781 100644 --- a/services/headless-lms/server/src/programs/doc_file_generator/mod.rs +++ b/services/headless-lms/server/src/programs/doc_file_generator/mod.rs @@ -123,7 +123,7 @@ use headless_lms_models::{ StudyRegistryCompletion, StudyRegistryGrade, }, courses::CourseBreadcrumbInfo, - exercise_task_submissions::PeerReviewsRecieved, + exercise_task_submissions::PeerOrSelfReviewsReceived, exercises::ExerciseStatusSummaryForUser, library::global_stats::{GlobalCourseModuleStatEntry, GlobalStatEntry}, page_audio_files::PageAudioFile, @@ -131,12 +131,13 @@ use headless_lms_models::{ page_visit_datum_summary_by_courses_countries::PageVisitDatumSummaryByCoursesCountries, page_visit_datum_summary_by_courses_device_types::PageVisitDatumSummaryByCourseDeviceTypes, page_visit_datum_summary_by_pages::PageVisitDatumSummaryByPages, - peer_review_configs::CourseMaterialPeerReviewConfig, - peer_review_question_submissions::{ - PeerReviewAnswer, PeerReviewQuestionAndAnswer, PeerReviewQuestionSubmission, + peer_or_self_review_configs::CourseMaterialPeerOrSelfReviewConfig, + peer_or_self_review_question_submissions::{ + PeerOrSelfReviewAnswer, PeerOrSelfReviewQuestionAndAnswer, + PeerOrSelfReviewQuestionSubmission, }, + peer_or_self_review_submissions::PeerOrSelfReviewSubmission, peer_review_queue_entries::PeerReviewQueueEntry, - peer_review_submissions::PeerReviewSubmission, proposed_block_edits::EditedBlockStillExistsData, research_forms::{ResearchForm, ResearchFormQuestion, ResearchFormQuestionAnswer}, student_countries::StudentCountry, @@ -151,7 +152,7 @@ use std::{collections::HashMap, fs}; use ts_rs::TS; use uuid::Uuid; -use crate::controllers::course_material::exercises::CourseMaterialPeerReviewDataWithToken; +use crate::controllers::course_material::exercises::CourseMaterialPeerOrSelfReviewDataWithToken; // Helper function to avoid typing out Example::example() fn ex() -> T { diff --git a/services/headless-lms/server/src/programs/peer_review_updater.rs b/services/headless-lms/server/src/programs/peer_review_updater.rs index 69efe13fca65..f83c90650eb8 100644 --- a/services/headless-lms/server/src/programs/peer_review_updater.rs +++ b/services/headless-lms/server/src/programs/peer_review_updater.rs @@ -46,7 +46,7 @@ pub async fn main() -> anyhow::Result<()> { let course_id = exercise.course_id; if let Some(course_id) = course_id { let exercise_config = - headless_lms_models::peer_review_configs::get_by_exercise_or_course_id( + headless_lms_models::peer_or_self_review_configs::get_by_exercise_or_course_id( &mut conn, exercise, course_id, ) .await?; diff --git a/services/headless-lms/server/src/programs/seed/seed_courses.rs b/services/headless-lms/server/src/programs/seed/seed_courses.rs index 98f9d9cd89c4..a233b8f53b8a 100644 --- a/services/headless-lms/server/src/programs/seed/seed_courses.rs +++ b/services/headless-lms/server/src/programs/seed/seed_courses.rs @@ -26,7 +26,7 @@ use headless_lms_models::{ page_history::HistoryChangeReason, pages::CmsPageUpdate, pages::{self, NewCoursePage}, - peer_review_configs::PeerReviewProcessingStrategy::{ + peer_or_self_review_configs::PeerReviewProcessingStrategy::{ AutomaticallyGradeByAverage, AutomaticallyGradeOrManualReviewByAverage, ManualReviewEverything, }, @@ -3335,8 +3335,8 @@ pub async fn seed_course_without_submissions( ManualReviewEverything, 3.0, true, + 2, 1, - 0, ) .await?; @@ -3442,10 +3442,10 @@ pub async fn seed_course_without_submissions( course_id, exercise_2_id, AutomaticallyGradeOrManualReviewByAverage, - 2.5, + 3.0, true, + 2, 1, - 0, ) .await?; @@ -3454,10 +3454,10 @@ pub async fn seed_course_without_submissions( course_id, exercise_3_id, AutomaticallyGradeByAverage, - 2.0, + 3.0, true, + 2, 1, - 0, ) .await?; @@ -4546,8 +4546,8 @@ pub async fn seed_peer_review_course_without_submissions( ManualReviewEverything, 3.0, true, + 2, 1, - 0, ) .await?; @@ -4602,10 +4602,10 @@ pub async fn seed_peer_review_course_without_submissions( course_id, exercise_2_id, AutomaticallyGradeOrManualReviewByAverage, - 2.5, + 3.0, true, + 2, 1, - 0, ) .await?; @@ -4660,10 +4660,10 @@ pub async fn seed_peer_review_course_without_submissions( course_id, exercise_3_id, AutomaticallyGradeByAverage, - 2.0, + 3.0, true, + 2, 1, - 0, ) .await?; @@ -4720,8 +4720,8 @@ pub async fn seed_peer_review_course_without_submissions( ManualReviewEverything, 3.0, true, + 2, 1, - 0, ) .await?; diff --git a/services/headless-lms/server/src/programs/seed/seed_helpers.rs b/services/headless-lms/server/src/programs/seed/seed_helpers.rs index f72d825d7bfd..4d86ed033974 100644 --- a/services/headless-lms/server/src/programs/seed/seed_helpers.rs +++ b/services/headless-lms/server/src/programs/seed/seed_helpers.rs @@ -12,8 +12,8 @@ use headless_lms_models::{ self, CmsPageExercise, CmsPageExerciseSlide, CmsPageExerciseTask, CmsPageUpdate, NewPage, PageUpdateArgs, }, - peer_review_configs::{self, CmsPeerReviewConfig}, - peer_review_questions::{self, CmsPeerReviewQuestion}, + peer_or_self_review_configs::{self, CmsPeerOrSelfReviewConfig}, + peer_or_self_review_questions::{self, CmsPeerOrSelfReviewQuestion}, user_exercise_slide_states, user_exercise_states, PKeyPolicy, }; use headless_lms_utils::{attributes, document_schema_processor::GutenbergBlock}; @@ -239,9 +239,10 @@ pub fn example_exercise_flexible( limit_number_of_tries: false, deadline: None, needs_peer_review: false, - use_course_default_peer_review_config: false, - peer_review_config: None, - peer_review_questions: None, + needs_self_review: false, + use_course_default_peer_or_self_review_config: false, + peer_or_self_review_config: None, + peer_or_self_review_questions: None, }; (block, exercise, slides, tasks) } @@ -285,9 +286,10 @@ pub fn quizzes_exercise( limit_number_of_tries: false, deadline, needs_peer_review, - use_course_default_peer_review_config: true, - peer_review_config: None, - peer_review_questions: None, + needs_self_review: false, + use_course_default_peer_or_self_review_config: true, + peer_or_self_review_config: None, + peer_or_self_review_questions: None, }; let exercise_slide = CmsPageExerciseSlide { id: exercise_slide_id, @@ -342,9 +344,10 @@ pub fn tmc_exercise( limit_number_of_tries: false, deadline, needs_peer_review, - use_course_default_peer_review_config: true, - peer_review_config: None, - peer_review_questions: None, + needs_self_review: false, + use_course_default_peer_or_self_review_config: true, + peer_or_self_review_config: None, + peer_or_self_review_questions: None, }; let exercise_slide = CmsPageExerciseSlide { id: exercise_slide_id, @@ -561,16 +564,16 @@ pub async fn create_best_peer_review( conn: &mut PgConnection, course_id: Uuid, exercise_id: Uuid, - processing_strategy: peer_review_configs::PeerReviewProcessingStrategy, + processing_strategy: peer_or_self_review_configs::PeerReviewProcessingStrategy, accepting_threshold: f32, points_are_all_or_nothing: bool, peer_reviews_to_give: i32, peer_reviews_to_receive: i32, ) -> Result<()> { - let prc = peer_review_configs::upsert_with_id( + let prc = peer_or_self_review_configs::upsert_with_id( conn, PKeyPolicy::Generate, - &CmsPeerReviewConfig { + &CmsPeerOrSelfReviewConfig { id: Uuid::new_v4(), course_id, exercise_id: Some(exercise_id), @@ -579,60 +582,62 @@ pub async fn create_best_peer_review( accepting_threshold, processing_strategy, points_are_all_or_nothing, + review_instructions: None, }, ) .await?; - peer_review_questions::insert( + peer_or_self_review_questions::insert( conn, PKeyPolicy::Generate, - &CmsPeerReviewQuestion { + &CmsPeerOrSelfReviewQuestion { id: Uuid::new_v4(), - peer_review_config_id: prc.id, + peer_or_self_review_config_id: prc.id, order_number: 0, question: "What are your thoughts on the answer".to_string(), - question_type: peer_review_questions::PeerReviewQuestionType::Essay, + question_type: peer_or_self_review_questions::PeerOrSelfReviewQuestionType::Essay, answer_required: true, weight: 0.0, }, ) .await?; - peer_review_questions::insert( + peer_or_self_review_questions::insert( conn, PKeyPolicy::Generate, - &CmsPeerReviewQuestion { + &CmsPeerOrSelfReviewQuestion { id: Uuid::new_v4(), - peer_review_config_id: prc.id, + peer_or_self_review_config_id: prc.id, order_number: 1, question: "Was the answer correct?".to_string(), - question_type: peer_review_questions::PeerReviewQuestionType::Scale, + question_type: peer_or_self_review_questions::PeerOrSelfReviewQuestionType::Scale, answer_required: true, weight: 0.0, }, ) .await?; - peer_review_questions::insert( + peer_or_self_review_questions::insert( conn, PKeyPolicy::Generate, - &CmsPeerReviewQuestion { + &CmsPeerOrSelfReviewQuestion { id: Uuid::new_v4(), - peer_review_config_id: prc.id, + peer_or_self_review_config_id: prc.id, order_number: 2, question: "Was the answer good?".to_string(), - question_type: peer_review_questions::PeerReviewQuestionType::Scale, + question_type: peer_or_self_review_questions::PeerOrSelfReviewQuestionType::Scale, answer_required: true, weight: 0.0, }, ) .await?; - exercises::set_exercise_to_use_exercise_specific_peer_review_config( + exercises::set_exercise_to_use_exercise_specific_peer_or_self_review_config( conn, exercise_id, true, false, + false, ) .await?; Ok(()) diff --git a/services/headless-lms/server/src/programs/seed/seed_organizations/uh_mathstat.rs b/services/headless-lms/server/src/programs/seed/seed_organizations/uh_mathstat.rs index 7f19a60cd939..87861d07c6b5 100644 --- a/services/headless-lms/server/src/programs/seed/seed_organizations/uh_mathstat.rs +++ b/services/headless-lms/server/src/programs/seed/seed_organizations/uh_mathstat.rs @@ -229,6 +229,22 @@ pub async fn seed_organization_uh_mathstat( ) .await?; + let self_review = seed_sample_course( + Uuid::parse_str("3cbaac48-59c4-4e31-9d7e-1f51c017390d")?, + "Self review", + "self-review", + uh_data.clone(), + ) + .await?; + + roles::insert( + &mut conn, + teacher_user_id, + UserRole::Teacher, + RoleDomain::Course(self_review), + ) + .await?; + let audio_course = seed_sample_course( Uuid::parse_str("2b80a0cb-ae0c-4f4b-843e-0322a3d18aff")?, "Audio course", diff --git a/services/headless-lms/server/src/ts_binding_generator.rs b/services/headless-lms/server/src/ts_binding_generator.rs index ad2d33373ca9..01acc7dd6dff 100644 --- a/services/headless-lms/server/src/ts_binding_generator.rs +++ b/services/headless-lms/server/src/ts_binding_generator.rs @@ -107,7 +107,7 @@ fn models(target: &mut File) { exercise_slide_submissions::ExerciseSlideSubmissionCountByExercise, exercise_slide_submissions::ExerciseSlideSubmissionCountByWeekAndHour, exercise_slide_submissions::ExerciseSlideSubmissionInfo, - exercise_task_submissions::PeerReviewsRecieved, + exercise_task_submissions::PeerOrSelfReviewsReceived, exercise_slides::CourseMaterialExerciseSlide, exercise_slides::ExerciseSlide, exercise_task_gradings::ExerciseTaskGrading, @@ -142,10 +142,10 @@ fn models(target: &mut File) { library::grading::StudentExerciseSlideSubmissionResult, library::grading::StudentExerciseTaskSubmission, library::grading::StudentExerciseTaskSubmissionResult, - library::peer_reviewing::CourseMaterialPeerReviewData, - library::peer_reviewing::CourseMaterialPeerReviewDataAnswerToReview, - library::peer_reviewing::CourseMaterialPeerReviewQuestionAnswer, - library::peer_reviewing::CourseMaterialPeerReviewSubmission, + library::peer_or_self_reviewing::CourseMaterialPeerOrSelfReviewData, + library::peer_or_self_reviewing::CourseMaterialPeerOrSelfReviewDataAnswerToReview, + library::peer_or_self_reviewing::CourseMaterialPeerOrSelfReviewQuestionAnswer, + library::peer_or_self_reviewing::CourseMaterialPeerOrSelfReviewSubmission, library::progressing::CompletionRegistrationLink, library::progressing::CourseInstanceCompletionSummary, library::custom_view_exercises::CustomViewExerciseSubmissions, @@ -188,20 +188,20 @@ fn models(target: &mut File) { pages::PageSearchResult, pages::PageWithExercises, pages::PageDetailsUpdate, - peer_review_configs::CmsPeerReviewConfig, - peer_review_configs::CmsPeerReviewConfiguration, - peer_review_configs::CourseMaterialPeerReviewConfig, - peer_review_configs::PeerReviewProcessingStrategy, - peer_review_configs::PeerReviewConfig, - peer_review_submissions::PeerReviewSubmission, - peer_review_question_submissions::PeerReviewAnswer, - peer_review_question_submissions::PeerReviewQuestionAndAnswer, - peer_review_question_submissions::PeerReviewQuestionSubmission, + peer_or_self_review_configs::CmsPeerOrSelfReviewConfig, + peer_or_self_review_configs::CmsPeerOrSelfReviewConfiguration, + peer_or_self_review_configs::CourseMaterialPeerOrSelfReviewConfig, + peer_or_self_review_configs::PeerReviewProcessingStrategy, + peer_or_self_review_configs::PeerOrSelfReviewConfig, + peer_or_self_review_submissions::PeerOrSelfReviewSubmission, + peer_or_self_review_question_submissions::PeerOrSelfReviewAnswer, + peer_or_self_review_question_submissions::PeerOrSelfReviewQuestionAndAnswer, + peer_or_self_review_question_submissions::PeerOrSelfReviewQuestionSubmission, peer_review_queue_entries::PeerReviewQueueEntry, - peer_review_question_submissions::PeerReviewWithQuestionsAndAnswers, - peer_review_questions::CmsPeerReviewQuestion, - peer_review_questions::PeerReviewQuestion, - peer_review_questions::PeerReviewQuestionType, + peer_or_self_review_question_submissions::PeerReviewWithQuestionsAndAnswers, + peer_or_self_review_questions::CmsPeerOrSelfReviewQuestion, + peer_or_self_review_questions::PeerOrSelfReviewQuestion, + peer_or_self_review_questions::PeerOrSelfReviewQuestionType, pending_roles::PendingRole, playground_examples::PlaygroundExample, playground_examples::PlaygroundExampleData, @@ -291,7 +291,7 @@ fn controllers(target: &mut File) { courses::CourseMaterialCourseModule, exams::ExamData, exams::ExamEnrollmentData, - exercises::CourseMaterialPeerReviewDataWithToken + exercises::CourseMaterialPeerOrSelfReviewDataWithToken }; } diff --git a/services/main-frontend/src/components/page-specific/manage/course-instances/id/points/user_id/PeerReviewSubmissionSummaryAccordion.tsx b/services/main-frontend/src/components/page-specific/manage/course-instances/id/points/user_id/PeerOrSelfReviewSubmissionSummaryAccordion.tsx similarity index 68% rename from services/main-frontend/src/components/page-specific/manage/course-instances/id/points/user_id/PeerReviewSubmissionSummaryAccordion.tsx rename to services/main-frontend/src/components/page-specific/manage/course-instances/id/points/user_id/PeerOrSelfReviewSubmissionSummaryAccordion.tsx index bfdd4bc1554f..7976655a7224 100644 --- a/services/main-frontend/src/components/page-specific/manage/course-instances/id/points/user_id/PeerReviewSubmissionSummaryAccordion.tsx +++ b/services/main-frontend/src/components/page-specific/manage/course-instances/id/points/user_id/PeerOrSelfReviewSubmissionSummaryAccordion.tsx @@ -4,18 +4,18 @@ import Link from "next/link" import { useTranslation } from "react-i18next" import { - PeerReviewQuestion, - PeerReviewQuestionSubmission, - PeerReviewSubmission, + PeerOrSelfReviewQuestion, + PeerOrSelfReviewQuestionSubmission, + PeerOrSelfReviewSubmission, } from "../../../../../../../shared-module/bindings" import Accordion from "../../../../../../../shared-module/components/Accordion" import HideTextInSystemTests from "../../../../../../../shared-module/components/system-tests/HideTextInSystemTests" import { baseTheme } from "../../../../../../../shared-module/styles" -export interface PeerReviewSubmissionSummaryAccordionProps { - peerReviewSubmission: PeerReviewSubmission - peerReviewQuestionSubmissions: PeerReviewQuestionSubmission[] - peerReviewQuestions: PeerReviewQuestion[] +export interface PeerOrSelfReviewSubmissionSummaryAccordionProps { + peerOrSelfReviewSubmission: PeerOrSelfReviewSubmission + peerOrSelfReviewQuestionSubmissions: PeerOrSelfReviewQuestionSubmission[] + peerOrSelfReviewQuestions: PeerOrSelfReviewQuestion[] showSubmissionBeingReviewed?: boolean } @@ -23,12 +23,12 @@ const PeerReviewDiv = styled.div` margin-bottom: 0.5rem; ` -const PeerReviewSubmissionSummaryAccordion = ({ - peerReviewSubmission, - peerReviewQuestionSubmissions, +const PeerOrSelfReviewSubmissionSummaryAccordion = ({ + peerOrSelfReviewSubmission, + peerOrSelfReviewQuestionSubmissions, showSubmissionBeingReviewed, - peerReviewQuestions, -}: PeerReviewSubmissionSummaryAccordionProps) => { + peerOrSelfReviewQuestions, +}: PeerOrSelfReviewSubmissionSummaryAccordionProps) => { const { t } = useTranslation() return (
{t("peer-review-submission-id")}:{" "} @@ -51,24 +51,24 @@ const PeerReviewSubmissionSummaryAccordion = ({ )} - {peerReviewQuestionSubmissions.map((prqs) => { - const peerReviewQuestion = peerReviewQuestions.find( - (prq) => prq.id === prqs.peer_review_question_id, + {peerOrSelfReviewQuestionSubmissions.map((prqs) => { + const peerOrSelfReviewQuestion = peerOrSelfReviewQuestions.find( + (prq) => prq.id === prqs.peer_or_self_review_question_id, ) return (

- {t("question")}: {peerReviewQuestion?.question}{" "} + {t("question")}: {peerOrSelfReviewQuestion?.question}{" "} {prqs.number_data !== null && ( > = ({ courseId }) => { const { t } = useTranslation() - const getCourseExercises = useQuery({ - queryKey: [`courses-${courseId}-exercises-and-count-of-answers-requiring-attention`], - queryFn: () => fetchCourseExercisesAndCountOfAnswersRequiringAttention(courseId), - }) + const getCourseExercises = useCourseExercisesAndCountAnswersRequitingAttentionQuery(courseId) const courseStructure = useCourseStructure(courseId) if (getCourseExercises.isError) { return diff --git a/services/main-frontend/src/components/page-specific/manage/exercises/id/submissions/AnswersRequiringAttentionItem.tsx b/services/main-frontend/src/components/page-specific/manage/exercises/id/submissions/AnswersRequiringAttentionItem.tsx index d17e5c484929..31d6a8666b8e 100644 --- a/services/main-frontend/src/components/page-specific/manage/exercises/id/submissions/AnswersRequiringAttentionItem.tsx +++ b/services/main-frontend/src/components/page-specific/manage/exercises/id/submissions/AnswersRequiringAttentionItem.tsx @@ -19,7 +19,7 @@ import { primaryFont } from "../../../../../../shared-module/styles" import { respondToOrLarger } from "../../../../../../shared-module/styles/respond" import SubmissionIFrame from "../../../../submissions/id/SubmissionIFrame" -import PeerReviewAccordion from "./PeerReviewAccordion" +import PeerReviewAccordion from "./PeerOrSelfReviewAccordion" interface Props { answerRequiringAttention: AnswerRequiringAttentionWithTasks @@ -426,13 +426,13 @@ const AnswersRequiringAttentionItem: React.FC = ({ `} >

diff --git a/services/main-frontend/src/components/page-specific/manage/exercises/id/submissions/PeerReviewAccordion.tsx b/services/main-frontend/src/components/page-specific/manage/exercises/id/submissions/PeerOrSelfReviewAccordion.tsx similarity index 59% rename from services/main-frontend/src/components/page-specific/manage/exercises/id/submissions/PeerReviewAccordion.tsx rename to services/main-frontend/src/components/page-specific/manage/exercises/id/submissions/PeerOrSelfReviewAccordion.tsx index 48118c41d50d..9cdaa2b1d7cf 100644 --- a/services/main-frontend/src/components/page-specific/manage/exercises/id/submissions/PeerReviewAccordion.tsx +++ b/services/main-frontend/src/components/page-specific/manage/exercises/id/submissions/PeerOrSelfReviewAccordion.tsx @@ -1,18 +1,19 @@ import { css } from "@emotion/css" import styled from "@emotion/styled" -import React from "react" +import React, { useMemo } from "react" import { useTranslation } from "react-i18next" import { - PeerReviewAnswer, + PeerOrSelfReviewAnswer, PeerReviewWithQuestionsAndAnswers, } from "../../../../../../shared-module/bindings" import Accordion from "../../../../../../shared-module/components/Accordion" import LikertScale from "../../../../../../shared-module/components/PeerReview/LikertScale" +import useUserInfo from "../../../../../../shared-module/hooks/useUserInfo" import { baseTheme } from "../../../../../../shared-module/styles" export interface PeerReviewAccordionProps { - peerReviews: Array + peerOrSelfReviews: Array title: string } @@ -24,10 +25,21 @@ const Question = styled.div` color: #1a2333; ` -const PeerReviewAccordion: React.FC = ({ peerReviews, title }) => { +// eslint-disable-next-line i18next/no-literal-string +const Title = styled.h5` + border-bottom: 1px solid ${baseTheme.colors.clear[600]}; + padding: 0 1.5rem 1rem; +` + +const QuestionWrapper = styled.div` + margin: 2rem 1.5rem 0rem; +` + +const PeerReviewAccordion: React.FC = ({ peerOrSelfReviews, title }) => { const { t } = useTranslation() + const userInfo = useUserInfo() - const mapToAnswer = (question: string, answer: PeerReviewAnswer) => { + const mapToAnswer = (question: string, answer: PeerOrSelfReviewAnswer) => { switch (answer.type) { case "essay": return ( @@ -62,6 +74,14 @@ const PeerReviewAccordion: React.FC = ({ peerReviews, } } + const peerReviews = useMemo(() => { + return peerOrSelfReviews.filter((x) => x.peer_review_giver_user_id !== userInfo.data?.user_id) + }, [peerOrSelfReviews, userInfo.data?.user_id]) + + const selfReviews = useMemo(() => { + return peerOrSelfReviews.filter((x) => x.peer_review_giver_user_id === userInfo.data?.user_id) + }, [peerOrSelfReviews, userInfo.data?.user_id]) + return (
@@ -81,7 +101,7 @@ const PeerReviewAccordion: React.FC = ({ peerReviews, height: 20px; `} > - {peerReviews.length} + {peerOrSelfReviews.length}
= ({ peerReviews, margin: 0.5rem 0; `} > + {selfReviews.map((selfReview) => ( +
+ {t("title-self-review")} + {selfReview.questions_and_answers.map((x, i) => ( + + {mapToAnswer( + `${t("question-n", { n: i + 1 })}: ${x.question}${ + x.answer_required ? " *" : "" + }`, + x.answer, + )} + + ))} +
+ ))} {peerReviews.map((peerReview, i) => ( -
-
- {t("peer-review-n", { n: i + 1 })} -
+
+ {t("peer-review-n", { n: i + 1 })} {peerReview.questions_and_answers.map((x, i) => ( -
+ {mapToAnswer( `${t("question-n", { n: i + 1 })}: ${x.question}${ x.answer_required ? " *" : "" }`, x.answer, )} -
+ ))}
))} diff --git a/services/main-frontend/src/hooks/count/useCountAnswersRequiringAttentionHook.tsx b/services/main-frontend/src/hooks/count/useCountAnswersRequiringAttentionHook.tsx new file mode 100644 index 000000000000..e4b0a6db2d56 --- /dev/null +++ b/services/main-frontend/src/hooks/count/useCountAnswersRequiringAttentionHook.tsx @@ -0,0 +1,19 @@ +import { UseQueryResult } from "@tanstack/react-query" + +import useCourseExercisesAndCountAnswersRequitingAttentionQuery from "../useCourseExercisesAndCountAnswersRequitingAttentionQuery" + +const useCountAnswersRequiringAttentionHook = (courseId: string) => { + const useAnswersRequiringAttention = () => { + return useCourseExercisesAndCountAnswersRequitingAttentionQuery(courseId, { + select: (data) => { + const res = data.reduce((acc, curr) => acc + (curr.count ?? 0), 0) + // The typescript signature is not ideal here, we have to work around it a bit + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return res as any + }, + }) as UseQueryResult + } + return useAnswersRequiringAttention +} + +export default useCountAnswersRequiringAttentionHook diff --git a/services/main-frontend/src/hooks/useCourseExercisesAndCountAnswersRequitingAttentionQuery.tsx b/services/main-frontend/src/hooks/useCourseExercisesAndCountAnswersRequitingAttentionQuery.tsx new file mode 100644 index 000000000000..7d18f02334ce --- /dev/null +++ b/services/main-frontend/src/hooks/useCourseExercisesAndCountAnswersRequitingAttentionQuery.tsx @@ -0,0 +1,25 @@ +import { useQuery, UseQueryOptions } from "@tanstack/react-query" + +import { fetchCourseExercisesAndCountOfAnswersRequiringAttention } from "../services/backend/courses" +import { ExerciseAnswersInCourseRequiringAttentionCount } from "../shared-module/bindings" +import { assertNotNullOrUndefined } from "../shared-module/utils/nullability" + +type OptionsType = Omit< + UseQueryOptions>, + "queryKey" | "queryFn" | "enabled" +> + +const useCourseExercisesAndCountAnswersRequitingAttentionQuery = ( + courseId: string | null | undefined, + options: OptionsType = {}, +) => { + return useQuery({ + queryKey: [`courses-exercises-and-count-of-answers-requiring-attention`, courseId], + enabled: courseId !== null && courseId !== undefined, + queryFn: () => + fetchCourseExercisesAndCountOfAnswersRequiringAttention(assertNotNullOrUndefined(courseId)), + ...options, + }) +} + +export default useCourseExercisesAndCountAnswersRequitingAttentionQuery diff --git a/services/main-frontend/src/pages/manage/course-instances/[id]/course-status-summary-for-user/[user_id].tsx b/services/main-frontend/src/pages/manage/course-instances/[id]/course-status-summary-for-user/[user_id].tsx index 5cff020ffaa2..1c0b1d01e5e6 100644 --- a/services/main-frontend/src/pages/manage/course-instances/[id]/course-status-summary-for-user/[user_id].tsx +++ b/services/main-frontend/src/pages/manage/course-instances/[id]/course-status-summary-for-user/[user_id].tsx @@ -6,7 +6,7 @@ import Link from "next/link" import React from "react" import { useTranslation } from "react-i18next" -import PeerReviewSubmissionSummaryAccordion from "../../../../../components/page-specific/manage/course-instances/id/points/user_id/PeerReviewSubmissionSummaryAccordion" +import PeerOrSelfReviewSubmissionSummaryAccordion from "../../../../../components/page-specific/manage/course-instances/id/points/user_id/PeerOrSelfReviewSubmissionSummaryAccordion" import { useCourseStructure } from "../../../../../hooks/useCourseStructure" import { getAllCourseModuleCompletionsForUserAndCourseInstance, @@ -426,31 +426,35 @@ const CourseInstanceExerciseStatusList: React.FC<

)} - {exerciseStatus.received_peer_review_submissions.length > - 0 ? ( + {exerciseStatus.received_peer_or_self_review_submissions + .length > 0 ? ( <>

{/* eslint-disable-next-line i18next/no-literal-string */} {t("peer-reviews-received")}: ( - {exerciseStatus.received_peer_review_submissions.length} + { + exerciseStatus + .received_peer_or_self_review_submissions.length + } )

- {exerciseStatus.received_peer_review_submissions.map( + {exerciseStatus.received_peer_or_self_review_submissions.map( (received) => { - const peerReviewQuestionSubmissions = - exerciseStatus.received_peer_review_question_submissions.filter( + const peerOrSelfReviewQuestionSubmissions = + exerciseStatus.received_peer_or_self_review_question_submissions.filter( (prqs) => - prqs.peer_review_submission_id === received.id, + prqs.peer_or_self_review_submission_id === + received.id, ) return ( - {t("no-peer-reviews-received")} )} - {exerciseStatus.given_peer_review_submissions.length > 0 ? ( + {exerciseStatus.given_peer_or_self_review_submissions.length > + 0 ? ( <>

{/* eslint-disable-next-line i18next/no-literal-string */} {t("peer-reviews-given")}: ( - {exerciseStatus.given_peer_review_submissions.length}) + { + exerciseStatus.given_peer_or_self_review_submissions + .length + } + )

- {exerciseStatus.given_peer_review_submissions.map( + {exerciseStatus.given_peer_or_self_review_submissions.map( (given) => { - const peerReviewQuestionSubmissions = - exerciseStatus.given_peer_review_question_submissions.filter( + const peerOrSelfReviewQuestionSubmissions = + exerciseStatus.given_peer_or_self_review_question_submissions.filter( (prqs) => - prqs.peer_review_submission_id === given.id, + prqs.peer_or_self_review_submission_id === + given.id, ) return ( - ) diff --git a/services/main-frontend/src/pages/manage/courses/[id]/[...path].tsx b/services/main-frontend/src/pages/manage/courses/[id]/[...path].tsx index c14c7b7a0b73..eba98a8c1d3f 100644 --- a/services/main-frontend/src/pages/manage/courses/[id]/[...path].tsx +++ b/services/main-frontend/src/pages/manage/courses/[id]/[...path].tsx @@ -14,6 +14,7 @@ import CoursePages from "../../../../components/page-specific/manage/courses/id/ import CoursePermissions from "../../../../components/page-specific/manage/courses/id/permissions/CoursePermissions" import References from "../../../../components/page-specific/manage/courses/id/references" import CourseStatsPage from "../../../../components/page-specific/manage/courses/id/stats/CourseStatsPage" +import useCountAnswersRequiringAttentionHook from "../../../../hooks/count/useCountAnswersRequiringAttentionHook" import createPendingChangeRequestCountHook from "../../../../hooks/count/usePendingChangeRequestCount" import createUnreadFeedbackCountHook from "../../../../hooks/count/useUnreadFeedbackCount" import TabLink from "../../../../shared-module/components/Navigation/TabLinks/TabLink" @@ -93,7 +94,11 @@ const CourseManagementPage: React.FC {t("link-change-requests")} - + {t("link-exercises")} diff --git a/shared-module/src/bindings.guard.ts b/shared-module/src/bindings.guard.ts index 2db8b4956237..b7f3b355ea1b 100644 --- a/shared-module/src/bindings.guard.ts +++ b/shared-module/src/bindings.guard.ts @@ -31,9 +31,9 @@ import { CmsPageExerciseSlide, CmsPageExerciseTask, CmsPageUpdate, - CmsPeerReviewConfig, - CmsPeerReviewConfiguration, - CmsPeerReviewQuestion, + CmsPeerOrSelfReviewConfig, + CmsPeerOrSelfReviewConfiguration, + CmsPeerOrSelfReviewQuestion, CompletionPolicy, CompletionRegistrationLink, ContentManagementPage, @@ -55,12 +55,12 @@ import { CourseMaterialExerciseServiceInfo, CourseMaterialExerciseSlide, CourseMaterialExerciseTask, - CourseMaterialPeerReviewConfig, - CourseMaterialPeerReviewData, - CourseMaterialPeerReviewDataAnswerToReview, - CourseMaterialPeerReviewDataWithToken, - CourseMaterialPeerReviewQuestionAnswer, - CourseMaterialPeerReviewSubmission, + CourseMaterialPeerOrSelfReviewConfig, + CourseMaterialPeerOrSelfReviewData, + CourseMaterialPeerOrSelfReviewDataAnswerToReview, + CourseMaterialPeerOrSelfReviewDataWithToken, + CourseMaterialPeerOrSelfReviewQuestionAnswer, + CourseMaterialPeerOrSelfReviewSubmission, CourseModule, CourseModuleCompletion, CourseModuleCompletionWithRegistrationInfo, @@ -170,16 +170,16 @@ import { PageWithExercises, Pagination, PaperSize, - PeerReviewAnswer, - PeerReviewConfig, + PeerOrSelfReviewAnswer, + PeerOrSelfReviewConfig, + PeerOrSelfReviewQuestion, + PeerOrSelfReviewQuestionAndAnswer, + PeerOrSelfReviewQuestionSubmission, + PeerOrSelfReviewQuestionType, + PeerOrSelfReviewsReceived, + PeerOrSelfReviewSubmission, PeerReviewProcessingStrategy, - PeerReviewQuestion, - PeerReviewQuestionAndAnswer, - PeerReviewQuestionSubmission, - PeerReviewQuestionType, PeerReviewQueueEntry, - PeerReviewsRecieved, - PeerReviewSubmission, PeerReviewWithQuestionsAndAnswers, PendingRole, PlaygroundExample, @@ -726,7 +726,8 @@ export function isCourseModuleCompletionWithRegistrationInfo( typeof typedObj["passed"] === "boolean" && typeof typedObj["prerequisite_modules_completed"] === "boolean" && typeof typedObj["registered"] === "boolean" && - typeof typedObj["user_id"] === "string" + typeof typedObj["user_id"] === "string" && + typeof typedObj["completion_date"] === "string" ) } @@ -1278,15 +1279,21 @@ export function isExerciseSlideSubmissionInfo(obj: unknown): obj is ExerciseSlid ) } -export function isPeerReviewsRecieved(obj: unknown): obj is PeerReviewsRecieved { - const typedObj = obj as PeerReviewsRecieved +export function isPeerOrSelfReviewsReceived(obj: unknown): obj is PeerOrSelfReviewsReceived { + const typedObj = obj as PeerOrSelfReviewsReceived return ( ((typedObj !== null && typeof typedObj === "object") || typeof typedObj === "function") && - Array.isArray(typedObj["peer_review_questions"]) && - typedObj["peer_review_questions"].every((e: any) => isPeerReviewQuestion(e) as boolean) && - Array.isArray(typedObj["peer_review_question_submissions"]) && - typedObj["peer_review_question_submissions"].every( - (e: any) => isPeerReviewQuestionSubmission(e) as boolean, + Array.isArray(typedObj["peer_or_self_review_questions"]) && + typedObj["peer_or_self_review_questions"].every( + (e: any) => isPeerOrSelfReviewQuestion(e) as boolean, + ) && + Array.isArray(typedObj["peer_or_self_review_question_submissions"]) && + typedObj["peer_or_self_review_question_submissions"].every( + (e: any) => isPeerOrSelfReviewQuestionSubmission(e) as boolean, + ) && + Array.isArray(typedObj["peer_or_self_review_submissions"]) && + typedObj["peer_or_self_review_submissions"].every( + (e: any) => isPeerOrSelfReviewSubmission(e) as boolean, ) ) } @@ -1444,8 +1451,10 @@ export function isCourseMaterialExercise(obj: unknown): obj is CourseMaterialExe Object.entries(typedObj["exercise_slide_submission_counts"]).every( ([key, value]) => typeof value === "number" && typeof key === "string", ) && - (typedObj["peer_review_config"] === null || - (isCourseMaterialPeerReviewConfig(typedObj["peer_review_config"]) as boolean)) && + (typedObj["peer_or_self_review_config"] === null || + (isCourseMaterialPeerOrSelfReviewConfig( + typedObj["peer_or_self_review_config"], + ) as boolean)) && (typedObj["previous_exercise_slide_submission"] === null || (isExerciseSlideSubmission(typedObj["previous_exercise_slide_submission"]) as boolean)) && Array.isArray(typedObj["user_course_instance_exercise_service_variables"]) && @@ -1476,7 +1485,8 @@ export function isExercise(obj: unknown): obj is Exercise { typeof typedObj["max_tries_per_slide"] === "number") && typeof typedObj["limit_number_of_tries"] === "boolean" && typeof typedObj["needs_peer_review"] === "boolean" && - typeof typedObj["use_course_default_peer_review_config"] === "boolean" && + typeof typedObj["needs_self_review"] === "boolean" && + typeof typedObj["use_course_default_peer_or_self_review_config"] === "boolean" && (typedObj["exercise_language_group_id"] === null || typeof typedObj["exercise_language_group_id"] === "string") ) @@ -1504,28 +1514,30 @@ export function isExerciseStatusSummaryForUser(obj: unknown): obj is ExerciseSta typedObj["exercise_slide_submissions"].every( (e: any) => isExerciseSlideSubmission(e) as boolean, ) && - Array.isArray(typedObj["given_peer_review_submissions"]) && - typedObj["given_peer_review_submissions"].every( - (e: any) => isPeerReviewSubmission(e) as boolean, + Array.isArray(typedObj["given_peer_or_self_review_submissions"]) && + typedObj["given_peer_or_self_review_submissions"].every( + (e: any) => isPeerOrSelfReviewSubmission(e) as boolean, ) && - Array.isArray(typedObj["given_peer_review_question_submissions"]) && - typedObj["given_peer_review_question_submissions"].every( - (e: any) => isPeerReviewQuestionSubmission(e) as boolean, + Array.isArray(typedObj["given_peer_or_self_review_question_submissions"]) && + typedObj["given_peer_or_self_review_question_submissions"].every( + (e: any) => isPeerOrSelfReviewQuestionSubmission(e) as boolean, ) && - Array.isArray(typedObj["received_peer_review_submissions"]) && - typedObj["received_peer_review_submissions"].every( - (e: any) => isPeerReviewSubmission(e) as boolean, + Array.isArray(typedObj["received_peer_or_self_review_submissions"]) && + typedObj["received_peer_or_self_review_submissions"].every( + (e: any) => isPeerOrSelfReviewSubmission(e) as boolean, ) && - Array.isArray(typedObj["received_peer_review_question_submissions"]) && - typedObj["received_peer_review_question_submissions"].every( - (e: any) => isPeerReviewQuestionSubmission(e) as boolean, + Array.isArray(typedObj["received_peer_or_self_review_question_submissions"]) && + typedObj["received_peer_or_self_review_question_submissions"].every( + (e: any) => isPeerOrSelfReviewQuestionSubmission(e) as boolean, ) && (typedObj["peer_review_queue_entry"] === null || (isPeerReviewQueueEntry(typedObj["peer_review_queue_entry"]) as boolean)) && (typedObj["teacher_grading_decision"] === null || (isTeacherGradingDecision(typedObj["teacher_grading_decision"]) as boolean)) && - Array.isArray(typedObj["peer_review_questions"]) && - typedObj["peer_review_questions"].every((e: any) => isPeerReviewQuestion(e) as boolean) + Array.isArray(typedObj["peer_or_self_review_questions"]) && + typedObj["peer_or_self_review_questions"].every( + (e: any) => isPeerOrSelfReviewQuestion(e) as boolean, + ) ) } @@ -1679,8 +1691,8 @@ export function isAnswerRequiringAttentionWithTasks( typedObj["given_peer_reviews"].every( (e: any) => isPeerReviewWithQuestionsAndAnswers(e) as boolean, ) && - Array.isArray(typedObj["received_peer_reviews"]) && - typedObj["received_peer_reviews"].every( + Array.isArray(typedObj["received_peer_or_self_reviews"]) && + typedObj["received_peer_or_self_reviews"].every( (e: any) => isPeerReviewWithQuestionsAndAnswers(e) as boolean, ) ) @@ -1752,23 +1764,29 @@ export function isStudentExerciseTaskSubmissionResult( ) } -export function isCourseMaterialPeerReviewData(obj: unknown): obj is CourseMaterialPeerReviewData { - const typedObj = obj as CourseMaterialPeerReviewData +export function isCourseMaterialPeerOrSelfReviewData( + obj: unknown, +): obj is CourseMaterialPeerOrSelfReviewData { + const typedObj = obj as CourseMaterialPeerOrSelfReviewData return ( ((typedObj !== null && typeof typedObj === "object") || typeof typedObj === "function") && (typedObj["answer_to_review"] === null || - (isCourseMaterialPeerReviewDataAnswerToReview(typedObj["answer_to_review"]) as boolean)) && - (isPeerReviewConfig(typedObj["peer_review_config"]) as boolean) && - Array.isArray(typedObj["peer_review_questions"]) && - typedObj["peer_review_questions"].every((e: any) => isPeerReviewQuestion(e) as boolean) && + (isCourseMaterialPeerOrSelfReviewDataAnswerToReview( + typedObj["answer_to_review"], + ) as boolean)) && + (isPeerOrSelfReviewConfig(typedObj["peer_or_self_review_config"]) as boolean) && + Array.isArray(typedObj["peer_or_self_review_questions"]) && + typedObj["peer_or_self_review_questions"].every( + (e: any) => isPeerOrSelfReviewQuestion(e) as boolean, + ) && typeof typedObj["num_peer_reviews_given"] === "number" ) } -export function isCourseMaterialPeerReviewDataAnswerToReview( +export function isCourseMaterialPeerOrSelfReviewDataAnswerToReview( obj: unknown, -): obj is CourseMaterialPeerReviewDataAnswerToReview { - const typedObj = obj as CourseMaterialPeerReviewDataAnswerToReview +): obj is CourseMaterialPeerOrSelfReviewDataAnswerToReview { + const typedObj = obj as CourseMaterialPeerOrSelfReviewDataAnswerToReview return ( ((typedObj !== null && typeof typedObj === "object") || typeof typedObj === "function") && typeof typedObj["exercise_slide_submission_id"] === "string" && @@ -1779,29 +1797,29 @@ export function isCourseMaterialPeerReviewDataAnswerToReview( ) } -export function isCourseMaterialPeerReviewQuestionAnswer( +export function isCourseMaterialPeerOrSelfReviewQuestionAnswer( obj: unknown, -): obj is CourseMaterialPeerReviewQuestionAnswer { - const typedObj = obj as CourseMaterialPeerReviewQuestionAnswer +): obj is CourseMaterialPeerOrSelfReviewQuestionAnswer { + const typedObj = obj as CourseMaterialPeerOrSelfReviewQuestionAnswer return ( ((typedObj !== null && typeof typedObj === "object") || typeof typedObj === "function") && - typeof typedObj["peer_review_question_id"] === "string" && + typeof typedObj["peer_or_self_review_question_id"] === "string" && (typedObj["text_data"] === null || typeof typedObj["text_data"] === "string") && (typedObj["number_data"] === null || typeof typedObj["number_data"] === "number") ) } -export function isCourseMaterialPeerReviewSubmission( +export function isCourseMaterialPeerOrSelfReviewSubmission( obj: unknown, -): obj is CourseMaterialPeerReviewSubmission { - const typedObj = obj as CourseMaterialPeerReviewSubmission +): obj is CourseMaterialPeerOrSelfReviewSubmission { + const typedObj = obj as CourseMaterialPeerOrSelfReviewSubmission return ( ((typedObj !== null && typeof typedObj === "object") || typeof typedObj === "function") && typeof typedObj["exercise_slide_submission_id"] === "string" && - typeof typedObj["peer_review_config_id"] === "string" && + typeof typedObj["peer_or_self_review_config_id"] === "string" && Array.isArray(typedObj["peer_review_question_answers"]) && typedObj["peer_review_question_answers"].every( - (e: any) => isCourseMaterialPeerReviewQuestionAnswer(e) as boolean, + (e: any) => isCourseMaterialPeerOrSelfReviewQuestionAnswer(e) as boolean, ) && typeof typedObj["token"] === "string" ) @@ -2079,14 +2097,15 @@ export function isCmsPageExercise(obj: unknown): obj is CmsPageExercise { typeof typedObj["limit_number_of_tries"] === "boolean" && (typedObj["deadline"] === null || typeof typedObj["deadline"] === "string") && typeof typedObj["needs_peer_review"] === "boolean" && - (typedObj["peer_review_config"] === null || - (isCmsPeerReviewConfig(typedObj["peer_review_config"]) as boolean)) && - (typedObj["peer_review_questions"] === null || - (Array.isArray(typedObj["peer_review_questions"]) && - typedObj["peer_review_questions"].every( - (e: any) => isCmsPeerReviewQuestion(e) as boolean, + typeof typedObj["needs_self_review"] === "boolean" && + (typedObj["peer_or_self_review_config"] === null || + (isCmsPeerOrSelfReviewConfig(typedObj["peer_or_self_review_config"]) as boolean)) && + (typedObj["peer_or_self_review_questions"] === null || + (Array.isArray(typedObj["peer_or_self_review_questions"]) && + typedObj["peer_or_self_review_questions"].every( + (e: any) => isCmsPeerOrSelfReviewQuestion(e) as boolean, ))) && - typeof typedObj["use_course_default_peer_review_config"] === "boolean" + typeof typedObj["use_course_default_peer_or_self_review_config"] === "boolean" ) } @@ -2138,10 +2157,14 @@ export function isContentManagementPage(obj: unknown): obj is ContentManagementP typedObj["exercise_slides"].every((e: any) => isCmsPageExerciseSlide(e) as boolean) && Array.isArray(typedObj["exercise_tasks"]) && typedObj["exercise_tasks"].every((e: any) => isCmsPageExerciseTask(e) as boolean) && - Array.isArray(typedObj["peer_review_configs"]) && - typedObj["peer_review_configs"].every((e: any) => isCmsPeerReviewConfig(e) as boolean) && - Array.isArray(typedObj["peer_review_questions"]) && - typedObj["peer_review_questions"].every((e: any) => isCmsPeerReviewQuestion(e) as boolean) && + Array.isArray(typedObj["peer_or_self_review_configs"]) && + typedObj["peer_or_self_review_configs"].every( + (e: any) => isCmsPeerOrSelfReviewConfig(e) as boolean, + ) && + Array.isArray(typedObj["peer_or_self_review_questions"]) && + typedObj["peer_or_self_review_questions"].every( + (e: any) => isCmsPeerOrSelfReviewQuestion(e) as boolean, + ) && typeof typedObj["organization_id"] === "string" ) } @@ -2345,8 +2368,8 @@ export function isPageDetailsUpdate(obj: unknown): obj is PageDetailsUpdate { ) } -export function isCmsPeerReviewConfig(obj: unknown): obj is CmsPeerReviewConfig { - const typedObj = obj as CmsPeerReviewConfig +export function isCmsPeerOrSelfReviewConfig(obj: unknown): obj is CmsPeerOrSelfReviewConfig { + const typedObj = obj as CmsPeerOrSelfReviewConfig return ( ((typedObj !== null && typeof typedObj === "object") || typeof typedObj === "function") && typeof typedObj["id"] === "string" && @@ -2360,20 +2383,24 @@ export function isCmsPeerReviewConfig(obj: unknown): obj is CmsPeerReviewConfig ) } -export function isCmsPeerReviewConfiguration(obj: unknown): obj is CmsPeerReviewConfiguration { - const typedObj = obj as CmsPeerReviewConfiguration +export function isCmsPeerOrSelfReviewConfiguration( + obj: unknown, +): obj is CmsPeerOrSelfReviewConfiguration { + const typedObj = obj as CmsPeerOrSelfReviewConfiguration return ( ((typedObj !== null && typeof typedObj === "object") || typeof typedObj === "function") && - (isCmsPeerReviewConfig(typedObj["peer_review_config"]) as boolean) && - Array.isArray(typedObj["peer_review_questions"]) && - typedObj["peer_review_questions"].every((e: any) => isCmsPeerReviewQuestion(e) as boolean) + (isCmsPeerOrSelfReviewConfig(typedObj["peer_or_self_review_config"]) as boolean) && + Array.isArray(typedObj["peer_or_self_review_questions"]) && + typedObj["peer_or_self_review_questions"].every( + (e: any) => isCmsPeerOrSelfReviewQuestion(e) as boolean, + ) ) } -export function isCourseMaterialPeerReviewConfig( +export function isCourseMaterialPeerOrSelfReviewConfig( obj: unknown, -): obj is CourseMaterialPeerReviewConfig { - const typedObj = obj as CourseMaterialPeerReviewConfig +): obj is CourseMaterialPeerOrSelfReviewConfig { + const typedObj = obj as CourseMaterialPeerOrSelfReviewConfig return ( ((typedObj !== null && typeof typedObj === "object") || typeof typedObj === "function") && typeof typedObj["id"] === "string" && @@ -2393,8 +2420,8 @@ export function isPeerReviewProcessingStrategy(obj: unknown): obj is PeerReviewP ) } -export function isPeerReviewConfig(obj: unknown): obj is PeerReviewConfig { - const typedObj = obj as PeerReviewConfig +export function isPeerOrSelfReviewConfig(obj: unknown): obj is PeerOrSelfReviewConfig { + const typedObj = obj as PeerOrSelfReviewConfig return ( ((typedObj !== null && typeof typedObj === "object") || typeof typedObj === "function") && typeof typedObj["id"] === "string" && @@ -2412,8 +2439,8 @@ export function isPeerReviewConfig(obj: unknown): obj is PeerReviewConfig { ) } -export function isPeerReviewSubmission(obj: unknown): obj is PeerReviewSubmission { - const typedObj = obj as PeerReviewSubmission +export function isPeerOrSelfReviewSubmission(obj: unknown): obj is PeerOrSelfReviewSubmission { + const typedObj = obj as PeerOrSelfReviewSubmission return ( ((typedObj !== null && typeof typedObj === "object") || typeof typedObj === "function") && typeof typedObj["id"] === "string" && @@ -2423,13 +2450,13 @@ export function isPeerReviewSubmission(obj: unknown): obj is PeerReviewSubmissio typeof typedObj["user_id"] === "string" && typeof typedObj["exercise_id"] === "string" && typeof typedObj["course_instance_id"] === "string" && - typeof typedObj["peer_review_config_id"] === "string" && + typeof typedObj["peer_or_self_review_config_id"] === "string" && typeof typedObj["exercise_slide_submission_id"] === "string" ) } -export function isPeerReviewAnswer(obj: unknown): obj is PeerReviewAnswer { - const typedObj = obj as PeerReviewAnswer +export function isPeerOrSelfReviewAnswer(obj: unknown): obj is PeerOrSelfReviewAnswer { + const typedObj = obj as PeerOrSelfReviewAnswer return ( (((typedObj !== null && typeof typedObj === "object") || typeof typedObj === "function") && typedObj["type"] === "no-answer") || @@ -2442,31 +2469,35 @@ export function isPeerReviewAnswer(obj: unknown): obj is PeerReviewAnswer { ) } -export function isPeerReviewQuestionAndAnswer(obj: unknown): obj is PeerReviewQuestionAndAnswer { - const typedObj = obj as PeerReviewQuestionAndAnswer +export function isPeerOrSelfReviewQuestionAndAnswer( + obj: unknown, +): obj is PeerOrSelfReviewQuestionAndAnswer { + const typedObj = obj as PeerOrSelfReviewQuestionAndAnswer return ( ((typedObj !== null && typeof typedObj === "object") || typeof typedObj === "function") && - typeof typedObj["peer_review_config_id"] === "string" && - typeof typedObj["peer_review_question_id"] === "string" && - typeof typedObj["peer_review_submission_id"] === "string" && + typeof typedObj["peer_or_self_review_config_id"] === "string" && + typeof typedObj["peer_or_self_review_question_id"] === "string" && + typeof typedObj["peer_or_self_review_submission_id"] === "string" && typeof typedObj["peer_review_question_submission_id"] === "string" && typeof typedObj["order_number"] === "number" && typeof typedObj["question"] === "string" && - (isPeerReviewAnswer(typedObj["answer"]) as boolean) && + (isPeerOrSelfReviewAnswer(typedObj["answer"]) as boolean) && typeof typedObj["answer_required"] === "boolean" ) } -export function isPeerReviewQuestionSubmission(obj: unknown): obj is PeerReviewQuestionSubmission { - const typedObj = obj as PeerReviewQuestionSubmission +export function isPeerOrSelfReviewQuestionSubmission( + obj: unknown, +): obj is PeerOrSelfReviewQuestionSubmission { + const typedObj = obj as PeerOrSelfReviewQuestionSubmission return ( ((typedObj !== null && typeof typedObj === "object") || typeof typedObj === "function") && typeof typedObj["id"] === "string" && typeof typedObj["created_at"] === "string" && typeof typedObj["updated_at"] === "string" && (typedObj["deleted_at"] === null || typeof typedObj["deleted_at"] === "string") && - typeof typedObj["peer_review_question_id"] === "string" && - typeof typedObj["peer_review_submission_id"] === "string" && + typeof typedObj["peer_or_self_review_question_id"] === "string" && + typeof typedObj["peer_or_self_review_submission_id"] === "string" && (typedObj["text_data"] === null || typeof typedObj["text_data"] === "string") && (typedObj["number_data"] === null || typeof typedObj["number_data"] === "number") ) @@ -2496,45 +2527,48 @@ export function isPeerReviewWithQuestionsAndAnswers( const typedObj = obj as PeerReviewWithQuestionsAndAnswers return ( ((typedObj !== null && typeof typedObj === "object") || typeof typedObj === "function") && - typeof typedObj["peer_review_submission_id"] === "string" && + typeof typedObj["peer_or_self_review_submission_id"] === "string" && + typeof typedObj["peer_review_giver_user_id"] === "string" && Array.isArray(typedObj["questions_and_answers"]) && - typedObj["questions_and_answers"].every((e: any) => isPeerReviewQuestionAndAnswer(e) as boolean) + typedObj["questions_and_answers"].every( + (e: any) => isPeerOrSelfReviewQuestionAndAnswer(e) as boolean, + ) ) } -export function isCmsPeerReviewQuestion(obj: unknown): obj is CmsPeerReviewQuestion { - const typedObj = obj as CmsPeerReviewQuestion +export function isCmsPeerOrSelfReviewQuestion(obj: unknown): obj is CmsPeerOrSelfReviewQuestion { + const typedObj = obj as CmsPeerOrSelfReviewQuestion return ( ((typedObj !== null && typeof typedObj === "object") || typeof typedObj === "function") && typeof typedObj["id"] === "string" && - typeof typedObj["peer_review_config_id"] === "string" && + typeof typedObj["peer_or_self_review_config_id"] === "string" && typeof typedObj["order_number"] === "number" && typeof typedObj["question"] === "string" && - (isPeerReviewQuestionType(typedObj["question_type"]) as boolean) && + (isPeerOrSelfReviewQuestionType(typedObj["question_type"]) as boolean) && typeof typedObj["answer_required"] === "boolean" && typeof typedObj["weight"] === "number" ) } -export function isPeerReviewQuestion(obj: unknown): obj is PeerReviewQuestion { - const typedObj = obj as PeerReviewQuestion +export function isPeerOrSelfReviewQuestion(obj: unknown): obj is PeerOrSelfReviewQuestion { + const typedObj = obj as PeerOrSelfReviewQuestion return ( ((typedObj !== null && typeof typedObj === "object") || typeof typedObj === "function") && typeof typedObj["id"] === "string" && typeof typedObj["created_at"] === "string" && typeof typedObj["updated_at"] === "string" && (typedObj["deleted_at"] === null || typeof typedObj["deleted_at"] === "string") && - typeof typedObj["peer_review_config_id"] === "string" && + typeof typedObj["peer_or_self_review_config_id"] === "string" && typeof typedObj["order_number"] === "number" && typeof typedObj["question"] === "string" && - (isPeerReviewQuestionType(typedObj["question_type"]) as boolean) && + (isPeerOrSelfReviewQuestionType(typedObj["question_type"]) as boolean) && typeof typedObj["answer_required"] === "boolean" && typeof typedObj["weight"] === "number" ) } -export function isPeerReviewQuestionType(obj: unknown): obj is PeerReviewQuestionType { - const typedObj = obj as PeerReviewQuestionType +export function isPeerOrSelfReviewQuestionType(obj: unknown): obj is PeerOrSelfReviewQuestionType { + const typedObj = obj as PeerOrSelfReviewQuestionType return typedObj === "Essay" || typedObj === "Scale" } @@ -3290,13 +3324,15 @@ export function isExamEnrollmentData(obj: unknown): obj is ExamEnrollmentData { ) } -export function isCourseMaterialPeerReviewDataWithToken( +export function isCourseMaterialPeerOrSelfReviewDataWithToken( obj: unknown, -): obj is CourseMaterialPeerReviewDataWithToken { - const typedObj = obj as CourseMaterialPeerReviewDataWithToken +): obj is CourseMaterialPeerOrSelfReviewDataWithToken { + const typedObj = obj as CourseMaterialPeerOrSelfReviewDataWithToken return ( ((typedObj !== null && typeof typedObj === "object") || typeof typedObj === "function") && - (isCourseMaterialPeerReviewData(typedObj["course_material_peer_review_data"]) as boolean) && + (isCourseMaterialPeerOrSelfReviewData( + typedObj["course_material_peer_or_self_review_data"], + ) as boolean) && (typedObj["token"] === null || typeof typedObj["token"] === "string") ) } diff --git a/shared-module/src/bindings.ts b/shared-module/src/bindings.ts index 8ef9c55e721a..7de9bb6205b6 100644 --- a/shared-module/src/bindings.ts +++ b/shared-module/src/bindings.ts @@ -297,6 +297,7 @@ export interface CourseModuleCompletionWithRegistrationInfo { prerequisite_modules_completed: boolean registered: boolean user_id: string + completion_date: string } export interface CourseModuleCompletion { @@ -649,9 +650,10 @@ export interface ExerciseSlideSubmissionInfo { exercise_slide_submission: ExerciseSlideSubmission } -export interface PeerReviewsRecieved { - peer_review_questions: Array - peer_review_question_submissions: Array +export interface PeerOrSelfReviewsReceived { + peer_or_self_review_questions: Array + peer_or_self_review_question_submissions: Array + peer_or_self_review_submissions: Array } export interface CourseMaterialExerciseSlide { @@ -752,7 +754,7 @@ export interface CourseMaterialExercise { current_exercise_slide: CourseMaterialExerciseSlide exercise_status: ExerciseStatus | null exercise_slide_submission_counts: Record - peer_review_config: CourseMaterialPeerReviewConfig | null + peer_or_self_review_config: CourseMaterialPeerOrSelfReviewConfig | null previous_exercise_slide_submission: ExerciseSlideSubmission | null user_course_instance_exercise_service_variables: Array } @@ -774,7 +776,8 @@ export interface Exercise { max_tries_per_slide: number | null limit_number_of_tries: boolean needs_peer_review: boolean - use_course_default_peer_review_config: boolean + needs_self_review: boolean + use_course_default_peer_or_self_review_config: boolean exercise_language_group_id: string | null } @@ -789,13 +792,13 @@ export interface ExerciseStatusSummaryForUser { exercise: Exercise user_exercise_state: UserExerciseState | null exercise_slide_submissions: Array - given_peer_review_submissions: Array - given_peer_review_question_submissions: Array - received_peer_review_submissions: Array - received_peer_review_question_submissions: Array + given_peer_or_self_review_submissions: Array + given_peer_or_self_review_question_submissions: Array + received_peer_or_self_review_submissions: Array + received_peer_or_self_review_question_submissions: Array peer_review_queue_entry: PeerReviewQueueEntry | null teacher_grading_decision: TeacherGradingDecision | null - peer_review_questions: Array + peer_or_self_review_questions: Array } export interface GlobalStatEntry { @@ -887,7 +890,7 @@ export interface AnswerRequiringAttentionWithTasks { exercise_id: string tasks: Array given_peer_reviews: Array - received_peer_reviews: Array + received_peer_or_self_reviews: Array } export interface AnswersRequiringAttention { @@ -919,28 +922,28 @@ export interface StudentExerciseTaskSubmissionResult { exercise_task_exercise_service_slug: string } -export interface CourseMaterialPeerReviewData { - answer_to_review: CourseMaterialPeerReviewDataAnswerToReview | null - peer_review_config: PeerReviewConfig - peer_review_questions: Array +export interface CourseMaterialPeerOrSelfReviewData { + answer_to_review: CourseMaterialPeerOrSelfReviewDataAnswerToReview | null + peer_or_self_review_config: PeerOrSelfReviewConfig + peer_or_self_review_questions: Array num_peer_reviews_given: number } -export interface CourseMaterialPeerReviewDataAnswerToReview { +export interface CourseMaterialPeerOrSelfReviewDataAnswerToReview { exercise_slide_submission_id: string course_material_exercise_tasks: Array } -export interface CourseMaterialPeerReviewQuestionAnswer { - peer_review_question_id: string +export interface CourseMaterialPeerOrSelfReviewQuestionAnswer { + peer_or_self_review_question_id: string text_data: string | null number_data: number | null } -export interface CourseMaterialPeerReviewSubmission { +export interface CourseMaterialPeerOrSelfReviewSubmission { exercise_slide_submission_id: string - peer_review_config_id: string - peer_review_question_answers: Array + peer_or_self_review_config_id: string + peer_review_question_answers: Array token: string } @@ -1100,9 +1103,10 @@ export interface CmsPageExercise { limit_number_of_tries: boolean deadline: string | null needs_peer_review: boolean - peer_review_config: CmsPeerReviewConfig | null - peer_review_questions: Array | null - use_course_default_peer_review_config: boolean + needs_self_review: boolean + peer_or_self_review_config: CmsPeerOrSelfReviewConfig | null + peer_or_self_review_questions: Array | null + use_course_default_peer_or_self_review_config: boolean } export interface CmsPageExerciseSlide { @@ -1135,8 +1139,8 @@ export interface ContentManagementPage { exercises: Array exercise_slides: Array exercise_tasks: Array - peer_review_configs: Array - peer_review_questions: Array + peer_or_self_review_configs: Array + peer_or_self_review_questions: Array organization_id: string } @@ -1270,7 +1274,7 @@ export interface PageDetailsUpdate { url_path: string } -export interface CmsPeerReviewConfig { +export interface CmsPeerOrSelfReviewConfig { id: string course_id: string exercise_id: string | null @@ -1279,14 +1283,15 @@ export interface CmsPeerReviewConfig { accepting_threshold: number processing_strategy: PeerReviewProcessingStrategy points_are_all_or_nothing: boolean + review_instructions: unknown | null } -export interface CmsPeerReviewConfiguration { - peer_review_config: CmsPeerReviewConfig - peer_review_questions: Array +export interface CmsPeerOrSelfReviewConfiguration { + peer_or_self_review_config: CmsPeerOrSelfReviewConfig + peer_or_self_review_questions: Array } -export interface CourseMaterialPeerReviewConfig { +export interface CourseMaterialPeerOrSelfReviewConfig { id: string course_id: string exercise_id: string | null @@ -1299,7 +1304,7 @@ export type PeerReviewProcessingStrategy = | "AutomaticallyGradeOrManualReviewByAverage" | "ManualReviewEverything" -export interface PeerReviewConfig { +export interface PeerOrSelfReviewConfig { id: string created_at: string updated_at: string @@ -1312,9 +1317,10 @@ export interface PeerReviewConfig { processing_strategy: PeerReviewProcessingStrategy manual_review_cutoff_in_days: number points_are_all_or_nothing: boolean + review_instructions: unknown | null } -export interface PeerReviewSubmission { +export interface PeerOrSelfReviewSubmission { id: string created_at: string updated_at: string @@ -1322,33 +1328,33 @@ export interface PeerReviewSubmission { user_id: string exercise_id: string course_instance_id: string - peer_review_config_id: string + peer_or_self_review_config_id: string exercise_slide_submission_id: string } -export type PeerReviewAnswer = +export type PeerOrSelfReviewAnswer = | { type: "no-answer" } | { type: "essay"; value: string } | { type: "scale"; value: number } -export interface PeerReviewQuestionAndAnswer { - peer_review_config_id: string - peer_review_question_id: string - peer_review_submission_id: string +export interface PeerOrSelfReviewQuestionAndAnswer { + peer_or_self_review_config_id: string + peer_or_self_review_question_id: string + peer_or_self_review_submission_id: string peer_review_question_submission_id: string order_number: number question: string - answer: PeerReviewAnswer + answer: PeerOrSelfReviewAnswer answer_required: boolean } -export interface PeerReviewQuestionSubmission { +export interface PeerOrSelfReviewQuestionSubmission { id: string created_at: string updated_at: string deleted_at: string | null - peer_review_question_id: string - peer_review_submission_id: string + peer_or_self_review_question_id: string + peer_or_self_review_submission_id: string text_data: string | null number_data: number | null } @@ -1368,34 +1374,35 @@ export interface PeerReviewQueueEntry { } export interface PeerReviewWithQuestionsAndAnswers { - peer_review_submission_id: string - questions_and_answers: Array + peer_or_self_review_submission_id: string + peer_review_giver_user_id: string + questions_and_answers: Array } -export interface CmsPeerReviewQuestion { +export interface CmsPeerOrSelfReviewQuestion { id: string - peer_review_config_id: string + peer_or_self_review_config_id: string order_number: number question: string - question_type: PeerReviewQuestionType + question_type: PeerOrSelfReviewQuestionType answer_required: boolean weight: number } -export interface PeerReviewQuestion { +export interface PeerOrSelfReviewQuestion { id: string created_at: string updated_at: string deleted_at: string | null - peer_review_config_id: string + peer_or_self_review_config_id: string order_number: number question: string - question_type: PeerReviewQuestionType + question_type: PeerOrSelfReviewQuestionType answer_required: boolean weight: number } -export type PeerReviewQuestionType = "Essay" | "Scale" +export type PeerOrSelfReviewQuestionType = "Essay" | "Scale" export interface PendingRole { id: string @@ -1866,8 +1873,8 @@ export type ExamEnrollmentData = | { tag: "NotYetStarted" } | { tag: "StudentTimeUp" } -export interface CourseMaterialPeerReviewDataWithToken { - course_material_peer_review_data: CourseMaterialPeerReviewData +export interface CourseMaterialPeerOrSelfReviewDataWithToken { + course_material_peer_or_self_review_data: CourseMaterialPeerOrSelfReviewData token: string | null } diff --git a/shared-module/src/components/MessageChannelIFrame.tsx b/shared-module/src/components/MessageChannelIFrame.tsx index e772dfa00499..b38e6ee2d6da 100644 --- a/shared-module/src/components/MessageChannelIFrame.tsx +++ b/shared-module/src/components/MessageChannelIFrame.tsx @@ -25,6 +25,7 @@ interface MessageChannelIFrameProps { title: string showBorders?: boolean disableSandbox?: boolean + headingBeforeIframe?: string } // const IFRAME_TITLE = "Exercise type specific content" @@ -35,7 +36,7 @@ const MessageChannelIFrame: React.FC< url, postThisStateToIFrame, onMessageFromIframe, - + headingBeforeIframe, title, showBorders = false, disableSandbox = false, @@ -199,6 +200,17 @@ const MessageChannelIFrame: React.FC< border-radius: 0.625rem; `} > + {headingBeforeIframe && ( +

+ {headingBeforeIframe} +

+ )}