feat: negative marking in quiz
This commit is contained in:
@@ -2,7 +2,7 @@
|
|||||||
<Dialog
|
<Dialog
|
||||||
v-model="show"
|
v-model="show"
|
||||||
:options="{
|
:options="{
|
||||||
size: '3xl',
|
size: '5xl',
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
<template #body>
|
<template #body>
|
||||||
@@ -21,7 +21,7 @@
|
|||||||
class="!p-0"
|
class="!p-0"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="!chooseFromExisting || editMode" class="space-y-2">
|
<div v-if="!chooseFromExisting || editMode">
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-xs text-ink-gray-5 mb-1">
|
<label class="block text-xs text-ink-gray-5 mb-1">
|
||||||
{{ __('Question') }}
|
{{ __('Question') }}
|
||||||
@@ -34,7 +34,7 @@
|
|||||||
editorClass="prose-sm max-w-none border-b border-x bg-surface-gray-2 rounded-b-md py-1 px-2 min-h-[7rem]"
|
editorClass="prose-sm max-w-none border-b border-x bg-surface-gray-2 rounded-b-md py-1 px-2 min-h-[7rem]"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="grid grid-cols-2 gap-4">
|
<div class="grid grid-cols-2 gap-8 mt-4">
|
||||||
<FormControl
|
<FormControl
|
||||||
v-model="question.marks"
|
v-model="question.marks"
|
||||||
:label="__('Marks')"
|
:label="__('Marks')"
|
||||||
@@ -51,7 +51,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="question.type == 'Choices'"
|
v-if="question.type == 'Choices'"
|
||||||
class="text-base font-semibold text-ink-gray-9 mb-5 mt-5"
|
class="text-base font-semibold text-ink-gray-9 mb-5 mt-10"
|
||||||
>
|
>
|
||||||
{{ __('Options') }}
|
{{ __('Options') }}
|
||||||
</div>
|
</div>
|
||||||
@@ -61,7 +61,10 @@
|
|||||||
>
|
>
|
||||||
{{ __('Possibilities') }}
|
{{ __('Possibilities') }}
|
||||||
</div>
|
</div>
|
||||||
<div v-if="question.type == 'Choices'" class="grid grid-cols-2 gap-4">
|
<div
|
||||||
|
v-if="question.type == 'Choices'"
|
||||||
|
class="grid grid-cols-2 gap-x-8 gap-y-4"
|
||||||
|
>
|
||||||
<div v-for="n in 4" class="space-y-4 py-2">
|
<div v-for="n in 4" class="space-y-4 py-2">
|
||||||
<FormControl
|
<FormControl
|
||||||
:label="__('Option') + ' ' + n"
|
:label="__('Option') + ' ' + n"
|
||||||
@@ -81,7 +84,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-else-if="question.type == 'User Input'"
|
v-else-if="question.type == 'User Input'"
|
||||||
class="grid grid-cols-2 gap-4 py-2"
|
class="grid grid-cols-2 gap-x-8 gap-y-4 py-2"
|
||||||
>
|
>
|
||||||
<div v-for="n in 4">
|
<div v-for="n in 4">
|
||||||
<FormControl
|
<FormControl
|
||||||
@@ -106,7 +109,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="flex items-center justify-end space-x-2 mt-5">
|
<div class="flex items-center justify-end space-x-2 mt-5">
|
||||||
<Button variant="solid" @click="submitQuestion()">
|
<Button variant="solid" @click="submitQuestion()">
|
||||||
{{ __('Submit') }}
|
{{ __('Save') }}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -217,7 +220,7 @@ const questionRow = createResource({
|
|||||||
return {
|
return {
|
||||||
doc: {
|
doc: {
|
||||||
doctype: 'LMS Quiz Question',
|
doctype: 'LMS Quiz Question',
|
||||||
parent: quiz.value.data.name,
|
parent: quiz.value.doc.name,
|
||||||
parentfield: 'questions',
|
parentfield: 'questions',
|
||||||
parenttype: 'LMS Quiz',
|
parenttype: 'LMS Quiz',
|
||||||
...values,
|
...values,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div v-if="quiz.data">
|
<div v-if="quiz.data">
|
||||||
<div
|
<div
|
||||||
class="bg-surface-blue-2 space-y-1 py-2 px-2 mb-4 rounded-md text-sm text-ink-blue-3 leading-5"
|
class="bg-surface-blue-2 space-y-2 py-2 px-3 mb-4 rounded-md text-sm text-ink-blue-2 leading-5"
|
||||||
>
|
>
|
||||||
<div v-if="inVideo">
|
<div v-if="inVideo">
|
||||||
{{ __('You will have to complete the quiz to continue the video') }}
|
{{ __('You will have to complete the quiz to continue the video') }}
|
||||||
@@ -41,6 +41,16 @@
|
|||||||
)
|
)
|
||||||
}}
|
}}
|
||||||
</div>
|
</div>
|
||||||
|
<div v-if="quiz.data.enable_negative_marking" class="leading-5">
|
||||||
|
{{
|
||||||
|
__(
|
||||||
|
'If you answer incorrectly, {0} {1} will be deducted from your score for each incorrect answer.'
|
||||||
|
).format(
|
||||||
|
quiz.data.marks_to_cut,
|
||||||
|
quiz.data.marks_to_cut == 1 ? 'mark' : 'marks'
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="quiz.data.duration" class="flex flex-col space-x-1 my-4">
|
<div v-if="quiz.data.duration" class="flex flex-col space-x-1 my-4">
|
||||||
|
|||||||
@@ -57,22 +57,24 @@
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col space-y-4 py-5 border-b">
|
<div class="flex flex-col space-y-4 pt-5 border-b">
|
||||||
<Code
|
<Code
|
||||||
v-model="code"
|
v-model="code"
|
||||||
:language="exercise.doc?.language.toLowerCase()"
|
:language="exercise.doc?.language.toLowerCase()"
|
||||||
height="400px"
|
height="400px"
|
||||||
maxHeight="1000px"
|
maxHeight="1000px"
|
||||||
/>
|
/>
|
||||||
<span v-if="error" class="text-xs text-ink-gray-5 px-2">
|
<div class="flex flex-col space-y-1">
|
||||||
{{ __('Compiler Message') }}:
|
<span v-if="error" class="text-xs text-ink-gray-5 px-1">
|
||||||
</span>
|
{{ __('Compiler Message') }}:
|
||||||
<textarea
|
</span>
|
||||||
v-if="error"
|
<textarea
|
||||||
v-model="errorMessage"
|
v-if="error"
|
||||||
class="bg-surface-gray-1 border-none text-sm h-32 leading-6"
|
v-model="errorMessage"
|
||||||
readonly
|
class="font-mono text-ink-red-3 bg-surface-gray-1 border-none text-sm h-32 leading-6"
|
||||||
/>
|
readonly
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<!-- <textarea v-else v-model="output" class="bg-surface-gray-1 border-none text-sm h-28 leading-6" readonly /> -->
|
<!-- <textarea v-else v-model="output" class="bg-surface-gray-1 border-none text-sm h-28 leading-6" readonly /> -->
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -93,6 +93,12 @@
|
|||||||
{{ row[column.key] }}
|
{{ row[column.key] }}
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
|
<div
|
||||||
|
v-else-if="column.key == 'modified'"
|
||||||
|
class="text-sm text-ink-gray-5"
|
||||||
|
>
|
||||||
|
{{ row[column.key] }}
|
||||||
|
</div>
|
||||||
<div v-else>
|
<div v-else>
|
||||||
{{ row[column.key] }}
|
{{ row[column.key] }}
|
||||||
</div>
|
</div>
|
||||||
@@ -261,7 +267,7 @@ const submissionColumns = computed(() => {
|
|||||||
{
|
{
|
||||||
label: __('Member'),
|
label: __('Member'),
|
||||||
key: 'member_name',
|
key: 'member_name',
|
||||||
width: '20%',
|
width: '30%',
|
||||||
icon: 'user',
|
icon: 'user',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -279,7 +285,7 @@ const submissionColumns = computed(() => {
|
|||||||
{
|
{
|
||||||
label: __('Modified'),
|
label: __('Modified'),
|
||||||
key: 'modified',
|
key: 'modified',
|
||||||
width: '20%',
|
width: '15%',
|
||||||
icon: 'clock',
|
icon: 'clock',
|
||||||
align: 'right',
|
align: 'right',
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -4,30 +4,39 @@
|
|||||||
>
|
>
|
||||||
<Breadcrumbs :items="breadcrumbs" />
|
<Breadcrumbs :items="breadcrumbs" />
|
||||||
<div v-if="!readOnlyMode" class="space-x-2">
|
<div v-if="!readOnlyMode" class="space-x-2">
|
||||||
|
<Badge v-if="quizDetails.isDirty" theme="orange">
|
||||||
|
{{ __('Not Saved') }}
|
||||||
|
</Badge>
|
||||||
<router-link
|
<router-link
|
||||||
v-if="quizDetails.data?.name"
|
v-if="quizDetails.doc?.name"
|
||||||
:to="{
|
:to="{
|
||||||
name: 'QuizPage',
|
name: 'QuizPage',
|
||||||
params: {
|
params: {
|
||||||
quizID: quizDetails.data.name,
|
quizID: quizDetails.doc.name,
|
||||||
},
|
},
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
<Button>
|
<Button>
|
||||||
{{ __('Open') }}
|
<template #prefix>
|
||||||
|
<ListChecks class="size-4 stroke-1.5" />
|
||||||
|
</template>
|
||||||
|
{{ __('Test Quiz') }}
|
||||||
</Button>
|
</Button>
|
||||||
</router-link>
|
</router-link>
|
||||||
<router-link
|
<router-link
|
||||||
v-if="quizDetails.data?.name"
|
v-if="quizDetails.doc?.name"
|
||||||
:to="{
|
:to="{
|
||||||
name: 'QuizSubmissionList',
|
name: 'QuizSubmissionList',
|
||||||
params: {
|
params: {
|
||||||
quizID: quizDetails.data.name,
|
quizID: quizDetails.doc.name,
|
||||||
},
|
},
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
<Button>
|
<Button>
|
||||||
{{ __('Submission List') }}
|
<template #prefix>
|
||||||
|
<ClipboardList class="size-4 stroke-1.5" />
|
||||||
|
</template>
|
||||||
|
{{ __('Check Submissions') }}
|
||||||
</Button>
|
</Button>
|
||||||
</router-link>
|
</router-link>
|
||||||
<Button variant="solid" @click="submitQuiz()">
|
<Button variant="solid" @click="submitQuiz()">
|
||||||
@@ -35,144 +44,152 @@
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
<div class="w-3/4 mx-auto py-5">
|
<div v-if="quizDetails.doc" class="py-5">
|
||||||
<!-- Details -->
|
<div class="px-20 pb-5 space-y-5 border-b mb-5">
|
||||||
<div class="mb-8">
|
<div class="text-lg text-ink-gray-9 font-semibold mb-4">
|
||||||
<div class="font-semibold text-ink-gray-9 mb-4">
|
|
||||||
{{ __('Details') }}
|
{{ __('Details') }}
|
||||||
</div>
|
</div>
|
||||||
<FormControl
|
<div class="grid grid-cols-2 gap-5">
|
||||||
v-model="quiz.title"
|
<div class="space-y-5">
|
||||||
:label="
|
<FormControl
|
||||||
quizDetails.data?.name
|
v-model="quizDetails.doc.title"
|
||||||
? __('Title')
|
:label="__('Title')"
|
||||||
: __('Enter a title and save the quiz to proceed')
|
:required="true"
|
||||||
"
|
/>
|
||||||
:required="true"
|
|
||||||
/>
|
|
||||||
<div v-if="quizDetails.data?.name">
|
|
||||||
<div class="grid grid-cols-2 gap-5 mt-4 mb-8">
|
|
||||||
<FormControl
|
<FormControl
|
||||||
type="number"
|
type="number"
|
||||||
v-model="quiz.max_attempts"
|
v-model="quizDetails.doc.max_attempts"
|
||||||
:label="__('Maximum Attempts')"
|
:label="__('Maximum Attempts')"
|
||||||
/>
|
/>
|
||||||
<FormControl
|
<FormControl
|
||||||
type="number"
|
type="number"
|
||||||
v-model="quiz.duration"
|
v-model="quizDetails.doc.duration"
|
||||||
:label="__('Duration (in minutes)')"
|
:label="__('Duration (in minutes)')"
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-5">
|
||||||
<FormControl
|
<FormControl
|
||||||
v-model="quiz.total_marks"
|
v-model="quizDetails.doc.total_marks"
|
||||||
:label="__('Total Marks')"
|
:label="__('Total Marks')"
|
||||||
disabled
|
disabled
|
||||||
/>
|
/>
|
||||||
<FormControl
|
<FormControl
|
||||||
v-model="quiz.passing_percentage"
|
v-model="quizDetails.doc.passing_percentage"
|
||||||
:label="__('Passing Percentage')"
|
:label="__('Passing Percentage')"
|
||||||
|
:required="true"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<!-- Settings -->
|
</div>
|
||||||
<div class="mb-8">
|
<div class="px-20 pb-5 space-y-5 border-b mb-5">
|
||||||
<div class="font-semibold text-ink-gray-9 mb-4">
|
<div class="text-lg text-ink-gray-9 font-semibold mb-4">
|
||||||
{{ __('Settings') }}
|
{{ __('Settings') }}
|
||||||
</div>
|
</div>
|
||||||
<div class="grid grid-cols-3 gap-5 my-4">
|
<div class="grid grid-cols-3 gap-5">
|
||||||
<FormControl
|
<div class="flex flex-col space-y-10">
|
||||||
v-model="quiz.show_answers"
|
<FormControl
|
||||||
type="checkbox"
|
v-model="quizDetails.doc.show_answers"
|
||||||
:label="__('Show Answers')"
|
type="checkbox"
|
||||||
/>
|
:label="__('Show Answers')"
|
||||||
<FormControl
|
/>
|
||||||
v-model="quiz.show_submission_history"
|
<FormControl
|
||||||
type="checkbox"
|
v-model="quizDetails.doc.show_submission_history"
|
||||||
:label="__('Show Submission History')"
|
type="checkbox"
|
||||||
/>
|
:label="__('Show Submission History')"
|
||||||
</div>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="flex flex-col space-y-5">
|
||||||
<div class="mb-8">
|
<FormControl
|
||||||
<div class="font-semibold text-ink-gray-9 mb-4">
|
v-model="quizDetails.doc.shuffle_questions"
|
||||||
{{ __('Shuffle Settings') }}
|
type="checkbox"
|
||||||
</div>
|
:label="__('Shuffle Questions')"
|
||||||
<div class="grid grid-cols-3">
|
/>
|
||||||
<FormControl
|
<FormControl
|
||||||
v-model="quiz.shuffle_questions"
|
v-if="quizDetails.doc.shuffle_questions"
|
||||||
type="checkbox"
|
v-model="quizDetails.doc.limit_questions_to"
|
||||||
:label="__('Shuffle Questions')"
|
:label="__('Limit Questions To')"
|
||||||
/>
|
/>
|
||||||
<FormControl
|
</div>
|
||||||
v-if="quiz.shuffle_questions"
|
<div class="flex flex-col space-y-5">
|
||||||
v-model="quiz.limit_questions_to"
|
<FormControl
|
||||||
:label="__('Limit Questions To')"
|
v-model="quizDetails.doc.enable_negative_marking"
|
||||||
/>
|
type="checkbox"
|
||||||
</div>
|
:label="__('Enable Negative Marking')"
|
||||||
</div>
|
/>
|
||||||
|
<FormControl
|
||||||
<!-- Questions -->
|
v-if="quizDetails.doc.enable_negative_marking"
|
||||||
<div>
|
v-model="quizDetails.doc.marks_to_cut"
|
||||||
<div class="flex items-center justify-between mb-4">
|
:label="__('Marks to Cut')"
|
||||||
<div class="font-semibold text-ink-gray-9">
|
/>
|
||||||
{{ __('Questions') }}
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Button v-if="!readOnlyMode" @click="openQuestionModal()">
|
</div>
|
||||||
<template #prefix>
|
|
||||||
<Plus class="w-4 h-4" />
|
<div class="px-20 pb-5 space-y-5 mb-5">
|
||||||
</template>
|
<div class="flex items-center justify-between mb-4">
|
||||||
{{ __('New Question') }}
|
<div class="text-lg font-semibold text-ink-gray-9">
|
||||||
</Button>
|
{{ __('Questions') }}
|
||||||
</div>
|
</div>
|
||||||
<ListView
|
<Button v-if="!readOnlyMode" @click="openQuestionModal()">
|
||||||
:columns="questionColumns"
|
<template #prefix>
|
||||||
:rows="quiz.questions"
|
<Plus class="w-4 h-4" />
|
||||||
row-key="name"
|
</template>
|
||||||
:options="{
|
{{ __('New Question') }}
|
||||||
showTooltip: false,
|
</Button>
|
||||||
}"
|
</div>
|
||||||
>
|
<ListView
|
||||||
<ListHeader
|
v-if="questions.length"
|
||||||
class="mb-2 grid items-center space-x-4 rounded bg-surface-gray-2 p-2"
|
:columns="questionColumns"
|
||||||
>
|
:rows="questions"
|
||||||
<ListHeaderItem :item="item" v-for="item in questionColumns" />
|
row-key="name"
|
||||||
</ListHeader>
|
:options="{
|
||||||
<ListRows>
|
showTooltip: false,
|
||||||
<ListRow
|
}"
|
||||||
:row="row"
|
>
|
||||||
v-slot="{ idx, column, item }"
|
<ListHeader
|
||||||
v-for="row in quiz.questions"
|
class="mb-2 grid items-center space-x-4 rounded bg-surface-gray-2 p-2"
|
||||||
@click="openQuestionModal(row)"
|
>
|
||||||
class="cursor-pointer"
|
<ListHeaderItem :item="item" v-for="item in questionColumns" />
|
||||||
>
|
</ListHeader>
|
||||||
<ListRowItem :item="item">
|
<ListRows>
|
||||||
<div
|
<ListRow
|
||||||
v-if="column.key == 'question_detail'"
|
:row="row"
|
||||||
class="text-xs truncate h-4"
|
v-slot="{ idx, column, item }"
|
||||||
v-html="item"
|
v-for="row in questions"
|
||||||
></div>
|
@click="openQuestionModal(row)"
|
||||||
<div v-else class="text-xs">
|
class="cursor-pointer"
|
||||||
{{ item }}
|
>
|
||||||
</div>
|
<ListRowItem :item="item">
|
||||||
</ListRowItem>
|
<div
|
||||||
</ListRow>
|
v-if="column.key == 'question_detail'"
|
||||||
</ListRows>
|
class="text-xs truncate h-4"
|
||||||
<ListSelectBanner>
|
v-html="item"
|
||||||
<template #actions="{ unselectAll, selections }">
|
></div>
|
||||||
<div class="flex gap-2">
|
<div v-else class="text-xs">
|
||||||
<Button
|
{{ item }}
|
||||||
variant="ghost"
|
</div>
|
||||||
@click="deleteQuestions(selections, unselectAll)"
|
</ListRowItem>
|
||||||
>
|
</ListRow>
|
||||||
<Trash2 class="h-4 w-4 stroke-1.5" />
|
</ListRows>
|
||||||
</Button>
|
<ListSelectBanner>
|
||||||
</div>
|
<template #actions="{ unselectAll, selections }">
|
||||||
</template>
|
<div class="flex gap-2">
|
||||||
</ListSelectBanner>
|
<Button
|
||||||
</ListView>
|
variant="ghost"
|
||||||
</div>
|
@click="deleteQuestions(selections, unselectAll)"
|
||||||
|
>
|
||||||
|
<Trash2 class="h-4 w-4 stroke-1.5" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</ListSelectBanner>
|
||||||
|
</ListView>
|
||||||
|
<div v-else class="text-ink-gray-6 text-sm">
|
||||||
|
{{ __('No questions added yet') }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Question
|
<Question
|
||||||
v-model="showQuestionModal"
|
v-model="showQuestionModal"
|
||||||
:questionDetail="currentQuestion"
|
:questionDetail="currentQuestion"
|
||||||
@@ -199,6 +216,8 @@ import {
|
|||||||
Button,
|
Button,
|
||||||
usePageMeta,
|
usePageMeta,
|
||||||
toast,
|
toast,
|
||||||
|
createDocumentResource,
|
||||||
|
Badge,
|
||||||
} from 'frappe-ui'
|
} from 'frappe-ui'
|
||||||
import {
|
import {
|
||||||
computed,
|
computed,
|
||||||
@@ -210,8 +229,7 @@ import {
|
|||||||
watch,
|
watch,
|
||||||
} from 'vue'
|
} from 'vue'
|
||||||
import { sessionStore } from '../stores/session'
|
import { sessionStore } from '../stores/session'
|
||||||
import { Plus, Trash2 } from 'lucide-vue-next'
|
import { ClipboardList, ListChecks, Plus, Trash2 } from 'lucide-vue-next'
|
||||||
import { updateDocumentTitle } from '@/utils'
|
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import Question from '@/components/Modals/Question.vue'
|
import Question from '@/components/Modals/Question.vue'
|
||||||
|
|
||||||
@@ -233,18 +251,7 @@ const props = defineProps({
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const quiz = reactive({
|
const questions = ref([])
|
||||||
title: '',
|
|
||||||
total_marks: 0,
|
|
||||||
passing_percentage: 0,
|
|
||||||
max_attempts: 0,
|
|
||||||
duration: 0,
|
|
||||||
limit_questions_to: 0,
|
|
||||||
show_answers: true,
|
|
||||||
show_submission_history: false,
|
|
||||||
shuffle_questions: false,
|
|
||||||
questions: [],
|
|
||||||
})
|
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
if (
|
if (
|
||||||
@@ -280,86 +287,26 @@ watch(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
const quizDetails = createResource({
|
const quizDetails = createDocumentResource({
|
||||||
url: 'frappe.client.get',
|
doctype: 'LMS Quiz',
|
||||||
makeParams(values) {
|
name: props.quizID,
|
||||||
return { doctype: 'LMS Quiz', name: props.quizID }
|
|
||||||
},
|
|
||||||
auto: false,
|
auto: false,
|
||||||
onSuccess(data) {
|
onSuccess(doc) {
|
||||||
Object.keys(data).forEach((key) => {
|
if (doc.questions && doc.questions.length > 0) {
|
||||||
if (Object.hasOwn(quiz, key)) quiz[key] = data[key]
|
questions.value = doc.questions.map((question) => question)
|
||||||
})
|
|
||||||
|
|
||||||
let checkboxes = [
|
|
||||||
'show_answers',
|
|
||||||
'show_submission_history',
|
|
||||||
'shuffle_questions',
|
|
||||||
]
|
|
||||||
for (let idx in checkboxes) {
|
|
||||||
let key = checkboxes[idx]
|
|
||||||
quiz[key] = quiz[key] ? true : false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
const quizCreate = createResource({
|
|
||||||
url: 'frappe.client.insert',
|
|
||||||
auto: false,
|
|
||||||
makeParams(values) {
|
|
||||||
return {
|
|
||||||
doc: {
|
|
||||||
doctype: 'LMS Quiz',
|
|
||||||
...quiz,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
const quizUpdate = createResource({
|
|
||||||
url: 'frappe.client.set_value',
|
|
||||||
auto: false,
|
|
||||||
makeParams(values) {
|
|
||||||
return {
|
|
||||||
doctype: 'LMS Quiz',
|
|
||||||
name: values.quizID,
|
|
||||||
fieldname: {
|
|
||||||
total_marks: calculateTotalMarks(),
|
|
||||||
...quiz,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const submitQuiz = () => {
|
const submitQuiz = () => {
|
||||||
if (quizDetails.data?.name) updateQuiz()
|
quizDetails.setValue.submit(
|
||||||
else createQuiz()
|
{
|
||||||
}
|
...quizDetails.doc,
|
||||||
|
total_marks: calculateTotalMarks(),
|
||||||
const createQuiz = () => {
|
},
|
||||||
quizCreate.submit(
|
|
||||||
{},
|
|
||||||
{
|
{
|
||||||
onSuccess(data) {
|
onSuccess(data) {
|
||||||
toast.success(__('Quiz created successfully'))
|
quizDetails.doc.total_marks = data.total_marks
|
||||||
router.push({
|
|
||||||
name: 'QuizForm',
|
|
||||||
params: { quizID: data.name },
|
|
||||||
})
|
|
||||||
},
|
|
||||||
onError(err) {
|
|
||||||
toast.error(err.messages?.[0] || err)
|
|
||||||
},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const updateQuiz = () => {
|
|
||||||
quizUpdate.submit(
|
|
||||||
{ quizID: quizDetails.data?.name },
|
|
||||||
{
|
|
||||||
onSuccess(data) {
|
|
||||||
quiz.total_marks = data.total_marks
|
|
||||||
toast.success(__('Quiz updated successfully'))
|
toast.success(__('Quiz updated successfully'))
|
||||||
},
|
},
|
||||||
onError(err) {
|
onError(err) {
|
||||||
@@ -371,9 +318,15 @@ const updateQuiz = () => {
|
|||||||
|
|
||||||
const calculateTotalMarks = () => {
|
const calculateTotalMarks = () => {
|
||||||
let totalMarks = 0
|
let totalMarks = 0
|
||||||
if (quiz.limit_questions_to && quiz.questions.length > 0)
|
if (
|
||||||
return quiz.questions[0].marks * quiz.limit_questions_to
|
quizDetails.doc?.limit_questions_to &&
|
||||||
quiz.questions.forEach((question) => {
|
quizDetails.doc?.questions.length > 0
|
||||||
|
)
|
||||||
|
return (
|
||||||
|
quizDetails.doc.questions[0].marks * quizDetails.doc.limit_questions_to
|
||||||
|
)
|
||||||
|
|
||||||
|
quizDetails.doc?.questions.forEach((question) => {
|
||||||
totalMarks += question.marks
|
totalMarks += question.marks
|
||||||
})
|
})
|
||||||
return totalMarks
|
return totalMarks
|
||||||
@@ -448,7 +401,7 @@ const breadcrumbs = computed(() => {
|
|||||||
]
|
]
|
||||||
|
|
||||||
crumbs.push({
|
crumbs.push({
|
||||||
label: props.quizID == 'new' ? __('New Quiz') : quizDetails.data?.title,
|
label: props.quizID == 'new' ? __('New Quiz') : quizDetails.doc?.title,
|
||||||
route: { name: 'QuizForm', params: { quizID: props.quizID } },
|
route: { name: 'QuizForm', params: { quizID: props.quizID } },
|
||||||
})
|
})
|
||||||
return crumbs
|
return crumbs
|
||||||
@@ -456,7 +409,7 @@ const breadcrumbs = computed(() => {
|
|||||||
|
|
||||||
usePageMeta(() => {
|
usePageMeta(() => {
|
||||||
return {
|
return {
|
||||||
title: props.quizID == 'new' ? __('New Quiz') : quizDetails.data?.title,
|
title: props.quizID == 'new' ? __('New Quiz') : quizDetails.doc?.title,
|
||||||
icon: brand.favicon,
|
icon: brand.favicon,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -3,37 +3,42 @@
|
|||||||
class="sticky top-0 z-10 flex items-center justify-between border-b bg-surface-white px-3 py-2.5 sm:px-5"
|
class="sticky top-0 z-10 flex items-center justify-between border-b bg-surface-white px-3 py-2.5 sm:px-5"
|
||||||
>
|
>
|
||||||
<Breadcrumbs :items="breadcrumbs" />
|
<Breadcrumbs :items="breadcrumbs" />
|
||||||
<router-link
|
<Button v-if="!readOnlyMode" variant="solid" @click="showForm = true">
|
||||||
v-if="!readOnlyMode"
|
<template #prefix>
|
||||||
:to="{
|
<Plus class="w-4 h-4" />
|
||||||
name: 'QuizForm',
|
</template>
|
||||||
params: {
|
{{ __('New') }}
|
||||||
quizID: 'new',
|
</Button>
|
||||||
},
|
|
||||||
}"
|
|
||||||
>
|
|
||||||
<Button variant="solid">
|
|
||||||
<template #prefix>
|
|
||||||
<Plus class="w-4 h-4" />
|
|
||||||
</template>
|
|
||||||
{{ __('New Quiz') }}
|
|
||||||
</Button>
|
|
||||||
</router-link>
|
|
||||||
</header>
|
</header>
|
||||||
<div v-if="quizzes.data?.length" class="md:w-3/4 md:mx-auto py-5 mx-5">
|
<div class="py-5 mx-5">
|
||||||
<div v-if="quizCount" class="text-xl font-semibold text-ink-gray-7 mb-4">
|
<div class="flex items-center justify-between mb-5">
|
||||||
{{ __('{0} Quizzes').format(quizCount) }}
|
<div class="text-xl font-semibold text-ink-gray-7">
|
||||||
|
{{
|
||||||
|
quizzes.data?.length
|
||||||
|
? __('{0} Quizzes').format(quizzes.data.length)
|
||||||
|
: __('No Quizzes')
|
||||||
|
}}
|
||||||
|
</div>
|
||||||
|
<FormControl v-model="search" type="text" placeholder="Search">
|
||||||
|
<template #prefix>
|
||||||
|
<FeatherIcon name="search" class="size-4 text-ink-gray-5" />
|
||||||
|
</template>
|
||||||
|
</FormControl>
|
||||||
</div>
|
</div>
|
||||||
<ListView
|
<ListView
|
||||||
|
v-if="quizzes.data?.length"
|
||||||
:columns="quizColumns"
|
:columns="quizColumns"
|
||||||
:rows="quizzes.data"
|
:rows="quizzes.data"
|
||||||
row-key="name"
|
row-key="name"
|
||||||
:options="{ showTooltip: false, selectable: false }"
|
:options="{ showTooltip: false, selectable: true }"
|
||||||
>
|
>
|
||||||
<ListHeader
|
<ListHeader
|
||||||
class="mb-2 grid items-center space-x-4 rounded bg-surface-gray-2 p-2"
|
class="mb-2 grid items-center space-x-4 rounded bg-surface-gray-2 p-2"
|
||||||
>
|
>
|
||||||
<ListHeaderItem :item="item" v-for="item in quizColumns">
|
<ListHeaderItem :item="item" v-for="item in quizColumns">
|
||||||
|
<template #prefix="{ item }">
|
||||||
|
<FeatherIcon :name="item.icon?.toString()" class="h-4 w-4" />
|
||||||
|
</template>
|
||||||
</ListHeaderItem>
|
</ListHeaderItem>
|
||||||
</ListHeader>
|
</ListHeader>
|
||||||
<ListRows>
|
<ListRows>
|
||||||
@@ -46,72 +51,176 @@
|
|||||||
},
|
},
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
<ListRow :row="row" />
|
<ListRow :row="row">
|
||||||
|
<template #default="{ column, item }">
|
||||||
|
<ListRowItem :item="row[column.key]" :align="column.align">
|
||||||
|
<div v-if="column.key == 'show_answers'">
|
||||||
|
<FormControl
|
||||||
|
type="checkbox"
|
||||||
|
v-model="row[column.key]"
|
||||||
|
:disabled="true"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-else-if="column.key == 'modified'"
|
||||||
|
class="text-xs text-ink-gray-5"
|
||||||
|
>
|
||||||
|
{{ row[column.key] }}
|
||||||
|
</div>
|
||||||
|
<div v-else>
|
||||||
|
{{ row[column.key] }}
|
||||||
|
</div>
|
||||||
|
</ListRowItem>
|
||||||
|
</template>
|
||||||
|
</ListRow>
|
||||||
</router-link>
|
</router-link>
|
||||||
</ListRows>
|
</ListRows>
|
||||||
|
<ListSelectBanner>
|
||||||
|
<template #actions="{ unselectAll, selections }">
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
@click="deleteQuiz(selections, unselectAll)"
|
||||||
|
>
|
||||||
|
<FeatherIcon name="trash-2" class="h-4 w-4 stroke-1.5" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</ListSelectBanner>
|
||||||
</ListView>
|
</ListView>
|
||||||
<div class="flex justify-center my-5">
|
<EmptyState v-else type="Quizzes" />
|
||||||
<Button v-if="quizzes.hasNextPage" @click="quizzes.next()">
|
<div v-if="quizzes.hasNextPage" class="flex justify-center my-5">
|
||||||
|
<Button @click="quizzes.next()">
|
||||||
{{ __('Load More') }}
|
{{ __('Load More') }}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<EmptyState v-else type="Quizzes" />
|
<Dialog
|
||||||
|
v-model="showForm"
|
||||||
|
:options="{
|
||||||
|
title: __('Create a Quiz'),
|
||||||
|
size: 'sm',
|
||||||
|
actions: [
|
||||||
|
{
|
||||||
|
label: __('Save'),
|
||||||
|
variant: 'solid',
|
||||||
|
onClick({ close }) {
|
||||||
|
insertQuiz(close)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<template #body-content>
|
||||||
|
<FormControl v-model="title" :label="__('Title')" type="text" />
|
||||||
|
</template>
|
||||||
|
</Dialog>
|
||||||
</template>
|
</template>
|
||||||
<script setup>
|
<script setup>
|
||||||
import {
|
import {
|
||||||
Breadcrumbs,
|
Breadcrumbs,
|
||||||
Button,
|
Button,
|
||||||
call,
|
|
||||||
createListResource,
|
createListResource,
|
||||||
|
Dialog,
|
||||||
|
FeatherIcon,
|
||||||
|
FormControl,
|
||||||
ListView,
|
ListView,
|
||||||
ListRows,
|
ListRows,
|
||||||
ListRow,
|
ListRow,
|
||||||
|
ListRowItem,
|
||||||
ListHeader,
|
ListHeader,
|
||||||
ListHeaderItem,
|
ListHeaderItem,
|
||||||
|
ListSelectBanner,
|
||||||
|
toast,
|
||||||
usePageMeta,
|
usePageMeta,
|
||||||
} from 'frappe-ui'
|
} from 'frappe-ui'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import { computed, inject, onMounted, ref } from 'vue'
|
import { computed, inject, onMounted, ref, watch } from 'vue'
|
||||||
import { Plus } from 'lucide-vue-next'
|
import { Plus } from 'lucide-vue-next'
|
||||||
import { sessionStore } from '@/stores/session'
|
import { sessionStore } from '@/stores/session'
|
||||||
import EmptyState from '@/components/EmptyState.vue'
|
import EmptyState from '@/components/EmptyState.vue'
|
||||||
|
|
||||||
const { brand } = sessionStore()
|
const { brand } = sessionStore()
|
||||||
const user = inject('$user')
|
const user = inject('$user')
|
||||||
|
const dayjs = inject('$dayjs')
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const quizCount = ref(0)
|
const search = ref('')
|
||||||
const readOnlyMode = window.read_only_mode
|
const readOnlyMode = window.read_only_mode
|
||||||
|
const quizFilters = ref({})
|
||||||
|
const showForm = ref(false)
|
||||||
|
const title = ref('')
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
if (!user.data?.is_moderator && !user.data?.is_instructor) {
|
if (!user.data?.is_moderator && !user.data?.is_instructor) {
|
||||||
router.push({ name: 'Courses' })
|
router.push({ name: 'Courses' })
|
||||||
|
} else if (!user.data?.is_moderator) {
|
||||||
|
quizFilters.value['owner'] = user.data?.name
|
||||||
}
|
}
|
||||||
getQuizCount()
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const quizFilter = computed(() => {
|
watch(search, () => {
|
||||||
if (user.data?.is_moderator) return {}
|
quizFilters.value['title'] = ['like', `%${search.value}%`]
|
||||||
return {
|
quizzes.update({
|
||||||
owner: user.data?.name,
|
filters: quizFilters.value,
|
||||||
}
|
})
|
||||||
|
quizzes.reload()
|
||||||
})
|
})
|
||||||
|
|
||||||
const quizzes = createListResource({
|
const quizzes = createListResource({
|
||||||
doctype: 'LMS Quiz',
|
doctype: 'LMS Quiz',
|
||||||
filters: quizFilter,
|
filters: quizFilters,
|
||||||
fields: ['name', 'title', 'passing_percentage', 'total_marks'],
|
fields: [
|
||||||
|
'name',
|
||||||
|
'title',
|
||||||
|
'passing_percentage',
|
||||||
|
'total_marks',
|
||||||
|
'show_answers',
|
||||||
|
'max_attempts',
|
||||||
|
'modified',
|
||||||
|
],
|
||||||
auto: true,
|
auto: true,
|
||||||
cache: ['quizzes', user.data?.name],
|
cache: ['quizzes', user.data?.name],
|
||||||
orderBy: 'modified desc',
|
orderBy: 'modified desc',
|
||||||
|
transform(data) {
|
||||||
|
return data.map((quiz) => {
|
||||||
|
return {
|
||||||
|
...quiz,
|
||||||
|
modified: dayjs(quiz.modified).fromNow(),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const getQuizCount = () => {
|
const insertQuiz = (close) => {
|
||||||
call('frappe.client.get_count', {
|
quizzes.insert.submit(
|
||||||
doctype: 'LMS Quiz',
|
{
|
||||||
}).then((data) => {
|
title: title.value,
|
||||||
quizCount.value = data
|
},
|
||||||
|
{
|
||||||
|
onSuccess(data) {
|
||||||
|
toast.success(__('Quiz created successfully'))
|
||||||
|
close()
|
||||||
|
title.value = ''
|
||||||
|
router.push({
|
||||||
|
name: 'QuizForm',
|
||||||
|
params: {
|
||||||
|
quizID: data.name,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
},
|
||||||
|
onError(error) {
|
||||||
|
toast.error(__('Error creating quiz: {0}', error.message))
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const deleteQuiz = (selections, unselectAll) => {
|
||||||
|
Array.from(selections).forEach(async (quizName) => {
|
||||||
|
await quizzes.delete.submit(quizName)
|
||||||
})
|
})
|
||||||
|
unselectAll()
|
||||||
|
toast.success(__('Quizzes deleted successfully'))
|
||||||
}
|
}
|
||||||
|
|
||||||
const quizColumns = computed(() => {
|
const quizColumns = computed(() => {
|
||||||
@@ -120,18 +229,42 @@ const quizColumns = computed(() => {
|
|||||||
label: __('Title'),
|
label: __('Title'),
|
||||||
key: 'title',
|
key: 'title',
|
||||||
width: 2,
|
width: 2,
|
||||||
|
icon: 'file-text',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: __('Total Marks'),
|
label: __('Total Marks'),
|
||||||
key: 'total_marks',
|
key: 'total_marks',
|
||||||
width: 1,
|
width: 1,
|
||||||
align: 'center',
|
align: 'center',
|
||||||
|
icon: 'hash',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: __('Passing Percentage'),
|
label: __('Passing Percentage'),
|
||||||
key: 'passing_percentage',
|
key: 'passing_percentage',
|
||||||
width: 1,
|
width: 1,
|
||||||
align: 'center',
|
align: 'center',
|
||||||
|
icon: 'percent',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: __('Max Attempts'),
|
||||||
|
key: 'max_attempts',
|
||||||
|
width: 1,
|
||||||
|
align: 'center',
|
||||||
|
icon: 'repeat',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: __('Show Answers'),
|
||||||
|
key: 'show_answers',
|
||||||
|
width: 1,
|
||||||
|
align: 'center',
|
||||||
|
icon: 'eye',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: __('Modified'),
|
||||||
|
key: 'modified',
|
||||||
|
width: 1,
|
||||||
|
align: 'right',
|
||||||
|
icon: 'clock',
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -104,24 +104,6 @@ export function getImgDimensions(imgSrc) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export function updateDocumentTitle(meta) {
|
|
||||||
watch(
|
|
||||||
() => meta,
|
|
||||||
(meta) => {
|
|
||||||
if (!meta.value.title) return
|
|
||||||
if (meta.value.title && meta.value.subtitle) {
|
|
||||||
document.title = `${meta.value.title} | ${meta.value.subtitle}`
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (meta.value.title) {
|
|
||||||
document.title = `${meta.value.title}`
|
|
||||||
return
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{ immediate: true, deep: true }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function htmlToText(html) {
|
export function htmlToText(html) {
|
||||||
const div = document.createElement('div')
|
const div = document.createElement('div')
|
||||||
div.innerHTML = html
|
div.innerHTML = html
|
||||||
|
|||||||
@@ -17,8 +17,10 @@
|
|||||||
"duration",
|
"duration",
|
||||||
"section_break_tzbu",
|
"section_break_tzbu",
|
||||||
"shuffle_questions",
|
"shuffle_questions",
|
||||||
"column_break_clsh",
|
|
||||||
"limit_questions_to",
|
"limit_questions_to",
|
||||||
|
"column_break_clsh",
|
||||||
|
"enable_negative_marking",
|
||||||
|
"marks_to_cut",
|
||||||
"section_break_sbjx",
|
"section_break_sbjx",
|
||||||
"questions",
|
"questions",
|
||||||
"section_break_3",
|
"section_break_3",
|
||||||
@@ -134,6 +136,18 @@
|
|||||||
"fieldname": "duration",
|
"fieldname": "duration",
|
||||||
"fieldtype": "Data",
|
"fieldtype": "Data",
|
||||||
"label": "Duration (in minutes)"
|
"label": "Duration (in minutes)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "0",
|
||||||
|
"fieldname": "enable_negative_marking",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"label": "Enable Negative Marking"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"depends_on": "enable_negative_marking",
|
||||||
|
"fieldname": "marks_to_cut",
|
||||||
|
"fieldtype": "Int",
|
||||||
|
"label": "Marks To Cut"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"grid_page_length": 50,
|
"grid_page_length": 50,
|
||||||
@@ -144,8 +158,8 @@
|
|||||||
"link_fieldname": "quiz"
|
"link_fieldname": "quiz"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"modified": "2025-04-07 15:03:48.525458",
|
"modified": "2025-06-27 18:33:13.714465",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "sayali@frappe.io",
|
||||||
"module": "LMS",
|
"module": "LMS",
|
||||||
"name": "LMS Quiz",
|
"name": "LMS Quiz",
|
||||||
"owner": "Administrator",
|
"owner": "Administrator",
|
||||||
|
|||||||
@@ -44,6 +44,11 @@ class LMSQuiz(Document):
|
|||||||
frappe.throw(_("All questions should have the same marks if the limit is set."))
|
frappe.throw(_("All questions should have the same marks if the limit is set."))
|
||||||
|
|
||||||
def calculate_total_marks(self):
|
def calculate_total_marks(self):
|
||||||
|
if len(self.questions) == 0:
|
||||||
|
self.total_marks = 0
|
||||||
|
self.passing_percentage = 100
|
||||||
|
return
|
||||||
|
|
||||||
if self.limit_questions_to:
|
if self.limit_questions_to:
|
||||||
self.total_marks = sum(
|
self.total_marks = sum(
|
||||||
question.marks for question in self.questions[: cint(self.limit_questions_to)]
|
question.marks for question in self.questions[: cint(self.limit_questions_to)]
|
||||||
@@ -102,11 +107,19 @@ def quiz_summary(quiz, results):
|
|||||||
quiz_details = frappe.db.get_value(
|
quiz_details = frappe.db.get_value(
|
||||||
"LMS Quiz",
|
"LMS Quiz",
|
||||||
quiz,
|
quiz,
|
||||||
["total_marks", "passing_percentage", "lesson", "course"],
|
[
|
||||||
|
"name",
|
||||||
|
"total_marks",
|
||||||
|
"passing_percentage",
|
||||||
|
"lesson",
|
||||||
|
"course",
|
||||||
|
"enable_negative_marking",
|
||||||
|
"marks_to_cut",
|
||||||
|
],
|
||||||
as_dict=1,
|
as_dict=1,
|
||||||
)
|
)
|
||||||
|
|
||||||
data = process_results(results, quiz)
|
data = process_results(results, quiz_details)
|
||||||
results = data["results"]
|
results = data["results"]
|
||||||
score = data["score"]
|
score = data["score"]
|
||||||
is_open_ended = data["is_open_ended"]
|
is_open_ended = data["is_open_ended"]
|
||||||
@@ -129,14 +142,14 @@ def quiz_summary(quiz, results):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def process_results(results, quiz):
|
def process_results(results, quiz_details):
|
||||||
score = 0
|
score = 0
|
||||||
is_open_ended = False
|
is_open_ended = False
|
||||||
|
|
||||||
for result in results:
|
for result in results:
|
||||||
question_details = frappe.db.get_value(
|
question_details = frappe.db.get_value(
|
||||||
"LMS Quiz Question",
|
"LMS Quiz Question",
|
||||||
{"parent": quiz, "question": result["question_name"]},
|
{"parent": quiz_details.name, "question": result["question_name"]},
|
||||||
["question", "marks", "question_detail", "type"],
|
["question", "marks", "question_detail", "type"],
|
||||||
as_dict=1,
|
as_dict=1,
|
||||||
)
|
)
|
||||||
@@ -154,7 +167,11 @@ def process_results(results, quiz):
|
|||||||
else:
|
else:
|
||||||
result["is_correct"] = 0
|
result["is_correct"] = 0
|
||||||
|
|
||||||
marks = question_details.marks if correct else 0
|
if correct:
|
||||||
|
marks = question_details.marks
|
||||||
|
else:
|
||||||
|
marks = -quiz_details.marks_to_cut if quiz_details.enable_negative_marking else 0
|
||||||
|
|
||||||
result["marks"] = marks
|
result["marks"] = marks
|
||||||
score += marks
|
score += marks
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user