feat: Quiz attempt limits is configurable

- Added a 'no limit' check to `check_quiz_completion` in utils.py
- Added disabled flag for questions in quiz component
- Refactored `get_quiz_without_answers` to send attempt limit data to client
This commit is contained in:
scmmishra 2019-03-29 12:45:08 +05:30
parent d4ed56af02
commit 2b7e158e50
5 changed files with 39 additions and 19 deletions

View File

@ -135,7 +135,7 @@ def check_content_completion(content_name, content_type, enrollment_name):
def check_quiz_completion(quiz, enrollment_name): def check_quiz_completion(quiz, enrollment_name):
attempts = frappe.get_all("Quiz Activity", filters={'enrollment': enrollment_name, 'quiz': quiz.name}, fields=["name", "activity_date", "score", "status"]) attempts = frappe.get_all("Quiz Activity", filters={'enrollment': enrollment_name, 'quiz': quiz.name}, fields=["name", "activity_date", "score", "status"])
status = bool(len(attempts) == quiz.max_attempts) status = False if quiz.max_attempts == 0 else bool(len(attempts) == quiz.max_attempts)
score = None score = None
result = None result = None
if attempts: if attempts:

View File

@ -10,21 +10,21 @@
<hr> <hr>
<div id="quiz" :name="content"> <div id="quiz" :name="content">
<div id="quiz-body"> <div id="quiz-body">
<component v-for="question in quizData" :key="question.name" v-bind:is="question.type" :question="question" @updateResponse="updateResponse"></component> <component v-for="question in quizData" :key="question.name" v-bind:is="question.type" :question="question" @updateResponse="updateResponse" :isDisabled="isDisabled"></component>
</div> </div>
<div class="mt-3"> <div class="mt-3">
<div> <div>
<div v-if="submitted" id="post-quiz-actions" class="row"> <div v-if="isDisabled || submitted" id="post-quiz-actions" class="row">
<div class="col-md-8 text-left"> <div class="col-md-8 text-left">
<h3>Your Score: <span id="result">{{ score }}</span></h3> <span v-html="message"></span>
</div> </div>
<div class="col-md-4 text-right"> <div class="col-md-4 text-right">
<slot></slot> <slot></slot>
</div> </div>
</div> </div>
<div v-else id="quiz-actions" class="text-right"> <div v-else id="quiz-actions" class="text-right">
<button class='btn btn-outline-secondary' type="reset">Reset</button> <button class='btn btn-outline-secondary' type="reset" :disabled="isDisabled">Reset</button>
<button class='btn btn-primary' @click="submitQuiz" type="button">Submit</button> <button class='btn btn-primary' @click="submitQuiz" type="button" :disabled="isDisabled">Submit</button>
</div> </div>
</div> </div>
</div> </div>
@ -50,12 +50,16 @@ export default {
quizData: '', quizData: '',
quizResponse: {}, quizResponse: {},
score: '', score: '',
submitted: false submitted: false,
isDisabled: false,
quizStatus: {},
} }
}, },
mounted() { mounted() {
this.getQuizWithoutAnswers().then(data => { this.getQuizWithoutAnswers().then(data => {
this.quizData = data this.quizData = data.quizData
this.quizStatus = data.status
this.isDisabled = data.status.is_complete
}); });
}, },
components: { components: {
@ -67,6 +71,7 @@ export default {
return lms.call("get_quiz_without_answers", return lms.call("get_quiz_without_answers",
{ {
quiz_name: this.content, quiz_name: this.content,
course_name: this.$route.params.course_name
} }
) )
}, },
@ -81,8 +86,8 @@ export default {
course: this.$route.params.course_name course: this.$route.params.course_name
} }
).then(data => { ).then(data => {
this.score = data, this.score = data
this.submitted = true, this.submitted = true
this.quizResponse = null this.quizResponse = null
}); });
} }
@ -96,6 +101,16 @@ export default {
return 'QuizSingleChoice' return 'QuizSingleChoice'
} }
}, },
message: function() {
if(this.submitted) {
return '<h3>Your Score: <span id="result">'+ this.score +'</span></h3>'
}
let message = '<h4>You have exhausted all attempts for this quiz.</h4>'
if(this.quizStatus.result == 'Pass') {
message = "<h4>You have successfully completed this quiz.</h4>Score: " + this.quizStatus.score
}
return message
}
}, },
}; };
</script> </script>

