diff options
author | Prabhu Ramachandran | 2011-11-25 18:48:13 +0530 |
---|---|---|
committer | Prabhu Ramachandran | 2011-11-25 18:48:13 +0530 |
commit | fdc531b561565345847812f409ee44af0a784e82 (patch) | |
tree | 447b297d28dccb700dcd244404e6cd748191890d | |
parent | b4023e17d6f97e51ffde740c17d19630b5a9c2d1 (diff) | |
download | online_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.py | 19 | ||||
-rw-r--r-- | docs/sample_questions.xml | 8 | ||||
-rw-r--r-- | exam/management/commands/dump_user_data.py | 12 | ||||
-rw-r--r-- | exam/management/commands/load_questions_xml.py | 14 | ||||
-rw-r--r-- | exam/models.py | 15 | ||||
-rw-r--r-- | exam/views.py | 39 | ||||
-rw-r--r-- | static/exam/css/base.css | 4 | ||||
-rw-r--r-- | templates/exam/grade_user.html | 36 | ||||
-rw-r--r-- | templates/exam/monitor.html | 12 | ||||
-rw-r--r-- | templates/exam/question.html | 17 | ||||
-rw-r--r-- | 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 <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) < 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 %} |