Working quiz taking interface
authorCharles Connell <charles@connells.org>
Sat, 1 Mar 2014 15:46:54 +0000 (10:46 -0500)
committerCharles Connell <charles@connells.org>
Sat, 1 Mar 2014 15:46:54 +0000 (10:46 -0500)
karmaworld/apps/quizzes/models.py
karmaworld/apps/quizzes/views.py
karmaworld/assets/css/quiz.css
karmaworld/assets/js/keyword.js [new file with mode: 0644]
karmaworld/assets/js/quiz.js
karmaworld/templates/quizzes/keyword_edit.html
karmaworld/templates/quizzes/quiz.html
karmaworld/urls.py

index cd161b5a5232c212cdc9bd129ad8c2e089f3ed00..76753d7dc5290111f49b8b7e717f713740bf8b31 100644 (file)
@@ -108,4 +108,9 @@ class TrueFalseQuestion(BaseQuizQuestion):
         return self.text
 
 ALL_QUESTION_CLASSES = (MultipleChoiceQuestion, FlashCardQuestion, TrueFalseQuestion)
+ALL_QUESTION_CLASSES_NAMES = {
+    MultipleChoiceQuestion.__name__: MultipleChoiceQuestion,
+    FlashCardQuestion.__name__: FlashCardQuestion,
+    TrueFalseQuestion.__name__: TrueFalseQuestion,
+}
 
index 6b5294c4660cb47c754e2cb7707d5e520dd7b7f3..c67205d70f7b902c51fe75db5a1b0bef76ef9055 100644 (file)
@@ -2,17 +2,20 @@
 # -*- coding:utf8 -*-
 # Copyright (C) 2014  FinalsClub Foundation
 from itertools import chain
+import json
 from django.core.exceptions import ObjectDoesNotExist
 from django.core.urlresolvers import reverse
 from django.forms.formsets import formset_factory
-from django.http import HttpResponseRedirect
+from django.http import HttpResponseRedirect, HttpResponseBadRequest, HttpResponseNotFound, HttpResponse
 
 from django.views.generic import DetailView, FormView
 from django.views.generic.detail import SingleObjectMixin
 from django.views.generic.edit import FormMixin, ProcessFormView
 from karmaworld.apps.notes.models import Note
 from karmaworld.apps.quizzes.forms import KeywordForm
-from karmaworld.apps.quizzes.models import Quiz, ALL_QUESTION_CLASSES, Keyword
+from karmaworld.apps.quizzes.models import Quiz, ALL_QUESTION_CLASSES, Keyword, BaseQuizQuestion, \
+    ALL_QUESTION_CLASSES_NAMES, MultipleChoiceQuestion, MultipleChoiceOption, TrueFalseQuestion, FlashCardQuestion
+from karmaworld.utils import ajax_base
 
 
 class QuizView(DetailView):
@@ -85,3 +88,40 @@ class KeywordEditView(FormView):
 
         return super(KeywordEditView, self).form_valid(formset)
 
+
+def quiz_answer(request):
+    """Handle an AJAX request checking if a quiz answer is correct"""
+    if not (request.method == 'POST' and request.is_ajax()):
+        # return that the api call failed
+        return HttpResponseBadRequest(json.dumps({'status': 'fail', 'message': 'must be a POST ajax request'}),
+                                      mimetype="application/json")
+
+    try:
+        question_type_str = request.POST['question_type']
+        if question_type_str not in ALL_QUESTION_CLASSES_NAMES:
+            raise Exception("Not a valid question type")
+        question_type_class = ALL_QUESTION_CLASSES_NAMES[question_type_str]
+        question = question_type_class.objects.get(id=request.POST['id'])
+
+        correct = False
+
+        if question_type_class is MultipleChoiceQuestion:
+            answer = MultipleChoiceOption.objects.get(id=request.POST['answer'])
+            if answer.question == question and answer.correct:
+                correct = True
+
+        elif question_type_class is TrueFalseQuestion:
+            answer = request.POST['answer'] == 'true'
+            correct = question.true == answer
+
+        elif question_type_class is FlashCardQuestion:
+            answer = request.POST['answer'].lower()
+            correct = question.keyword_side.lower() == answer
+
+    except Exception, e:
+        return HttpResponseBadRequest(json.dumps({'status': 'fail',
+                                                'message': e.message,
+                                                'exception': e.__class__.__name__}),
+                                    mimetype="application/json")
+
+    return HttpResponse(json.dumps({'correct': correct}), mimetype="application/json")
\ No newline at end of file
index 946431af1bd7a3d30c4bba633c80b0633b6d3b58..4b51e874688273dec29bc3d2ba967d72184ae0c9 100644 (file)
 #add-row-btn
 {
   cursor: pointer;
+}
+
+.correct-answer-flash
+{
+  background-color: #67ff50;
+}
+
+.correct-answer
+{
+  background-color: #b5ffaa;
+}
+
+.wrong-answer-flash
+{
+  background-color: #ff3a3a;
+}
+
+.wrong-answer
+{
+  background-color: #ffb4b4;
 }
