summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPrabhu Ramachandran2011-11-25 18:48:13 +0530
committerPrabhu Ramachandran2011-11-25 18:48:13 +0530
commitfdc531b561565345847812f409ee44af0a784e82 (patch)
tree447b297d28dccb700dcd244404e6cd748191890d
parentb4023e17d6f97e51ffde740c17d19630b5a9c2d1 (diff)
downloadonline_test-fdc531b561565345847812f409ee44af0a784e82.tar.gz
online_test-fdc531b561565345847812f409ee44af0a784e82.tar.bz2
online_test-fdc531b561565345847812f409ee44af0a784e82.zip
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.
-rw-r--r--docs/sample_questions.py19
-rw-r--r--docs/sample_questions.xml8
-rw-r--r--exam/management/commands/dump_user_data.py12
-rw-r--r--exam/management/commands/load_questions_xml.py14
-rw-r--r--exam/models.py15
-rw-r--r--exam/views.py39
-rw-r--r--static/exam/css/base.css4
-rw-r--r--templates/exam/grade_user.html36
-rw-r--r--templates/exam/monitor.html12
-rw-r--r--templates/exam/question.html17
-rw-r--r--templates/exam/user_data.html31
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 <code>fact</code> which takes a single integer argument
(say <code>n</code>) 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 <code>sqr</code> which takes a single
argument and returns the square of the argument. For example: <br/>
<code>sqr(3) -> 9</code>.''',
@@ -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
</description>
<points>2</points>
-<language>python</language>
+<type>python</type>
<test>
assert fact(0) == 1
assert fact(5) == 120
</test>
+<options>
+</options>
</question>
<question>
@@ -27,12 +29,14 @@ returns the square of the argument
For example sqr(3) -> 9.
</description>
<points>1</points>
-<language>python</language>
+<type>python</type>
<test>
import math
assert sqr(3) == 9
assert abs(sqr(math.sqrt(2)) - 2.0) &lt; 1e-14
</test>
+<options>
+</options>
</question>
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 }} <br/>
action="{{URL_ROOT}}/exam/grade_user/{{data.user.username}}/" method="post">
{% csrf_token %}
{% for question, answers in paper.get_question_answers.items %}
-<p><strong> Question: {{ question.id }}. {{ question.summary }} (Points: {{ question.points }})</strong> </p>
-{% for answer in answers %}
-<pre>
-################################################################################
-{{ answer.answer|safe }}
-# Autocheck: {{ answer.error }}
-</pre>
+<p><strong>
+ <a href="{{URL_ROOT}}/admin/exam/question/{{question.id}}">
+ Question: {{ question.id }}. {{ question.summary }} </a>
+ (Points: {{ question.points }})</strong> </p>
+{% if question.type == "mcq" %}
+<p> Choices:
+{% for option in question.options.strip.splitlines %} {{option}}, {% endfor %}
+</p>
+<p>Student answer: {{ answers.0|safe }}</p>
+{% else %}{# non-mcq questions #}
+<pre>
+{% for answer in answers %}################################################################################
+{{ answer.answer.strip|safe }}
+# Autocheck: {{ answer.error|safe }}
+{% endfor %}</pre>
+{% endif %} {# if question.type #}
+{% with answers|last as answer %}
Marks: <input id="q{{ question.id }}" type="text"
name="q{{ question.id }}_marks" size="4"
value="{{ answer.marks }}" />
-{% endfor %} {# for answer in answers #}
+{% endwith %}
{% endfor %} {# for question, answers ... #}
<h3>Teacher comments: </h3>
<textarea id="comments_{{paper.quiz.id}}" rows="10" cols="80"
@@ -59,5 +69,15 @@ Marks: <input id="q{{ question.id }}" type="text"
{% endif %} {# if data.papers #}
+{% if data.papers.count > 1 %}
+<a href="{{URL_ROOT}}/exam/monitor/">
+ Monitor quiz</a>
+{% else %}
+{% with data.papers.0 as paper %}
+<a href="{{URL_ROOT}}/exam/monitor/{{paper.quiz.id}}/">
+ Monitor quiz</a>
+{% endwith %}
+{% endif %}
+<br />
<a href="{{URL_ROOT}}/admin/">Admin</a>
{% endblock content %}
diff --git a/templates/exam/monitor.html b/templates/exam/monitor.html
index a15b2a2..fb6cb58 100644
--- a/templates/exam/monitor.html
+++ b/templates/exam/monitor.html
@@ -6,7 +6,7 @@
{% block content %}
-{% if not quizzes and not papers %}
+{% if not quizzes and not quiz %}
<h1> Quiz results </h1>
<p> No quizzes available. </p>
@@ -27,9 +27,9 @@
{# ############################################################### #}
{# This is rendered when we are just viewing exam/monitor/quiz_num #}
-{% if papers %}
+{% if quiz %}
<h1> {{ quiz.description }} results </h1>
-
+{% if papers %}
{# <p> Quiz: {{ quiz_name }}</p> #}
<p>Number of papers: {{ papers|length }} </p>
@@ -57,9 +57,9 @@
</tr>
{% endfor %}
</table>
-{% else %} {% if quiz %}
- <p> No answer papers so far. </p>
- {% endif %}
+{% else %}
+<p> No answer papers so far. </p>
+{% endif %} {# if papers #}
{% endif %}
<a href="{{URL_ROOT}}/admin/">Admin</a>
diff --git a/templates/exam/question.html b/templates/exam/question.html
index f4d3f67..62b8b3d 100644
--- a/templates/exam/question.html
+++ b/templates/exam/question.html
@@ -60,15 +60,20 @@ function update_time()
<form id="code" action="{{URL_ROOT}}/exam/{{ question.id }}/check/" method="post">
{% csrf_token %}
-
-<textarea rows="20" cols="100" name="answer">
-{% if last_attempt %}{{last_attempt}}{% else %}# Enter your answer here.{% endif %}
-</textarea>
-
+{% if question.type == "mcq" %}
+{% for option in question.options.strip.splitlines %}
+<input name="answer" type="radio" value="{{option}}" />{{option}} <br/>
+{% endfor %}
+{% else %}
+<textarea rows="20" cols="100" name="answer">{% if last_attempt %}{{last_attempt.strip}}{% else %}# Enter your answer here.{% endif %}</textarea>
+{% endif %}
<br/>
-
+{% if question.type == "mcq" %}
+<input id="check" type="submit" name="check" value="Submit answer"/>
+{% else %}
<input id="check" type="submit" name="check" value="Check Answer"
onclick="submitCode();"/>
+{% endif %}
<input id="skip" type="submit" name="skip" value="Skip question" />
</form>
diff --git a/templates/exam/user_data.html b/templates/exam/user_data.html
index 7db0af2..77de5ce 100644
--- a/templates/exam/user_data.html
+++ b/templates/exam/user_data.html
@@ -41,14 +41,21 @@ User IP address: {{ paper.user_ip }}
<h3> Answers </h3>
{% for question, answers in paper.get_question_answers.items %}
<p><strong> Question: {{ question.id }}. {{ question.summary }} (Points: {{ question.points }})</strong> </p>
-{% for answer in answers %}
+{% if question.type == "mcq" %}
+<p> Choices:
+{% for option in question.options.strip.splitlines %} {{option}}, {% endfor %}
+</p>
+<p>Student answer: {{ answers.0|safe }}</p>
+{% else %}{# non-mcq questions #}
<pre>
-################################################################################
-{{ answer.answer|safe }}
-# Autocheck: {{ answer.error }}
-# Marks: {{ answer.marks }}
-</pre>
-{% endfor %} {# for answer in answers #}
+{% for answer in answers %}################################################################################
+{{ answer.answer.strip|safe }}
+# Autocheck: {{ answer.error|safe }}
+{% endfor %}</pre>
+{% endif %}
+{% with answers|last as answer %}
+<p><em>Marks: {{answer.marks}} </em> </p>
+{% endwith %}
{% endfor %} {# for question, answers ... #}
<h3>Teacher comments: </h3>
{{ paper.comments|default:"None" }}
@@ -62,6 +69,16 @@ User IP address: {{ paper.user_ip }}
<a href="{{URL_ROOT}}/exam/grade_user/{{ data.user.username }}/">
Grade/correct paper</a>
<br/>
+{% if data.papers.count > 1 %}
+<a href="{{URL_ROOT}}/exam/monitor/">
+ Monitor quiz</a>
+{% else %}
+{% with data.papers.0 as paper %}
+<a href="{{URL_ROOT}}/exam/monitor/{{paper.quiz.id}}/">
+ Monitor quiz</a>
+{% endwith %}
+{% endif %}
+<br />
<a href="{{URL_ROOT}}/admin/">Admin</a>
{% endblock content %}