mirror of
https://github.com/frappe/lms.git
synced 2026-04-19 22:52:29 +03:00
Merge pull request #2188 from pateljannat/quiz-navigation
feat: navigate between questions in quiz
This commit is contained in:
1
.github/workflows/ui-tests.yml
vendored
1
.github/workflows/ui-tests.yml
vendored
@@ -17,7 +17,6 @@ jobs:
|
||||
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.repository_owner == 'frappe' }}
|
||||
timeout-minutes: 60
|
||||
|
||||
strategy:
|
||||
|
||||
@@ -189,7 +189,6 @@ import {
|
||||
Plus,
|
||||
SquareCode,
|
||||
Trash2,
|
||||
Notebook,
|
||||
} from 'lucide-vue-next'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import ChapterModal from '@/components/Modals/ChapterModal.vue'
|
||||
|
||||
@@ -1,56 +1,72 @@
|
||||
<template>
|
||||
<div v-if="quiz.data">
|
||||
<div
|
||||
class="bg-surface-blue-2 space-y-2 py-2 px-3 mb-4 rounded-md text-sm text-ink-blue-2 leading-5"
|
||||
class="bg-surface-blue-2 text-ink-blue-3 space-y-2 p-3 mb-4 rounded-lg leading-5"
|
||||
>
|
||||
<div v-if="inVideo">
|
||||
{{ __('You will have to complete the quiz to continue the video') }}
|
||||
</div>
|
||||
<div class="leading-5">
|
||||
{{
|
||||
__('This quiz consists of {0} questions.').format(questions.length)
|
||||
}}
|
||||
</div>
|
||||
<div v-if="quiz.data?.duration" class="leading-5">
|
||||
<div class="font-medium">
|
||||
{{
|
||||
__(
|
||||
'Please ensure that you complete all the questions in {0} minutes.'
|
||||
).format(quiz.data.duration)
|
||||
}}
|
||||
</div>
|
||||
<div v-if="quiz.data?.duration" class="leading-5">
|
||||
{{
|
||||
__(
|
||||
'If you fail to do so, the quiz will be automatically submitted when the timer ends.'
|
||||
)
|
||||
}}
|
||||
</div>
|
||||
<div v-if="quiz.data.passing_percentage" class="leading-relaxed">
|
||||
{{
|
||||
__(
|
||||
'You will have to get {0}% correct answers in order to pass the quiz.'
|
||||
).format(quiz.data.passing_percentage)
|
||||
}}
|
||||
</div>
|
||||
<div v-if="quiz.data.max_attempts" class="leading-5">
|
||||
{{
|
||||
__('You can attempt this quiz {0}.').format(
|
||||
quiz.data.max_attempts == 1
|
||||
? '1 time'
|
||||
: `${quiz.data.max_attempts} times`
|
||||
)
|
||||
}}
|
||||
</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'
|
||||
'Please read the following instructions carefully before starting the quiz'
|
||||
)
|
||||
}}
|
||||
</div>
|
||||
<ol class="list-decimal list-inside space-y-2">
|
||||
<li v-if="inVideo">
|
||||
{{ __('You will have to complete the quiz to continue the video') }}
|
||||
</li>
|
||||
<li>
|
||||
{{
|
||||
__(
|
||||
'Do not refresh the page or close this window. If you do, the quiz will be submitted automatically.'
|
||||
)
|
||||
}}
|
||||
</li>
|
||||
<li>
|
||||
{{
|
||||
__('This quiz consists of {0} questions.').format(questions.length)
|
||||
}}
|
||||
</li>
|
||||
<li v-if="quiz.data?.duration">
|
||||
{{
|
||||
__(
|
||||
'Please ensure that you complete all the questions in {0} minutes.'
|
||||
).format(quiz.data.duration)
|
||||
}}
|
||||
</li>
|
||||
<li v-if="quiz.data?.duration">
|
||||
{{
|
||||
__(
|
||||
'If you fail to do so, the quiz will be automatically submitted when the timer ends.'
|
||||
)
|
||||
}}
|
||||
</li>
|
||||
<li v-if="quiz.data.passing_percentage">
|
||||
{{
|
||||
__(
|
||||
'You will have to get {0}% correct answers in order to pass the quiz.'
|
||||
).format(quiz.data.passing_percentage)
|
||||
}}
|
||||
</li>
|
||||
<li v-if="quiz.data.max_attempts">
|
||||
{{
|
||||
__('You can attempt this quiz {0}.').format(
|
||||
quiz.data.max_attempts == 1
|
||||
? '1 time'
|
||||
: `${quiz.data.max_attempts} times`
|
||||
)
|
||||
}}
|
||||
</li>
|
||||
<li v-if="quiz.data.enable_negative_marking">
|
||||
{{
|
||||
__(
|
||||
'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'
|
||||
)
|
||||
}}
|
||||
</li>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
<div v-if="quiz.data.duration" class="flex flex-col space-x-1 my-4">
|
||||
@@ -135,6 +151,7 @@
|
||||
:name="encodeURIComponent(questionDetails.data.question)"
|
||||
class="w-3.5 h-3.5 text-ink-gray-9 focus:ring-outline-gray-modals"
|
||||
@change="markAnswer(index)"
|
||||
:checked="selectedOptions[index - 1]"
|
||||
/>
|
||||
|
||||
<input
|
||||
@@ -143,6 +160,7 @@
|
||||
:name="encodeURIComponent(questionDetails.data.question)"
|
||||
class="w-3.5 h-3.5 text-ink-gray-9 rounded-sm focus:ring-outline-gray-modals"
|
||||
@change="markAnswer(index)"
|
||||
:checked="selectedOptions[index - 1]"
|
||||
/>
|
||||
<div
|
||||
v-else-if="quiz.data.show_answers"
|
||||
@@ -208,7 +226,7 @@
|
||||
editorClass="prose-sm max-w-none border-b border-x border-outline-gray-modals bg-surface-gray-2 rounded-b-md py-1 px-2 min-h-[7rem]"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex items-center justify-between mt-4">
|
||||
<div class="flex items-center justify-between mt-8">
|
||||
<div class="text-sm text-ink-gray-5">
|
||||
{{
|
||||
__('Question {0} of {1}').format(
|
||||
@@ -217,6 +235,48 @@
|
||||
)
|
||||
}}
|
||||
</div>
|
||||
<div
|
||||
v-if="!quiz.data.show_answers"
|
||||
class="flex items-center space-x-2"
|
||||
>
|
||||
<Button
|
||||
@click="switchQuestion(activeQuestion - 1)"
|
||||
:disabled="activeQuestion == 1"
|
||||
class="rounded-full"
|
||||
>
|
||||
<template #icon>
|
||||
<ChevronLeft class="size-4 stroke-1.5" />
|
||||
</template>
|
||||
</Button>
|
||||
<span
|
||||
v-for="item in paginationWindow"
|
||||
:key="item"
|
||||
class="w-6 h-6 rounded-full flex items-center justify-center text-sm"
|
||||
:class="{
|
||||
'cursor-pointer': item !== '...',
|
||||
'bg-surface-gray-4 border border-outline-gray-5 font-medium':
|
||||
activeQuestion == item,
|
||||
'bg-surface-gray-3 text-ink-gray-6':
|
||||
activeQuestion != item && item !== '...',
|
||||
'text-ink-gray-5': item === '...',
|
||||
'bg-surface-blue-3 text-ink-white':
|
||||
attemptedQuestions.includes(item) && activeQuestion != item,
|
||||
}"
|
||||
@click="item !== '...' && switchQuestion(item)"
|
||||
>
|
||||
{{ item }}
|
||||
</span>
|
||||
|
||||
<Button
|
||||
@click="switchQuestion(activeQuestion + 1)"
|
||||
:disabled="activeQuestion == questions.length"
|
||||
class="rounded-full"
|
||||
>
|
||||
<template #icon>
|
||||
<ChevronRight class="size-4 stroke-1.5" />
|
||||
</template>
|
||||
</Button>
|
||||
</div>
|
||||
<Button
|
||||
v-if="
|
||||
quiz.data.show_answers &&
|
||||
@@ -230,14 +290,16 @@
|
||||
</span>
|
||||
</Button>
|
||||
<Button
|
||||
v-else-if="activeQuestion != questions.length"
|
||||
v-else-if="
|
||||
activeQuestion != questions.length && quiz.data.show_answers
|
||||
"
|
||||
@click="nextQuestion()"
|
||||
>
|
||||
<span>
|
||||
{{ __('Next') }}
|
||||
</span>
|
||||
</Button>
|
||||
<Button v-else @click="submitQuiz()">
|
||||
<Button variant="solid" v-else @click="handleSubmitClick()">
|
||||
<span>
|
||||
{{ __('Submit') }}
|
||||
</span>
|
||||
@@ -310,6 +372,54 @@
|
||||
</ListView>
|
||||
</div>
|
||||
</div>
|
||||
<Dialog
|
||||
v-model="showSubmissionConfirmation"
|
||||
:options="{
|
||||
title: __('Are you sure you want to submit the quiz?'),
|
||||
actions: [
|
||||
{
|
||||
size: 'sm',
|
||||
label: __('Submit'),
|
||||
variant: 'solid',
|
||||
onClick() {
|
||||
submitQuiz()
|
||||
showSubmissionConfirmation = false
|
||||
},
|
||||
},
|
||||
],
|
||||
}"
|
||||
>
|
||||
<template #body-content>
|
||||
<div class="border border-outline-gray-modals rounded-lg text-base">
|
||||
<div class="divide-y divide-outline-gray-modals">
|
||||
<div class="grid grid-cols-2 divide-x divide-outline-gray-modals">
|
||||
<div class="p-2">
|
||||
{{ __('Total Questions') }}
|
||||
</div>
|
||||
<div class="p-2">
|
||||
{{ questions.length }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 divide-x divide-outline-gray-modals">
|
||||
<div class="p-2">
|
||||
{{ __('Attempted Questions') }}
|
||||
</div>
|
||||
<div class="p-2">
|
||||
{{ attemptedQuestions.length }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 divide-x divide-outline-gray-modals">
|
||||
<div class="p-2">
|
||||
{{ __('Unattempted Questions') }}
|
||||
</div>
|
||||
<div class="p-2">
|
||||
{{ questions.length - attemptedQuestions.length }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</Dialog>
|
||||
</template>
|
||||
<script setup>
|
||||
import {
|
||||
@@ -317,23 +427,39 @@ import {
|
||||
Button,
|
||||
call,
|
||||
createResource,
|
||||
Dialog,
|
||||
ListView,
|
||||
TextEditor,
|
||||
FormControl,
|
||||
toast,
|
||||
} from 'frappe-ui'
|
||||
import { ref, watch, reactive, inject, computed } from 'vue'
|
||||
import { CheckCircle, XCircle, MinusCircle } from 'lucide-vue-next'
|
||||
import {
|
||||
computed,
|
||||
inject,
|
||||
onMounted,
|
||||
onUnmounted,
|
||||
reactive,
|
||||
ref,
|
||||
watch,
|
||||
} from 'vue'
|
||||
import {
|
||||
CheckCircle,
|
||||
ChevronLeft,
|
||||
ChevronRight,
|
||||
XCircle,
|
||||
MinusCircle,
|
||||
} from 'lucide-vue-next'
|
||||
import { timeAgo } from '@/utils'
|
||||
import { useRouter } from 'vue-router'
|
||||
import ProgressBar from '@/components/ProgressBar.vue'
|
||||
|
||||
const user = inject('$user')
|
||||
const activeQuestion = ref(0)
|
||||
const currentQuestion = ref('')
|
||||
const selectedOptions = reactive([0, 0, 0, 0])
|
||||
const selectedOptions = ref([0, 0, 0, 0])
|
||||
const showAnswers = reactive([])
|
||||
let questions = reactive([])
|
||||
const attemptedQuestions = ref([])
|
||||
const showSubmissionConfirmation = ref(false)
|
||||
const possibleAnswer = ref(null)
|
||||
const timer = ref(0)
|
||||
let timerInterval = null
|
||||
@@ -353,6 +479,40 @@ const props = defineProps({
|
||||
},
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
window.addEventListener('pagehide', handlePageHide)
|
||||
window.addEventListener('beforeunload', handleBeforeUnload)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('pagehide', handlePageHide)
|
||||
window.removeEventListener('beforeunload', handleBeforeUnload)
|
||||
})
|
||||
|
||||
const handlePageHide = () => {
|
||||
if (activeQuestion.value > 0 && !quizSubmission.data) {
|
||||
const params = new URLSearchParams({
|
||||
quiz: quiz.data.name,
|
||||
results: localStorage.getItem(quiz.data.title),
|
||||
})
|
||||
|
||||
navigator.sendBeacon(
|
||||
'/api/method/lms.lms.doctype.lms_quiz.lms_quiz.submit_quiz?' +
|
||||
params.toString()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const handleBeforeUnload = (event) => {
|
||||
if (activeQuestion.value > 0 && !quizSubmission.data) {
|
||||
if (attemptedQuestions.value.length) {
|
||||
switchQuestion(activeQuestion.value)
|
||||
}
|
||||
event.preventDefault()
|
||||
event.returnValue = ''
|
||||
}
|
||||
}
|
||||
|
||||
const quiz = createResource({
|
||||
url: 'frappe.client.get',
|
||||
makeParams(values) {
|
||||
@@ -486,10 +646,58 @@ const questionDetails = createResource({
|
||||
watch(activeQuestion, (value) => {
|
||||
if (value > 0) {
|
||||
currentQuestion.value = quiz.data.questions[value - 1].question
|
||||
questionDetails.reload()
|
||||
questionDetails.reload(
|
||||
{},
|
||||
{
|
||||
onSuccess() {
|
||||
if (!quiz.data.show_answers) {
|
||||
loadSavedAnswers()
|
||||
}
|
||||
},
|
||||
}
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
const switchQuestion = (questionNumber) => {
|
||||
let answers = getAnswers()
|
||||
if (answers.length) {
|
||||
if (!attemptedQuestions.value.includes(activeQuestion.value)) {
|
||||
attemptedQuestions.value.push(activeQuestion.value)
|
||||
}
|
||||
addToLocalStorage()
|
||||
resetQuestion()
|
||||
}
|
||||
|
||||
if (questionNumber < 1 || questionNumber > questions.length) return
|
||||
activeQuestion.value = questionNumber
|
||||
}
|
||||
|
||||
const loadSavedAnswers = () => {
|
||||
let quizData = JSON.parse(localStorage.getItem(quiz.data.title))
|
||||
if (quizData) {
|
||||
let localQuestion = quizData.find(
|
||||
(q) => q.question_name == currentQuestion.value
|
||||
)
|
||||
if (localQuestion) {
|
||||
let localAnswers = localQuestion.answer
|
||||
if (localAnswers.length) {
|
||||
if (questionDetails.data.type == 'Choices') {
|
||||
localAnswers.forEach((answer) => {
|
||||
for (let i = 1; i <= 4; i++) {
|
||||
if (questionDetails.data[`option_${i}`] == answer) {
|
||||
selectedOptions.value[i - 1] = 1
|
||||
}
|
||||
}
|
||||
})
|
||||
} else {
|
||||
possibleAnswer.value = localAnswers[0]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.quizName,
|
||||
(newName) => {
|
||||
@@ -507,17 +715,20 @@ const startQuiz = () => {
|
||||
|
||||
const markAnswer = (index) => {
|
||||
if (!questionDetails.data.multiple)
|
||||
selectedOptions.splice(0, selectedOptions.length, ...[0, 0, 0, 0])
|
||||
selectedOptions[index - 1] = selectedOptions[index - 1] ? 0 : 1
|
||||
selectedOptions.value.splice(
|
||||
0,
|
||||
selectedOptions.value.length,
|
||||
...[0, 0, 0, 0]
|
||||
)
|
||||
selectedOptions.value[index - 1] = selectedOptions.value[index - 1] ? 0 : 1
|
||||
}
|
||||
|
||||
const getAnswers = () => {
|
||||
let answers = []
|
||||
const type = questionDetails.data.type
|
||||
|
||||
if (type == 'Choices') {
|
||||
selectedOptions.forEach((value, index) => {
|
||||
if (selectedOptions[index])
|
||||
selectedOptions.value.forEach((value, index) => {
|
||||
if (selectedOptions.value[index])
|
||||
answers.push(questionDetails.data[`option_${index + 1}`])
|
||||
})
|
||||
} else {
|
||||
@@ -545,7 +756,7 @@ const checkAnswer = () => {
|
||||
onSuccess(data) {
|
||||
let type = questionDetails.data.type
|
||||
if (type == 'Choices') {
|
||||
selectedOptions.forEach((option, index) => {
|
||||
selectedOptions.value.forEach((option, index) => {
|
||||
if (option) {
|
||||
showAnswers[index] = option && data[index]
|
||||
} else if (data[index] == 2) {
|
||||
@@ -571,12 +782,13 @@ const addToLocalStorage = () => {
|
||||
question_name: currentQuestion.value,
|
||||
answer: getAnswers(),
|
||||
}
|
||||
|
||||
if (quizData) {
|
||||
let existingQuestion = quizData.find(
|
||||
(q) => q.question_name == questionData.question_name
|
||||
)
|
||||
if (!existingQuestion) {
|
||||
if (existingQuestion) {
|
||||
existingQuestion.answer = questionData.answer
|
||||
} else {
|
||||
quizData.push(questionData)
|
||||
}
|
||||
} else {
|
||||
@@ -586,18 +798,15 @@ const addToLocalStorage = () => {
|
||||
}
|
||||
|
||||
const nextQuestion = () => {
|
||||
if (!quiz.data.show_answers && questionDetails.data?.type != 'Open Ended') {
|
||||
checkAnswer()
|
||||
} else {
|
||||
if (questionDetails.data?.type == 'Open Ended') addToLocalStorage()
|
||||
resetQuestion()
|
||||
}
|
||||
if (!quiz.data.show_answers) return
|
||||
if (questionDetails.data?.type == 'Open Ended') addToLocalStorage()
|
||||
resetQuestion()
|
||||
}
|
||||
|
||||
const resetQuestion = () => {
|
||||
if (activeQuestion.value == quiz.data.questions.length) return
|
||||
activeQuestion.value = activeQuestion.value + 1
|
||||
selectedOptions.splice(0, selectedOptions.length, ...[0, 0, 0, 0])
|
||||
selectedOptions.value.splice(0, selectedOptions.value.length, ...[0, 0, 0, 0])
|
||||
showAnswers.length = 0
|
||||
possibleAnswer.value = null
|
||||
}
|
||||
@@ -605,7 +814,6 @@ const resetQuestion = () => {
|
||||
const submitQuiz = () => {
|
||||
if (!quiz.data.show_answers) {
|
||||
if (questionDetails.data.type == 'Open Ended') addToLocalStorage()
|
||||
else checkAnswer()
|
||||
setTimeout(() => {
|
||||
createSubmission()
|
||||
}, 500)
|
||||
@@ -639,8 +847,10 @@ const createSubmission = () => {
|
||||
|
||||
const resetQuiz = () => {
|
||||
activeQuestion.value = 0
|
||||
selectedOptions.splice(0, selectedOptions.length, ...[0, 0, 0, 0])
|
||||
selectedOptions.value.splice(0, selectedOptions.value.length, ...[0, 0, 0, 0])
|
||||
showAnswers.length = 0
|
||||
possibleAnswer.value = null
|
||||
attemptedQuestions.value = []
|
||||
quizSubmission.reset()
|
||||
populateQuestions()
|
||||
setupTimer()
|
||||
@@ -669,6 +879,37 @@ const markLessonProgress = () => {
|
||||
}
|
||||
}
|
||||
|
||||
const handleSubmitClick = () => {
|
||||
if (attemptedQuestions.value.length) {
|
||||
switchQuestion(activeQuestion.value)
|
||||
}
|
||||
showSubmissionConfirmation.value = true
|
||||
}
|
||||
|
||||
const paginationWindow = computed(() => {
|
||||
const total = questions.length
|
||||
const current = activeQuestion.value
|
||||
const pages = []
|
||||
const size = 5
|
||||
|
||||
let start = Math.floor((current - 1) / size) * size + 1
|
||||
let end = Math.min(start + size - 1, total)
|
||||
|
||||
if (start > 1) {
|
||||
pages.push('...')
|
||||
}
|
||||
|
||||
for (let i = start; i <= end; i++) {
|
||||
pages.push(i)
|
||||
}
|
||||
|
||||
if (end < total) {
|
||||
pages.push('...')
|
||||
}
|
||||
|
||||
return pages
|
||||
})
|
||||
|
||||
const getSubmissionColumns = () => {
|
||||
return [
|
||||
{
|
||||
|
||||
@@ -151,7 +151,6 @@ def process_results(results: list, quiz_details: dict):
|
||||
["question", "marks", "question_detail", "type"],
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
result["question_name"] = question_details.question
|
||||
result["question"] = question_details.question_detail
|
||||
result["marks_out_of"] = question_details.marks
|
||||
@@ -165,12 +164,13 @@ def process_results(results: list, quiz_details: dict):
|
||||
result["marks"] = -quiz_details.marks_to_cut if quiz_details.enable_negative_marking else 0
|
||||
|
||||
score += result["marks"]
|
||||
result["is_correct"] = 1 if correct else 0
|
||||
|
||||
else:
|
||||
is_open_ended = True
|
||||
result["is_correct"] = 0
|
||||
result["answer"] = re.sub(
|
||||
r'<img[^>]*src\s*=\s*["\'](?=data:)(.*?)["\']', _save_file, result["answer"]
|
||||
r'<img[^>]*src\s*=\s*["\'](?=data:)(.*?)["\']', _save_file, result["answer"][0]
|
||||
)
|
||||
|
||||
return {
|
||||
@@ -263,10 +263,12 @@ def create_submission(quiz: str, results: list, score_out_of: int, passing_perce
|
||||
|
||||
|
||||
def save_progress_after_quiz(quiz_details: dict, percentage: float):
|
||||
if percentage >= quiz_details.passing_percentage and quiz_details.lesson and quiz_details.course:
|
||||
save_progress(quiz_details.lesson, quiz_details.course)
|
||||
elif not quiz_details.passing_percentage:
|
||||
save_progress(quiz_details.lesson, quiz_details.course)
|
||||
if not quiz_details.lesson or not quiz_details.course:
|
||||
return
|
||||
|
||||
if quiz_details.passing_percentage and percentage < quiz_details.passing_percentage:
|
||||
return
|
||||
save_progress(quiz_details.lesson, quiz_details.course)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
|
||||
@@ -47,8 +47,8 @@ class TestLMSAPI(BaseTestUtils):
|
||||
for quiz in progress.quizzes:
|
||||
self.assertEqual(quiz.quiz, self.quiz.name)
|
||||
self.assertEqual(quiz.quiz_title, self.quiz.title)
|
||||
self.assertEqual(quiz.score, 12)
|
||||
self.assertEqual(quiz.percentage, 80)
|
||||
self.assertEqual(quiz.score, 10)
|
||||
self.assertEqual(quiz.percentage, 66)
|
||||
|
||||
self.assertEqual(len(progress.assignments), 1)
|
||||
for assignment in progress.assignments:
|
||||
@@ -61,3 +61,25 @@ class TestLMSAPI(BaseTestUtils):
|
||||
self.assertEqual(exercise.exercise, self.programming_exercise.name)
|
||||
self.assertEqual(exercise.exercise_title, self.programming_exercise.title)
|
||||
self.assertEqual(exercise.status, "Passed")
|
||||
|
||||
def test_quiz_submission(self):
|
||||
submission = frappe.get_all(
|
||||
"LMS Quiz Submission", filters={"quiz": self.quiz.name, "member": self.student1.name}
|
||||
)
|
||||
self.assertEqual(len(submission), 1)
|
||||
submission = submission[0]
|
||||
submission = frappe.get_doc("LMS Quiz Submission", submission.name)
|
||||
|
||||
self.assertEqual(submission.score, 10)
|
||||
self.assertEqual(submission.score_out_of, 15)
|
||||
self.assertEqual(submission.percentage, 66)
|
||||
self.assertEqual(submission.passing_percentage, 70)
|
||||
self.assertEqual(len(submission.result), 3)
|
||||
for index, result in enumerate(submission.result):
|
||||
self.assertEqual(result.question_name, self.quiz.questions[index].question)
|
||||
self.assertEqual(
|
||||
result.answer,
|
||||
self.questions[index].option_1 if index % 2 == 0 else self.questions[index].option_2,
|
||||
)
|
||||
self.assertEqual(result.is_correct, 1 if index % 2 == 0 else 0)
|
||||
self.assertEqual(result.marks, 5 if index % 2 == 0 else 0)
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import json
|
||||
|
||||
import frappe
|
||||
from frappe.tests import UnitTestCase
|
||||
from frappe.utils import add_days, nowdate
|
||||
|
||||
from lms.lms.doctype.lms_certificate.lms_certificate import get_default_certificate_template
|
||||
from lms.lms.doctype.lms_quiz.lms_quiz import submit_quiz
|
||||
|
||||
|
||||
class BaseTestUtils(UnitTestCase):
|
||||
@@ -267,7 +270,7 @@ class BaseTestUtils(UnitTestCase):
|
||||
}
|
||||
)
|
||||
question.save()
|
||||
self.cleanup_items.append(("LMS Quiz Question", question.name))
|
||||
self.cleanup_items.append(("LMS Question", question.name))
|
||||
questions.append(question)
|
||||
return questions
|
||||
|
||||
@@ -450,29 +453,22 @@ class BaseTestUtils(UnitTestCase):
|
||||
existing = frappe.db.exists("LMS Quiz Submission", {"quiz": self.quiz.name, "member": member})
|
||||
if existing:
|
||||
return frappe.get_doc("LMS Quiz Submission", existing)
|
||||
submission = frappe.new_doc("LMS Quiz Submission")
|
||||
submission.update(
|
||||
{
|
||||
"quiz": self.quiz.name,
|
||||
"member": member,
|
||||
"score_out_of": self.quiz.total_marks,
|
||||
"passing_percentage": self.quiz.passing_percentage,
|
||||
}
|
||||
)
|
||||
|
||||
for question in self.questions:
|
||||
submission.append(
|
||||
"result",
|
||||
frappe.session.user = member
|
||||
results = []
|
||||
for index, question in enumerate(self.questions):
|
||||
results.append(
|
||||
{
|
||||
"question": question.name,
|
||||
"marks": 4,
|
||||
"marks_out_of": 5,
|
||||
},
|
||||
"question_name": question.name,
|
||||
"answer": [question.option_1 if index % 2 == 0 else question.option_2],
|
||||
}
|
||||
)
|
||||
|
||||
submission.insert()
|
||||
self.cleanup_items.append(("LMS Quiz Submission", submission.name))
|
||||
return submission
|
||||
submit_quiz(self.quiz.name, json.dumps(results))
|
||||
submission = frappe.db.get_value(
|
||||
"LMS Quiz Submission", {"quiz": self.quiz.name, "member": member}, "name"
|
||||
)
|
||||
self.cleanup_items.append(("LMS Quiz Submission", submission))
|
||||
frappe.session.user = "Administrator"
|
||||
|
||||
def _create_assignment_submission(self, member):
|
||||
existing = frappe.db.exists(
|
||||
|
||||
Reference in New Issue
Block a user