Merge pull request #1062 from pateljannat/quiz-timer

feat: timer in quiz
This commit is contained in:
Jannat Patel
2024-10-14 16:11:55 +05:30
committed by GitHub
5 changed files with 123 additions and 42 deletions

View File

@@ -21,7 +21,7 @@
<script setup>
import { Star } from 'lucide-vue-next'
import { computed, ref, watch } from 'vue'
import { ref, watch } from 'vue'
const props = defineProps({
id: {

View File

@@ -1,11 +1,27 @@
<template>
<div v-if="quiz.data">
<div class="bg-blue-100 py-2 px-2 mb-4 rounded-md text-sm text-blue-800">
<div class="leading-relaxed">
<div
class="bg-blue-100 space-y-1 py-2 px-2 rounded-md text-sm text-blue-800"
>
<div class="leading-5">
{{
__('This quiz consists of {0} questions.').format(questions.length)
}}
</div>
<div v-if="quiz.data?.duration" class="leading-5">
{{
__(
'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">
{{
__(
@@ -22,14 +38,16 @@
)
}}
</div>
<div v-if="quiz.data.time" class="leading-relaxed">
{{
__(
'The quiz has a time limit. For each question you will be given {0} seconds.'
).format(quiz.data.time)
}}
</div>
</div>
<div v-if="quiz.data.duration" class="flex items-center space-x-2 my-4">
<span class="text-gray-600 text-xs"> {{ __('Time') }}: </span>
<ProgressBar :progress="timerProgress" />
<span class="font-semibold">
{{ formatTimer(timer) }}
</span>
</div>
<div v-if="activeQuestion == 0">
<div class="border text-center p-20 rounded-md">
<div class="font-semibold text-lg">
@@ -63,7 +81,7 @@
class="border rounded-md p-5"
>
<div class="flex justify-between">
<div class="text-sm">
<div class="text-sm text-gray-600">
<span class="mr-2">
{{ __('Question {0}').format(activeQuestion) }}:
</span>
@@ -162,8 +180,8 @@
editorClass="prose-sm max-w-none border-b border-x bg-gray-100 rounded-b-md py-1 px-2 min-h-[7rem]"
/>
</div>
<div class="flex items-center justify-between mt-5">
<div>
<div class="flex items-center justify-between mt-4">
<div class="text-sm text-gray-600">
{{
__('Question {0} of {1}').format(
activeQuestion,
@@ -250,20 +268,29 @@
</div>
</template>
<script setup>
import { Badge, Button, createResource, ListView, TextEditor } from 'frappe-ui'
import { ref, watch, reactive, inject } from 'vue'
import {
Badge,
Button,
createResource,
ListView,
TextEditor,
FormControl,
} from 'frappe-ui'
import { ref, watch, reactive, inject, computed } from 'vue'
import { createToast } from '@/utils/'
import { CheckCircle, XCircle, MinusCircle } from 'lucide-vue-next'
import { timeAgo } from '@/utils'
import FormControl from 'frappe-ui/src/components/FormControl.vue'
const user = inject('$user')
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 showAnswers = reactive([])
let questions = reactive([])
const possibleAnswer = ref(null)
const timer = ref(0)
let timerInterval = null
const props = defineProps({
quizName: {
@@ -284,6 +311,7 @@ const quiz = createResource({
auto: true,
onSuccess(data) {
populateQuestions()
setupTimer()
},
})
@@ -299,6 +327,37 @@ const populateQuestions = () => {
}
}
const setupTimer = () => {
if (quiz.data.duration) {
timer.value = quiz.data.duration * 60
}
}
const startTimer = () => {
timerInterval = setInterval(() => {
timer.value--
if (timer.value == 0) {
clearInterval(timerInterval)
submitQuiz()
}
}, 1000)
}
const formatTimer = (seconds) => {
const hrs = Math.floor(seconds / 3600)
.toString()
.padStart(2, '0')
const mins = Math.floor((seconds % 3600) / 60)
.toString()
.padStart(2, '0')
const secs = (seconds % 60).toString().padStart(2, '0')
return hrs != '00' ? `${hrs}:${mins}:${secs}` : `${mins}:${secs}`
}
const timerProgress = computed(() => {
return (timer.value / (quiz.data.duration * 60)) * 100
})
const shuffleArray = (array) => {
for (let i = array.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1))
@@ -383,6 +442,7 @@ watch(
const startQuiz = () => {
activeQuestion.value = 1
localStorage.removeItem(quiz.data.title)
if (quiz.data.duration) startTimer()
}
const markAnswer = (index) => {
@@ -493,9 +553,15 @@ const submitQuiz = () => {
}
const createSubmission = () => {
quizSubmission.reload().then(() => {
if (quiz.data && quiz.data.max_attempts) attempts.reload()
})
quizSubmission.submit(
{},
{
onSuccess(data) {
if (quiz.data && quiz.data.max_attempts) attempts.reload()
if (quiz.data.duration) clearInterval(timerInterval)
},
}
)
}
const resetQuiz = () => {
@@ -504,6 +570,7 @@ const resetQuiz = () => {
showAnswers.length = 0
quizSubmission.reset()
populateQuestions()
setupTimer()
}
const getInstructions = (question) => {

View File

@@ -38,7 +38,7 @@
<div class="w-3/4 mx-auto py-5">
<!-- Details -->
<div class="mb-8">
<div class="text-sm font-semibold mb-4">
<div class="font-semibold mb-4">
{{ __('Details') }}
</div>
<FormControl
@@ -50,11 +50,17 @@
"
/>
<div v-if="quizDetails.data?.name">
<div class="grid grid-cols-3 gap-5 mt-4 mb-8">
<div class="grid grid-cols-2 gap-5 mt-4 mb-8">
<FormControl
type="number"
v-model="quiz.max_attempts"
:label="__('Maximun Attempts')"
/>
<FormControl
type="number"
v-model="quiz.duration"
:label="__('Duration (in minutes)')"
/>
<FormControl
v-model="quiz.total_marks"
:label="__('Total Marks')"
@@ -68,7 +74,7 @@
<!-- Settings -->
<div class="mb-8">
<div class="text-sm font-semibold mb-4">
<div class="font-semibold mb-4">
{{ __('Settings') }}
</div>
<div class="grid grid-cols-3 gap-5 my-4">
@@ -86,7 +92,7 @@
</div>
<div class="mb-8">
<div class="text-sm font-semibold mb-4">
<div class="font-semibold mb-4">
{{ __('Shuffle Settings') }}
</div>
<div class="grid grid-cols-3">
@@ -106,7 +112,7 @@
<!-- Questions -->
<div>
<div class="flex items-center justify-between mb-4">
<div class="text-sm font-semibold">
<div class="font-semibold">
{{ __('Questions') }}
</div>
<Button @click="openQuestionModal()">
@@ -226,6 +232,7 @@ const quiz = reactive({
total_marks: 0,
passing_percentage: 0,
max_attempts: 0,
duration: 0,
limit_questions_to: 0,
show_answers: true,
show_submission_history: false,

View File

@@ -10,10 +10,11 @@
"title",
"max_attempts",
"show_answers",
"show_submission_history",
"column_break_gaac",
"total_marks",
"passing_percentage",
"show_submission_history",
"duration",
"section_break_tzbu",
"shuffle_questions",
"column_break_clsh",
@@ -128,11 +129,16 @@
{
"fieldname": "column_break_clsh",
"fieldtype": "Column Break"
},
{
"fieldname": "duration",
"fieldtype": "Duration",
"label": "Duration"
}
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2024-08-09 12:21:36.256522",
"modified": "2024-10-11 22:39:40.381183",
"modified_by": "Administrator",
"module": "LMS",
"name": "LMS Quiz",

View File

@@ -100,6 +100,16 @@ def quiz_summary(quiz, results):
score = 0
results = results and json.loads(results)
is_open_ended = False
percentage = 0
quiz_details = frappe.db.get_value(
"LMS Quiz",
quiz,
["total_marks", "passing_percentage", "lesson", "course"],
as_dict=1,
)
score_out_of = quiz_details.total_marks
for result in results:
question_details = frappe.db.get_value(
@@ -113,17 +123,6 @@ def quiz_summary(quiz, results):
result["question"] = question_details.question_detail
result["marks_out_of"] = question_details.marks
quiz_details = frappe.db.get_value(
"LMS Quiz",
quiz,
["total_marks", "passing_percentage", "lesson", "course"],
as_dict=1,
)
score = 0
percentage = 0
score_out_of = quiz_details.total_marks
if question_details.type != "Open Ended":
correct = result["is_correct"][0]
for point in result["is_correct"]:
@@ -135,24 +134,26 @@ def quiz_summary(quiz, results):
score += marks
del result["question_name"]
percentage = (score / score_out_of) * 100
else:
result["is_correct"] = 0
is_open_ended = True
percentage = (score / score_out_of) * 100
result["answer"] = re.sub(
r'<img[^>]*src\s*=\s*["\'](?=data:)(.*?)["\']', _save_file, result["answer"]
)
submission = frappe.get_doc(
submission = frappe.new_doc("LMS Quiz Submission")
# Score and percentage are calculated by the controller function
submission.update(
{
"doctype": "LMS Quiz Submission",
"quiz": quiz,
"result": results,
"score": score,
"score": 0,
"score_out_of": score_out_of,
"member": frappe.session.user,
"percentage": percentage,
"percentage": 0,
"passing_percentage": quiz_details.passing_percentage,
}
)