diff options
-rw-r--r-- | yaksh/evaluator_tests/test_python_evaluation.py | 154 | ||||
-rw-r--r-- | yaksh/python_code_evaluator.py | 6 | ||||
-rw-r--r-- | yaksh/static/yaksh/css/base.css | 36 | ||||
-rw-r--r-- | yaksh/static/yaksh/css/question.css | 2 | ||||
-rw-r--r-- | yaksh/templates/base.html | 2 | ||||
-rw-r--r-- | yaksh/templates/yaksh/add_question.html | 1 | ||||
-rw-r--r-- | yaksh/templates/yaksh/login.html | 2 | ||||
-rw-r--r-- | yaksh/templates/yaksh/question.html | 113 | ||||
-rw-r--r-- | yaksh/views.py | 33 |
9 files changed, 246 insertions, 103 deletions
diff --git a/yaksh/evaluator_tests/test_python_evaluation.py b/yaksh/evaluator_tests/test_python_evaluation.py index 0478353..c55f04f 100644 --- a/yaksh/evaluator_tests/test_python_evaluation.py +++ b/yaksh/evaluator_tests/test_python_evaluation.py @@ -2,52 +2,146 @@ import unittest import os from yaksh.python_code_evaluator import PythonCodeEvaluator from yaksh.settings import SERVER_TIMEOUT +from textwrap import dedent + class PythonEvaluationTestCases(unittest.TestCase): def setUp(self): self.language = "Python" self.test = None - self.test_case_data = [{"func_name": "add", - "expected_answer": "5", - "test_id": u'null', - "pos_args": ["3", "2"], - "kw_args": {} + self.test_case_data = [{"func_name": "add", + "expected_answer": "5", + "test_id": u'null', + "pos_args": ["3", "2"], + "kw_args": {} }] - self.timeout_msg = ("Code took more than {0} seconds to run. " - "You probably have an infinite loop in your code.").format(SERVER_TIMEOUT) def test_correct_answer(self): - user_answer = "def add(a, b):\n\treturn a + b""" - get_class = PythonCodeEvaluator(self.test_case_data, self.test, self.language, user_answer, ref_code_path=None, in_dir=None) - result = get_class.evaluate() + user_answer = dedent(""" + def add(a, b): + return a + b + """) + get_evaluator = PythonCodeEvaluator(self.test_case_data, self.test, + self.language, user_answer) + result = get_evaluator.evaluate() self.assertTrue(result.get("success")) self.assertEqual(result.get("error"), "Correct answer") def test_incorrect_answer(self): - user_answer = "def add(a, b):\n\treturn a - b" - test_case_data = [{"func_name": "add", - "expected_answer": "5", - "test_id": u'null', - "pos_args": ["3", "2"], - "kw_args": {} - }] - get_class = PythonCodeEvaluator(self.test_case_data, self.test, self.language, user_answer, ref_code_path=None, in_dir=None) - result = get_class.evaluate() + user_answer = dedent(""" + def add(a, b): + return a - b + """) + get_evaluator = PythonCodeEvaluator(self.test_case_data, self.test, + self.language, user_answer) + result = get_evaluator.evaluate() self.assertFalse(result.get("success")) self.assertEqual(result.get("error"), "AssertionError in: assert add(3, 2) == 5") def test_infinite_loop(self): - user_answer = "def add(a, b):\n\twhile True:\n\t\tpass""" - test_case_data = [{"func_name": "add", - "expected_answer": "5", - "test_id": u'null', - "pos_args": ["3", "2"], - "kw_args": {} - }] - get_class = PythonCodeEvaluator(self.test_case_data, self.test, self.language, user_answer, ref_code_path=None, in_dir=None) - result = get_class.evaluate() + user_answer = dedent(""" + def add(a, b): + while True: + pass + """) + timeout_msg = ("Code took more than {0} seconds to run. " + "You probably have an infinite loop in your code.").format(SERVER_TIMEOUT) + get_evaluator = PythonCodeEvaluator(self.test_case_data, self.test, + self.language, user_answer) + result = get_evaluator.evaluate() + self.assertFalse(result.get("success")) + self.assertEquals(result.get("error"), timeout_msg) + + def test_syntax_error(self): + user_answer = dedent(""" + def add(a, b); + return a + b + """) + syntax_error_msg = ["Traceback", "call", "File", "line", "<string>", + "SyntaxError", "invalid syntax"] + get_evaluator = PythonCodeEvaluator(self.test_case_data, self.test, + self.language, user_answer) + result = get_evaluator.evaluate() + err = result.get("error").splitlines() + self.assertFalse(result.get("success")) + self.assertEqual(5, len(err)) + for msg in syntax_error_msg: + self.assertIn(msg, result.get("error")) + + def test_indent_error(self): + user_answer = dedent(""" + def add(a, b): + return a + b + """) + indent_error_msg = ["Traceback", "call", "File", "line", "<string>", + "IndentationError", "indented block"] + get_evaluator = PythonCodeEvaluator(self.test_case_data, self.test, + self.language, user_answer) + result = get_evaluator.evaluate() + err = result.get("error").splitlines() + self.assertFalse(result.get("success")) + self.assertEqual(5, len(err)) + for msg in indent_error_msg: + self.assertIn(msg, result.get("error")) + + def test_name_error(self): + user_answer = "" + name_error_msg = ["Traceback", "call", "NameError", "name", "defined"] + get_evaluator = PythonCodeEvaluator(self.test_case_data, self.test, + self.language, user_answer) + result = get_evaluator.evaluate() + err = result.get("error").splitlines() + self.assertFalse(result.get("success")) + self.assertEqual(2, len(err)) + for msg in name_error_msg: + self.assertIn(msg, result.get("error")) + + def test_recursion_error(self): + user_answer = dedent(""" + def add(a, b): + return add(3, 3) + """) + recursion_error_msg = ["Traceback", "call", "RuntimeError", + "maximum recursion depth exceeded"] + get_evaluator = PythonCodeEvaluator(self.test_case_data, self.test, + self.language, user_answer) + result = get_evaluator.evaluate() + err = result.get("error").splitlines() + self.assertFalse(result.get("success")) + self.assertEqual(2, len(err)) + for msg in recursion_error_msg: + self.assertIn(msg, result.get("error")) + + def test_type_error(self): + user_answer = dedent(""" + def add(a): + return a + b + """) + type_error_msg = ["Traceback", "call", "TypeError", "exactly", "argument"] + get_evaluator = PythonCodeEvaluator(self.test_case_data, self.test, + self.language, user_answer) + result = get_evaluator.evaluate() + err = result.get("error").splitlines() + self.assertFalse(result.get("success")) + self.assertEqual(2, len(err)) + for msg in type_error_msg: + self.assertIn(msg, result.get("error")) + + def test_value_error(self): + user_answer = dedent(""" + def add(a, b): + c = 'a' + return int(a) + int(b) + int(c) + """) + value_error_msg = ["Traceback", "call", "ValueError", "invalid literal", "base"] + get_evaluator = PythonCodeEvaluator(self.test_case_data, self.test, + self.language, user_answer) + result = get_evaluator.evaluate() + err = result.get("error").splitlines() self.assertFalse(result.get("success")) - self.assertEquals(result.get("error"), self.timeout_msg) + self.assertEqual(2, len(err)) + for msg in value_error_msg: + self.assertIn(msg, result.get("error")) if __name__ == '__main__': - unittest.main() + unittest.main()
\ No newline at end of file diff --git a/yaksh/python_code_evaluator.py b/yaksh/python_code_evaluator.py index 0c473cf..c87c420 100644 --- a/yaksh/python_code_evaluator.py +++ b/yaksh/python_code_evaluator.py @@ -6,7 +6,7 @@ from os.path import join import importlib # local imports -from code_evaluator import CodeEvaluator +from code_evaluator import CodeEvaluator, TimeoutException class PythonCodeEvaluator(CodeEvaluator): @@ -29,6 +29,10 @@ class PythonCodeEvaluator(CodeEvaluator): fname, lineno, func, text = info[-1] text = str(test_code).splitlines()[lineno-1] err = "{0} {1} in: {2}".format(type.__name__, str(value), text) + except TimeoutException: + raise + except Exception: + err = traceback.format_exc(limit=0) else: success = True err = 'Correct answer' diff --git a/yaksh/static/yaksh/css/base.css b/yaksh/static/yaksh/css/base.css index 362f401..af3ba8b 100644 --- a/yaksh/static/yaksh/css/base.css +++ b/yaksh/static/yaksh/css/base.css @@ -221,7 +221,7 @@ body { box-shadow: 0 1px 2px rgba(0,0,0,.15); } .content .span10, -.content .span14 { +.content { min-height: 475px; } .content .span4 { @@ -431,6 +431,7 @@ a:hover { /* Typography.less * Headings, body text, lists, code, and more for a versatile and durable typography system * ---------------------------------------------------------------------------------------- */ + p { font-size: 13px; font-weight: normal; @@ -478,6 +479,7 @@ h4, h5, h6 { line-height: 36px; + } h3 { font-size: 18px; @@ -493,6 +495,8 @@ h4 small { } h5 { font-size: 14px; + color:white; + text-align:left; } h6 { font-size: 13px; @@ -963,8 +967,7 @@ input[disabled], select[disabled], textarea[disabled], input[readonly], -select[readonly], -textarea[readonly] { +select[readonly]{ background-color: #f5f5f5; border-color: #ddd; cursor: not-allowed; @@ -1793,6 +1796,7 @@ footer { -ms-transition: 0.1s linear all; -o-transition: 0.1s linear all; transition: 0.1s linear all; + margin-right:50px } .btn:hover { background-position: 0 -15px; @@ -2340,3 +2344,29 @@ blink { -webkit-animation-timing-function: cubic-bezier(1.0, 0, 0, 1.0); -webkit-animation-duration: 1s; } + +.error{ +padding:0; +height:100px; +width:730px; +resize:None; +overflow-y:scroll; +background-color:white; +border: 0 None white; +} +.error_msg{ +padding:0; +height:100px; +width:730px; +resize:None; +overflow:hidden; + +} +.bash{ +padding:0; +height:auto; +width:750px; +resize:none; +overflow:hidden; +background-color:white; +} diff --git a/yaksh/static/yaksh/css/question.css b/yaksh/static/yaksh/css/question.css index b72f873..06109e5 100644 --- a/yaksh/static/yaksh/css/question.css +++ b/yaksh/static/yaksh/css/question.css @@ -13,7 +13,7 @@ } .td1-class { - width:175px; + width:300px; } .td2-class { diff --git a/yaksh/templates/base.html b/yaksh/templates/base.html index 5284a77..d3e4f91 100644 --- a/yaksh/templates/base.html +++ b/yaksh/templates/base.html @@ -37,7 +37,7 @@ </div> </div> <footer> - <p>© FOSSEE group, IIT Bombay</p> + <p align="center">© FOSSEE group, IIT Bombay</p> </footer> </div> diff --git a/yaksh/templates/yaksh/add_question.html b/yaksh/templates/yaksh/add_question.html index b896081..61b146c 100644 --- a/yaksh/templates/yaksh/add_question.html +++ b/yaksh/templates/yaksh/add_question.html @@ -47,4 +47,3 @@ <button class="btn" type="button" name="button" onClick='location.replace("{{URL_ROOT}}/exam/manage/questions/");'>Cancel</button> </center> </form> {% endblock %} - diff --git a/yaksh/templates/yaksh/login.html b/yaksh/templates/yaksh/login.html index dfeac1e..d679748 100644 --- a/yaksh/templates/yaksh/login.html +++ b/yaksh/templates/yaksh/login.html @@ -14,7 +14,7 @@ <center><table class=span1> {{ form.as_table }} </table></center> - <center><button class="btn" type="submit">Login</button> <button class="btn" type="reset">Cancel</button></center> + <center><button class="btn" type="submit" style="margin-left: 50px">Login</button> <button class="btn" type="reset">Cancel</button></center> <br><center><a href="{{URL_ROOT}}/exam/forgotpassword/">Forgot Password?</a></center><br> <center><a href="{{URL_ROOT}}/exam/register/">New User? Sign-Up </a></center> </form> diff --git a/yaksh/templates/yaksh/question.html b/yaksh/templates/yaksh/question.html index a0b74fa..0d1daee 100644 --- a/yaksh/templates/yaksh/question.html +++ b/yaksh/templates/yaksh/question.html @@ -45,6 +45,7 @@ function updateClock(){ var ss = ('0' + t.seconds).slice(-2); if(t.total<0){ + document.forms["code"].submit(); clearInterval(timeinterval); return null; @@ -89,58 +90,58 @@ function call_skip(url) form.action = url form.submit(); } - + {% if question.type == 'code' and success == 'True'%} + {% if to_attempt|length != 0 %} + window.setTimeout(function() + { + {% for qid, num in questions.items %} + location.href="{{ URL_ROOT }}/exam/{{ qid.id }}/{{ paper.attempt_number }}/{{ paper.question_paper.id }}/" + {% endfor %} + }, 1000); + {% else %} + window.setTimeout(function() + { + location.href="{{ URL_ROOT }}/exam/{{ question.id }}/check/{{ paper.attempt_number }}/{{ paper.question_paper.id }}/" + }, 1000); + {% endif %} + {% endif %} </script> -{% endblock script %} - +{% endblock script %} {% block onload %} onload="updateTime();setSnippetHeight()" {% endblock %} -{% block pagetitle %} - -<table><h6><div> - <tr><td class=td1-class><h5>You have {{ paper.questions_left }} question(s) left in {{ quiz_name }}</h5> - <td class=td2-class><div class=time-div id="time_left"> - </div> -</div></h6></table> - -{% endblock %} - {% block content %} <div class="topbar"> <div class="fill"> <div class="container"> <h3 class="brand"><strong>Online Test</h3></strong> <ul> - <li> <h5><a> Hi {{user.first_name.title}} {{user.last_name.title}} </a></h5> - </ul> - <form id="logout" action="{{URL_ROOT}}/exam/quit/{{ paper.attempt_number }}/{{ paper.question_paper.id }}/" method="post" class="pull-right"> - {% csrf_token %} - <button class="btn" type="submit" name="quit">Quit Exam</button> </li> - - </form> + <li><h5><a> Hi {{user.first_name.title}} {{user.last_name.title}} </a></h5></li> + </ul><br> + <div class=time-div id="time_left"></div> + <h5 class=td1-class>You have {{ paper.questions_left }} question(s) left in {{ quiz_name }}</h5> </div> </div> </div> -<div class = container> +<div class = "container"> <div class="sidebar"> <p>Question Navigator </p> <div class="pagination"> - <ul> - {% for qid, num in questions.items %} - {% if qid in to_attempt %} - {% if qid == question.id|slugify %} - <li class="active"><a href="#" onclick="call_skip('{{ URL_ROOT }}/exam/{{ qid }}/{{ paper.attempt_number }}/{{ paper.question_paper.id }}/')">{{ num }}</a></li> - {% else %} - <li><a href="#" onclick="call_skip('{{ URL_ROOT }}/exam/{{ qid }}/{{ paper.attempt_number }}/{{ paper.question_paper.id }}/')">{{ num }}</a></li> - {% endif %} - {% endif %} - {% if qid in submitted %} - <li class="disabled"><a href="#">{{ num }}</a></li> - {% endif %} - {% endfor %} - </ul> + <ul> + {% for qid, num in questions.items %} + {% if qid.id|slugify in to_attempt %} + {% if qid.id|slugify == question.id|slugify %} + <li class="active"><a href="#" data-toggle="tooltip" title="{{ qid.description }}" onclick="call_skip('{{ URL_ROOT }}/exam/{{ qid.id }}/{{ paper.attempt_number }}/{{ paper.question_paper.id }}/')">{{ num }}</a></li> + {% else %} + <li><a href="#" data-toggle="tooltip" title="{{ qid.description }}" onclick="call_skip('{{ URL_ROOT }}/exam/{{ qid.id }}/{{ paper.attempt_number }}/{{ paper.question_paper.id }}/')">{{ num }}</a></li> + {% endif %} + {% endif %} + {% if qid.id|slugify in submitted %} + <li class="disabled"><a href="#">{{ num }}</a></li> + {% endif %} + {% endfor %} + </ul> </div> </div> </div> @@ -149,22 +150,22 @@ function call_skip(url) <h4><u> {{ question.summary }} </u><font class=pull-right>(Marks : {{ question.points }}) </font></h4><br> <font size=3 face=arial> {{ question.description|safe }} </font> <br><font size=3 face=arial> Language: {{ question.language }} </font><br> -</div> -{% if error_message %} - <div class="alert alert-error"> - {% for e in error_message.splitlines %} - {{ e|join:"" }} - <br/> - {% endfor%} - </div> -{% endif %} - + {% if question.type == "code" %} + <br><h3>Output:</h3></br> + {% if error_message %} + <div class="alert alert-error"> + <textarea class="error" readonly="yes">{{ error_message }}</textarea> + {% else %} + <textarea class="error_msg" readonly="yes" placeholder="Please submit your answer below"></textarea> + {% endif %} + </div> + {% endif %} +<br> <p id="status"></p> - <form id="code" action="{{URL_ROOT}}/exam/{{ question.id }}/check/{{ paper.attempt_number }}/{{ paper.question_paper.id }}/" method="post" enctype="multipart/form-data"> - {% csrf_token %} - <input type=hidden name="question_id" id="question_id" value={{ question.id }}></input> + {% csrf_token %} + <input type=hidden name="question_id" id="question_id" value={{ question.id }}></input> {% if question.type == "mcq" %} {% for option in question.options.strip.splitlines %} @@ -183,10 +184,9 @@ function call_skip(url) {% endfor %} {% endif %} {% if question.type == "code" %} - - <textarea rows="1" style="padding:0;height:auto;width:750px;overflow:hidden;background-color:white;border: 0 none white;" readonly="yes" name="snippet" id="snippet" wrap="off">{% if last_attempt %}{{ question.snippet }}{% else %}{% if question.type == "bash" %} #!/bin/bash {{ question.snippet }}{% else %}{{ question.snippet }}{% endif %}{% endif %}</textarea> - - <textarea tabindex=1 rows="10" style="padding:0;height:auto; box-shadow: none;width:750px;margin-bottom:10px;overflow:hidden;border:none;" name="answer" id="answer" wrap="off" onkeydown="return catchTab(this,event)">{% if last_attempt %}{{last_attempt}}{% else %}{% if question.type == "bash" %}{% else %}{% endif %}{% endif %}</textarea> + <h3>Program:</h3> + <textarea rows="1" class="bash" readonly="yes" name="snippet" id="snippet" wrap="off" >{% if last_attempt %}{{ question.snippet }}{% else %}{% if question.type == "bash" %} #!/bin/bash {{ question.snippet }}{% else %}{{ question.snippet }}{% endif %}{% endif %}</textarea> + <textarea rows="10" class="bash" name="answer" id="answer" wrap="off" onkeydown="return catchTab(this,event)">{% if last_attempt %}{{last_attempt}}{% else %}{% if question.type == "bash" %}{% else %}{% endif %}{% endif %}</textarea> <br> <script type="text/javascript"> @@ -194,7 +194,7 @@ function call_skip(url) </script> <script>addLineNumbers('snippet');</script> {% endif %} - + {% if question.type == "mcq" or question.type == "mcc "%} <br><button class="btn" type="submit" name="check" id="check">Submit Answer</button> {% elif question.type == "upload" %} @@ -206,7 +206,7 @@ function call_skip(url) <button class="btn" type="submit" name="skip" id="skip">Attempt Later</button> {% endif %} </form> - +</div> <!-- Modal --> <div class="modal fade " id="upload_alert" > @@ -224,5 +224,8 @@ function call_skip(url) </div> </div> </div> - + <form id="logout" action="{{URL_ROOT}}/exam/quit/{{ paper.attempt_number }}/{{ paper.question_paper.id }}/" method="post" class="pull-right"> + {% csrf_token %} + <button class="btn" type="submit" name="quit">Quit Exam</button> + </form> {% endblock content %} diff --git a/yaksh/views.py b/yaksh/views.py index bdb86b9..03f4f61 100644 --- a/yaksh/views.py +++ b/yaksh/views.py @@ -868,7 +868,8 @@ def get_questions(paper): q_unanswered = paper.get_unanswered_questions() q_unanswered.sort() to_attempt = q_unanswered - for index, value in enumerate(all_questions, 1): + question = Question.objects.filter(id__in=all_questions) + for index, value in enumerate(question, 1): questions[value] = index questions = collections.OrderedDict(sorted(questions.items(), key=lambda x:x[1])) return questions, to_attempt, submitted @@ -928,6 +929,7 @@ def show_question(request, q_id, attempt_num, questionpaper_id, success_msg=None return complete(request, msg, attempt_num, questionpaper_id) else: return question(request, q_id, attempt_num, questionpaper_id, success_msg) + def _save_skipped_answer(old_skipped, user_answer, paper, question): @@ -1042,25 +1044,38 @@ def check(request, q_id, attempt_num=None, questionpaper_id=None): if time_left <= 0: reason = 'Your time is up!' return complete(request, reason, attempt_num, questionpaper_id) - # Display the same question if user_answer is None elif not user_answer: msg = "Please submit a valid option or code" time_left = paper.time_left() questions, to_attempt, submitted = get_questions(paper) - context = {'question': question, 'error_message': msg, - 'paper': paper, 'quiz_name': paper.question_paper.quiz.description, - 'time_left': time_left, 'questions': questions, - 'to_attempt': to_attempt, 'submitted': submitted} + context = {'question': question, 'paper': paper, + 'quiz_name': paper.question_paper.quiz.description, + 'time_left': time_left, 'questions': questions, + 'to_attempt': to_attempt, 'submitted': submitted, + 'error_message': msg} + ci = RequestContext(request) + + elif question.type == 'code' and user_answer: + msg = "Correct Output" + success = "True" + paper.completed_question(question.id) + time_left = paper.time_left() + questions, to_attempt, submitted = get_questions(paper) + context = {'question': question, 'paper': paper, + 'quiz_name': paper.question_paper.quiz.description, + 'time_left': time_left, 'questions': questions, + 'to_attempt': to_attempt, 'submitted': submitted, + 'error_message': msg, 'success': success} ci = RequestContext(request) - return my_render_to_response('yaksh/question.html', context, - context_instance=ci) else: next_q = paper.completed_question(question.id) return show_question(request, next_q, attempt_num, questionpaper_id, success_msg) + return my_render_to_response('yaksh/question.html', context, + context_instance=ci) def validate_answer(user, user_answer, question, json_data=None): """ @@ -1091,7 +1106,6 @@ def validate_answer(user, user_answer, question, json_data=None): result = json.loads(json_result) if result.get('success'): correct = True - return correct, result def get_question_labels(request, attempt_num=None, questionpaper_id=None): @@ -1536,7 +1550,6 @@ def show_all_questions(request): return my_render_to_response('yaksh/showquestions.html', context, context_instance=ci) - @login_required def user_data(request, username, questionpaper_id=None): """Render user data.""" |