\ No newline at end of file
diff --git a/karmaworld/assets/js/keyword.js b/karmaworld/assets/js/keyword.js
new file mode 100644 (file)
index 0000000..6f0e6f1
--- /dev/null
@@ -0,0 +1,41 @@
+
+function tabHandler(event) {
+  // check for:
+  // key pressed was TAB
+  // key was pressed in last row
+  if (event.which == 9 &&
+      (!$(this).closest('div.keyword-form-row').next().hasClass('keyword-form-row'))) {
+    addForm(event);
+  }
+}
+
+function addForm(event) {
+  var prototypeForm = $('#keyword-form-prototype div.keyword-form-row').clone().appendTo('#keyword-form-rows');
+  var newForm = $('.keyword-form-row:last');
+  var totalForms = $('#id_form-TOTAL_FORMS').attr('value');
+  var newIdRoot = 'id_form-' + totalForms + '-';
+  var newNameRoot = 'form-' + totalForms + '-';
+
+  var keywordInput = newForm.find('.keyword');
+  keywordInput.attr('id', newIdRoot + 'keyword');
+  keywordInput.attr('name', newNameRoot + 'keyword');
+
+  var definitionInput = newForm.find('.definition');
+  definitionInput.attr('id', newIdRoot + 'definition');
+  definitionInput.attr('name', newNameRoot + 'definition');
+  definitionInput.keydown(tabHandler);
+
+  var objectIdInput = newForm.find('.object-id');
+  objectIdInput.attr('id', newIdRoot + 'id');
+  objectIdInput.attr('name', newNameRoot + 'id');
+
+  $('#id_form-TOTAL_FORMS').attr('value', parseInt(totalForms)+1);
+}
+
+$(function() {
+  $('.definition').keydown(tabHandler);
+  $('#add-row-btn').click(addForm);
+});
+
+
+
index fb863015eaa7b41cfdadbdb293c1cc8495ac4aad..f78f4dc26fd499ca8846d4d79287913a5312f270 100644 (file)
@@ -1,39 +1,80 @@
 
