diff options
-rw-r--r-- | requirements/requirements-common.txt | 1 | ||||
-rw-r--r-- | yaksh/error_messages.py | 3 | ||||
-rw-r--r-- | yaksh/forms.py | 24 | ||||
-rw-r--r-- | yaksh/models.py | 1 | ||||
-rw-r--r-- | yaksh/python_stdio_evaluator.py | 3 | ||||
-rw-r--r-- | yaksh/r_code_evaluator.py | 96 | ||||
-rw-r--r-- | yaksh/settings.py | 4 | ||||
-rw-r--r-- | yaksh/templates/yaksh/add_question.html | 2 | ||||
-rw-r--r-- | yaksh/templates/yaksh/error_template.html | 11 | ||||
-rw-r--r-- | yaksh/templates/yaksh/grade_user.html | 4 | ||||
-rw-r--r-- | yaksh/templates/yaksh/user_data.html | 8 | ||||
-rw-r--r-- | yaksh/templates/yaksh/view_answerpaper.html | 4 | ||||
-rw-r--r-- | yaksh/templatetags/custom_filters.py | 26 | ||||
-rw-r--r-- | yaksh/templatetags/test_custom_filters.py | 11 | ||||
-rw-r--r-- | yaksh/views.py | 28 |
15 files changed, 200 insertions, 26 deletions
diff --git a/requirements/requirements-common.txt b/requirements/requirements-common.txt index 913ef1f..54e4c84 100644 --- a/requirements/requirements-common.txt +++ b/requirements/requirements-common.txt @@ -9,3 +9,4 @@ selenium==2.53.6 coverage ruamel.yaml==0.15.23 markdown==2.6.9 +pygments==2.2.0 diff --git a/yaksh/error_messages.py b/yaksh/error_messages.py index f34bf28..2d27417 100644 --- a/yaksh/error_messages.py +++ b/yaksh/error_messages.py @@ -34,8 +34,7 @@ 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()): + if user_line != expected_line: err_line_numbers.append(line_no) return err_line_numbers diff --git a/yaksh/forms.py b/yaksh/forms.py index 414bff5..d82cd55 100644 --- a/yaksh/forms.py +++ b/yaksh/forms.py @@ -3,6 +3,7 @@ from yaksh.models import ( get_model_class, Profile, Quiz, Question, Course, QuestionPaper, Lesson, LearningModule ) +from grades.models import GradingSystem from django.contrib.auth import authenticate from django.contrib.auth.models import User from django.conf import settings @@ -345,6 +346,13 @@ class QuestionFilterForm(forms.Form): class CourseForm(forms.ModelForm): """ course form for moderators """ + class Meta: + model = Course + fields = [ + 'name', 'enrollment', 'active', 'code', 'instructions', + 'start_enroll_time', 'end_enroll_time', 'grading_system', + 'view_grade' + ] def __init__(self, *args, **kwargs): super(CourseForm, self).__init__(*args, **kwargs) @@ -381,13 +389,15 @@ class CourseForm(forms.ModelForm): instance.save() return instance - class Meta: - model = Course - fields = [ - 'name', 'enrollment', 'active', 'code', 'instructions', - 'start_enroll_time', 'end_enroll_time', 'grading_system', - 'view_grade' - ] + def __init__(self, user, *args, **kwargs): + super(CourseForm, self).__init__(*args, **kwargs) + if self.instance.id and self.instance.teachers.filter(id=user.id).exists(): + self.fields['grading_system'].widget.attrs['disabled'] = True + else: + grading_choices = GradingSystem.objects.filter( + creator=user + ) + self.fields['grading_system'].queryset = grading_choices class ProfileForm(forms.ModelForm): diff --git a/yaksh/models.py b/yaksh/models.py index 6fb9807..d1b53e7 100644 --- a/yaksh/models.py +++ b/yaksh/models.py @@ -46,6 +46,7 @@ languages = ( ("cpp", "C++ Language"), ("java", "Java Language"), ("scilab", "Scilab"), + ("r", "R"), ) question_types = ( diff --git a/yaksh/python_stdio_evaluator.py b/yaksh/python_stdio_evaluator.py index 64a2809..a1e8f72 100644 --- a/yaksh/python_stdio_evaluator.py +++ b/yaksh/python_stdio_evaluator.py @@ -46,6 +46,7 @@ class PythonStdIOEvaluator(BaseEvaluator): if self.file_paths: self.files = copy_files(self.file_paths) submitted = compile(self.user_answer, '<string>', mode='exec') + self.expected_output = self.expected_output.replace('\r', '') if self.expected_input: self.expected_input = self.expected_input.replace('\r', '') input_buffer = StringIO() @@ -55,7 +56,7 @@ class PythonStdIOEvaluator(BaseEvaluator): with redirect_stdout() as output_buffer: exec_scope = {} exec(submitted, exec_scope) - self.output_value = output_buffer.getvalue().rstrip("\n") + self.output_value = output_buffer.getvalue() return self.output_value def check_code(self): diff --git a/yaksh/r_code_evaluator.py b/yaksh/r_code_evaluator.py new file mode 100644 index 0000000..ca4c94a --- /dev/null +++ b/yaksh/r_code_evaluator.py @@ -0,0 +1,96 @@ +#!/usr/bin/env python +from __future__ import unicode_literals +import os +import subprocess +import re + +# Local imports +from .base_evaluator import BaseEvaluator +from .file_utils import copy_files, delete_files + + +class RCodeEvaluator(BaseEvaluator): + """Tests the R code obtained from Code Server""" + def __init__(self, metadata, test_case_data): + self.files = [] + self.submit_code_path = "" + self.test_code_path = "" + # Set metadata values + self.user_answer = metadata.get('user_answer') + self.file_paths = metadata.get('file_paths') + self.partial_grading = metadata.get('partial_grading') + + # Set test case data values + self.test_case = test_case_data.get('test_case') + self.weight = test_case_data.get('weight') + + def teardown(self): + # Delete the created file. + if os.path.exists(self.submit_code_path): + os.remove(self.submit_code_path) + if os.path.exists(self.test_code_path): + os.remove(self.test_code_path) + if self.files: + delete_files(self.files) + + def check_code(self): + self.submit_code_path = self.create_submit_code_file('function.r') + self.test_code_path = self.create_submit_code_file('main.r') + if self.file_paths: + self.files = copy_files(self.file_paths) + clean_ref_path = self.test_code_path + self.user_answer, terminate_commands = \ + self._remove_r_quit(self.user_answer.lstrip()) + + success = False + mark_fraction = 0.0 + self.write_to_submit_code_file(self.submit_code_path, self.user_answer) + self.write_to_submit_code_file(self.test_code_path, self.test_case) + # Throw message if there are commmands that terminates scilab + add_err = "" + if terminate_commands: + add_err = "Please do not use quit() in your\ + code.\n Otherwise your code will not be evaluated\ + correctly.\n" + + cmd = 'Rscript main.r' + ret = self._run_command(cmd, shell=True, stdout=subprocess.PIPE, + stderr=subprocess.PIPE + ) + proc, stdout, stderr = ret + + if stderr is '': + # Clean output + stdout = self._strip_output(stdout) + if proc.returncode == 31: + success, err = True, None + mark_fraction = 1.0 if self.partial_grading else 0.0 + else: + err = add_err + stdout + else: + err = add_err + stderr + + return success, err, mark_fraction + + def _remove_r_quit(self, string): + """ + Removes quit from the R code + """ + new_string = "" + terminate_commands = False + for line in string.splitlines(): + new_line = re.sub(r"quit.*$", "", line) + if line != new_line: + terminate_commands = True + new_string = new_string + '\n' + new_line + return new_string, terminate_commands + + def _strip_output(self, out): + """ + Cleans whitespace from the output + """ + strip_out = "Message" + for l in out.split('\n'): + if l.strip(): + strip_out = strip_out+"\n"+l.strip() + return strip_out + out diff --git a/yaksh/settings.py b/yaksh/settings.py index 9e9597d..7b42298 100644 --- a/yaksh/settings.py +++ b/yaksh/settings.py @@ -55,4 +55,8 @@ code_evaluators = { "standardtestcase": "yaksh.scilab_code_evaluator.ScilabCodeEvaluator", "hooktestcase": "yaksh.hook_evaluator.HookEvaluator" }, + "r": { + "standardtestcase": "yaksh.r_code_evaluator.RCodeEvaluator", + "hooktestcase": "yaksh.hook_evaluator.HookEvaluator" + }, } diff --git a/yaksh/templates/yaksh/add_question.html b/yaksh/templates/yaksh/add_question.html index 692af48..c4cd8a7 100644 --- a/yaksh/templates/yaksh/add_question.html +++ b/yaksh/templates/yaksh/add_question.html @@ -50,7 +50,9 @@ {% for form in formset %} <div class="link-formset well"> + {% autoescape off %} {{ form.as_p }} + {% endautoescape %} </div> {% endfor %} diff --git a/yaksh/templates/yaksh/error_template.html b/yaksh/templates/yaksh/error_template.html index 5530844..00fa306 100644 --- a/yaksh/templates/yaksh/error_template.html +++ b/yaksh/templates/yaksh/error_template.html @@ -1,4 +1,11 @@ +{% block css%} + <link rel="stylesheet" href="{{ URL_ROOT }}/static/yaksh/css/dashboard.css" type="text/css" /> +{% endblock %} +{% block script %} + <script src="{{ URL_ROOT }}/static/yaksh/js/jquery-ui.js"></script> +{% endblock %} + {% load custom_filters %} {% if error_message %} <div class="container-fluid row justify-content-center"> @@ -64,8 +71,8 @@ </tr> {% for expected,user in error.expected_output|zip:error.user_output %} <td> {{forloop.counter}} </td> - <td>{{expected|default:""}} </td> - <td>{{user|default:""}}</td> + <td>{{expected|default:""|highlight_spaces|safe}} </td> + <td>{{user|default:""|highlight_spaces|safe}}</td> {% if forloop.counter0 in error.error_line_numbers or not expected or not user %} <td><span class ="fa fa-times text-warning"/></td> {% else %} diff --git a/yaksh/templates/yaksh/grade_user.html b/yaksh/templates/yaksh/grade_user.html index 9311590..a8472c7 100644 --- a/yaksh/templates/yaksh/grade_user.html +++ b/yaksh/templates/yaksh/grade_user.html @@ -422,7 +422,9 @@ $(document).ready(function() {% endfor %} {% endwith %} {% if question.type == "code" %} - <pre><code>{{ ans.answer.answer.strip }}</code></pre> + {% pygmentise_user_answer question.language ans.answer.answer.strip as user_answer %} + <style type="text/css">{{user_answer.1}}</style> + <pre><code>{{user_answer.0|safe}}</code></pre> {% elif question.type == "mcc" or question.type == "mcq" %} {% for testcases in question.get_test_cases %} {% if testcases.id|stringformat:"i" in ans.answer.answer.strip %} diff --git a/yaksh/templates/yaksh/user_data.html b/yaksh/templates/yaksh/user_data.html index 2ef9655..ff600c3 100644 --- a/yaksh/templates/yaksh/user_data.html +++ b/yaksh/templates/yaksh/user_data.html @@ -310,7 +310,9 @@ {% endfor %} {% endwith %} {% if question.type == "code" %} - <pre><code>{{ ans.answer.answer.strip }}</code></pre> + {% pygmentise_user_answer question.language ans.answer.answer.strip as user_answer %} + <style type="text/css">{{user_answer.1}}</style> + <pre><code>{{user_answer.0|safe}}</code></pre> {% elif question.type == "mcc" or question.type == "mcq" %} {% for testcases in question.get_test_cases %} {% if testcases.id|stringformat:"i" in ans.answer.answer.strip %} @@ -339,7 +341,7 @@ <div class="col-md-2"> <label class="col-form-label" for="q{{ question.id }}">Marks:</label> {% with answers|last as answer %} - <input id="q{{ question.id }}" type="text" name="q{{ question.id }}_marks" size="4" class="form-control" value="{{ answer.answer.marks }}"><br><br> + <input id="q{{ question.id }}" type="text" name="q{{ question.id }}_marks" size="4" class="form-control" value="{{ answer.answer.marks }}" readonly=""><br><br> {% endwith %} </div> </div> @@ -348,7 +350,7 @@ <div class="form-group"> <h3>Teacher comments: </h3> <textarea id="comments_{{paper.question_paper.id}}" class="form-control" - name="comments_{{ paper.question_paper.id }}">{{ paper.comments }}</textarea> + name="comments_{{ paper.question_paper.id }}" readonly="">{{ paper.comments }}</textarea> </div> </div> </div> diff --git a/yaksh/templates/yaksh/view_answerpaper.html b/yaksh/templates/yaksh/view_answerpaper.html index b87c818..8e085b6 100644 --- a/yaksh/templates/yaksh/view_answerpaper.html +++ b/yaksh/templates/yaksh/view_answerpaper.html @@ -241,7 +241,9 @@ {% endfor %} {% endwith %} <div class="panel-body"> - <pre><code>{{ answer.answer.answer.strip }}</code></pre> + {% pygmentise_user_answer question.language answer.answer.answer.strip as user_answer %} + <style type="text/css">{{user_answer.1}}</style> + <pre><code>{{user_answer.0|safe}}</code></pre> </div> </div> {% endif %} diff --git a/yaksh/templatetags/custom_filters.py b/yaksh/templatetags/custom_filters.py index 1cd3ca2..a78440d 100644 --- a/yaksh/templatetags/custom_filters.py +++ b/yaksh/templatetags/custom_filters.py @@ -7,6 +7,9 @@ try: from itertools import zip_longest except ImportError: from itertools import izip_longest as zip_longest +from pygments import highlight +from pygments.lexers import get_lexer_by_name +from pygments.formatters import HtmlFormatter register = template.Library() @@ -93,3 +96,26 @@ def course_grade(course, user): @register.filter(name='is_checkbox') def is_checkbox(value): return isinstance(value, CheckboxInput) + + +@register.simple_tag +def pygmentise_user_answer(language, answer): + lexer = get_lexer_by_name(language, stripall=True) + formatter = HtmlFormatter(linenos="inline", + cssclass="highlight", + style="colorful") + style = formatter.get_style_defs('.highlight') + result = highlight(answer, lexer, formatter) + return result, style + + +@register.simple_tag +def course_grade(course, user): + return course.get_grade(user) + + +@register.filter(name='highlight_spaces') +def highlight_spaces(text): + return text.replace( + " ", '<span style="background-color:#ffb6db"> </span>' + ) diff --git a/yaksh/templatetags/test_custom_filters.py b/yaksh/templatetags/test_custom_filters.py index eb1f0fb..9d7f246 100644 --- a/yaksh/templatetags/test_custom_filters.py +++ b/yaksh/templatetags/test_custom_filters.py @@ -11,7 +11,8 @@ from yaksh.models import (User, Profile, Question, Quiz, QuestionPaper, from yaksh.templatetags.custom_filters import (completed, inprogress, get_ordered_testcases, - get_answer_for_arrange_options + get_answer_for_arrange_options, + highlight_spaces ) @@ -57,6 +58,7 @@ def tearDownModule(): User.objects.get(username="teacher2000").delete() Group.objects.all().delete() + class CustomFiltersTestCases(unittest.TestCase): @classmethod @@ -148,3 +150,10 @@ class CustomFiltersTestCases(unittest.TestCase): self.assertSequenceEqual(testcases, ordered_testcases) new_answerpaper.delete() + + def test_highlight_spaces(self): + expected_output = "A " + highlighted_output = highlight_spaces(expected_output) + self.assertEqual(highlighted_output, + 'A<span style="background-color:#ffb6db"> </span>' + ) diff --git a/yaksh/views.py b/yaksh/views.py index 5bbe547..6cebe3a 100644 --- a/yaksh/views.py +++ b/yaksh/views.py @@ -6,10 +6,12 @@ from django.shortcuts import render, get_object_or_404, redirect from django.template import Context, Template from django.http import Http404 from django.db.models import Max, Q, F +from django.db import models from django.views.decorators.csrf import csrf_exempt from django.contrib.auth.decorators import login_required from django.contrib.auth.models import Group from django.forms.models import inlineformset_factory +from django.forms import fields from django.utils import timezone from django.core.exceptions import ( MultipleObjectsReturned, ObjectDoesNotExist @@ -100,6 +102,13 @@ def get_html_text(md_text): return Markdown().convert(md_text) +def formfield_callback(field): + if (isinstance(field, models.TextField) and field.name == 'expected_output' + or field.name == 'expected_input'): + return fields.CharField(strip=False) + return field.formfield() + + @email_verified def index(request, next_url=None): """The start page. @@ -246,8 +255,12 @@ def add_question(request, question_id=None): file.toggle_hide_status() formsets = [] for testcase in TestCase.__subclasses__(): - formset = inlineformset_factory(Question, testcase, extra=0, - fields='__all__') + + formset = inlineformset_factory( + Question, testcase, extra=0, + fields='__all__', + formfield_callback=formfield_callback + ) formsets.append(formset( request.POST, request.FILES, instance=question ) @@ -916,10 +929,9 @@ def complete(request, reason=None, attempt_num=None, questionpaper_id=None, """Show a page to inform user that the quiz has been completed.""" user = request.user if questionpaper_id is None: - message = ( - reason or "An Unexpected Error occurred." - " Please contact your instructor/administrator." - ) + message = reason or ("An Unexpected Error occurred. Please " + "contact your instructor/administrator." + ) context = {'message': message} return my_render_to_response(request, 'yaksh/complete.html', context) else: @@ -957,7 +969,7 @@ def add_course(request, course_id=None): if not is_moderator(user): raise Http404('You are not allowed to view this page') if request.method == 'POST': - form = CourseForm(request.POST, instance=course) + form = CourseForm(user, request.POST, instance=course) if form.is_valid(): new_course = form.save(commit=False) if course_id is None: @@ -972,7 +984,7 @@ def add_course(request, course_id=None): request, 'yaksh/add_course.html', {'form': form} ) else: - form = CourseForm(instance=course) + form = CourseForm(user, instance=course) return my_render_to_response( request, 'yaksh/add_course.html', {'form': form} ) |