diff options
author | Prabhu Ramachandran | 2017-05-25 18:58:31 +0530 |
---|---|---|
committer | GitHub | 2017-05-25 18:58:31 +0530 |
commit | eae6ee7ceb25f78b216a5e2c9d6165513272e4cb (patch) | |
tree | 2758e1ec86c533b58d0fa3e7f4f5304bcd2431ec /yaksh | |
parent | 80b67d07ceaf4c73705a27ee0bfc905e30b19ac4 (diff) | |
parent | 77f05d3df90a70ff97285deb5bda2d91d99e65d5 (diff) | |
download | online_test-eae6ee7ceb25f78b216a5e2c9d6165513272e4cb.tar.gz online_test-eae6ee7ceb25f78b216a5e2c9d6165513272e4cb.tar.bz2 online_test-eae6ee7ceb25f78b216a5e2c9d6165513272e4cb.zip |
Merge pull request #297 from maheshgudi/difflib
StdIO error output simplification
Diffstat (limited to 'yaksh')
-rw-r--r-- | yaksh/compare_stdio.py | 43 | ||||
-rw-r--r-- | yaksh/evaluator_tests/test_bash_evaluation.py | 3 | ||||
-rw-r--r-- | yaksh/evaluator_tests/test_c_cpp_evaluation.py | 14 | ||||
-rw-r--r-- | yaksh/evaluator_tests/test_java_evaluation.py | 7 | ||||
-rw-r--r-- | yaksh/evaluator_tests/test_python_evaluation.py | 4 | ||||
-rw-r--r-- | yaksh/evaluator_tests/test_python_stdio_evaluator.py | 39 | ||||
-rw-r--r-- | yaksh/python_stdio_evaluator.py | 39 | ||||
-rw-r--r-- | yaksh/static/yaksh/css/exam.css | 7 | ||||
-rw-r--r-- | yaksh/stdio_evaluator.py | 19 | ||||
-rw-r--r-- | yaksh/templates/exam.html | 54 | ||||
-rw-r--r-- | yaksh/templates/yaksh/grade_user.html | 57 | ||||
-rw-r--r-- | yaksh/templates/yaksh/question.html | 3 | ||||
-rw-r--r-- | yaksh/templates/yaksh/user_data.html | 54 | ||||
-rw-r--r-- | yaksh/templates/yaksh/view_answerpaper.html | 52 | ||||
-rw-r--r-- | yaksh/templatetags/custom_filters.py | 8 |
15 files changed, 299 insertions, 104 deletions
diff --git a/yaksh/compare_stdio.py b/yaksh/compare_stdio.py new file mode 100644 index 0000000..c4076de --- /dev/null +++ b/yaksh/compare_stdio.py @@ -0,0 +1,43 @@ +try: + from itertools import zip_longest +except ImportError: + from itertools import izip_longest as zip_longest + + +def _get_incorrect_user_lines(exp_lines, user_lines): + err_line_numbers = [] + for line_no, (expected_line, user_line) in \ + enumerate(zip_longest(exp_lines, user_lines)): + if not user_line or not expected_line or \ + user_line.strip() != expected_line.strip(): + err_line_numbers.append(line_no) + return err_line_numbers + +def compare_outputs(expected_output, user_output, given_input=None): + given_lines = user_output.splitlines() + exp_lines = expected_output.splitlines() + msg = {"given_input":given_input, + "expected_output": exp_lines, + "user_output":given_lines + } + ng = len(given_lines) + ne = len(exp_lines) + err_line_numbers = _get_incorrect_user_lines(exp_lines, given_lines) + msg["error_line_numbers"] = err_line_numbers + if ng != ne: + msg["error_msg"] = ("Incorrect Answer: " + + "We had expected {} number of lines. ".format(ne) + + "We got {} number of lines.".format(ng) + ) + return False, msg + else: + if err_line_numbers: + msg["error_msg"] = ("Incorrect Answer: " + + "Line number(s) {0} did not match." + .format(", ".join(map( + str,[x+1 for x in err_line_numbers] + )))) + return False, msg + else: + msg["error_msg"] = "Correct Answer" + return True, msg diff --git a/yaksh/evaluator_tests/test_bash_evaluation.py b/yaksh/evaluator_tests/test_bash_evaluation.py index 8bb8c81..6e7410e 100644 --- a/yaksh/evaluator_tests/test_bash_evaluation.py +++ b/yaksh/evaluator_tests/test_bash_evaluation.py @@ -242,7 +242,8 @@ class BashStdIOEvaluationTestCases(EvaluatorBaseTest): result = grader.evaluate(kwargs) # Then - self.assert_correct_output("Incorrect", result.get('error')) + result_error = result.get('error')[0].get('error_msg') + self.assert_correct_output("Incorrect", result_error) self.assertFalse(result.get('success')) def test_stdout_only(self): diff --git a/yaksh/evaluator_tests/test_c_cpp_evaluation.py b/yaksh/evaluator_tests/test_c_cpp_evaluation.py index b15f766..5ff4e4c 100644 --- a/yaksh/evaluator_tests/test_c_cpp_evaluation.py +++ b/yaksh/evaluator_tests/test_c_cpp_evaluation.py @@ -350,10 +350,11 @@ class CppStdIOEvaluationTestCases(EvaluatorBaseTest): result = grader.evaluate(kwargs) # Then - lines_of_error = len(result.get('error')[0].splitlines()) + lines_of_error = len(result.get('error')[0].get('error_line_numbers')) + result_error = result.get('error')[0].get('error_msg') self.assertFalse(result.get('success')) - self.assert_correct_output("Incorrect", result.get('error')) - self.assertTrue(lines_of_error > 1) + self.assert_correct_output("Incorrect", result_error) + self.assertTrue(lines_of_error > 0) def test_error(self): # Given @@ -558,10 +559,11 @@ class CppStdIOEvaluationTestCases(EvaluatorBaseTest): result = grader.evaluate(kwargs) # Then - lines_of_error = len(result.get('error')[0].splitlines()) + lines_of_error = len(result.get('error')[0].get('error_line_numbers')) + result_error = result.get('error')[0].get('error_msg') self.assertFalse(result.get('success')) - self.assert_correct_output("Incorrect", result.get('error')) - self.assertTrue(lines_of_error > 1) + self.assert_correct_output("Incorrect", result_error) + self.assertTrue(lines_of_error > 0) def test_cpp_error(self): # Given diff --git a/yaksh/evaluator_tests/test_java_evaluation.py b/yaksh/evaluator_tests/test_java_evaluation.py index ea558ed..c733586 100644 --- a/yaksh/evaluator_tests/test_java_evaluation.py +++ b/yaksh/evaluator_tests/test_java_evaluation.py @@ -349,10 +349,11 @@ class JavaStdIOEvaluationTestCases(EvaluatorBaseTest): result = grader.evaluate(kwargs) # Then - lines_of_error = len(result.get('error')[0].splitlines()) + lines_of_error = len(result.get('error')[0].get('error_line_numbers')) + result_error = result.get('error')[0].get('error_msg') self.assertFalse(result.get('success')) - self.assert_correct_output("Incorrect", result.get('error')) - self.assertTrue(lines_of_error > 1) + self.assert_correct_output("Incorrect", result_error) + self.assertTrue(lines_of_error > 0) def test_error(self): # Given diff --git a/yaksh/evaluator_tests/test_python_evaluation.py b/yaksh/evaluator_tests/test_python_evaluation.py index a751c40..a2faf77 100644 --- a/yaksh/evaluator_tests/test_python_evaluation.py +++ b/yaksh/evaluator_tests/test_python_evaluation.py @@ -613,8 +613,8 @@ class PythonStdIOEvaluationTestCases(EvaluatorBaseTest): # Then self.assertFalse(result.get('success')) self.assert_correct_output( - "ERROR:\nExpected:\n3\nGiven:\n-1\n\nError in line 1 of output.", - result.get('error') + "Incorrect Answer: Line number(s) 1 did not match.", + result.get('error')[0].get('error_msg') ) def test_file_based_answer(self): diff --git a/yaksh/evaluator_tests/test_python_stdio_evaluator.py b/yaksh/evaluator_tests/test_python_stdio_evaluator.py index db5028a..8877544 100644 --- a/yaksh/evaluator_tests/test_python_stdio_evaluator.py +++ b/yaksh/evaluator_tests/test_python_stdio_evaluator.py @@ -1,7 +1,4 @@ -from textwrap import dedent - -from yaksh.python_stdio_evaluator import compare_outputs - +from yaksh.compare_stdio import compare_outputs def test_compare_outputs(): exp = "5\n5\n" @@ -27,36 +24,16 @@ def test_compare_outputs(): exp = "5\n5\n" given = "5 5" success, msg = compare_outputs(given, exp) + error_msg = msg.get('error_msg') assert not success - m = dedent("""\ - ERROR: Got 1 lines in output, we expected 2. - Expected: - 5 - 5 - - Given: - 5 5 - """) - assert m == msg + m = ("Incorrect Answer: We had expected 1 number of lines. " + + "We got 2 number of lines.") + assert m == error_msg exp = "5\n5\n" given = "5\n6" success, msg = compare_outputs(given, exp) + error_msg = msg.get('error_msg') + m = "Incorrect Answer: Line number(s) 2 did not match." assert not success - m = dedent("""\ - ERROR: - Expected: - 5 - 5 - - Given: - 5 - 6 - - Error in line 2 of output. - Expected line 2: - 5 - Given line 2: - 6 - """) - assert m == msg + assert m == error_msg diff --git a/yaksh/python_stdio_evaluator.py b/yaksh/python_stdio_evaluator.py index a8c797d..2b443a7 100644 --- a/yaksh/python_stdio_evaluator.py +++ b/yaksh/python_stdio_evaluator.py @@ -1,7 +1,6 @@ import sys from contextlib import contextmanager - try: from StringIO import StringIO except ImportError: @@ -10,6 +9,7 @@ except ImportError: # Local imports from .file_utils import copy_files, delete_files from .base_evaluator import BaseEvaluator +from .compare_stdio import compare_outputs @contextmanager @@ -21,37 +21,6 @@ def redirect_stdout(): finally: sys.stdout = old_target # restore to the previous value - -def _show_expected_given(expected, given): - return "Expected:\n{0}\nGiven:\n{1}\n".format(expected, given) - - -def compare_outputs(given, expected): - given_lines = given.splitlines() - ng = len(given_lines) - exp_lines = expected.splitlines() - ne = len(exp_lines) - if ng != ne: - msg = "ERROR: Got {0} lines in output, we expected {1}.\n".format( - ng, ne - ) - msg += _show_expected_given(expected, given) - return False, msg - else: - for i, (given_line, expected_line) in \ - enumerate(zip(given_lines, exp_lines)): - if given_line.strip() != expected_line.strip(): - msg = "ERROR:\n" - msg += _show_expected_given(expected, given) - msg += "\nError in line %d of output.\n" % (i+1) - msg += "Expected line {0}:\n{1}\nGiven line {0}:\n{2}\n"\ - .format( - i+1, expected_line, given_line - ) - return False, msg - return True, "Correct answer." - - class PythonStdIOEvaluator(BaseEvaluator): """Tests the Python code obtained from Code Server""" def __init__(self, metadata, test_case_data): @@ -77,6 +46,7 @@ class PythonStdIOEvaluator(BaseEvaluator): self.files = copy_files(self.file_paths) submitted = compile(self.user_answer, '<string>', mode='exec') if self.expected_input: + self.expected_input = self.expected_input.replace('\r', '') input_buffer = StringIO() input_buffer.write(self.expected_input) input_buffer.seek(0) @@ -89,5 +59,8 @@ class PythonStdIOEvaluator(BaseEvaluator): def check_code(self): mark_fraction = self.weight - success, err = compare_outputs(self.output_value, self.expected_output) + success, err = compare_outputs(self.expected_output, + self.output_value, + self.expected_input + ) return success, err, mark_fraction diff --git a/yaksh/static/yaksh/css/exam.css b/yaksh/static/yaksh/css/exam.css new file mode 100644 index 0000000..fff904e --- /dev/null +++ b/yaksh/static/yaksh/css/exam.css @@ -0,0 +1,7 @@ +table td, table th { border: black solid 1px !important; + word-wrap: break-word !important; + white-space: pre-wrap !important; + } +output{ + table-layout: fixed +}
\ No newline at end of file diff --git a/yaksh/stdio_evaluator.py b/yaksh/stdio_evaluator.py index 554d4c5..932ae7e 100644 --- a/yaksh/stdio_evaluator.py +++ b/yaksh/stdio_evaluator.py @@ -5,6 +5,7 @@ import signal # Local imports from .base_evaluator import BaseEvaluator from .grader import TimeoutException +from .compare_stdio import compare_outputs class StdIOEvaluator(BaseEvaluator): @@ -20,18 +21,8 @@ class StdIOEvaluator(BaseEvaluator): os.killpg(os.getpgid(proc.pid), signal.SIGTERM) raise expected_output = expected_output.replace("\r", "") - if not expected_input: - error_msg = "Expected Output is\n{0} ".\ - format(str(expected_output)) - else: - error_msg = "Given Input is\n{0}\nExpected Output is\n{1}".\ - format(expected_input, str(expected_output)) - if output_err == '': - if user_output == expected_output: - success, err = True, None - else: - err = "Incorrect answer:\n" + error_msg +\ - "\nYour output is\n{0}".format(str(user_output)) - else: - err = "Error:\n{0}".format(output_err) + success, err = compare_outputs(expected_output, + user_output, + expected_input + ) return success, err diff --git a/yaksh/templates/exam.html b/yaksh/templates/exam.html index a18a962..45b85f0 100644 --- a/yaksh/templates/exam.html +++ b/yaksh/templates/exam.html @@ -1,4 +1,5 @@ {% extends "base.html" %} +{% load custom_filters %} {% block css%} <link rel="stylesheet" href="{{ URL_ROOT }}/static/yaksh/css/dashboard.css" type="text/css" /> {% endblock %} @@ -77,13 +78,56 @@ {% if error_message %} <div class="row" id="error_panel"> {% for error in error_message %} - {% if error == "Correct answer" %} - <div class="panel panel-success"> - {% else %} <div class="panel panel-danger"> - {% endif %} <div class="panel-heading">Testcase No. {{ forloop.counter }}</div> - <div class="panel-body"><pre><code>{{ error }}</code></pre></div> + <div class="panel-body"> + <div class="well well-sm"> + {% if not error.expected_output %} + <pre><code> {{error|safe}} </code></pre> + {% else %} + {% if error.given_input %} + <table class="table table-bordered"> + <col width="30%"> + <tr class = "active"> + <td> For given Input value(s):</td> + <td>{{error.given_input}}</td> + </tr> + </table> + {% endif %} + <table class="table table-bordered" width="100%" id="output"> + <col width="10%"> + <col width="40%"> + <col width="40%"> + <col width="10%"> + <tr class="info"> + <th><center>Line No.</center></th> + <th><center>Expected Output</center></th> + <th><center>User output</center></th> + <th><center>Status</center></th> + </tr> + {% for expected,user in error.expected_output|zip:error.user_output %} + <td> {{forloop.counter}} </td> + <td>{{expected|default:""}} </td> + <td>{{user|default:""}}</td> + {% if forloop.counter0 in error.error_line_numbers or not expected or not user %} + <td><span class ="glyphicon glyphicon-remove text-warning"/></td> + {% else %} + <td><span class ="glyphicon glyphicon-ok text-success"/></td> + {% endif %} + </tr> + {% endfor %} + </table> + <table width="100%" class='table table-bordered'> + <col width="10"> + <tr class = "danger"> + <td><b>Error:</b></td> + <td>{{error.error_msg}}</td> + </tr> + </table> + + {% endif %} + </div> + </div> </div> {% endfor %} diff --git a/yaksh/templates/yaksh/grade_user.html b/yaksh/templates/yaksh/grade_user.html index c93ec10..9cdfb1a 100644 --- a/yaksh/templates/yaksh/grade_user.html +++ b/yaksh/templates/yaksh/grade_user.html @@ -1,4 +1,5 @@ {% extends "manage.html" %} +{% load custom_filters %} {% block title %} Grade User {% endblock %} @@ -132,6 +133,7 @@ Status : <b style="color: green;"> Passed </b><br/> {% csrf_token %} {% for question, answers in paper.get_question_answers.items %} +<div class = "well well-sm"> <div class="panel panel-info"> <div class="panel-heading"> <strong> Details: {{forloop.counter}}. {{ question.summary }} @@ -198,10 +200,57 @@ Status : <b style="color: green;"> Passed </b><br/> <div class="panel panel-danger"> <div class="panel-heading">Error: {% endif %} - {% for err in ans.error_list %} - <div><pre>{{ err }}</pre></div> - {% endfor %} + {% with ans.error_list as err %} + {% for error in err %} + {% if not error.expected_output %} + <pre><code> {{error|safe}} </code></pre> + {% else %} + <div class = "well well-sm"> + {% if error.given_input %} + <table class="table table-bordered"> + <col width="30%"> + <tr class = "active"> + <td> For given Input value(s):</td> + <td>{{error.given_input}}</td> + </tr> + </table> + {% endif %} + <table class="table table-bordered" width="100%" id="output"> + <col width="10%"> + <col width="40%"> + <col width="40%"> + <col width="10%"> + <tr class="info"> + <th><center>Line No.</center></th> + <th><center>Expected Output</center></th> + <th><center>User output</center></th> + <th><center>Status</center></th> + </tr> + {% for expected,user in error.expected_output|zip:error.user_output %} + <td> {{forloop.counter}} </td> + <td>{{expected|default:""}} </td> + <td>{{user|default:""}}</td> + {% if forloop.counter0 in error.error_line_numbers or not expected or not user %} + <td><span class ="glyphicon glyphicon-remove text-warning"/></td> + {% else %} + <td><span class ="glyphicon glyphicon-ok text-success"/></td> + {% endif %} + </tr> + {% endfor %} + </table> + <table width="100%" class='table table-bordered'> + <col width="10"> + <tr class = "danger"> + <td><b>Error:</b></td> + <td>{{error.error_msg}}</td> + </tr> + </table> </div> + {% endif %} + {% endfor %} + {% endwith %} + </div> + <div class="panel-body"> {% if question.type != "code" %} <div class="well well-sm"> @@ -220,6 +269,7 @@ Status : <b style="color: green;"> Passed </b><br/> value="{{ answer.answer.marks }}"><br><br> {% endwith %} <hr/> + </div> {% endfor %} {# for question, answers ... #} <div class="form-group"> <h3>Teacher comments: </h3> @@ -233,6 +283,7 @@ Status : <b style="color: green;"> Passed </b><br/> {% endif %} {# if paper.answers.count #} + {% endfor %} {# for paper in data.papers #} {% endif %} {# if data.papers #} diff --git a/yaksh/templates/yaksh/question.html b/yaksh/templates/yaksh/question.html index 0dad59d..ee33523 100644 --- a/yaksh/templates/yaksh/question.html +++ b/yaksh/templates/yaksh/question.html @@ -6,6 +6,7 @@ <link rel="stylesheet" href="{{ URL_ROOT }}/static/yaksh/css/dashboard.css" type="text/css" /> <link rel="stylesheet" href="{{ URL_ROOT }}/static/yaksh/css/question.css" type="text/css" /> <link rel="stylesheet" href="{{ URL_ROOT }}/static/yaksh/css/codemirror/lib/codemirror.css" type="text/css" /> +<link rel="stylesheet" href="{{ URL_ROOT }}/static/yaksh/css/exam.css" type="text/css" /> <style> .CodeMirror{ border-style: groove; @@ -238,7 +239,7 @@ function call_skip(url) <div class="from-group"> {% if question.type == "mcq" or "mcc" or "integer" or "float" or "string" %} - <br><button class="btn btn-primary" type="submit" name="check" id="check">Submit Answer</button> + <br><button class="btn btn-primary" type="submit" name="check" id="check">Submit Answer</button> <br/> {% elif question.type == "upload" %} <br><button class="btn btn-primary" type="submit" name="check" id="check" onClick="return validate();">Upload</button> diff --git a/yaksh/templates/yaksh/user_data.html b/yaksh/templates/yaksh/user_data.html index 6e62b66..a8adc22 100644 --- a/yaksh/templates/yaksh/user_data.html +++ b/yaksh/templates/yaksh/user_data.html @@ -1,4 +1,5 @@ {% extends "manage.html" %} +{% load custom_filters %} {% block pagetitle %} Data for user {{ data.user.get_full_name.title }} {% endblock pagetitle %} @@ -118,11 +119,56 @@ User IP address: {{ paper.user_ip }} {% else %} <div class="panel panel-danger"> <div class="panel-heading">Error - - {% for error in answer.error_list %} - <div><pre><code>{{ error }}</code></pre></div> + {% with answer.error_list as err %} + {% for error in err %} + {% if not error.expected_output %} + <pre><code> {{error|safe}} </code></pre> + {% else %} + <div class = "well well-sm"> + {% if error.given_input %} + <table class="table table-bordered"> + <col width="30%"> + <tr class = "active"> + <td> For given Input value(s):</td> + <td>{{error.given_input}}</td> + </tr> + </table> + {% endif %} + <table class="table table-bordered" width="100%" id="output"> + <col width="10%"> + <col width="40%"> + <col width="40%"> + <col width="10%"> + <tr class="info"> + <th><center>Line No.</center></th> + <th><center>Expected Output</center></th> + <th><center>User output</center></th> + <th><center>Status</center></th> + </tr> + {% for expected,user in error.expected_output|zip:error.user_output %} + <td> {{forloop.counter}} </td> + <td>{{expected|default:""}} </td> + <td>{{user|default:""}}</td> + {% if forloop.counter0 in error.error_line_numbers or not expected or not user %} + <td><span class ="glyphicon glyphicon-remove text-warning"/></td> + {% else %} + <td><span class ="glyphicon glyphicon-ok text-success"/></td> + {% endif %} + </tr> + {% endfor %} + </table> + <table width="100%" class='table table-bordered'> + <col width="10"> + <tr class = "danger"> + <td><b>Error:</b></td> + <td>{{error.error_msg}}</td> + </tr> + </table> + </div> + {% endif %} {% endfor %} - {% endif %} + {% endwith %} + {% endif %} </div> <div class="panel-body"> diff --git a/yaksh/templates/yaksh/view_answerpaper.html b/yaksh/templates/yaksh/view_answerpaper.html index f4c8846..9edff5a 100644 --- a/yaksh/templates/yaksh/view_answerpaper.html +++ b/yaksh/templates/yaksh/view_answerpaper.html @@ -1,4 +1,5 @@ {% extends "user.html" %} +{% load custom_filters %} {% block pagetitle %} Answer Paper for {{ quiz.description }}{% endblock pagetitle %} @@ -102,7 +103,56 @@ {% else %} <div class="panel panel-danger"> {% endif %} - <div class="panel-heading">Autocheck: {{ answer.error }}</div> + <div class="panel-heading">Error:</div> + {% with answer.error_list as err %} + {% for error in err %} + {% if not error.expected_output %} + <pre><code> {{error|safe}} </code></pre> + {% else %} + <div class = "well well-sm"> + {% if error.given_input %} + <table class="table table-bordered"> + <col width="30%"> + <tr class = "active"> + <td> For given Input value(s):</td> + <td>{{error.given_input}}</td> + </tr> + </table> + {% endif %} + <table class="table table-bordered" width="100%" id="output"> + <col width="10%"> + <col width="40%"> + <col width="40%"> + <col width="10%"> + <tr class="info"> + <th><center>Line No.</center></th> + <th><center>Expected Output</center></th> + <th><center>User output</center></th> + <th><center>Status</center></th> + </tr> + {% for expected,user in error.expected_output|zip:error.user_output %} + <td> {{forloop.counter}} </td> + <td>{{expected|default:""}} </td> + <td>{{user|default:""}}</td> + {% if forloop.counter0 in error.error_line_numbers or not expected or not user %} + <td><span class ="glyphicon glyphicon-remove text-warning"/></td> + {% else %} + <td><span class ="glyphicon glyphicon-ok text-success"/></td> + {% endif %} + </tr> + {% endfor %} + </table> + <table width="100%" class='table table-bordered'> + <col width="10"> + <tr class = "danger"> + <td><b>Error:</b></td> + <td>{{error.error_msg}}</td> + </tr> + </table> + </div> + {% endif %} + {% endfor %} + {% endwith %} <div class="panel-body"> <pre><code>{{ answer.answer.answer.strip }}</code></pre> </div> diff --git a/yaksh/templatetags/custom_filters.py b/yaksh/templatetags/custom_filters.py index f610cc6..df6ecce 100644 --- a/yaksh/templatetags/custom_filters.py +++ b/yaksh/templatetags/custom_filters.py @@ -1,5 +1,9 @@ from django import template from django.template.defaultfilters import stringfilter +try: + from itertools import zip_longest +except ImportError: + from itertools import izip_longest as zip_longest register = template.Library() @@ -19,3 +23,7 @@ def completed(answerpaper): @register.assignment_tag(name="inprogress") def inprogress(answerpaper): return answerpaper.filter(status="inprogress").count() + +@register.filter(name='zip') +def zip_longest_out(a, b): + return zip_longest(a, b) |