-function tabHandler(event) {
-  // check for:
-  // key pressed was TAB
-  // key was pressed in last row
-  if (event.which == 9 &&
-      (!$(this).closest('div.keyword-form-row').next().hasClass('keyword-form-row'))) {
-    addForm(event);
+
+function showQuestion() {
+  $('div.question').hide();
+  $('div[data-question-index="' + current_question_index + '"]').show();
+}
+
+function nextQuestion() {
+  if (current_question_index+1 < num_quiz_questions) {
+    current_question_index += 1;
+    showQuestion();
   }
 }
 
-function addForm(event) {
-  var prototypeForm = $('#keyword-form-prototype div.keyword-form-row').clone().appendTo('#keyword-form-rows');
-  var newForm = $('.keyword-form-row:last');
-  var totalForms = $('#id_form-TOTAL_FORMS').attr('value');
-  var newIdRoot = 'id_form-' + totalForms + '-';
-  var newNameRoot = 'form-' + totalForms + '-';
+function prevQuestion() {
+  if (current_question_index-1 >= 0) {
+    current_question_index -= 1;
+    showQuestion();
+  }
+}
 
-  var keywordInput = newForm.find('.keyword');
-  keywordInput.attr('id', newIdRoot + 'keyword');
-  keywordInput.attr('name', newNameRoot + 'keyword');
+function checkAnswerCallback(data, textStatus, jqXHR) {
+  var question = $('div[data-question-index="' + current_question_index + '"]');
+  var question_text = question.find('p.question-text');
+  if (data.correct == true) {
+    question_text.removeClass('wrong-answer');
+    question_text.removeClass('correct-answer');
+    question_text.addClass('correct-answer-flash');
+    question_text.switchClass('correct-answer-flash', 'correct-answer',1000);
+  }
+  if (data.correct == false) {
+    question_text.removeClass('wrong-answer');
+    question_text.removeClass('correct-answer');
+    question_text.addClass('wrong-answer-flash');
+    question_text.switchClass('wrong-answer-flash', 'wrong-answer',1000);
+  }
+}
 
-  var definitionInput = newForm.find('.definition');
-  definitionInput.attr('id', newIdRoot + 'definition');
-  definitionInput.attr('name', newNameRoot + 'definition');
-  definitionInput.keydown(tabHandler);
+function checkAnswer() {
+  var question = $('div[data-question-index="' + current_question_index + '"]');
+  var question_id = question.attr('data-question-id');
+  var question_type = question.attr('data-question-type');
 
-  var objectIdInput = newForm.find('.object-id');
-  objectIdInput.attr('id', newIdRoot + 'id');
-  objectIdInput.attr('name', newNameRoot + 'id');
+  var chosen_answer = null;
 
-  $('#id_form-TOTAL_FORMS').attr('value', parseInt(totalForms)+1);
-}
+  if (question_type == 'MultipleChoiceQuestion') {
+    var checked = question.find('input:checked');
+    if (checked.length == 1) {
+      chosen_answer = checked[0].getAttribute('data-choice-id');
+    }
+  }
+
+  else if (question_type == 'FlashCardQuestion') {
+    chosen_answer = question.find('input').val();
+  }
 
-$(function() {
-  $('.definition').keydown(tabHandler);
-  $('#add-row-btn').click(addForm);
-});
+  else if (question_type == 'TrueFalseQuestion') {
+    var checked = question.find('input:checked');
+    if (checked.attr('value') == 'true') {
+      chosen_answer = true;
+    }
+    else if (checked.attr('value') == 'false') {
+     chosen_answer = false;
+    }
+  }
+
+  message = {
+    question_type: question_type,
+    id: question_id,
+    answer: chosen_answer
+  };
+  $.post(check_answer_url, message, checkAnswerCallback);
+}
 
+$(function () {
+  showQuestion();
+  $('button.check-answer').click(checkAnswer);
+  $('button.prev-question').click(prevQuestion);
+  $('button.next-question').click(nextQuestion);
+});
\ No newline at end of file
index eb8af251397af3ff6f776d44717856cde9eea60b..52b3d43c65df76afb4bc0f4cb7ca3a7d2bd63c82 100644 (file)
@@ -7,7 +7,7 @@
 {% endblock %}
 
 {% block bodyscripts %}
-  <script src="{{ STATIC_URL }}js/quiz.js"></script>
+  <script src="{{ STATIC_URL }}js/keyword.js"></script>
 {% endblock %}
 
 {% block title %}
index c69140958fcc02533fd5c7a53d7711d65c689b88..4b2ac84a2fcdccb444f4ada31da043fd1f019cbc 100644 (file)
@@ -6,6 +6,17 @@
   <link rel="stylesheet" type="text/css" media="all" href="{{ STATIC_URL }}css/quiz.css">
 {% endblock %}
 
+{% block bodyscripts %}
+  <script>
+    var csrf_token = "{{ csrf_token }}";
+    var num_quiz_questions = {{ questions|length }};
+    var current_question_index = 0;
+    var check_answer_url = '{% url 'quiz_answer' %}';
+  </script>
+  <script src="{{ STATIC_URL }}js/setup-ajax.js"></script>
+  <script src="{{ STATIC_URL }}js/quiz.js"></script>
+{% endblock %}
+
 {% block title %}
   {{ quiz.name }}
 {% endblock %}
         </div>
       {% endif %}
 
-      <form action="{{ quiz.get_absolute_url }}" method="post">
-        {% csrf_token %}
-        {% for item in questions %}
-
-          <div class="row">
-            <div class="small-10 columns small-centered quiz-center">
+      {% for item in questions %}
+        {% with question=item.1 %}
+          <div class="row hide question"
+               data-question-id="{{ question.id }}"
+               data-question-index="{{ forloop.counter0 }}"
+               data-question-type="{{ item.0 }}">
+            <div class="small-10 large-6 columns small-centered">
               {% if 'MultipleChoiceQuestion' in item.0 %}
-                {% with question=item.1 %}
-                  <div class="question multiple-choice">
-                    <p>{{ question.question_text }}</p>
-                    <ul>
-                      {% for choice in question.choices.all %}
-                        <li><input id="choice_{{ question.id }}_{{ choice.id }}"
-                                   type="radio"
-                                   name="{{ question.id }}"
-                                   value="{{ choice.id }}">
-                          <label for="choice_{{ question.id }}_{{ choice.id }}">{{ choice.text }}</label></li>
-                      {% endfor %}
-                    </ul>
-                  </div>
-                {% endwith %}
-              {% endif %}
-
-              {% if 'TrueFalseQuestion' in item.0 %}
-                {% with question=item.1 %}
-                  <div class="question true-false">
-                    <p>{{ question.text }}</p>
-                    <li><input id="choice_{{ question.id }}_true"
+                <p class="question-text">{{ question.question_text }}</p>
+                <ul>
+                  {% for choice in question.choices.all %}
+                    <li><input id="choice_{{ question.id }}_{{ choice.id }}"
                                type="radio"
                                name="{{ question.id }}"
-                               value="true">
-                    <label for="choice_{{ question.id }}_true">True</label></li>
-                    <li><input id="choice_{{ question.id }}_false"
-                               type="radio"
-                               name="{{ question.id }}"
-                               value="false">
-                    <label for="choice_{{ question.id }}_false">False</label></li>
-                  </div>
-                {% endwith %}
+                               value="{{ choice.id }}"
+                               data-choice-id="{{ choice.id }}">
+                      <label for="choice_{{ question.id }}_{{ choice.id }}">{{ choice.text }}</label></li>
+                  {% endfor %}
+                </ul>
               {% endif %}
 
-              {% if 'FlashCardQuestion' in item.0 %}
-                {% with question=item.1 %}
-                  <div class="question flash-card">
-                    <p>{{ question.definition_side }}</p>
-                    <input type="text"
+              {% if 'TrueFalseQuestion' in item.0 %}
+                <p class="question-text">{{ question.text }}</p>
+                <li><input id="choice_{{ question.id }}_true"
+                           type="radio"
+                           name="{{ question.id }}"
+                           value="true">
+                <label for="choice_{{ question.id }}_true">True</label></li>
+                <li><input id="choice_{{ question.id }}_false"
+                           type="radio"
                            name="{{ question.id }}"
-                           id="text_{{ question.id }}">
+                           value="false">
+                <label for="choice_{{ question.id }}_false">False</label></li>
+              {% endif %}
 
-                  </div>
-                {% endwith %}
+              {% if 'FlashCardQuestion' in item.0 %}
+                <p class="question-text">{{ question.definition_side }}</p>
+                <input type="text"
+                       name="{{ question.id }}"
+                       id="text_{{ question.id }}">
               {% endif %}
+
+              <div class="row">
+                <div class="small-4 columns">
+                  <button class="prev-question {% if forloop.first %}disabled{% endif %}">Previous</button>
+                </div>
+                <div class="small-4 columns">
+                  <button class="check-answer">Check Answer</button>
+                </div>
+                <div class="small-4 columns">
+                  <button class="next-question {% if forloop.last %}disabled{% endif %}">Next</button>
+                </div>
+              </div>
+
             </div>
           </div>
-          <br/>
-        {% endfor %}
-        <div class="row">
-          <div class="small-12 columns center">
-            <button type="submit">Submit</button>
-          </div>
-        </div>
-      </form>
+        {% endwith %}
+      {% endfor %}
 
     </div>
 
index 0834cd8eb62cfc421f76a1d9ec18ec020eb6f02b..6f2046b2d4ec577e586911a8585c5bde251e8dfc 100644 (file)
@@ -19,7 +19,7 @@ from karmaworld.apps.notes.views import NoteView, thank_note, NoteSearchView, fl
 from karmaworld.apps.notes.views import RawNoteDetailView
 from karmaworld.apps.moderation import moderator
 from karmaworld.apps.document_upload.views import save_fp_upload
-from karmaworld.apps.quizzes.views import QuizView, KeywordEditView
+from karmaworld.apps.quizzes.views import QuizView, KeywordEditView, quiz_answer
 from karmaworld.apps.users.views import ProfileView
 
 # See: https://docs.djangoproject.com/en/dev/ref/contrib/admin/#hooking-adminsite-instances-into-your-urlconf
@@ -102,6 +102,9 @@ urlpatterns = patterns('',
     # Ajax endpoint to edit a course
     url(r'^ajax/course/edit/(?P<pk>[\d]+)/$', edit_course, name='edit_course'),
 
+    # Check if a quiz answer is correct
+    url(r'^ajax/quiz/check/$', quiz_answer, name='quiz_answer'),
+
     # Valid url cases to the Note page
     # a: school/course/id
     # b: school/course/id/slug