Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: timer in quiz #1062

Merged
merged 2 commits into from
Oct 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion frontend/src/components/Controls/Rating.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down
105 changes: 86 additions & 19 deletions frontend/src/components/Quiz.vue
Original file line number Diff line number Diff line change
@@ -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">
{{
__(
Expand All @@ -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">
Expand Down Expand Up @@ -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>
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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: {
Expand All @@ -284,6 +311,7 @@ const quiz = createResource({
auto: true,
onSuccess(data) {
populateQuestions()
setupTimer()
},
})

Expand All @@ -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))
Expand Down Expand Up @@ -383,6 +442,7 @@ watch(
const startQuiz = () => {
activeQuestion.value = 1
localStorage.removeItem(quiz.data.title)
if (quiz.data.duration) startTimer()
}

const markAnswer = (index) => {
Expand Down Expand Up @@ -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 = () => {
Expand All @@ -504,6 +570,7 @@ const resetQuiz = () => {
showAnswers.length = 0
quizSubmission.reset()
populateQuestions()
setupTimer()
}

const getInstructions = (question) => {
Expand Down
17 changes: 12 additions & 5 deletions frontend/src/pages/QuizForm.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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')"
Expand All @@ -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">
Expand All @@ -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">
Expand All @@ -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()">
Expand Down Expand Up @@ -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,
Expand Down
10 changes: 8 additions & 2 deletions lms/lms/doctype/lms_quiz/lms_quiz.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down
31 changes: 16 additions & 15 deletions lms/lms/doctype/lms_quiz/lms_quiz.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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"]:
Expand All @@ -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,
}
)
Expand Down
Loading