View File

@ -3,7 +3,7 @@
<h5>{{ question.question }}</h5> <h5>{{ question.question }}</h5>
<div class="options ml-2"> <div class="options ml-2">
<div v-for="option in question.options" :key="option.name" class="form-check pb-1"> <div v-for="option in question.options" :key="option.name" class="form-check pb-1">
<input v-model="checked" class="form-check-input" type="checkbox" :name="question.name" :id="option.name" :value="option.name" @change="emitResponse(question.name, option.name)"> <input v-model="checked" class="form-check-input" type="checkbox" :name="question.name" :id="option.name" :value="option.name" @change="emitResponse(question.name, option.name)" :disabled="isDisabled">
<label class="form-check-label" :for="option.name"> <label class="form-check-label" :for="option.name">
{{ option.option }} {{ option.option }}
</label> </label>
@ -14,7 +14,7 @@
<script> <script>
export default { export default {
props: ['question'], props: ['question', 'isDisabled'],
name: 'QuizSingleChoice', name: 'QuizSingleChoice',
data() { data() {
return { return {

View File

@ -3,7 +3,7 @@
<h5>{{ question.question }}</h5> <h5>{{ question.question }}</h5>
<div class="options ml-2"> <div class="options ml-2">
<div v-for="option in question.options" :key="option.name" class="form-check pb-1"> <div v-for="option in question.options" :key="option.name" class="form-check pb-1">
<input class="form-check-input" type="radio" :name="question.name" :id="option.name" :value="option.name" @change="emitResponse(question.name, option.name)"> <input class="form-check-input" type="radio" :name="question.name" :id="option.name" :value="option.name" @change="emitResponse(question.name, option.name)" :disabled="isDisabled">
<label class="form-check-label" :for="option.name"> <label class="form-check-label" :for="option.name">
{{ option.option }} {{ option.option }}
</label> </label>
@ -14,7 +14,7 @@
<script> <script>
export default { export default {
props: ['question'], props: ['question', 'isDisabled'],
name: 'QuizSingleChoice', name: 'QuizSingleChoice',
methods: { methods: {
emitResponse(q, o) { emitResponse(q, o) {

View File

@ -93,15 +93,20 @@ def get_quiz_with_answers(quiz_name):
return None return None
@frappe.whitelist() @frappe.whitelist()
def get_quiz_without_answers(quiz_name): def get_quiz_without_answers(quiz_name, course_name):
try: try:
quiz = frappe.get_doc("Quiz", quiz_name).get_questions() quiz = frappe.get_doc("Quiz", quiz_name)
quiz_output = [{'name':question.name, 'question':question.question, 'type': question.type, 'options':[{'name': option.name, 'option':option.option} for option in question.options]} for question in quiz] questions = quiz.get_questions()
return quiz_output
except: except:
frappe.throw("Quiz {0} does not exist".format(quiz_name)) frappe.throw("Quiz {0} does not exist".format(quiz_name))
return None return None
enrollment = utils.get_course_enrollment(course_name).name
quiz_status = {}
quiz_status['is_complete'], quiz_status['score'], quiz_status['result'] = utils.check_quiz_completion(quiz, enrollment)
quiz_output = [{'name':question.name, 'question':question.question, 'type': question.type, 'options':[{'name': option.name, 'option':option.option} for option in question.options]} for question in questions]
return { 'quizData': quiz_output, 'status': quiz_status}
@frappe.whitelist() @frappe.whitelist()
def evaluate_quiz(course, quiz_response, quiz_name): def evaluate_quiz(course, quiz_response, quiz_name):
"""LMS Function: Evaluates a simple multiple choice quiz. """LMS Function: Evaluates a simple multiple choice quiz.