From fdc531b561565345847812f409ee44af0a784e82 Mon Sep 17 00:00:00 2001
From: Prabhu Ramachandran
Date: Fri, 25 Nov 2011 18:48:13 +0530
Subject: ENH: Adding support for Multiple Choice Questions
Adds simple support for multiple choice questions that are also
auto-checked. Many fixes to the templates and useful feature additions.
This changes the database.
---
docs/sample_questions.py | 19 +++++++++++--
docs/sample_questions.xml | 8 ++++--
exam/management/commands/dump_user_data.py | 12 ++++++--
exam/management/commands/load_questions_xml.py | 14 +++++----
exam/models.py | 15 ++++++----
exam/views.py | 39 ++++++++++++++++----------
static/exam/css/base.css | 4 +--
templates/exam/grade_user.html | 36 ++++++++++++++++++------
templates/exam/monitor.html | 12 ++++----
templates/exam/question.html | 17 +++++++----
templates/exam/user_data.html | 31 +++++++++++++++-----
11 files changed, 145 insertions(+), 62 deletions(-)
diff --git a/docs/sample_questions.py b/docs/sample_questions.py
index eac9479..5af9c4b 100644
--- a/docs/sample_questions.py
+++ b/docs/sample_questions.py
@@ -4,7 +4,7 @@ questions = [
Question(
summary='Factorial',
points=2,
- language="python",
+ type="python",
description='''
Write a function called fact
which takes a single integer argument
(say n
) and returns the factorial of the number.
@@ -19,7 +19,7 @@ assert fact(5) == 120
Question(
summary='Simple function',
points=1,
- language="python",
+ type="python",
description='''Create a simple function called sqr
which takes a single
argument and returns the square of the argument. For example:
sqr(3) -> 9
.''',
@@ -31,7 +31,7 @@ assert abs(sqr(math.sqrt(2)) - 2.0) < 1e-14
Question(
summary='Bash addition',
points=2,
- language="bash",
+ type="bash",
description='''Write a shell script which takes two arguments on the
command line and prints the sum of the two on the output.''',
test='''\
@@ -41,6 +41,19 @@ Question(
1 2
2 1
'''),
+Question(
+ summary='Size of integer in Python',
+ points=0.5,
+ type="mcq",
+ description='''What is the largest integer value that can be represented
+in Python?''',
+ options='''No Limit
+2**32
+2**32 - 1
+None of the above
+''',
+ test = "No Limit"
+ ),
]
quiz = Quiz(start_date=date.today(),
diff --git a/docs/sample_questions.xml b/docs/sample_questions.xml
index cad205b..53c76f8 100644
--- a/docs/sample_questions.xml
+++ b/docs/sample_questions.xml
@@ -10,11 +10,13 @@ and returns the factorial of the number.
For example fact(3) -> 6
2
-python
+python
assert fact(0) == 1
assert fact(5) == 120
+
+
@@ -27,12 +29,14 @@ returns the square of the argument
For example sqr(3) -> 9.
1
-python
+python
import math
assert sqr(3) == 9
assert abs(sqr(math.sqrt(2)) - 2.0) < 1e-14
+
+
diff --git a/exam/management/commands/dump_user_data.py b/exam/management/commands/dump_user_data.py
index f081565..6e0ca2a 100644
--- a/exam/management/commands/dump_user_data.py
+++ b/exam/management/commands/dump_user_data.py
@@ -34,12 +34,20 @@ Answers
-------
{% for question, answers in paper.get_question_answers.items %}
Question: {{ question.id }}. {{ question.summary }} (Points: {{ question.points }})
+{% if question.type == "mcq" %}\
+###############################################################################
+Choices: {% for option in question.options.strip.splitlines %} {{option}}, {% endfor %}
+Student answer: {{ answers.0|safe }}
+{% else %}{# non-mcq questions #}\
{% for answer in answers %}\
###############################################################################
-{{ answer.answer|safe }}
+{{ answer.answer.strip|safe }}
# Autocheck: {{ answer.error|safe }}
-# Marks: {{ answer.marks }}
{% endfor %}{# for answer in answers #}\
+{% endif %}\
+{% with answers|last as answer %}\
+Marks: {{answer.marks}}
+{% endwith %}\
{% endfor %}{# for question, answers ... #}\
Teacher comments
diff --git a/exam/management/commands/load_questions_xml.py b/exam/management/commands/load_questions_xml.py
index b4151ae..8bc2701 100644
--- a/exam/management/commands/load_questions_xml.py
+++ b/exam/management/commands/load_questions_xml.py
@@ -35,20 +35,24 @@ def load_questions_xml(filename):
desc_node = question.getElementsByTagName("description")[0]
description = (desc_node.childNodes[0].data).strip()
- lang_node = question.getElementsByTagName("language")[0]
- language = (lang_node.childNodes[0].data).strip()
+ type_node = question.getElementsByTagName("type")[0]
+ type = (type_node.childNodes[0].data).strip()
points_node = question.getElementsByTagName("points")[0]
- points = int((points_node.childNodes[0].data).strip()) \
- if points_node else 1
+ points = float((points_node.childNodes[0].data).strip()) \
+ if points_node else 1.0
test_node = question.getElementsByTagName("test")[0]
test = decode_html((test_node.childNodes[0].data).strip())
+ opt_node = question.getElementsByTagName("options")[0]
+ opt = decode_html((opt_node.childNodes[0].data).strip())
+
new_question = Question(summary=summary,
description=description,
points=points,
- language=language,
+ options=opt,
+ type=type,
test=test)
new_question.save()
diff --git a/exam/models.py b/exam/models.py
index ef4312f..717e02e 100644
--- a/exam/models.py
+++ b/exam/models.py
@@ -12,9 +12,10 @@ class Profile(models.Model):
position = models.CharField(max_length=64)
-LANGUAGE_CHOICES = (
+QUESTION_TYPE_CHOICES = (
("python", "Python"),
("bash", "Bash"),
+ ("mcq", "MultipleChoice"),
)
################################################################################
@@ -28,14 +29,16 @@ class Question(models.Model):
description = models.TextField()
# Number of points for the question.
- points = models.IntegerField(default=1)
+ points = models.FloatField(default=1.0)
# Test cases for the question in the form of code that is run.
- # This is simple Python code.
- test = models.TextField()
+ test = models.TextField(blank=True)
- # The language being tested.
- language = models.CharField(max_length=10, choices=LANGUAGE_CHOICES)
+ # Any multiple choice options. Place one option per line.
+ options = models.TextField(blank=True)
+
+ # The type of question.
+ type = models.CharField(max_length=24, choices=QUESTION_TYPE_CHOICES)
# Is this question active or not. If it is inactive it will not be used
# when creating a QuestionPaper.
diff --git a/exam/views.py b/exam/views.py
index ed73adf..e8e2e73 100644
--- a/exam/views.py
+++ b/exam/views.py
@@ -200,24 +200,31 @@ def check(request, q_id):
new_answer = Answer(question=question, answer=answer, correct=False)
new_answer.save()
paper.answers.add(new_answer)
-
- # Otherwise we were asked to check. We obtain the results via XML-RPC
- # with the code executed safely in a separate process (the python_server.py)
- # running as nobody.
- user_dir = get_user_dir(user)
- success, err_msg = code_server.run_code(answer, question.test,
- user_dir, question.language)
- new_answer.error = err_msg
-
- if success:
- # Note the success and save it along with the marks.
- new_answer.correct = success
- new_answer.marks = question.points
+
+ # If we were not skipped, we were asked to check. For any non-mcq
+ # questions, we obtain the results via XML-RPC with the code executed
+ # safely in a separate process (the code_server.py) running as nobody.
+ if question.type == 'mcq':
+ success = True # Only one attempt allowed for MCQ's.
+ if answer.strip() == question.test.strip():
+ new_answer.correct = True
+ new_answer.marks = question.points
+ new_answer.error = 'Correct answer'
+ else:
+ new_answer.error = 'Incorrect answer'
+ else:
+ user_dir = get_user_dir(user)
+ success, err_msg = code_server.run_code(answer, question.test,
+ user_dir, question.type)
+ new_answer.error = err_msg
+ if success:
+ # Note the success and save it along with the marks.
+ new_answer.correct = success
+ new_answer.marks = question.points
new_answer.save()
- ci = RequestContext(request)
- if not success:
+ if not success: # Should only happen for non-mcq questions.
time_left = paper.time_left()
if time_left == 0:
return complete(request, reason='Your time is up!')
@@ -228,6 +235,7 @@ def check(request, q_id):
'paper': paper, 'last_attempt': answer,
'quiz_name': paper.quiz.description,
'time_left': time_left}
+ ci = RequestContext(request)
return my_render_to_response('exam/question.html', context,
context_instance=ci)
@@ -271,6 +279,7 @@ def monitor(request, quiz_id=None):
quiz = Quiz.objects.get(id=quiz_id)
except Quiz.DoesNotExist:
papers = []
+ quiz = None
else:
papers = QuestionPaper.objects.filter(quiz=quiz,
user__profile__isnull=False)
diff --git a/static/exam/css/base.css b/static/exam/css/base.css
index d51be30..1323116 100644
--- a/static/exam/css/base.css
+++ b/static/exam/css/base.css
@@ -2,8 +2,8 @@ body { font-family: 'Georgia', serif; font-size: 17px; color: #000; back
h1, h2, h3, h4 { font-family: 'Garamond', 'Georgia', serif; font-weight: normal; }
h1 { margin: 0 0 30px 0; font-size: 36px;}
h1 span { display: none; }
-h2 { font-size: 24px; margin: 15px 0 5px 0; }
-h3 { font-size: 19px; margin: 15px 0 5px 0; }
+h2 { font-size: 26px; margin: 15px 0 5px 0; }
+h3 { font-size: 22px; margin: 15px 0 5px 0; }
h4 { font-size: 15px; margin: 15px 0 5px 0; }
.box { width: 700px; margin: 10px auto ; }
diff --git a/templates/exam/grade_user.html b/templates/exam/grade_user.html
index 2994ee2..2a109af 100644
--- a/templates/exam/grade_user.html
+++ b/templates/exam/grade_user.html
@@ -35,17 +35,27 @@ Start time: {{ paper.start_time }}
action="{{URL_ROOT}}/exam/grade_user/{{data.user.username}}/" method="post">
{% csrf_token %}
{% for question, answers in paper.get_question_answers.items %}
-
Question: {{ question.id }}. {{ question.summary }} (Points: {{ question.points }})
-{% for answer in answers %}
-
-################################################################################
-{{ answer.answer|safe }}
-# Autocheck: {{ answer.error }}
-
+
+
+ Question: {{ question.id }}. {{ question.summary }}
+ (Points: {{ question.points }})
+{% if question.type == "mcq" %}
+ Choices:
+{% for option in question.options.strip.splitlines %} {{option}}, {% endfor %}
+
+Student answer: {{ answers.0|safe }}
+{% else %}{# non-mcq questions #}
+
+{% for answer in answers %}################################################################################
+{{ answer.answer.strip|safe }}
+# Autocheck: {{ answer.error|safe }}
+{% endfor %}
+{% endif %} {# if question.type #}
+{% with answers|last as answer %}
Marks:
-{% endfor %} {# for answer in answers #}
+{% endwith %}
{% endfor %} {# for question, answers ... #}
Teacher comments: