summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.coveragerc1
-rw-r--r--.travis.yml1
-rw-r--r--CHANGELOG.txt36
-rw-r--r--README_production.rst30
-rw-r--r--grades/__init__.py0
-rw-r--r--grades/admin.py9
-rw-r--r--grades/apps.py5
-rw-r--r--grades/forms.py8
-rw-r--r--grades/migrations/0001_initial.py43
-rw-r--r--grades/migrations/__init__.py0
-rw-r--r--grades/migrations/default_grading_system.py41
-rw-r--r--grades/models.py46
-rw-r--r--grades/templates/add_grades.html35
-rw-r--r--grades/templates/grading_systems.html74
-rw-r--r--grades/tests/__init__.py0
-rw-r--r--grades/tests/test_models.py28
-rw-r--r--grades/tests/test_views.py105
-rw-r--r--grades/urls.py10
-rw-r--r--grades/views.py45
-rw-r--r--online_test/__init__.py2
-rw-r--r--online_test/settings.py1
-rw-r--r--online_test/urls.py3
-rw-r--r--requirements/requirements-codeserver.txt1
-rw-r--r--requirements/requirements-common.txt2
-rw-r--r--setup.cfg2
-rw-r--r--setup.py4
-rw-r--r--yaksh/decorators.py12
-rw-r--r--yaksh/error_messages.py38
-rw-r--r--yaksh/evaluator_tests/test_python_evaluation.py554
-rw-r--r--yaksh/evaluator_tests/test_simple_question_types.py342
-rw-r--r--yaksh/forms.py3
-rw-r--r--yaksh/grader.py44
-rw-r--r--yaksh/migrations/0009_auto_20180113_1124.py149
-rw-r--r--yaksh/migrations/0010_auto_20180226_1324.py34
-rw-r--r--yaksh/migrations/0011_release_0_8_0.py68
-rw-r--r--yaksh/models.py116
-rw-r--r--yaksh/python_assertion_evaluator.py20
-rw-r--r--yaksh/send_emails.py2
-rw-r--r--yaksh/static/yaksh/js/jquery-sortable.js693
-rw-r--r--yaksh/static/yaksh/js/requesthandler.js32
-rw-r--r--yaksh/templates/manage.html2
-rw-r--r--yaksh/templates/yaksh/add_question.html1
-rw-r--r--yaksh/templates/yaksh/course_detail.html6
-rw-r--r--yaksh/templates/yaksh/course_modules.html1
-rw-r--r--yaksh/templates/yaksh/courses.html4
-rw-r--r--yaksh/templates/yaksh/error_template.html2
-rw-r--r--yaksh/templates/yaksh/grade_user.html19
-rw-r--r--yaksh/templates/yaksh/question.html37
-rw-r--r--yaksh/templates/yaksh/user_data.html18
-rw-r--r--yaksh/templates/yaksh/view_answerpaper.html21
-rw-r--r--yaksh/templatetags/custom_filters.py21
-rw-r--r--yaksh/templatetags/test_custom_filters.py152
-rw-r--r--yaksh/test_models.py177
-rw-r--r--yaksh/test_views.py2
-rw-r--r--yaksh/tests/test_code_server.py34
-rw-r--r--yaksh/urls.py2
-rw-r--r--yaksh/urls_password_reset.py2
-rw-r--r--yaksh/views.py271
58 files changed, 2808 insertions, 603 deletions
diff --git a/.coveragerc b/.coveragerc
index 1ddb3fc..a762d08 100644
--- a/.coveragerc
+++ b/.coveragerc
@@ -17,6 +17,7 @@ omit =
yaksh/pipeline/*
yaksh/migrations/*
yaksh/templatetags/__init__.py
+ yaksh/templatetags/test_custom_filters.py
yaksh/middleware/__init__.py
setup.py
tasks.py
diff --git a/.travis.yml b/.travis.yml
index b1a8402..59eaa66 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -21,6 +21,7 @@ install:
script:
- coverage erase
- coverage run -p manage.py test -v 2 --settings online_test.test_settings yaksh
+ - coverage run -p manage.py test -v 2 --settings online_test.test_settings grades
- coverage run -p manage.py test -v 2 --settings online_test.test_settings yaksh.live_server_tests.load_test
after_success:
diff --git a/CHANGELOG.txt b/CHANGELOG.txt
index d3364e0..01d80dd 100644
--- a/CHANGELOG.txt
+++ b/CHANGELOG.txt
@@ -1,8 +1,29 @@
-=== 0.8.0 ===
+=== 0.8.0 (23-03-2018) ===
+
+* Refactored the add_group command to allow creation of moderator group and add users to moderator group and renamed to create_moderator.
+* Deprecated multiple management commands: dump_user_data, load_exam, load_question_xml, results2csv.
+* Changed the unit of time_between_attempts field within Quiz model from days to hours.
+* Fixed assignment upload feature.
+* Error output is displayed using Ajax instead of reloading the entire page.
+* Added Mathjax to the repository.
+* Added Yaksh logo on the website.
+* Changed travis build distribution from precise (Ubuntu 12.04 LTS) to trusty (Ubuntu 14.04 LTS).
+* Fixed a bug that allowed creation of multiple answerpapers.
+* Added MCQ/MCC Shuffle testcases feature for question paper.
+* Added Arrange in Correct Order question type feature.
+* Added a feature to create a course with lessons, quizzes and exercises.
+* Fixed a bug where a oauth users' email is not verified.
+* Added a feature to show per student course completion status.
+* Fixed a bug where a moderator could change question paper of other moderator.
+* Fixed a bug where a teacher could not access question paper for a course.
+* Fixed a bug where a teacher could become the course creator while editing a course.
+* Updated clone course feature to create copy of course, lessons, quizzes and learning modules.
+* Changed Student dashboard to show the days remaining for a course to start.
+* Changed UI for student and moderator dashboard.
+* Updated documentation
+* Added a feature where a moderator can create exercises.
+* Added grading feature which allows a moderator to create a grading system and apply it for a course.
-* Added a management command to add users to moderator group
-* Renamed add_group management command to create_mod_group
-* Deprecated multiple management commands: dump_user_data, load_exam, load_question_xml, results2csv
=== 0.7.0 (15-11-2017) ===
@@ -24,8 +45,8 @@
* Added a Datetimepicker to edit course Page.
* Added invoke script for quickstart and docker deployment.
* Added the facility to send mails to all students enrolled in a course.
-* Fixed a bug that would cause issue during email activation key check
-* Added comprehensive tests for views.py
+* Fixed a bug that would cause issue during email activation key check.
+* Added comprehensive tests for views.py.
* Fixed a bug that allowed moderators to set cyclic quiz prerequisites in a course.
* Added a feature that redirects to edit profile page if user has no profile.
* Fixed a bug that would allow enrolled students to attempt quizzes for deactivated courses.
@@ -48,7 +69,7 @@
=== 0.6.0 (11-05-2017) ===
* Added a course code field that can be used to search a course.
-* Updated the documentation to describe the course code feature
+* Updated the documentation to describe the course code feature.
* Fixed a bug that prevented redirection based on 'next' parameter after login.
* Fixed a bug that littered residual system processes created by code evaluators.
* Added the facility to allow moderators to see and download uploaded assignments.
@@ -71,4 +92,3 @@
* Fixed a bug that displayed the elements of stdout testcase output as unicode.
* Fixed a bug that prevented users from logging in using Google OAuth.
* Added coverage reports to travis.yml.
-
diff --git a/README_production.rst b/README_production.rst
index a9bd55b..13fb8f9 100644
--- a/README_production.rst
+++ b/README_production.rst
@@ -222,33 +222,3 @@ Follow these steps to deploy and run the Django Server, MySQL instance and Code
invoke remove
12. You can use ``invoke --list`` to get a list of all the available commands
-
-
-.. _add-commands:
-
-######################################
-Additional commands available
-######################################
-
-We provide several convenient commands for you to use:
-
-- load\_exam : load questions and a quiz from a python file. See
- docs/sample\_questions.py
-
-- load\_questions\_xml : load questions from XML file, see
- docs/sample\_questions.xml use of this is deprecated in favor of
- load\_exam.
-
-- results2csv : Dump the quiz results into a CSV file for further
- processing.
-
-- dump\_user\_data : Dump out relevalt user data for either all users
- or specified users.
-
-For more information on these do this:
-
-::
-
- $ python manage.py help [command]
-
-where [command] is one of the above.
diff --git a/grades/__init__.py b/grades/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/grades/__init__.py
diff --git a/grades/admin.py b/grades/admin.py
new file mode 100644
index 0000000..548791e
--- /dev/null
+++ b/grades/admin.py
@@ -0,0 +1,9 @@
+from django.contrib import admin
+from grades.models import GradingSystem, GradeRange
+
+
+class GradingSystemAdmin(admin.ModelAdmin):
+ readonly_fields = ('creator',)
+
+admin.site.register(GradingSystem, GradingSystemAdmin)
+admin.site.register(GradeRange)
diff --git a/grades/apps.py b/grades/apps.py
new file mode 100644
index 0000000..6d0985e
--- /dev/null
+++ b/grades/apps.py
@@ -0,0 +1,5 @@
+from django.apps import AppConfig
+
+
+class GradesConfig(AppConfig):
+ name = 'grades'
diff --git a/grades/forms.py b/grades/forms.py
new file mode 100644
index 0000000..130659d
--- /dev/null
+++ b/grades/forms.py
@@ -0,0 +1,8 @@
+from grades.models import GradingSystem
+from django import forms
+
+
+class GradingSystemForm(forms.ModelForm):
+ class Meta:
+ model = GradingSystem
+ fields = ['name', 'description']
diff --git a/grades/migrations/0001_initial.py b/grades/migrations/0001_initial.py
new file mode 100644
index 0000000..04a3006
--- /dev/null
+++ b/grades/migrations/0001_initial.py
@@ -0,0 +1,43 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.9.5 on 2018-02-12 11:12
+from __future__ import unicode_literals
+
+from django.conf import settings
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+ initial = True
+
+ dependencies = [
+ migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='GradeRange',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('lower_limit', models.FloatField()),
+ ('upper_limit', models.FloatField()),
+ ('grade', models.CharField(max_length=10)),
+ ('description', models.CharField(blank=True, max_length=127, null=True)),
+ ],
+ ),
+ migrations.CreateModel(
+ name='GradingSystem',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('name', models.CharField(max_length=255, unique=True)),
+ ('description', models.TextField(default='About the grading system!')),
+ ('creator', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
+ ],
+ ),
+ migrations.AddField(
+ model_name='graderange',
+ name='system',
+ field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='grades.GradingSystem'),
+ ),
+ ]
diff --git a/grades/migrations/__init__.py b/grades/migrations/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/grades/migrations/__init__.py
diff --git a/grades/migrations/default_grading_system.py b/grades/migrations/default_grading_system.py
new file mode 100644
index 0000000..85390d6
--- /dev/null
+++ b/grades/migrations/default_grading_system.py
@@ -0,0 +1,41 @@
+from django.db import migrations
+
+
+def create_default_system(apps, schema_editor):
+ GradingSystem = apps.get_model('grades', 'GradingSystem')
+ GradeRange = apps.get_model('grades', 'GradeRange')
+ db = schema_editor.connection.alias
+
+ default_system = GradingSystem.objects.using(db).create(name='default')
+
+ graderanges_objects = [
+ GradeRange(system=default_system, lower_limit=0, upper_limit=40,
+ grade='F', description='Fail'),
+ GradeRange(system=default_system, lower_limit=40, upper_limit=55,
+ grade='P', description='Pass'),
+ GradeRange(system=default_system, lower_limit=55, upper_limit=60,
+ grade='C', description='Average'),
+ GradeRange(system=default_system, lower_limit=60, upper_limit=75,
+ grade='B', description='Satisfactory'),
+ GradeRange(system=default_system, lower_limit=75, upper_limit=90,
+ grade='A', description='Good'),
+ GradeRange(system=default_system, lower_limit=90, upper_limit=101,
+ grade='A+', description='Excellent')
+ ]
+ GradeRange.objects.using(db).bulk_create(graderanges_objects)
+
+
+def delete_default_system(apps, schema_editor):
+ GradingSystem = apps.get_model('grades', 'GradingSystem')
+ GradeRange = apps.get_model('grades', 'GradeRange')
+ db = schema_editor.connection.alias
+
+ default_system = GradingSystem.objects.using(db).get(creator=None)
+ GradeRange.object.using(db).filter(system=default_system).delete()
+ default_system.delete()
+
+
+class Migration(migrations.Migration):
+ dependencies = [('grades', '0001_initial'), ]
+ operations = [migrations.RunPython(create_default_system,
+ delete_default_system), ]
diff --git a/grades/models.py b/grades/models.py
new file mode 100644
index 0000000..fcea510
--- /dev/null
+++ b/grades/models.py
@@ -0,0 +1,46 @@
+from django.db import models
+from django.contrib.auth.models import User
+
+
+class GradingSystem(models.Model):
+ name = models.CharField(max_length=255, unique=True)
+ description = models.TextField(default='About the grading system!')
+ creator = models.ForeignKey(User, null=True, blank=True)
+
+ def get_grade(self, marks):
+ ranges = self.graderange_set.all()
+ lower_limits = ranges.values_list('lower_limit', flat=True)
+ upper_limits = ranges.values_list('upper_limit', flat=True)
+ lower_limit = self._get_lower_limit(marks, lower_limits)
+ upper_limit = self._get_upper_limit(marks, upper_limits)
+ grade_range = ranges.filter(lower_limit=lower_limit,
+ upper_limit=upper_limit).first()
+ if grade_range:
+ return grade_range.grade
+
+ def _get_upper_limit(self, marks, upper_limits):
+ greater_than = [upper_limit for upper_limit in upper_limits
+ if upper_limit > marks]
+ if greater_than:
+ return min(greater_than, key=lambda x: x-marks)
+
+ def _get_lower_limit(self, marks, lower_limits):
+ less_than = []
+ for lower_limit in lower_limits:
+ if lower_limit == marks:
+ return lower_limit
+ if lower_limit < marks:
+ less_than.append(lower_limit)
+ if less_than:
+ return max(less_than, key=lambda x: x-marks)
+
+ def __str__(self):
+ return self.name.title()
+
+
+class GradeRange(models.Model):
+ system = models.ForeignKey(GradingSystem)
+ lower_limit = models.FloatField()
+ upper_limit = models.FloatField()
+ grade = models.CharField(max_length=10)
+ description = models.CharField(max_length=127, null=True, blank=True)
diff --git a/grades/templates/add_grades.html b/grades/templates/add_grades.html
new file mode 100644
index 0000000..a3f52da
--- /dev/null
+++ b/grades/templates/add_grades.html
@@ -0,0 +1,35 @@
+{% extends "manage.html" %}
+{% block main %}
+<html>
+<a href="{% url 'grades:grading_systems'%}" class="btn btn-danger"> Back to Grading Systems </a>
+<br><br>
+<p><b>Note: For grade range lower limit is inclusive and upper limit is exclusive</b></p>
+<br>
+{% if not system_id %}
+ <form action="{% url 'grades:add_grade' %}" method="POST">
+{% else %}
+ <form action="{% url 'grades:edit_grade' system_id %}" method="POST">
+{% endif %}
+ {% csrf_token %}
+ <table class="table">
+ {{ grade_form }}
+ </table>
+ {{ formset.management_form }}
+ <br>
+ <b><u>Grade Ranges</u></b>
+ <hr>
+ {% for form in formset %}
+ <div>
+ {{ form }}
+ </div>
+ <hr>
+ {% endfor %}
+ {% if not is_default %}
+ <input type="submit" id="add" name="add" value="Add" class="btn btn-info">
+ <input type="submit" id="save" name="save" value="Save" class="btn btn-success">
+ {% else %}
+ <p><b>Note: This is a default grading system. You cannot change this.</b></p>
+ {% endif %}
+</form>
+</html>
+{% endblock %}
diff --git a/grades/templates/grading_systems.html b/grades/templates/grading_systems.html
new file mode 100644
index 0000000..3a71ebf
--- /dev/null
+++ b/grades/templates/grading_systems.html
@@ -0,0 +1,74 @@
+{% extends "manage.html" %}
+{% block main %}
+<html>
+ <a href="{% url 'grades:add_grade' %}" class="btn btn-primary"> Add a Grading System </a>
+ <a href="{% url 'yaksh:courses' %}" class="btn btn-danger"> Back to Courses </a>
+ <br><br>
+ <b> Available Grading Systems: </b>
+ <table class="table">
+ <tr>
+ <th>Grading System</th>
+ <th>Grading Ranges</th>
+ </tr>
+ <tr>
+ <td>
+ <a href="{% url 'grades:edit_grade' default_grading_system.id %}">
+ {{ default_grading_system.name }}</a> (<b>Default Grading System</b>)
+ </td>
+ <td>
+ <table class="table">
+ <tr>
+ <th>Lower Limit</th>
+ <th>Upper Limit</th>
+ <th>Grade</th>
+ <th>Description</th>
+ </tr>
+ {% for range in default_grading_system.graderange_set.all %}
+ <tr>
+ <td>{{range.lower_limit}}</td>
+ <td>{{range.upper_limit}}</td>
+ <td>{{range.grade}}</td>
+ {% if range.description %}
+ <td>{{range.description}}</td>
+ {% else %}
+ <td>------</td>
+ {% endif %}
+ </tr>
+ {% endfor %}
+ </table>
+ </td>
+ </tr>
+ {% if grading_systems %}
+ {% for system in grading_systems %}
+ <tr>
+ <td>
+ <a href="{% url 'grades:edit_grade' system.id %}">{{ system.name }}</a>
+ </td>
+ <td>
+ <table class="table">
+ <tr>
+ <th>Lower Limit</th>
+ <th>Upper Limit</th>
+ <th>Grade</th>
+ <th>Description</th>
+ </tr>
+ {% for range in system.graderange_set.all %}
+ <tr>
+ <td>{{range.lower_limit}}</td>
+ <td>{{range.upper_limit}}</td>
+ <td>{{range.grade}}</td>
+ {% if range.description %}
+ <td>{{range.description}}</td>
+ {% else %}
+ <td>------</td>
+ {% endif %}
+ </tr>
+ {% endfor %}
+ </table>
+ </td>
+ </tr>
+ {% endfor %}
+ </table>
+ {% endif %}
+</html>
+{% endblock %}
diff --git a/grades/tests/__init__.py b/grades/tests/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/grades/tests/__init__.py
diff --git a/grades/tests/test_models.py b/grades/tests/test_models.py
new file mode 100644
index 0000000..f8d5c5c
--- /dev/null
+++ b/grades/tests/test_models.py
@@ -0,0 +1,28 @@
+from django.test import TestCase
+from grades.models import GradingSystem, GradeRange
+
+
+class GradingSystemTestCase(TestCase):
+ def setUp(self):
+ GradingSystem.objects.create(name='unusable')
+
+ def test_get_grade(self):
+ # Given
+ grading_system = GradingSystem.objects.get(name='default')
+ expected_grades = {0: 'F', 31: 'F', 49: 'P', 55: 'C', 60: 'B', 80: 'A',
+ 95: 'A+', 100: 'A+', 100.5: 'A+', 101: None,
+ 109: None}
+ for marks in expected_grades.keys():
+ # When
+ grade = grading_system.get_grade(marks)
+ # Then
+ self.assertEqual(expected_grades.get(marks), grade)
+
+ def test_grade_system_unusable(self):
+ # Given
+ # System with out ranges
+ grading_system = GradingSystem.objects.get(name='unusable')
+ # When
+ grade = grading_system.get_grade(29)
+ # Then
+ self.assertIsNone(grade)
diff --git a/grades/tests/test_views.py b/grades/tests/test_views.py
new file mode 100644
index 0000000..c944f03
--- /dev/null
+++ b/grades/tests/test_views.py
@@ -0,0 +1,105 @@
+from django.test import TestCase, Client
+from django.contrib.auth.models import User
+from django.core.urlresolvers import reverse
+from grades.models import GradingSystem
+
+
+def setUpModule():
+ user = User.objects.create_user(username='grades_user',
+ password='grades_user')
+
+
+def tearDownModule():
+ User.objects.all().delete()
+
+
+class GradeViewTest(TestCase):
+ def setUp(self):
+ self.client = Client()
+
+ def tearDown(self):
+ self.client.logout()
+
+ def test_grade_view(self):
+ # Given
+ # URL redirection due to no login credentials
+ status_code = 302
+ # When
+ response = self.client.get(reverse('grades:grading_systems'))
+ # Then
+ self.assertEqual(response.status_code, status_code)
+
+ # Given
+ # successful login and grading systems views
+ self.client.login(username='grades_user', password='grades_user')
+ status_code = 200
+ # When
+ response = self.client.get(reverse('grades:grading_systems'))
+ # Then
+ self.assertEqual(response.status_code, status_code)
+ self.assertTemplateUsed(response, 'grading_systems.html')
+
+
+class AddGradingSystemTest(TestCase):
+ def setUp(self):
+ self.client = Client()
+
+ def tearDown(self):
+ self.client.logout()
+
+ def test_add_grades_view(self):
+ # Given
+ status_code = 302
+ # When
+ response = self.client.get(reverse('grades:add_grade'))
+ # Then
+ self.assertEqual(response.status_code, status_code)
+
+ # Given
+ status_code = 200
+ self.client.login(username='grades_user', password='grades_user')
+ # When
+ response = self.client.get(reverse('grades:add_grade'))
+ # Then
+ self.assertEqual(response.status_code, status_code)
+ self.assertTemplateUsed(response, 'add_grades.html')
+
+ def test_add_grades_post(self):
+ # Given
+ self.client.login(username='grades_user', password='grades_user')
+ data = {'name': ['new_sys'], 'description': ['About grading system!'],
+ 'graderange_set-MIN_NUM_FORMS': ['0'],
+ 'graderange_set-TOTAL_FORMS': ['0'],
+ 'graderange_set-MAX_NUM_FORMS': ['1000'], 'add': ['Add'],
+ 'graderange_set-INITIAL_FORMS': ['0']}
+ # When
+ response = self.client.post(reverse('grades:add_grade'), data)
+ # Then
+ grading_systems = GradingSystem.objects.filter(name='new_sys')
+ self.assertEqual(len(grading_systems), 1)
+
+ # Given
+ grading_system = grading_systems.first()
+ # When
+ ranges = grading_system.graderange_set.all()
+ # Then
+ self.assertEqual(len(ranges), 0)
+
+ # Given
+ data = {'graderange_set-0-upper_limit': ['40'],
+ 'graderange_set-0-description': ['Fail'],
+ 'graderange_set-0-lower_limit': ['0'],
+ 'graderange_set-0-system': [''], 'name': ['new_sys'],
+ 'graderange_set-MIN_NUM_FORMS': ['0'],
+ 'graderange_set-TOTAL_FORMS': ['1'],
+ 'graderange_set-MAX_NUM_FORMS': ['1000'],
+ 'graderange_set-0-id': [''],
+ 'description': ['About the grading system!'],
+ 'graderange_set-0-grade': ['F'],
+ 'graderange_set-INITIAL_FORMS': ['0'], 'save': ['Save']}
+ # When
+ response = self.client.post(reverse('grades:edit_grade',
+ kwargs={'system_id': 2}), data)
+ # Then
+ ranges = grading_system.graderange_set.all()
+ self.assertEqual(len(ranges), 1)
diff --git a/grades/urls.py b/grades/urls.py
new file mode 100644
index 0000000..32a7e4d
--- /dev/null
+++ b/grades/urls.py
@@ -0,0 +1,10 @@
+from django.conf.urls import url
+from grades import views
+
+urlpatterns = [
+ url(r'^$', views.grading_systems, name="grading_systems_home"),
+ url(r'^grading_systems/$', views.grading_systems, name="grading_systems"),
+ url(r'^add_grade/$', views.add_grading_system, name="add_grade"),
+ url(r'^add_grade/(?P<system_id>\d+)/$', views.add_grading_system,
+ name="edit_grade"),
+]
diff --git a/grades/views.py b/grades/views.py
new file mode 100644
index 0000000..10f9999
--- /dev/null
+++ b/grades/views.py
@@ -0,0 +1,45 @@
+from django.shortcuts import render
+from django.contrib.auth.decorators import login_required
+from django.forms import inlineformset_factory
+from grades.forms import GradingSystemForm
+from grades.models import GradingSystem, GradeRange
+
+
+@login_required
+def grading_systems(request):
+ user = request.user
+ default_grading_system = GradingSystem.objects.get(name='default')
+ grading_systems = GradingSystem.objects.filter(creator=user)
+ return render(request, 'grading_systems.html', {'default_grading_system':
+ default_grading_system, 'grading_systems': grading_systems})
+
+
+@login_required
+def add_grading_system(request, system_id=None):
+ user = request.user
+ grading_system = None
+ if system_id is not None:
+ grading_system = GradingSystem.objects.get(id=system_id)
+ GradeRangeFormSet = inlineformset_factory(GradingSystem, GradeRange,
+ fields='__all__', extra=0)
+ grade_form = GradingSystemForm(instance=grading_system)
+ is_default = grading_system is not None and grading_system.name == 'default'
+
+ if request.method == 'POST':
+ formset = GradeRangeFormSet(request.POST, instance=grading_system)
+ grade_form = GradingSystemForm(request.POST, instance=grading_system)
+ if grade_form.is_valid():
+ system = grade_form.save(commit=False)
+ system.creator = user
+ system.save()
+ system_id = system.id
+ if formset.is_valid():
+ formset.save()
+ if 'add' in request.POST:
+ GradeRangeFormSet = inlineformset_factory(GradingSystem, GradeRange,
+ fields='__all__', extra=1)
+ formset = GradeRangeFormSet(instance=grading_system)
+
+ return render(request, 'add_grades.html', {'formset': formset,
+ 'grade_form': grade_form, "system_id": system_id,
+ 'is_default': is_default})
diff --git a/online_test/__init__.py b/online_test/__init__.py
index a71c5c7..32a90a3 100644
--- a/online_test/__init__.py
+++ b/online_test/__init__.py
@@ -1 +1 @@
-__version__ = '0.7.0'
+__version__ = '0.8.0'
diff --git a/online_test/settings.py b/online_test/settings.py
index c55a056..797d982 100644
--- a/online_test/settings.py
+++ b/online_test/settings.py
@@ -44,6 +44,7 @@ INSTALLED_APPS = (
'yaksh',
'taggit',
'social.apps.django_app.default',
+ 'grades',
)
MIDDLEWARE_CLASSES = (
diff --git a/online_test/urls.py b/online_test/urls.py
index ce0de41..e55864a 100644
--- a/online_test/urls.py
+++ b/online_test/urls.py
@@ -1,4 +1,4 @@
-from django.conf.urls import patterns, include, url
+from django.conf.urls import include, url
from django.conf import settings
from django.conf.urls.static import static
from django.contrib import admin
@@ -13,5 +13,6 @@ urlpatterns = [
url(r'^exam/', include('yaksh.urls', namespace='yaksh', app_name='yaksh')),
url(r'^exam/reset/', include('yaksh.urls_password_reset')),
url(r'^', include('social.apps.django_app.urls', namespace='social')),
+ url(r'^grades/', include('grades.urls', namespace='grades', app_name='grades')),
]
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
diff --git a/requirements/requirements-codeserver.txt b/requirements/requirements-codeserver.txt
index 004e45b..11bc0a2 100644
--- a/requirements/requirements-codeserver.txt
+++ b/requirements/requirements-codeserver.txt
@@ -4,3 +4,4 @@ six
requests
tornado==4.5.3
psutil
+nose==1.3.7
diff --git a/requirements/requirements-common.txt b/requirements/requirements-common.txt
index b170694..484111e 100644
--- a/requirements/requirements-common.txt
+++ b/requirements/requirements-common.txt
@@ -1,6 +1,6 @@
-r requirements-codeserver.txt
invoke==0.21.0
-django==1.9.5
+django==1.10
django-taggit==0.18.1
pytz==2016.4
python-social-auth==0.2.19
diff --git a/setup.cfg b/setup.cfg
index b88034e..5aef279 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -1,2 +1,2 @@
[metadata]
-description-file = README.md
+description-file = README.rst
diff --git a/setup.py b/setup.py
index 9fe5be5..e8e1394 100644
--- a/setup.py
+++ b/setup.py
@@ -6,6 +6,7 @@ README = open(os.path.join(os.path.dirname(__file__), 'README.rst')).read()
# allow setup.py to be run from any path
os.chdir(os.path.normpath(os.path.join(os.path.abspath(__file__), os.pardir)))
+
def get_version():
import os
data = {}
@@ -14,7 +15,7 @@ def get_version():
return data.get('__version__')
install_requires = [
- 'django==1.9.5',
+ 'django==1.10',
'django-taggit==0.18.1',
'pytz==2016.4',
'python-social-auth==0.2.19',
@@ -24,6 +25,7 @@ install_requires = [
'invoke==0.21.0',
'six',
'requests',
+ 'markdown==2.6.9',
]
setup(
diff --git a/yaksh/decorators.py b/yaksh/decorators.py
index 9e9bc6d..4b886a3 100644
--- a/yaksh/decorators.py
+++ b/yaksh/decorators.py
@@ -1,4 +1,4 @@
-from django.shortcuts import render_to_response, redirect
+from django.shortcuts import render, redirect
from django.conf import settings
from django.template import RequestContext
@@ -20,15 +20,13 @@ def has_profile(func):
def _wrapped_view(request, *args, **kwargs):
if user_has_profile(request.user):
return func(request, *args, **kwargs)
- ci = RequestContext(request)
if request.user.groups.filter(name='moderator').exists():
template = 'manage.html'
else:
template = 'user.html'
form = ProfileForm(user=request.user, instance=None)
context = {'template': template, 'form': form}
- return render_to_response('yaksh/editprofile.html', context,
- context_instance=ci)
+ return render(request, 'yaksh/editprofile.html', context)
return _wrapped_view
@@ -40,7 +38,6 @@ def email_verified(func):
"""
def is_email_verified(request, *args, **kwargs):
- ci = RequestContext(request)
user = request.user
context = {}
if not settings.IS_DEVELOPMENT:
@@ -49,7 +46,8 @@ def email_verified(func):
context['success'] = False
context['msg'] = "Your account is not verified. \
Please verify your account"
- return render_to_response('yaksh/activation_status.html',
- context, context_instance=ci)
+ return render(
+ request, 'yaksh/activation_status.html', context
+ )
return func(request, *args, **kwargs)
return is_email_verified \ No newline at end of file
diff --git a/yaksh/error_messages.py b/yaksh/error_messages.py
index 7ea8618..7a18c22 100644
--- a/yaksh/error_messages.py
+++ b/yaksh/error_messages.py
@@ -3,7 +3,9 @@ try:
except ImportError:
from itertools import izip_longest as zip_longest
-def prettify_exceptions(exception, message, traceback=None, testcase=None):
+
+def prettify_exceptions(exception, message, traceback=None,
+ testcase=None, line_no=None):
err = {"type": "assertion",
"exception": exception,
"traceback": traceback,
@@ -13,23 +15,28 @@ def prettify_exceptions(exception, message, traceback=None, testcase=None):
err["traceback"] = None
if exception == 'AssertionError':
- value = ("Expected answer from the"
- + " test case did not match the output")
- err["message"] = value
+ value = ("Expected answer from the" +
+ " test case did not match the output")
+ if message:
+ err["message"] = message
+ else:
+ err["message"] = value
err["traceback"] = None
- if testcase:
- err["test_case"] = testcase
+ err["test_case"] = testcase
+ err["line_no"] = line_no
return err
+
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():
+ 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()
@@ -44,18 +51,17 @@ def compare_outputs(expected_output, user_output, given_input=None):
msg["error_line_numbers"] = err_line_numbers
if ng != ne:
msg["error_msg"] = ("Incorrect Answer: "
- + "We had expected {} number of lines. "\
- .format(ne)
+ + "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]
- ))))
+ + "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"
diff --git a/yaksh/evaluator_tests/test_python_evaluation.py b/yaksh/evaluator_tests/test_python_evaluation.py
index 71d7732..1933d17 100644
--- a/yaksh/evaluator_tests/test_python_evaluation.py
+++ b/yaksh/evaluator_tests/test_python_evaluation.py
@@ -1,7 +1,6 @@
from __future__ import unicode_literals
import unittest
import os
-import sys
import tempfile
import shutil
from textwrap import dedent
@@ -26,13 +25,13 @@ class PythonAssertionEvaluationTestCases(EvaluatorBaseTest):
self.in_dir = tmp_in_dir_path
self.test_case_data = [{"test_case_type": "standardtestcase",
"test_case": 'assert(add(1,2)==3)',
- 'weight': 0.0},
+ 'weight': 0.0},
{"test_case_type": "standardtestcase",
"test_case": 'assert(add(-1,2)==1)',
- 'weight': 0.0},
+ 'weight': 0.0},
{"test_case_type": "standardtestcase",
"test_case": 'assert(add(-1,-2)==-3)',
- 'weight': 0.0},
+ 'weight': 0.0},
]
self.timeout_msg = ("Code took more than {0} seconds to run. "
"You probably have an infinite loop in"
@@ -46,14 +45,12 @@ class PythonAssertionEvaluationTestCases(EvaluatorBaseTest):
def test_correct_answer(self):
# Given
user_answer = "def add(a,b):\n\treturn a + b"
- kwargs = {
- 'metadata': {
- 'user_answer': user_answer,
- 'file_paths': self.file_paths,
- 'partial_grading': False,
- 'language': 'python'
- },
- 'test_case_data': self.test_case_data,
+ kwargs = {'metadata': {
+ 'user_answer': user_answer,
+ 'file_paths': self.file_paths,
+ 'partial_grading': False,
+ 'language': 'python'},
+ 'test_case_data': self.test_case_data,
}
# When
@@ -66,14 +63,12 @@ class PythonAssertionEvaluationTestCases(EvaluatorBaseTest):
def test_incorrect_answer(self):
# Given
user_answer = "def add(a,b):\n\treturn a - b"
- kwargs = {
- 'metadata': {
- 'user_answer': user_answer,
- 'file_paths': self.file_paths,
- 'partial_grading': False,
- 'language': 'python'
- },
- 'test_case_data': self.test_case_data,
+ kwargs = {'metadata': {
+ 'user_answer': user_answer,
+ 'file_paths': self.file_paths,
+ 'partial_grading': False,
+ 'language': 'python'},
+ 'test_case_data': self.test_case_data,
}
# When
@@ -85,13 +80,13 @@ class PythonAssertionEvaluationTestCases(EvaluatorBaseTest):
given_test_case_list = [tc["test_case"] for tc in self.test_case_data]
for error in result.get("error"):
self.assertEqual(error['exception'], 'AssertionError')
- self.assertEqual(error['message'],
- "Expected answer from the test case did not match the output"
- )
+ self.assertEqual(
+ error['message'],
+ "Expected answer from the test case did not match the output"
+ )
error_testcase_list = [tc['test_case'] for tc in result.get('error')]
self.assertEqual(error_testcase_list, given_test_case_list)
-
def test_partial_incorrect_answer(self):
# Given
user_answer = "def add(a,b):\n\treturn abs(a) + abs(b)"
@@ -100,19 +95,17 @@ class PythonAssertionEvaluationTestCases(EvaluatorBaseTest):
'weight': 1.0},
{"test_case_type": "standardtestcase",
"test_case": 'assert(add(-1,-2)==-3)',
- 'weight': 1.0},
+ 'weight': 1.0},
{"test_case_type": "standardtestcase",
"test_case": 'assert(add(1,2)==3)',
'weight': 2.0}
]
- kwargs = {
- 'metadata': {
- 'user_answer': user_answer,
- 'file_paths': self.file_paths,
- 'partial_grading': True,
- 'language': 'python'
- },
- 'test_case_data': test_case_data,
+ kwargs = {'metadata': {
+ 'user_answer': user_answer,
+ 'file_paths': self.file_paths,
+ 'partial_grading': True,
+ 'language': 'python'},
+ 'test_case_data': test_case_data,
}
# When
@@ -126,22 +119,22 @@ class PythonAssertionEvaluationTestCases(EvaluatorBaseTest):
given_test_case_list.remove('assert(add(1,2)==3)')
for error in result.get("error"):
self.assertEqual(error['exception'], 'AssertionError')
- self.assertEqual(error['message'],
- "Expected answer from the test case did not match the output"
- )
+ self.assertEqual(
+ error['message'],
+ "Expected answer from the test case did not match the output"
+ )
error_testcase_list = [tc['test_case'] for tc in result.get('error')]
self.assertEqual(error_testcase_list, given_test_case_list)
+
def test_infinite_loop(self):
# Given
user_answer = "def add(a, b):\n\twhile True:\n\t\tpass"
- kwargs = {
- 'metadata': {
- 'user_answer': user_answer,
- 'file_paths': self.file_paths,
- 'partial_grading': False,
- 'language': 'python'
- },
- 'test_case_data': self.test_case_data,
+ kwargs = {'metadata': {
+ 'user_answer': user_answer,
+ 'file_paths': self.file_paths,
+ 'partial_grading': False,
+ 'language': 'python'},
+ 'test_case_data': self.test_case_data,
}
# When
@@ -168,14 +161,12 @@ class PythonAssertionEvaluationTestCases(EvaluatorBaseTest):
"SyntaxError",
"invalid syntax"
]
- kwargs = {
- 'metadata': {
- 'user_answer': user_answer,
- 'file_paths': self.file_paths,
- 'partial_grading': False,
- 'language': 'python'
- },
- 'test_case_data': self.test_case_data,
+ kwargs = {'metadata': {
+ 'user_answer': user_answer,
+ 'file_paths': self.file_paths,
+ 'partial_grading': False,
+ 'language': 'python'},
+ 'test_case_data': self.test_case_data,
}
# When
@@ -201,14 +192,12 @@ class PythonAssertionEvaluationTestCases(EvaluatorBaseTest):
"IndentationError",
"indented block"
]
- kwargs = {
- 'metadata': {
- 'user_answer': user_answer,
- 'file_paths': self.file_paths,
- 'partial_grading': False,
- 'language': 'python'
- },
- 'test_case_data': self.test_case_data,
+ kwargs = {'metadata': {
+ 'user_answer': user_answer,
+ 'file_paths': self.file_paths,
+ 'partial_grading': False,
+ 'language': 'python'},
+ 'test_case_data': self.test_case_data,
}
# When
@@ -220,9 +209,9 @@ class PythonAssertionEvaluationTestCases(EvaluatorBaseTest):
self.assertFalse(result.get("success"))
self.assertEqual(5, len(err))
for msg in indent_error_msg:
- self.assert_correct_output(msg,
- result.get("error")[0]['traceback']
- )
+ self.assert_correct_output(
+ msg, result.get("error")[0]['traceback']
+ )
def test_name_error(self):
# Given
@@ -234,15 +223,13 @@ class PythonAssertionEvaluationTestCases(EvaluatorBaseTest):
"defined"
]
- kwargs = {
- 'metadata': {
- 'user_answer': user_answer,
- 'file_paths': self.file_paths,
- 'partial_grading': False,
- 'language': 'python'
- },
- 'test_case_data': self.test_case_data,
- }
+ kwargs = {'metadata': {
+ 'user_answer': user_answer,
+ 'file_paths': self.file_paths,
+ 'partial_grading': False,
+ 'language': 'python'},
+ 'test_case_data': self.test_case_data,
+ }
# When
grader = Grader(self.in_dir)
@@ -258,15 +245,13 @@ class PythonAssertionEvaluationTestCases(EvaluatorBaseTest):
return add(3, 3)
""")
recursion_error_msg = "maximum recursion depth exceeded"
- kwargs = {
- 'metadata': {
- 'user_answer': user_answer,
- 'file_paths': self.file_paths,
- 'partial_grading': False,
- 'language': 'python'
- },
- 'test_case_data': self.test_case_data,
- }
+ kwargs = {'metadata': {
+ 'user_answer': user_answer,
+ 'file_paths': self.file_paths,
+ 'partial_grading': False,
+ 'language': 'python'},
+ 'test_case_data': self.test_case_data,
+ }
# When
grader = Grader(self.in_dir)
@@ -289,15 +274,13 @@ class PythonAssertionEvaluationTestCases(EvaluatorBaseTest):
"argument"
]
- kwargs = {
- 'metadata': {
- 'user_answer': user_answer,
- 'file_paths': self.file_paths,
- 'partial_grading': False,
- 'language': 'python'
- },
- 'test_case_data': self.test_case_data,
- }
+ kwargs = {'metadata': {
+ 'user_answer': user_answer,
+ 'file_paths': self.file_paths,
+ 'partial_grading': False,
+ 'language': 'python'},
+ 'test_case_data': self.test_case_data,
+ }
# When
grader = Grader(self.in_dir)
@@ -323,25 +306,26 @@ class PythonAssertionEvaluationTestCases(EvaluatorBaseTest):
"base"
]
- kwargs = {
- 'metadata': {
- 'user_answer': user_answer,
- 'file_paths': self.file_paths,
- 'partial_grading': False,
- 'language': 'python'
- },
- 'test_case_data': self.test_case_data,
- }
+ kwargs = {'metadata': {
+ 'user_answer': user_answer,
+ 'file_paths': self.file_paths,
+ 'partial_grading': False,
+ 'language': 'python'},
+ 'test_case_data': self.test_case_data,
+ }
# When
grader = Grader(self.in_dir)
result = grader.evaluate(kwargs)
- err = result.get("error")[0]['traceback']
+ errors = result.get("error")
# Then
self.assertFalse(result.get("success"))
for msg in value_error_msg:
- self.assert_correct_output(msg, err)
+ self.assert_correct_output(msg, errors[0]['traceback'])
+ for index, error in enumerate(errors):
+ self.assertEqual(error['test_case'],
+ self.test_case_data[index]['test_case'])
def test_file_based_assert(self):
# Given
@@ -356,15 +340,13 @@ class PythonAssertionEvaluationTestCases(EvaluatorBaseTest):
return f.read()[0]
""")
- kwargs = {
- 'metadata': {
- 'user_answer': user_answer,
- 'file_paths': self.file_paths,
- 'partial_grading': False,
- 'language': 'python'
- },
- 'test_case_data': self.test_case_data,
- }
+ kwargs = {'metadata': {
+ 'user_answer': user_answer,
+ 'file_paths': self.file_paths,
+ 'partial_grading': False,
+ 'language': 'python'},
+ 'test_case_data': self.test_case_data,
+ }
# When
grader = Grader(self.in_dir)
@@ -390,25 +372,23 @@ class PythonAssertionEvaluationTestCases(EvaluatorBaseTest):
]
kwargs = {'metadata': {
- 'user_answer': user_answer,
- 'file_paths': self.file_paths,
- 'partial_grading': False,
- 'language': 'python'
- },
- 'test_case_data': test_case_data,
- }
+ 'user_answer': user_answer,
+ 'file_paths': self.file_paths,
+ 'partial_grading': False,
+ 'language': 'python'},
+ 'test_case_data': test_case_data,
+ }
# When
grader = Grader(self.in_dir)
result = grader.evaluate(kwargs)
- err = result.get("error")[0]['traceback']
+ err = result.get("error")[0]['traceback']
# Then
self.assertFalse(result.get("success"))
for msg in syntax_error_msg:
self.assert_correct_output(msg, err)
-
def test_multiple_testcase_error(self):
""" Tests the user answer with an correct test case
first and then with an incorrect test case """
@@ -418,7 +398,8 @@ class PythonAssertionEvaluationTestCases(EvaluatorBaseTest):
"test_case": 'assert(palindrome("abba")==True)',
"weight": 0.0},
{"test_case_type": "standardtestcase",
- "test_case": 's="abbb"\nassert palindrome(S)==False',
+ "test_case": 's="abbb"\n'
+ 'assert palindrome(S)==False',
"weight": 0.0}
]
name_error_msg = ["Traceback",
@@ -426,15 +407,13 @@ class PythonAssertionEvaluationTestCases(EvaluatorBaseTest):
"NameError",
"name 'S' is not defined"
]
- kwargs = {
- 'metadata': {
- 'user_answer': user_answer,
- 'file_paths': self.file_paths,
- 'partial_grading': False,
- 'language': 'python'
- },
- 'test_case_data': test_case_data,
- }
+ kwargs = {'metadata': {
+ 'user_answer': user_answer,
+ 'file_paths': self.file_paths,
+ 'partial_grading': False,
+ 'language': 'python'},
+ 'test_case_data': test_case_data,
+ }
# When
grader = Grader(self.in_dir)
@@ -454,18 +433,15 @@ class PythonAssertionEvaluationTestCases(EvaluatorBaseTest):
return type(a)
""")
test_case_data = [{"test_case_type": "standardtestcase",
- "test_case": 'assert(strchar("hello")==str)',
- "weight": 0.0
- },]
- kwargs = {
- 'metadata': {
- 'user_answer': user_answer,
- 'file_paths': self.file_paths,
- 'partial_grading': False,
- 'language': 'python'
- },
- 'test_case_data': test_case_data,
- }
+ "test_case": 'assert(strchar("hello")==str)',
+ "weight": 0.0}]
+ kwargs = {'metadata': {
+ 'user_answer': user_answer,
+ 'file_paths': self.file_paths,
+ 'partial_grading': False,
+ 'language': 'python'},
+ 'test_case_data': test_case_data,
+ }
# When
grader = Grader(self.in_dir)
result = grader.evaluate(kwargs)
@@ -473,6 +449,31 @@ class PythonAssertionEvaluationTestCases(EvaluatorBaseTest):
# Then
self.assertTrue(result.get("success"))
+ def test_incorrect_answer_with_nose_assert(self):
+ user_answer = dedent("""\
+ def add(a, b):
+ return a - b
+ """)
+ test_case_data = [{"test_case_type": "standardtestcase",
+ "test_case": 'assert_equal(add(1, 2), 3)',
+ "weight": 0.0}]
+ kwargs = {'metadata': {
+ 'user_answer': user_answer,
+ 'file_paths': self.file_paths,
+ 'partial_grading': False,
+ 'language': 'python'},
+ 'test_case_data': test_case_data,
+ }
+ # When
+ grader = Grader(self.in_dir)
+ result = grader.evaluate(kwargs)
+
+ # Then
+ self.assertFalse(result.get("success"))
+ error = result.get("error")[0]
+ self.assertEqual(error['exception'], 'AssertionError')
+ self.assertEqual(error['message'], '-1 != 3')
+
class PythonStdIOEvaluationTestCases(EvaluatorBaseTest):
def setUp(self):
@@ -501,13 +502,12 @@ class PythonStdIOEvaluationTestCases(EvaluatorBaseTest):
"""
)
kwargs = {'metadata': {
- 'user_answer': user_answer,
- 'file_paths': self.file_paths,
- 'partial_grading': False,
- 'language': 'python'
- },
- 'test_case_data': self.test_case_data
- }
+ 'user_answer': user_answer,
+ 'file_paths': self.file_paths,
+ 'partial_grading': False,
+ 'language': 'python'},
+ 'test_case_data': self.test_case_data
+ }
# When
grader = Grader(self.in_dir)
@@ -534,13 +534,12 @@ class PythonStdIOEvaluationTestCases(EvaluatorBaseTest):
)
kwargs = {'metadata': {
- 'user_answer': user_answer,
- 'file_paths': self.file_paths,
- 'partial_grading': False,
- 'language': 'python'
- },
- 'test_case_data': self.test_case_data
- }
+ 'user_answer': user_answer,
+ 'file_paths': self.file_paths,
+ 'partial_grading': False,
+ 'language': 'python'},
+ 'test_case_data': self.test_case_data
+ }
# When
grader = Grader(self.in_dir)
@@ -551,11 +550,13 @@ class PythonStdIOEvaluationTestCases(EvaluatorBaseTest):
def test_correct_answer_string(self):
# Given
- self.test_case_data = [{"test_case_type": "stdiobasedtestcase",
- "expected_input": ("the quick brown fox jumps over the lazy dog\nthe"),
- "expected_output": "2",
- "weight": 0.0
- }]
+ self.test_case_data = [{
+ "test_case_type": "stdiobasedtestcase",
+ "expected_input": ("the quick brown fox jumps over "
+ "the lazy dog\nthe"),
+ "expected_output": "2",
+ "weight": 0.0
+ }]
user_answer = dedent("""
from six.moves import input
a = str(input())
@@ -565,13 +566,12 @@ class PythonStdIOEvaluationTestCases(EvaluatorBaseTest):
)
kwargs = {'metadata': {
- 'user_answer': user_answer,
- 'file_paths': self.file_paths,
- 'partial_grading': False,
- 'language': 'python'
- },
- 'test_case_data': self.test_case_data
- }
+ 'user_answer': user_answer,
+ 'file_paths': self.file_paths,
+ 'partial_grading': False,
+ 'language': 'python'},
+ 'test_case_data': self.test_case_data
+ }
# When
grader = Grader(self.in_dir)
@@ -594,13 +594,12 @@ class PythonStdIOEvaluationTestCases(EvaluatorBaseTest):
"""
)
kwargs = {'metadata': {
- 'user_answer': user_answer,
- 'file_paths': self.file_paths,
- 'partial_grading': False,
- 'language': 'python'
- },
- 'test_case_data': self.test_case_data
- }
+ 'user_answer': user_answer,
+ 'file_paths': self.file_paths,
+ 'partial_grading': False,
+ 'language': 'python'},
+ 'test_case_data': self.test_case_data
+ }
# When
grader = Grader(self.in_dir)
@@ -629,13 +628,12 @@ class PythonStdIOEvaluationTestCases(EvaluatorBaseTest):
"""
)
kwargs = {'metadata': {
- 'user_answer': user_answer,
- 'file_paths': self.file_paths,
- 'partial_grading': False,
- 'language': 'python'
- },
- 'test_case_data': self.test_case_data
- }
+ 'user_answer': user_answer,
+ 'file_paths': self.file_paths,
+ 'partial_grading': False,
+ 'language': 'python'},
+ 'test_case_data': self.test_case_data
+ }
# When
grader = Grader(self.in_dir)
@@ -646,24 +644,24 @@ class PythonStdIOEvaluationTestCases(EvaluatorBaseTest):
def test_infinite_loop(self):
# Given
- self.test_case_data = [{"test_case_type": "stdiobasedtestcase",
- "expected_input": "1\n2",
- "expected_output": "3",
- "weight": 0.0
- }]
+ self.test_case_data = [{
+ "test_case_type": "stdiobasedtestcase",
+ "expected_input": "1\n2",
+ "expected_output": "3",
+ "weight": 0.0
+ }]
timeout_msg = ("Code took more than {0} seconds to run. "
- "You probably have an infinite loop in"
- " your code.").format(SERVER_TIMEOUT)
+ "You probably have an infinite loop in"
+ " your code.").format(SERVER_TIMEOUT)
user_answer = "while True:\n\tpass"
kwargs = {'metadata': {
- 'user_answer': user_answer,
- 'file_paths': self.file_paths,
- 'partial_grading': False,
- 'language': 'python'
- },
- 'test_case_data': self.test_case_data
- }
+ 'user_answer': user_answer,
+ 'file_paths': self.file_paths,
+ 'partial_grading': False,
+ 'language': 'python'},
+ 'test_case_data': self.test_case_data
+ }
# When
grader = Grader(self.in_dir)
@@ -675,7 +673,6 @@ class PythonStdIOEvaluationTestCases(EvaluatorBaseTest):
)
self.assertFalse(result.get('success'))
-
def test_unicode_literal_bug(self):
# Given
user_answer = dedent("""\
@@ -687,21 +684,44 @@ class PythonStdIOEvaluationTestCases(EvaluatorBaseTest):
"expected_output": "str",
"weight": 0.0
}]
- kwargs = {
- 'metadata': {
- 'user_answer': user_answer,
- 'file_paths': self.file_paths,
- 'partial_grading': False,
- 'language': 'python'
- },
- 'test_case_data': test_case_data,
- }
+ kwargs = {'metadata': {
+ 'user_answer': user_answer,
+ 'file_paths': self.file_paths,
+ 'partial_grading': False,
+ 'language': 'python'},
+ 'test_case_data': test_case_data,
+ }
# When
grader = Grader(self.in_dir)
result = grader.evaluate(kwargs)
# Then
self.assertTrue(result.get("success"))
+ def test_get_error_lineno(self):
+ user_answer = dedent("""\
+ print(1/0)
+ """)
+ test_case_data = [{"test_case_type": "stdiobasedtestcase",
+ "expected_input": "",
+ "expected_output": "1",
+ "weight": 0.0
+ }]
+ kwargs = {'metadata': {
+ 'user_answer': user_answer,
+ 'file_paths': self.file_paths,
+ 'partial_grading': False,
+ 'language': 'python'},
+ 'test_case_data': test_case_data,
+ }
+ # When
+ grader = Grader(self.in_dir)
+ result = grader.evaluate(kwargs)
+ # Then
+ self.assertFalse(result.get("success"))
+ error = result.get("error")[0]
+ self.assertEqual(error['line_no'], 1)
+ self.assertEqual(error['exception'], "ZeroDivisionError")
+
class PythonHookEvaluationTestCases(EvaluatorBaseTest):
@@ -733,19 +753,17 @@ class PythonHookEvaluationTestCases(EvaluatorBaseTest):
success, err, mark_fraction = True, "", 1.0
return success, err, mark_fraction
"""
- )
+ )
test_case_data = [{"test_case_type": "hooktestcase",
- "hook_code": hook_code,"weight": 1.0
- }]
- kwargs = {
- 'metadata': {
- 'user_answer': user_answer,
- 'file_paths': self.file_paths,
- 'partial_grading': True,
- 'language': 'python'
- },
- 'test_case_data': test_case_data,
+ "hook_code": hook_code, "weight": 1.0
+ }]
+ kwargs = {'metadata': {
+ 'user_answer': user_answer,
+ 'file_paths': self.file_paths,
+ 'partial_grading': True,
+ 'language': 'python'},
+ 'test_case_data': test_case_data,
}
# When
@@ -768,20 +786,18 @@ class PythonHookEvaluationTestCases(EvaluatorBaseTest):
success, err, mark_fraction = True, "", 1.0
return success, err, mark_fraction
"""
- )
+ )
test_case_data = [{"test_case_type": "hooktestcase",
- "hook_code": hook_code,"weight": 1.0
- }]
-
- kwargs = {
- 'metadata': {
- 'user_answer': user_answer,
- 'file_paths': self.file_paths,
- 'partial_grading': False,
- 'language': 'python'
- },
- 'test_case_data': test_case_data,
+ "hook_code": hook_code, "weight": 1.0
+ }]
+
+ kwargs = {'metadata': {
+ 'user_answer': user_answer,
+ 'file_paths': self.file_paths,
+ 'partial_grading': False,
+ 'language': 'python'},
+ 'test_case_data': test_case_data,
}
# When
@@ -805,21 +821,19 @@ class PythonHookEvaluationTestCases(EvaluatorBaseTest):
success, err, mark_fraction = True, "", 1.0
return success, err, mark_fraction
"""
- )
+ )
test_case_data = [{"test_case_type": "standardtestcase",
"test_case": assert_test_case, 'weight': 1.0},
{"test_case_type": "hooktestcase",
"hook_code": hook_code, 'weight': 1.0},
]
- kwargs = {
- 'metadata': {
- 'user_answer': user_answer,
- 'file_paths': self.file_paths,
- 'partial_grading': True,
- 'language': 'python'
- },
- 'test_case_data': test_case_data,
+ kwargs = {'metadata': {
+ 'user_answer': user_answer,
+ 'file_paths': self.file_paths,
+ 'partial_grading': True,
+ 'language': 'python'},
+ 'test_case_data': test_case_data,
}
# When
@@ -842,7 +856,7 @@ class PythonHookEvaluationTestCases(EvaluatorBaseTest):
success, err, mark_fraction = True, "", 0.5
return success, err, mark_fraction
"""
- )
+ )
hook_code_2 = dedent("""\
def check_answer(user_answer):
success = False
@@ -853,22 +867,19 @@ class PythonHookEvaluationTestCases(EvaluatorBaseTest):
success, err, mark_fraction = True, "", 1.0
return success, err, mark_fraction
"""
- )
-
+ )
test_case_data = [{"test_case_type": "hooktestcase",
"hook_code": hook_code_1, 'weight': 1.0},
{"test_case_type": "hooktestcase",
"hook_code": hook_code_2, 'weight': 1.0},
]
- kwargs = {
- 'metadata': {
- 'user_answer': user_answer,
- 'file_paths': self.file_paths,
- 'partial_grading': True,
- 'language': 'python'
- },
- 'test_case_data': test_case_data,
+ kwargs = {'metadata': {
+ 'user_answer': user_answer,
+ 'file_paths': self.file_paths,
+ 'partial_grading': True,
+ 'language': 'python'},
+ 'test_case_data': test_case_data,
}
# When
@@ -892,19 +903,18 @@ class PythonHookEvaluationTestCases(EvaluatorBaseTest):
success, err, mark_fraction = True, "", 1.0
return success, err, mark_fraction
"""
- )
+ )
+
test_case_data = [{"test_case_type": "hooktestcase",
- "hook_code": hook_code,"weight": 1.0
- }]
-
- kwargs = {
- 'metadata': {
- 'user_answer': user_answer,
- 'file_paths': self.file_paths,
- 'partial_grading': False,
- 'language': 'python'
- },
- 'test_case_data': test_case_data,
+ "hook_code": hook_code, "weight": 1.0
+ }]
+
+ kwargs = {'metadata': {
+ 'user_answer': user_answer,
+ 'file_paths': self.file_paths,
+ 'partial_grading': False,
+ 'language': 'python'},
+ 'test_case_data': test_case_data,
}
# When
@@ -931,19 +941,18 @@ class PythonHookEvaluationTestCases(EvaluatorBaseTest):
success, err, mark_fraction = True, "", 1.0
return success, err, mark_fraction
"""
- )
+ )
+
test_case_data = [{"test_case_type": "hooktestcase",
- "hook_code": hook_code,"weight": 1.0
- }]
- kwargs = {
- 'metadata': {
- 'user_answer': user_answer,
- 'file_paths': self.file_paths,
- 'assign_files': [(self.tmp_file, False)],
- 'partial_grading': False,
- 'language': 'python'
- },
- 'test_case_data': test_case_data,
+ "hook_code": hook_code, "weight": 1.0
+ }]
+ kwargs = {'metadata': {
+ 'user_answer': user_answer,
+ 'file_paths': self.file_paths,
+ 'assign_files': [(self.tmp_file, False)],
+ 'partial_grading': False,
+ 'language': 'python'},
+ 'test_case_data': test_case_data,
}
# When
@@ -953,5 +962,6 @@ class PythonHookEvaluationTestCases(EvaluatorBaseTest):
# Then
self.assertTrue(result.get('success'))
+
if __name__ == '__main__':
unittest.main()
diff --git a/yaksh/evaluator_tests/test_simple_question_types.py b/yaksh/evaluator_tests/test_simple_question_types.py
index cbf2abd..dfb82a2 100644
--- a/yaksh/evaluator_tests/test_simple_question_types.py
+++ b/yaksh/evaluator_tests/test_simple_question_types.py
@@ -1,10 +1,11 @@
import unittest
from datetime import datetime, timedelta
from django.utils import timezone
+from textwrap import dedent
import pytz
from yaksh.models import User, Profile, Question, Quiz, QuestionPaper,\
QuestionSet, AnswerPaper, Answer, Course, IntegerTestCase, FloatTestCase,\
- StringTestCase, McqTestCase
+ StringTestCase, McqTestCase, ArrangeTestCase
def setUpModule():
@@ -39,15 +40,17 @@ def setUpModule():
duration=30, active=True, attempts_allowed=1,
time_between_attempts=0, pass_criteria=0,
description='demo quiz 100',
- instructions="Demo Instructions"
+ instructions="Demo Instructions",
+ creator=user
)
question_paper = QuestionPaper.objects.create(quiz=quiz,
total_marks=1.0)
def tearDownModule():
- User.objects.get(username="demo_user_100").delete()
- User.objects.get(username="demo_user_101").delete()
+ User.objects.filter(username__in=["demo_user_100", "demo_user_101"])\
+ .delete()
+
class IntegerQuestionTestCases(unittest.TestCase):
@classmethod
@@ -116,11 +119,9 @@ class IntegerQuestionTestCases(unittest.TestCase):
# Regrade
# Given
- self.answer.correct = True
- self.answer.marks = 1
-
- self.answer.answer = 200
- self.answer.save()
+ regrade_answer = Answer.objects.get(id=self.answer.id)
+ regrade_answer.answer = 200
+ regrade_answer.save()
# When
details = self.answerpaper.regrade(self.question1.id)
@@ -128,6 +129,7 @@ class IntegerQuestionTestCases(unittest.TestCase):
# Then
self.answer = self.answerpaper.answers.filter(question=self.question1
).last()
+ self.assertEqual(self.answer, regrade_answer)
self.assertTrue(details[0])
self.assertEqual(self.answer.marks, 0)
self.assertFalse(self.answer.correct)
@@ -153,11 +155,9 @@ class IntegerQuestionTestCases(unittest.TestCase):
# Regrade
# Given
- self.answer.correct = True
- self.answer.marks = 1
-
- self.answer.answer = 25
- self.answer.save()
+ regrade_answer = Answer.objects.get(id=self.answer.id)
+ regrade_answer.answer = 25
+ regrade_answer.save()
# When
details = self.answerpaper.regrade(self.question1.id)
@@ -165,6 +165,7 @@ class IntegerQuestionTestCases(unittest.TestCase):
# Then
self.answer = self.answerpaper.answers.filter(question=self.question1
).last()
+ self.assertEqual(self.answer, regrade_answer)
self.assertTrue(details[0])
self.assertEqual(self.answer.marks, 1)
self.assertTrue(self.answer.correct)
@@ -250,11 +251,9 @@ class StringQuestionTestCases(unittest.TestCase):
# Regrade
# Given
- answer.correct = True
- answer.marks = 1
-
- answer.answer = "hello, mars!"
- answer.save()
+ regrade_answer = Answer.objects.get(id=answer.id)
+ regrade_answer.answer = "hello, mars!"
+ regrade_answer.save()
# When
details = self.answerpaper.regrade(self.question1.id)
@@ -262,6 +261,7 @@ class StringQuestionTestCases(unittest.TestCase):
# Then
answer = self.answerpaper.answers.filter(question=self.question1)\
.last()
+ self.assertEqual(answer, regrade_answer)
self.assertTrue(details[0])
self.assertEqual(answer.marks, 0)
self.assertFalse(answer.correct)
@@ -284,11 +284,9 @@ class StringQuestionTestCases(unittest.TestCase):
# Regrade
# Given
- answer.correct = True
- answer.marks = 1
-
- answer.answer = "hello, earth!"
- answer.save()
+ regrade_answer = Answer.objects.get(id=answer.id)
+ regrade_answer.answer = "hello, earth!"
+ regrade_answer.save()
# When
details = self.answerpaper.regrade(self.question1.id)
@@ -296,6 +294,7 @@ class StringQuestionTestCases(unittest.TestCase):
# Then
answer = self.answerpaper.answers.filter(question=self.question1)\
.last()
+ self.assertEqual(answer, regrade_answer)
self.assertTrue(details[0])
self.assertEqual(answer.marks, 1)
self.assertTrue(answer.correct)
@@ -317,11 +316,9 @@ class StringQuestionTestCases(unittest.TestCase):
# Regrade
# Given
- answer.correct = True
- answer.marks = 1
-
- answer.answer = "hello, earth!"
- answer.save()
+ regrade_answer = Answer.objects.get(id=answer.id)
+ regrade_answer.answer = "hello, earth!"
+ regrade_answer.save()
# When
details = self.answerpaper.regrade(self.question2.id)
@@ -329,6 +326,7 @@ class StringQuestionTestCases(unittest.TestCase):
# Then
answer = self.answerpaper.answers.filter(question=self.question2)\
.last()
+ self.assertEqual(answer, regrade_answer)
self.assertTrue(details[0])
self.assertEqual(answer.marks, 0)
self.assertFalse(answer.correct)
@@ -351,11 +349,9 @@ class StringQuestionTestCases(unittest.TestCase):
# Regrade
# Given
- answer.correct = True
- answer.marks = 1
-
- answer.answer = "Hello, EARTH!"
- answer.save()
+ regrade_answer = Answer.objects.get(id=answer.id)
+ regrade_answer.answer = "Hello, EARTH!"
+ regrade_answer.save()
# When
details = self.answerpaper.regrade(self.question2.id)
@@ -363,6 +359,7 @@ class StringQuestionTestCases(unittest.TestCase):
# Then
answer = self.answerpaper.answers.filter(question=self.question2)\
.last()
+ self.assertEqual(answer, regrade_answer)
self.assertTrue(details[0])
self.assertEqual(answer.marks, 1)
self.assertTrue(answer.correct)
@@ -432,13 +429,11 @@ class FloatQuestionTestCases(unittest.TestCase):
# Then
self.assertTrue(result['success'])
- # Regrade
+ # Regrade with wrong answer
# Given
- self.answer.correct = True
- self.answer.marks = 1
-
- self.answer.answer = 0.0
- self.answer.save()
+ regrade_answer = Answer.objects.get(id=self.answer.id)
+ regrade_answer.answer = 0.0
+ regrade_answer.save()
# When
details = self.answerpaper.regrade(self.question1.id)
@@ -446,6 +441,7 @@ class FloatQuestionTestCases(unittest.TestCase):
# Then
self.answer = self.answerpaper.answers.filter(question=self.question1
).last()
+ self.assertEqual(self.answer, regrade_answer)
self.assertTrue(details[0])
self.assertEqual(self.answer.marks, 0)
self.assertFalse(self.answer.correct)
@@ -470,11 +466,270 @@ class FloatQuestionTestCases(unittest.TestCase):
# Regrade
# Given
- self.answer.correct = True
- self.answer.marks = 1
+ regrade_answer = Answer.objects.get(id=self.answer.id)
+ regrade_answer.answer = 99.9
+ regrade_answer.save()
+
+ # When
+ details = self.answerpaper.regrade(self.question1.id)
+
+ # Then
+ self.answer = self.answerpaper.answers.filter(question=self.question1
+ ).last()
+ self.assertEqual(self.answer, regrade_answer)
+ self.assertTrue(details[0])
+ self.assertEqual(self.answer.marks, 1)
+ self.assertTrue(self.answer.correct)
+class MCQQuestionTestCases(unittest.TestCase):
+ @classmethod
+ def setUpClass(self):
+ #Creating User
+ self.user = User.objects.get(username='demo_user_100')
+ self.user2 = User.objects.get(username='demo_user_101')
+ self.user_ip = '127.0.0.1'
+
+ #Creating Course
+ self.course = Course.objects.get(name="Python Course 100")
+ # Creating Quiz
+ self.quiz = Quiz.objects.get(description="demo quiz 100")
+ # Creating Question paper
+ self.question_paper = QuestionPaper.objects.get(quiz=self.quiz)
+ self.question_paper.shuffle_testcases = True
+ self.question_paper.save()
+ #Creating Question
+ self.question1 = Question.objects.create(summary='mcq1', points=1,
+ type='code', user=self.user,
+ )
+ self.question1.language = 'python'
+ self.question1.type = "mcq"
+ self.question1.test_case_type = 'Mcqtestcase'
+ self.question1.description = 'Which option is Correct?'
+ self.question1.save()
+
+ # For questions
+ self.mcq_based_testcase_1 = McqTestCase(question=self.question1,
+ options="Correct",
+ correct=True,
+ type='mcqtestcase',
+ )
+ self.mcq_based_testcase_1.save()
+
+ self.mcq_based_testcase_2 = McqTestCase(question=self.question1,
+ options="Incorrect",
+ correct=False,
+ type='mcqtestcase',
+ )
+ self.mcq_based_testcase_2.save()
+
+ self.mcq_based_testcase_3 = McqTestCase(question=self.question1,
+ options="Incorrect",
+ correct=False,
+ type='mcqtestcase',
+ )
+ self.mcq_based_testcase_3.save()
+
+ self.mcq_based_testcase_4 = McqTestCase(question=self.question1,
+ options="Incorrect",
+ correct=False,
+ type='mcqtestcase',
+ )
+ self.mcq_based_testcase_4.save()
+
+ self.question_paper.fixed_questions.add(self.question1)
+
+ self.answerpaper = self.question_paper.make_answerpaper(
+ user=self.user, ip=self.user_ip,
+ attempt_num=1,
+ course_id=self.course.id
+ )
+
+ # Answerpaper for user 2
+ self.answerpaper2 = self.question_paper.make_answerpaper(
+ user=self.user2, ip=self.user_ip,
+ attempt_num=1,
+ course_id=self.course.id
+ )
+
+ @classmethod
+ def tearDownClass(self):
+ self.question1.delete()
+ self.answerpaper.delete()
+ self.answerpaper2.delete()
- self.answer.answer = 99.9
+ def test_shuffle_test_cases(self):
+ # Given
+ # When
+
+ user_testcase = self.question1.get_ordered_test_cases(
+ self.answerpaper
+ )
+ order1 = [tc.id for tc in user_testcase]
+ user2_testcase = self.question1.get_ordered_test_cases(
+ self.answerpaper2
+ )
+ order2 = [tc.id for tc in user2_testcase]
+ self.question_paper.shuffle_testcases = False
+ self.question_paper.save()
+ answerpaper3 = self.question_paper.make_answerpaper(
+ user=self.user2, ip=self.user_ip,
+ attempt_num=self.answerpaper.attempt_number+1,
+ course_id=self.course.id
+ )
+ not_ordered_testcase = self.question1.get_ordered_test_cases(
+ answerpaper3 )
+ get_test_cases = self.question1.get_test_cases()
+ # Then
+ self.assertNotEqual(order1, order2)
+ self.assertEqual(get_test_cases, not_ordered_testcase)
+
+
+class ArrangeQuestionTestCases(unittest.TestCase):
+ @classmethod
+ def setUpClass(self):
+ # Creating Course
+ self.course = Course.objects.get(name="Python Course 100")
+ # Creating Quiz
+ self.quiz = Quiz.objects.get(description="demo quiz 100")
+ # Creating Question paper
+ self.question_paper = QuestionPaper.objects.get(quiz=self.quiz,
+ total_marks=1.0)
+
+ #Creating User
+ self.user = User.objects.get(username='demo_user_100')
+ #Creating Question
+ self.question1 = Question.objects.create(summary='arrange1',
+ points=1.0,
+ user=self.user
+ )
+ self.question1.language = 'python'
+ self.question1.type = "arrange"
+ self.question1.description = "Arrange alphabets in ascending order"
+ self.question1.test_case_type = 'arrangetestcase'
+ self.question1.save()
+
+ #Creating answerpaper
+
+ self.answerpaper = AnswerPaper.objects.create(user=self.user,
+ user_ip='101.0.0.1',
+ start_time=timezone.now(),
+ question_paper=self.question_paper,
+ end_time=timezone.now()
+ +timedelta(minutes=5),
+ attempt_number=1,
+ course=self.course
+ )
+ self.answerpaper.questions.add(self.question1)
+ self.answerpaper.save()
+ # For question
+ self.arrange_testcase_1 = ArrangeTestCase(question=self.question1,
+ options="A",
+ type = 'arrangetestcase',
+ )
+ self.arrange_testcase_1.save()
+ self.testcase_1_id = self.arrange_testcase_1.id
+ self.arrange_testcase_2 = ArrangeTestCase(question=self.question1,
+ options="B",
+ type = 'arrangetestcase',
+ )
+ self.arrange_testcase_2.save()
+ self.testcase_2_id = self.arrange_testcase_2.id
+ self.arrange_testcase_3 = ArrangeTestCase(question=self.question1,
+ options="C",
+ type = 'arrangetestcase',
+ )
+ self.arrange_testcase_3.save()
+ self.testcase_3_id = self.arrange_testcase_3.id
+ @classmethod
+ def tearDownClass(self):
+ self.question1.delete()
+ self.answerpaper.delete()
+
+ def test_validate_regrade_arrange_correct_answer(self):
+ # Given
+ arrange_answer = [self.testcase_1_id,
+ self.testcase_2_id,
+ self.testcase_3_id,
+ ]
+ self.answer = Answer(question=self.question1,
+ answer=arrange_answer,
+ )
+ self.answer.save()
+ self.answerpaper.answers.add(self.answer)
+
+ # When
+ json_data = None
+ result = self.answerpaper.validate_answer(arrange_answer,
+ self.question1,
+ json_data,
+ )
+ # Then
+ self.assertTrue(result['success'])
+
+ # Regrade with wrong answer
+ # Given
+ regrade_answer = Answer.objects.get(id=self.answer.id)
+
+ # Try regrade with wrong data structure
+ # When
+ regrade_answer.answer = 1
+ regrade_answer.save()
+ details = self.answerpaper.regrade(self.question1.id)
+ err_msg = dedent("""\
+ User: {0}; Quiz: {1}; Question: {2}.
+ {3} answer not a list.""".format(
+ self.user.username,
+ self.quiz.description,
+ self.question1.summary,
+ self.question1.type
+ ) )
+ self.assertFalse(details[0])
+ self.assertEqual(details[1], err_msg)
+
+
+ # Try regrade with incorrect answer
+ # When
+ regrade_answer.answer = [self.testcase_1_id,
+ self.testcase_3_id,
+ self.testcase_2_id,
+ ]
+ regrade_answer.save()
+ # Then
+ details = self.answerpaper.regrade(self.question1.id)
+ self.answer = self.answerpaper.answers.filter(question=self.question1
+ ).last()
+ self.assertEqual(self.answer, regrade_answer)
+ self.assertTrue(details[0])
+ self.assertEqual(self.answer.marks, 0)
+ self.assertFalse(self.answer.correct)
+
+ def test_validate_regrade_arrange_incorrect_answer(self):
+ # Given
+ arrange_answer = [self.testcase_1_id,
+ self.testcase_3_id,
+ self.testcase_2_id,
+ ]
+ self.answer = Answer(question=self.question1,
+ answer=arrange_answer,
+ )
self.answer.save()
+ self.answerpaper.answers.add(self.answer)
+
+ # When
+ json_data = None
+ result = self.answerpaper.validate_answer(arrange_answer,
+ self.question1, json_data
+ )
+
+ # Then
+ self.assertFalse(result['success'])
+ # Regrade with wrong answer
+ # Given
+ regrade_answer = Answer.objects.get(id=self.answer.id)
+ regrade_answer.answer = [self.testcase_1_id,
+ self.testcase_2_id,
+ self.testcase_3_id,
+ ]
+ regrade_answer.save()
# When
details = self.answerpaper.regrade(self.question1.id)
@@ -482,6 +737,7 @@ class FloatQuestionTestCases(unittest.TestCase):
# Then
self.answer = self.answerpaper.answers.filter(question=self.question1
).last()
+ self.assertEqual(self.answer, regrade_answer)
self.assertTrue(details[0])
self.assertEqual(self.answer.marks, 1)
self.assertTrue(self.answer.correct)
diff --git a/yaksh/forms.py b/yaksh/forms.py
index 97b3108..41c9176 100644
--- a/yaksh/forms.py
+++ b/yaksh/forms.py
@@ -35,6 +35,7 @@ question_types = (
("integer", "Answer in Integer"),
("string", "Answer in String"),
("float", "Answer in Float"),
+ ("arrange", "Arrange in Correct Order"),
)
test_case_types = (
@@ -281,7 +282,7 @@ class CourseForm(forms.ModelForm):
class Meta:
model = Course
fields = ['name', 'enrollment', 'active', 'code', 'instructions',
- 'start_enroll_time', 'end_enroll_time']
+ 'start_enroll_time', 'end_enroll_time', 'grading_system']
class ProfileForm(forms.ModelForm):
diff --git a/yaksh/grader.py b/yaksh/grader.py
index 38cce8d..320e7e7 100644
--- a/yaksh/grader.py
+++ b/yaksh/grader.py
@@ -1,22 +1,12 @@
#!/usr/bin/env python
from __future__ import unicode_literals
import sys
-import pwd
import os
-import stat
import contextlib
-from os.path import isdir, dirname, abspath, join, isfile, exists
+from os.path import dirname, abspath
import signal
import traceback
-from multiprocessing import Process, Queue
-import subprocess
-import re
-try:
- from SimpleXMLRPCServer import SimpleXMLRPCServer
-except ImportError:
- # The above import will not work on Python-3.x.
- from xmlrpc.server import SimpleXMLRPCServer
# Local imports
from .settings import SERVER_TIMEOUT
@@ -26,11 +16,13 @@ from .error_messages import prettify_exceptions
MY_DIR = abspath(dirname(__file__))
registry = None
+
# Raised when the code times-out.
# c.f. http://pguides.net/python/timeout-a-function
class TimeoutException(Exception):
pass
+
@contextlib.contextmanager
def change_dir(path):
cur_dir = abspath(dirname(MY_DIR))
@@ -75,7 +67,6 @@ class Grader(object):
self.timeout_msg = msg
self.in_dir = in_dir if in_dir else MY_DIR
-
def evaluate(self, kwargs):
"""Evaluates given code with the test cases based on
given arguments in test_case_data.
@@ -122,7 +113,6 @@ class Grader(object):
test_case_instances.append(test_case_instance)
return test_case_instances
-
def safe_evaluate(self, test_case_instances):
"""
Handles code evaluation along with compilation, signal handling
@@ -131,7 +121,9 @@ class Grader(object):
# Add a new signal handler for the execution of this code.
prev_handler = create_signal_handler()
success = False
- test_case_success_status = [False] * len(test_case_instances)
+ test_case_success_status = [False]
+ if len(test_case_instances) != 0:
+ test_case_success_status = [False] * len(test_case_instances)
error = []
weight = 0.0
@@ -155,20 +147,24 @@ class Grader(object):
test_case_instance.teardown()
except TimeoutException:
- error.append(prettify_exceptions("TimeoutException",
- self.timeout_msg
- )
- )
- except Exception:
+ error.append(
+ prettify_exceptions("TimeoutException", self.timeout_msg)
+ )
+ except Exception as e:
exc_type, exc_value, exc_tb = sys.exc_info()
tb_list = traceback.format_exception(exc_type, exc_value, exc_tb)
+ try:
+ line_no = e.lineno
+ except AttributeError:
+ line_no = traceback.extract_tb(exc_tb)[-1][1]
if len(tb_list) > 2:
del tb_list[1:3]
- error.append(prettify_exceptions(exc_type.__name__,
- str(exc_value),
- "".join(tb_list),
- )
- )
+ error.append(
+ prettify_exceptions(
+ exc_type.__name__, str(exc_value), "".join(tb_list),
+ line_no=line_no
+ )
+ )
finally:
# Set back any original signal handler.
set_original_signal_handler(prev_handler)
diff --git a/yaksh/migrations/0009_auto_20180113_1124.py b/yaksh/migrations/0009_auto_20180113_1124.py
new file mode 100644
index 0000000..dbbcb30
--- /dev/null
+++ b/yaksh/migrations/0009_auto_20180113_1124.py
@@ -0,0 +1,149 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.9.5 on 2018-01-13 11:24
+from __future__ import unicode_literals
+
+from django.conf import settings
+from django.db import migrations, models
+import django.db.models.deletion
+import yaksh.models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+ ('yaksh', '0008_release_0_7_0'),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='CourseStatus',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('grade', models.CharField(blank=True, max_length=255, null=True)),
+ ('total_marks', models.FloatField(default=0.0)),
+ ],
+ ),
+ migrations.CreateModel(
+ name='LearningModule',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('name', models.CharField(max_length=255)),
+ ('description', models.TextField(blank=True, default=None, null=True)),
+ ('order', models.IntegerField(default=0)),
+ ('check_prerequisite', models.BooleanField(default=True)),
+ ('html_data', models.TextField(blank=True, null=True)),
+ ('is_trial', models.BooleanField(default=False)),
+ ('creator', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='module_creator', to=settings.AUTH_USER_MODEL)),
+ ],
+ ),
+ migrations.CreateModel(
+ name='LearningUnit',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('order', models.IntegerField()),
+ ('type', models.CharField(max_length=16)),
+ ('check_prerequisite', models.BooleanField(default=True)),
+ ],
+ ),
+ migrations.CreateModel(
+ name='Lesson',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('name', models.CharField(max_length=255)),
+ ('description', models.TextField()),
+ ('html_data', models.TextField(blank=True, null=True)),
+ ('creator', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
+ ],
+ ),
+ migrations.CreateModel(
+ name='LessonFile',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('file', models.FileField(upload_to=yaksh.models.get_file_dir)),
+ ('lesson', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='lesson', to='yaksh.Lesson')),
+ ],
+ ),
+ migrations.RemoveField(
+ model_name='quiz',
+ name='course',
+ ),
+ migrations.RemoveField(
+ model_name='quiz',
+ name='language',
+ ),
+ migrations.RemoveField(
+ model_name='quiz',
+ name='prerequisite',
+ ),
+ migrations.AddField(
+ model_name='answerpaper',
+ name='course',
+ field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='yaksh.Course'),
+ ),
+ migrations.AddField(
+ model_name='question',
+ name='min_time',
+ field=models.IntegerField(default=0, verbose_name='time in minutes'),
+ ),
+ migrations.AddField(
+ model_name='question',
+ name='solution',
+ field=models.TextField(blank=True),
+ ),
+ migrations.AddField(
+ model_name='quiz',
+ name='creator',
+ field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL),
+ ),
+ migrations.AddField(
+ model_name='quiz',
+ name='is_exercise',
+ field=models.BooleanField(default=False),
+ ),
+ migrations.AddField(
+ model_name='quiz',
+ name='weightage',
+ field=models.FloatField(default=1.0),
+ ),
+ migrations.AddField(
+ model_name='learningunit',
+ name='lesson',
+ field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='yaksh.Lesson'),
+ ),
+ migrations.AddField(
+ model_name='learningunit',
+ name='quiz',
+ field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='yaksh.Quiz'),
+ ),
+ migrations.AddField(
+ model_name='learningmodule',
+ name='learning_unit',
+ field=models.ManyToManyField(related_name='learning_unit', to='yaksh.LearningUnit'),
+ ),
+ migrations.AddField(
+ model_name='coursestatus',
+ name='completed_units',
+ field=models.ManyToManyField(related_name='completed_units', to='yaksh.LearningUnit'),
+ ),
+ migrations.AddField(
+ model_name='coursestatus',
+ name='course',
+ field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='yaksh.Course'),
+ ),
+ migrations.AddField(
+ model_name='coursestatus',
+ name='current_unit',
+ field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='current_unit', to='yaksh.LearningUnit'),
+ ),
+ migrations.AddField(
+ model_name='coursestatus',
+ name='user',
+ field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL),
+ ),
+ migrations.AddField(
+ model_name='course',
+ name='learning_module',
+ field=models.ManyToManyField(related_name='learning_module', to='yaksh.LearningModule'),
+ ),
+ ]
diff --git a/yaksh/migrations/0010_auto_20180226_1324.py b/yaksh/migrations/0010_auto_20180226_1324.py
new file mode 100644
index 0000000..b400da7
--- /dev/null
+++ b/yaksh/migrations/0010_auto_20180226_1324.py
@@ -0,0 +1,34 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.9.5 on 2018-02-26 13:24
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('yaksh', '0009_auto_20180113_1124'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='learningmodule',
+ name='active',
+ field=models.BooleanField(default=True),
+ ),
+ migrations.AddField(
+ model_name='lesson',
+ name='active',
+ field=models.BooleanField(default=True),
+ ),
+ migrations.AlterField(
+ model_name='quiz',
+ name='time_between_attempts',
+ field=models.FloatField(verbose_name='Time Between Quiz Attempts in hours'),
+ ),
+ migrations.AlterUniqueTogether(
+ name='answerpaper',
+ unique_together=set([('user', 'question_paper', 'attempt_number', 'course')]),
+ ),
+ ]
diff --git a/yaksh/migrations/0011_release_0_8_0.py b/yaksh/migrations/0011_release_0_8_0.py
new file mode 100644
index 0000000..41a2abd
--- /dev/null
+++ b/yaksh/migrations/0011_release_0_8_0.py
@@ -0,0 +1,68 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.9.5 on 2018-03-23 10:46
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('grades', 'default_grading_system'),
+ ('yaksh', '0010_auto_20180226_1324'),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='ArrangeTestCase',
+ fields=[
+ ('testcase_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='yaksh.TestCase')),
+ ('options', models.TextField(default=None)),
+ ],
+ bases=('yaksh.testcase',),
+ ),
+ migrations.CreateModel(
+ name='TestCaseOrder',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('order', models.TextField()),
+ ('answer_paper', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='answer_paper', to='yaksh.AnswerPaper')),
+ ],
+ ),
+ migrations.RenameField(
+ model_name='coursestatus',
+ old_name='total_marks',
+ new_name='percentage',
+ ),
+ migrations.AddField(
+ model_name='course',
+ name='grading_system',
+ field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='grades.GradingSystem'),
+ ),
+ migrations.AddField(
+ model_name='questionpaper',
+ name='shuffle_testcases',
+ field=models.BooleanField(default=True, verbose_name='Shuffle testcase for each user'),
+ ),
+ migrations.AlterField(
+ model_name='question',
+ name='type',
+ field=models.CharField(choices=[('mcq', 'Single Correct Choice'), ('mcc', 'Multiple Correct Choices'), ('code', 'Code'), ('upload', 'Assignment Upload'), ('integer', 'Answer in Integer'), ('string', 'Answer in String'), ('float', 'Answer in Float'), ('arrange', 'Arrange in Correct Order')], max_length=24),
+ ),
+ migrations.AlterField(
+ model_name='quiz',
+ name='weightage',
+ field=models.FloatField(default=100, help_text='Will be considered as percentage'),
+ ),
+ migrations.AlterField(
+ model_name='testcase',
+ name='type',
+ field=models.CharField(choices=[('standardtestcase', 'Standard Testcase'), ('stdiobasedtestcase', 'StdIO Based Testcase'), ('mcqtestcase', 'MCQ Testcase'), ('hooktestcase', 'Hook Testcase'), ('integertestcase', 'Integer Testcase'), ('stringtestcase', 'String Testcase'), ('floattestcase', 'Float Testcase'), ('arrangetestcase', 'Arrange Testcase')], max_length=24, null=True),
+ ),
+ migrations.AddField(
+ model_name='testcaseorder',
+ name='question',
+ field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='yaksh.Question'),
+ ),
+ ]
diff --git a/yaksh/models.py b/yaksh/models.py
index ecf7035..1ca293b 100644
--- a/yaksh/models.py
+++ b/yaksh/models.py
@@ -35,7 +35,7 @@ from yaksh.code_server import (
from yaksh.settings import SERVER_POOL_PORT, SERVER_HOST_NAME
from django.conf import settings
from django.forms.models import model_to_dict
-
+from grades.models import GradingSystem
languages = (
("python", "Python"),
@@ -54,6 +54,8 @@ question_types = (
("integer", "Answer in Integer"),
("string", "Answer in String"),
("float", "Answer in Float"),
+ ("arrange", "Arrange in Correct Order"),
+
)
enrollment_methods = (
@@ -69,6 +71,7 @@ test_case_types = (
("integertestcase", "Integer Testcase"),
("stringtestcase", "String Testcase"),
("floattestcase", "Float Testcase"),
+ ("arrangetestcase", "Arrange Testcase"),
)
string_check_type = (
@@ -314,7 +317,7 @@ class Quiz(models.Model):
attempts_allowed = models.IntegerField(default=1, choices=attempts)
time_between_attempts = models.FloatField(
- "Time Between Quiz Attempts in hours"
+ "Time Between Quiz Attempts in hours", default=0.0
)
is_trial = models.BooleanField(default=False)
@@ -328,7 +331,8 @@ class Quiz(models.Model):
allow_skip = models.BooleanField("Allow students to skip questions",
default=True)
- weightage = models.FloatField(default=1.0)
+ weightage = models.FloatField(help_text='Will be considered as percentage',
+ default=100)
is_exercise = models.BooleanField(default=False)
@@ -618,6 +622,8 @@ class Course(models.Model):
null=True
)
+ grading_system = models.ForeignKey(GradingSystem, null=True, blank=True)
+
objects = CourseManager()
def _create_duplicate_instance(self, creator, course_name=None):
@@ -788,6 +794,14 @@ class Course(models.Model):
percent = round((count / len(modules)))
return percent
+ def get_grade(self, user):
+ course_status = CourseStatus.objects.filter(course=self, user=user)
+ if course_status.exists():
+ grade = course_status.first().get_grade()
+ else:
+ grade = "NA"
+ return grade
+
def days_before_start(self):
""" Get the days remaining for the start of the course """
if timezone.now() < self.start_enroll_time:
@@ -809,7 +823,44 @@ class CourseStatus(models.Model):
course = models.ForeignKey(Course)
user = models.ForeignKey(User)
grade = models.CharField(max_length=255, null=True, blank=True)
- total_marks = models.FloatField(default=0.0)
+ percentage = models.FloatField(default=0.0)
+
+ def get_grade(self):
+ return self.grade
+
+ def set_grade(self):
+ if self.is_course_complete():
+ self.calculate_percentage()
+ if self.course.grading_system is None:
+ grading_system = GradingSystem.objects.get(name='default')
+ else:
+ grading_system = self.course.grading_system
+ grade = grading_system.get_grade(self.percentage)
+ self.grade = grade
+ self.save()
+
+ def calculate_percentage(self):
+ if self.is_course_complete():
+ quizzes = self.course.get_quizzes()
+ total_weightage = 0
+ sum = 0
+ for quiz in quizzes:
+ total_weightage += quiz.weightage
+ marks = AnswerPaper.objects.get_user_best_of_attempts_marks(
+ quiz, self.user.id, self.course.id)
+ out_of = quiz.questionpaper_set.first().total_marks
+ sum += (marks/out_of)*quiz.weightage
+ self.percentage = (sum/total_weightage)*100
+ self.save()
+
+ def is_course_complete(self):
+ modules = self.course.get_learning_modules()
+ complete = False
+ for module in modules:
+ complete = module.get_status(self.user, self.course) == 'completed'
+ if not complete:
+ break
+ return complete
###############################################################################
@@ -1292,8 +1343,8 @@ class QuestionPaper(models.Model):
question_ids = []
for question in questions:
question_ids.append(str(question.id))
- if self.shuffle_testcases and \
- question.type in ["mcq", "mcc"]:
+ if (question.type == "arrange") or (self.shuffle_testcases
+ and question.type in ["mcq", "mcc"]):
testcases = question.get_test_cases()
random.shuffle(testcases)
testcases_ids = ",".join([str(tc.id) for tc in testcases]
@@ -1321,11 +1372,18 @@ class QuestionPaper(models.Model):
)
if last_attempt:
time_lag = (timezone.now() - last_attempt.start_time).total_seconds() / 3600
- return time_lag >= self.quiz.time_between_attempts
+ can_attempt = time_lag >= self.quiz.time_between_attempts
+ msg = "You cannot start the next attempt for this quiz before {0} hour(s)".format(
+ self.quiz.time_between_attempts
+ ) if not can_attempt else None
+ return can_attempt, msg
else:
- return True
+ return True, None
else:
- return False
+ msg = "You cannot attempt {0} quiz more than {1} time(s)".format(
+ self.quiz.description, self.quiz.attempts_allowed
+ )
+ return False, msg
def create_demo_quiz_ppr(self, demo_quiz, user):
question_paper = QuestionPaper.objects.create(quiz=demo_quiz,
@@ -1865,6 +1923,15 @@ class AnswerPaper(models.Model):
result['success'] = True
result['error'] = ['Correct answer']
+ elif question.type == 'arrange':
+ testcase_ids = sorted(
+ [tc.id for tc in question.get_test_cases()]
+ )
+ if user_answer == testcase_ids:
+ result['success'] = True
+ result['error'] = ['Correct answer']
+
+
elif question.type == 'code' or question.type == "upload":
user_dir = self.user.profile.get_user_dir()
url = '{0}:{1}'.format(SERVER_HOST_NAME, server_port)
@@ -1886,13 +1953,21 @@ class AnswerPaper(models.Model):
user_answer = self.answers.filter(question=question).last()
if not user_answer:
return False, msg + 'Did not answer.'
- if question.type == 'mcc':
+ if question.type in ['mcc', 'arrange']:
try:
- answer = eval(user_answer.answer)
+ answer = literal_eval(user_answer.answer)
if type(answer) is not list:
- return False, msg + 'MCC answer not a list.'
+ return (False,
+ msg + '{0} answer not a list.'.format(
+ question.type
+ )
+ )
except Exception:
- return False, msg + 'MCC answer submission error'
+ return (False,
+ msg + '{0} answer submission error'.format(
+ question.type
+ )
+ )
else:
answer = user_answer.answer
json_data = question.consolidate_answer_data(answer) \
@@ -2085,6 +2160,18 @@ class FloatTestCase(TestCase):
)
+class ArrangeTestCase(TestCase):
+
+ options = models.TextField(default=None)
+
+ def get_field_value(self):
+ return {"test_case_type": "arrangetestcase",
+ "options": self.options}
+
+ def __str__(self):
+ return u'Arrange Testcase | Option: {0}'.format(self.options)
+
+
##############################################################################
class TestCaseOrder(models.Model):
"""Testcase order contains a set of ordered test cases for a given question
@@ -2099,3 +2186,6 @@ class TestCaseOrder(models.Model):
#Order of the test case for a question.
order = models.TextField()
+
+
+##############################################################################
diff --git a/yaksh/python_assertion_evaluator.py b/yaksh/python_assertion_evaluator.py
index 440f422..4b016a1 100644
--- a/yaksh/python_assertion_evaluator.py
+++ b/yaksh/python_assertion_evaluator.py
@@ -1,10 +1,6 @@
#!/usr/bin/env python
import sys
import traceback
-import os
-import re
-from os.path import join
-import importlib
# Local imports
from .file_utils import copy_files, delete_files
@@ -53,22 +49,24 @@ class PythonAssertionEvaluator(BaseEvaluator):
--------
Returns a tuple (success, error, test_case_weight)
- success - Boolean, indicating if code was executed successfully, correctly
+ success - Boolean, indicating if code was executed successfully,
+ correctly
weight - Float, indicating total weight of all successful test cases
error - String, error message if success is false
- returns (True, "Correct answer", 1.0) : If the student script passes all
- test cases/have same output, when compared to the instructor script
+ returns (True, "Correct answer", 1.0) : If the student script passes
+ all test cases/have same output, when compared to the instructor script
returns (False, error_msg, 0.0): If the student script fails a single
test/have dissimilar output, when compared to the instructor script.
- Returns (False, error_msg, 0.0): If mandatory arguments are not files or if
- the required permissions are not given to the file(s).
+ Returns (False, error_msg, 0.0): If mandatory arguments are not files
+ or if the required permissions are not given to the file(s).
"""
success = False
mark_fraction = 0.0
try:
+ exec("from nose.tools import *", self.exec_scope)
_tests = compile(self.test_case, '<string>', mode='exec')
exec(_tests, self.exec_scope)
except TimeoutException:
@@ -76,12 +74,14 @@ class PythonAssertionEvaluator(BaseEvaluator):
except Exception:
exc_type, exc_value, exc_tb = sys.exc_info()
tb_list = traceback.format_exception(exc_type, exc_value, exc_tb)
+ line_no = traceback.extract_tb(exc_tb)[-1][1]
if len(tb_list) > 2:
del tb_list[1:3]
err = prettify_exceptions(exc_type.__name__,
str(exc_value),
"".join(tb_list),
- self.test_case
+ self.test_case,
+ line_no
)
else:
success = True
diff --git a/yaksh/send_emails.py b/yaksh/send_emails.py
index ae49f23..4c9a7dc 100644
--- a/yaksh/send_emails.py
+++ b/yaksh/send_emails.py
@@ -65,7 +65,7 @@ def send_bulk_mail(subject, email_body, recipients, attachments):
try:
text_msg = ""
msg = EmailMultiAlternatives(subject, text_msg, settings.SENDER_EMAIL,
- recipients
+ [settings.SENDER_EMAIL], bcc=recipients
)
msg.attach_alternative(email_body, "text/html")
if attachments:
diff --git a/yaksh/static/yaksh/js/jquery-sortable.js b/yaksh/static/yaksh/js/jquery-sortable.js
new file mode 100644
index 0000000..376880c
--- /dev/null
+++ b/yaksh/static/yaksh/js/jquery-sortable.js
@@ -0,0 +1,693 @@
+/* ===================================================
+ * jquery-sortable.js v0.9.13
+ * http://johnny.github.com/jquery-sortable/
+ * ===================================================
+ * Copyright (c) 2012 Jonas von Andrian
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ * * The name of the author may not be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ * ========================================================== */
+
+
+!function ( $, window, pluginName, undefined){
+ var containerDefaults = {
+ // If true, items can be dragged from this container
+ drag: true,
+ // If true, items can be droped onto this container
+ drop: true,
+ // Exclude items from being draggable, if the
+ // selector matches the item
+ exclude: "",
+ // If true, search for nested containers within an item.If you nest containers,
+ // either the original selector with which you call the plugin must only match the top containers,
+ // or you need to specify a group (see the bootstrap nav example)
+ nested: true,
+ // If true, the items are assumed to be arranged vertically
+ vertical: true
+ }, // end container defaults
+ groupDefaults = {
+ // This is executed after the placeholder has been moved.
+ // $closestItemOrContainer contains the closest item, the placeholder
+ // has been put at or the closest empty Container, the placeholder has
+ // been appended to.
+ afterMove: function ($placeholder, container, $closestItemOrContainer) {
+ },
+ // The exact css path between the container and its items, e.g. "> tbody"
+ containerPath: "",
+ // The css selector of the containers
+ containerSelector: "ol, ul",
+ // Distance the mouse has to travel to start dragging
+ distance: 0,
+ // Time in milliseconds after mousedown until dragging should start.
+ // This option can be used to prevent unwanted drags when clicking on an element.
+ delay: 0,
+ // The css selector of the drag handle
+ handle: "",
+ // The exact css path between the item and its subcontainers.
+ // It should only match the immediate items of a container.
+ // No item of a subcontainer should be matched. E.g. for ol>div>li the itemPath is "> div"
+ itemPath: "",
+ // The css selector of the items
+ itemSelector: "li",
+ // The class given to "body" while an item is being dragged
+ bodyClass: "dragging",
+ // The class giving to an item while being dragged
+ draggedClass: "dragged",
+ // Check if the dragged item may be inside the container.
+ // Use with care, since the search for a valid container entails a depth first search
+ // and may be quite expensive.
+ isValidTarget: function ($item, container) {
+ return true
+ },
+ // Executed before onDrop if placeholder is detached.
+ // This happens if pullPlaceholder is set to false and the drop occurs outside a container.
+ onCancel: function ($item, container, _super, event) {
+ },
+ // Executed at the beginning of a mouse move event.
+ // The Placeholder has not been moved yet.
+ onDrag: function ($item, position, _super, event) {
+ $item.css(position)
+ },
+ // Called after the drag has been started,
+ // that is the mouse button is being held down and
+ // the mouse is moving.
+ // The container is the closest initialized container.
+ // Therefore it might not be the container, that actually contains the item.
+ onDragStart: function ($item, container, _super, event) {
+ $item.css({
+ height: $item.outerHeight(),
+ width: $item.outerWidth()
+ })
+ $item.addClass(container.group.options.draggedClass)
+ $("body").addClass(container.group.options.bodyClass)
+ },
+ // Called when the mouse button is being released
+ onDrop: function ($item, container, _super, event) {
+ $item.removeClass(container.group.options.draggedClass).removeAttr("style")
+ $("body").removeClass(container.group.options.bodyClass)
+ },
+ // Called on mousedown. If falsy value is returned, the dragging will not start.
+ // Ignore if element clicked is input, select or textarea
+ onMousedown: function ($item, _super, event) {
+ if (!event.target.nodeName.match(/^(input|select|textarea)$/i)) {
+ event.preventDefault()
+ return true
+ }
+ },
+ // The class of the placeholder (must match placeholder option markup)
+ placeholderClass: "placeholder",
+ // Template for the placeholder. Can be any valid jQuery input
+ // e.g. a string, a DOM element.
+ // The placeholder must have the class "placeholder"
+ placeholder: '<li class="placeholder"></li>',
+ // If true, the position of the placeholder is calculated on every mousemove.
+ // If false, it is only calculated when the mouse is above a container.
+ pullPlaceholder: true,
+ // Specifies serialization of the container group.
+ // The pair $parent/$children is either container/items or item/subcontainers.
+ serialize: function ($parent, $children, parentIsContainer) {
+ var result = $.extend({}, $parent.data())
+
+ if(parentIsContainer)
+ return [$children]
+ else if ($children[0]){
+ result.children = $children
+ }
+
+ delete result.subContainers
+ delete result.sortable
+
+ return result
+ },
+ // Set tolerance while dragging. Positive values decrease sensitivity,
+ // negative values increase it.
+ tolerance: 0
+ }, // end group defaults
+ containerGroups = {},
+ groupCounter = 0,
+ emptyBox = {
+ left: 0,
+ top: 0,
+ bottom: 0,
+ right:0
+ },
+ eventNames = {
+ start: "touchstart.sortable mousedown.sortable",
+ drop: "touchend.sortable touchcancel.sortable mouseup.sortable",
+ drag: "touchmove.sortable mousemove.sortable",
+ scroll: "scroll.sortable"
+ },
+ subContainerKey = "subContainers"
+
+ /*
+ * a is Array [left, right, top, bottom]
+ * b is array [left, top]
+ */
+ function d(a,b) {
+ var x = Math.max(0, a[0] - b[0], b[0] - a[1]),
+ y = Math.max(0, a[2] - b[1], b[1] - a[3])
+ return x+y;
+ }
+
+ function setDimensions(array, dimensions, tolerance, useOffset) {
+ var i = array.length,
+ offsetMethod = useOffset ? "offset" : "position"
+ tolerance = tolerance || 0
+
+ while(i--){
+ var el = array[i].el ? array[i].el : $(array[i]),
+ // use fitting method
+ pos = el[offsetMethod]()
+ pos.left += parseInt(el.css('margin-left'), 10)
+ pos.top += parseInt(el.css('margin-top'),10)
+ dimensions[i] = [
+ pos.left - tolerance,
+ pos.left + el.outerWidth() + tolerance,
+ pos.top - tolerance,
+ pos.top + el.outerHeight() + tolerance
+ ]
+ }
+ }
+
+ function getRelativePosition(pointer, element) {
+ var offset = element.offset()
+ return {
+ left: pointer.left - offset.left,
+ top: pointer.top - offset.top
+ }
+ }
+
+ function sortByDistanceDesc(dimensions, pointer, lastPointer) {
+ pointer = [pointer.left, pointer.top]
+ lastPointer = lastPointer && [lastPointer.left, lastPointer.top]
+
+ var dim,
+ i = dimensions.length,
+ distances = []
+
+ while(i--){
+ dim = dimensions[i]
+ distances[i] = [i,d(dim,pointer), lastPointer && d(dim, lastPointer)]
+ }
+ distances = distances.sort(function (a,b) {
+ return b[1] - a[1] || b[2] - a[2] || b[0] - a[0]
+ })
+
+ // last entry is the closest
+ return distances
+ }
+
+ function ContainerGroup(options) {
+ this.options = $.extend({}, groupDefaults, options)
+ this.containers = []
+
+ if(!this.options.rootGroup){
+ this.scrollProxy = $.proxy(this.scroll, this)
+ this.dragProxy = $.proxy(this.drag, this)
+ this.dropProxy = $.proxy(this.drop, this)
+ this.placeholder = $(this.options.placeholder)
+
+ if(!options.isValidTarget)
+ this.options.isValidTarget = undefined
+ }
+ }
+
+ ContainerGroup.get = function (options) {
+ if(!containerGroups[options.group]) {
+ if(options.group === undefined)
+ options.group = groupCounter ++
+
+ containerGroups[options.group] = new ContainerGroup(options)
+ }
+
+ return containerGroups[options.group]
+ }
+
+ ContainerGroup.prototype = {
+ dragInit: function (e, itemContainer) {
+ this.$document = $(itemContainer.el[0].ownerDocument)
+
+ // get item to drag
+ var closestItem = $(e.target).closest(this.options.itemSelector);
+ // using the length of this item, prevents the plugin from being started if there is no handle being clicked on.
+ // this may also be helpful in instantiating multidrag.
+ if (closestItem.length) {
+ this.item = closestItem;
+ this.itemContainer = itemContainer;
+ if (this.item.is(this.options.exclude) || !this.options.onMousedown(this.item, groupDefaults.onMousedown, e)) {
+ return;
+ }
+ this.setPointer(e);
+ this.toggleListeners('on');
+ this.setupDelayTimer();
+ this.dragInitDone = true;
+ }
+ },
+ drag: function (e) {
+ if(!this.dragging){
+ if(!this.distanceMet(e) || !this.delayMet)
+ return
+
+ this.options.onDragStart(this.item, this.itemContainer, groupDefaults.onDragStart, e)
+ this.item.before(this.placeholder)
+ this.dragging = true
+ }
+
+ this.setPointer(e)
+ // place item under the cursor
+ this.options.onDrag(this.item,
+ getRelativePosition(this.pointer, this.item.offsetParent()),
+ groupDefaults.onDrag,
+ e)
+
+ var p = this.getPointer(e),
+ box = this.sameResultBox,
+ t = this.options.tolerance
+
+ if(!box || box.top - t > p.top || box.bottom + t < p.top || box.left - t > p.left || box.right + t < p.left)
+ if(!this.searchValidTarget()){
+ this.placeholder.detach()
+ this.lastAppendedItem = undefined
+ }
+ },
+ drop: function (e) {
+ this.toggleListeners('off')
+
+ this.dragInitDone = false
+
+ if(this.dragging){
+ // processing Drop, check if placeholder is detached
+ if(this.placeholder.closest("html")[0]){
+ this.placeholder.before(this.item).detach()
+ } else {
+ this.options.onCancel(this.item, this.itemContainer, groupDefaults.onCancel, e)
+ }
+ this.options.onDrop(this.item, this.getContainer(this.item), groupDefaults.onDrop, e)
+
+ // cleanup
+ this.clearDimensions()
+ this.clearOffsetParent()
+ this.lastAppendedItem = this.sameResultBox = undefined
+ this.dragging = false
+ }
+ },
+ searchValidTarget: function (pointer, lastPointer) {
+ if(!pointer){
+ pointer = this.relativePointer || this.pointer
+ lastPointer = this.lastRelativePointer || this.lastPointer
+ }
+
+ var distances = sortByDistanceDesc(this.getContainerDimensions(),
+ pointer,
+ lastPointer),
+ i = distances.length
+
+ while(i--){
+ var index = distances[i][0],
+ distance = distances[i][1]
+
+ if(!distance || this.options.pullPlaceholder){
+ var container = this.containers[index]
+ if(!container.disabled){
+ if(!this.$getOffsetParent()){
+ var offsetParent = container.getItemOffsetParent()
+ pointer = getRelativePosition(pointer, offsetParent)
+ lastPointer = getRelativePosition(lastPointer, offsetParent)
+ }
+ if(container.searchValidTarget(pointer, lastPointer))
+ return true
+ }
+ }
+ }
+ if(this.sameResultBox)
+ this.sameResultBox = undefined
+ },
+ movePlaceholder: function (container, item, method, sameResultBox) {
+ var lastAppendedItem = this.lastAppendedItem
+ if(!sameResultBox && lastAppendedItem && lastAppendedItem[0] === item[0])
+ return;
+
+ item[method](this.placeholder)
+ this.lastAppendedItem = item
+ this.sameResultBox = sameResultBox
+ this.options.afterMove(this.placeholder, container, item)
+ },
+ getContainerDimensions: function () {
+ if(!this.containerDimensions)
+ setDimensions(this.containers, this.containerDimensions = [], this.options.tolerance, !this.$getOffsetParent())
+ return this.containerDimensions
+ },
+ getContainer: function (element) {
+ return element.closest(this.options.containerSelector).data(pluginName)
+ },
+ $getOffsetParent: function () {
+ if(this.offsetParent === undefined){
+ var i = this.containers.length - 1,
+ offsetParent = this.containers[i].getItemOffsetParent()
+
+ if(!this.options.rootGroup){
+ while(i--){
+ if(offsetParent[0] != this.containers[i].getItemOffsetParent()[0]){
+ // If every container has the same offset parent,
+ // use position() which is relative to this parent,
+ // otherwise use offset()
+ // compare #setDimensions
+ offsetParent = false
+ break;
+ }
+ }
+ }
+
+ this.offsetParent = offsetParent
+ }
+ return this.offsetParent
+ },
+ setPointer: function (e) {
+ var pointer = this.getPointer(e)
+
+ if(this.$getOffsetParent()){
+ var relativePointer = getRelativePosition(pointer, this.$getOffsetParent())
+ this.lastRelativePointer = this.relativePointer
+ this.relativePointer = relativePointer
+ }
+
+ this.lastPointer = this.pointer
+ this.pointer = pointer
+ },
+ distanceMet: function (e) {
+ var currentPointer = this.getPointer(e)
+ return (Math.max(
+ Math.abs(this.pointer.left - currentPointer.left),
+ Math.abs(this.pointer.top - currentPointer.top)
+ ) >= this.options.distance)
+ },
+ getPointer: function(e) {
+ var o = e.originalEvent || e.originalEvent.touches && e.originalEvent.touches[0]
+ return {
+ left: e.pageX || o.pageX,
+ top: e.pageY || o.pageY
+ }
+ },
+ setupDelayTimer: function () {
+ var that = this
+ this.delayMet = !this.options.delay
+
+ // init delay timer if needed
+ if (!this.delayMet) {
+ clearTimeout(this._mouseDelayTimer);
+ this._mouseDelayTimer = setTimeout(function() {
+ that.delayMet = true
+ }, this.options.delay)
+ }
+ },
+ scroll: function (e) {
+ this.clearDimensions()
+ this.clearOffsetParent() // TODO is this needed?
+ },
+ toggleListeners: function (method) {
+ var that = this,
+ events = ['drag','drop','scroll']
+
+ $.each(events,function (i,event) {
+ that.$document[method](eventNames[event], that[event + 'Proxy'])
+ })
+ },
+ clearOffsetParent: function () {
+ this.offsetParent = undefined
+ },
+ // Recursively clear container and item dimensions
+ clearDimensions: function () {
+ this.traverse(function(object){
+ object._clearDimensions()
+ })
+ },
+ traverse: function(callback) {
+ callback(this)
+ var i = this.containers.length
+ while(i--){
+ this.containers[i].traverse(callback)
+ }
+ },
+ _clearDimensions: function(){
+ this.containerDimensions = undefined
+ },
+ _destroy: function () {
+ containerGroups[this.options.group] = undefined
+ }
+ }
+
+ function Container(element, options) {
+ this.el = element
+ this.options = $.extend( {}, containerDefaults, options)
+
+ this.group = ContainerGroup.get(this.options)
+ this.rootGroup = this.options.rootGroup || this.group
+ this.handle = this.rootGroup.options.handle || this.rootGroup.options.itemSelector
+
+ var itemPath = this.rootGroup.options.itemPath
+ this.target = itemPath ? this.el.find(itemPath) : this.el
+
+ this.target.on(eventNames.start, this.handle, $.proxy(this.dragInit, this))
+
+ if(this.options.drop)
+ this.group.containers.push(this)
+ }
+
+ Container.prototype = {
+ dragInit: function (e) {
+ var rootGroup = this.rootGroup
+
+ if( !this.disabled &&
+ !rootGroup.dragInitDone &&
+ this.options.drag &&
+ this.isValidDrag(e)) {
+ rootGroup.dragInit(e, this)
+ }
+ },
+ isValidDrag: function(e) {
+ return e.which == 1 ||
+ e.type == "touchstart" && e.originalEvent.touches.length == 1
+ },
+ searchValidTarget: function (pointer, lastPointer) {
+ var distances = sortByDistanceDesc(this.getItemDimensions(),
+ pointer,
+ lastPointer),
+ i = distances.length,
+ rootGroup = this.rootGroup,
+ validTarget = !rootGroup.options.isValidTarget ||
+ rootGroup.options.isValidTarget(rootGroup.item, this)
+
+ if(!i && validTarget){
+ rootGroup.movePlaceholder(this, this.target, "append")
+ return true
+ } else
+ while(i--){
+ var index = distances[i][0],
+ distance = distances[i][1]
+ if(!distance && this.hasChildGroup(index)){
+ var found = this.getContainerGroup(index).searchValidTarget(pointer, lastPointer)
+ if(found)
+ return true
+ }
+ else if(validTarget){
+ this.movePlaceholder(index, pointer)
+ return true
+ }
+ }
+ },
+ movePlaceholder: function (index, pointer) {
+ var item = $(this.items[index]),
+ dim = this.itemDimensions[index],
+ method = "after",
+ width = item.outerWidth(),
+ height = item.outerHeight(),
+ offset = item.offset(),
+ sameResultBox = {
+ left: offset.left,
+ right: offset.left + width,
+ top: offset.top,
+ bottom: offset.top + height
+ }
+ if(this.options.vertical){
+ var yCenter = (dim[2] + dim[3]) / 2,
+ inUpperHalf = pointer.top <= yCenter
+ if(inUpperHalf){
+ method = "before"
+ sameResultBox.bottom -= height / 2
+ } else
+ sameResultBox.top += height / 2
+ } else {
+ var xCenter = (dim[0] + dim[1]) / 2,
+ inLeftHalf = pointer.left <= xCenter
+ if(inLeftHalf){
+ method = "before"
+ sameResultBox.right -= width / 2
+ } else
+ sameResultBox.left += width / 2
+ }
+ if(this.hasChildGroup(index))
+ sameResultBox = emptyBox
+ this.rootGroup.movePlaceholder(this, item, method, sameResultBox)
+ },
+ getItemDimensions: function () {
+ if(!this.itemDimensions){
+ this.items = this.$getChildren(this.el, "item").filter(
+ ":not(." + this.group.options.placeholderClass + ", ." + this.group.options.draggedClass + ")"
+ ).get()
+ setDimensions(this.items, this.itemDimensions = [], this.options.tolerance)
+ }
+ return this.itemDimensions
+ },
+ getItemOffsetParent: function () {
+ var offsetParent,
+ el = this.el
+ // Since el might be empty we have to check el itself and
+ // can not do something like el.children().first().offsetParent()
+ if(el.css("position") === "relative" || el.css("position") === "absolute" || el.css("position") === "fixed")
+ offsetParent = el
+ else
+ offsetParent = el.offsetParent()
+ return offsetParent
+ },
+ hasChildGroup: function (index) {
+ return this.options.nested && this.getContainerGroup(index)
+ },
+ getContainerGroup: function (index) {
+ var childGroup = $.data(this.items[index], subContainerKey)
+ if( childGroup === undefined){
+ var childContainers = this.$getChildren(this.items[index], "container")
+ childGroup = false
+
+ if(childContainers[0]){
+ var options = $.extend({}, this.options, {
+ rootGroup: this.rootGroup,
+ group: groupCounter ++
+ })
+ childGroup = childContainers[pluginName](options).data(pluginName).group
+ }
+ $.data(this.items[index], subContainerKey, childGroup)
+ }
+ return childGroup
+ },
+ $getChildren: function (parent, type) {
+ var options = this.rootGroup.options,
+ path = options[type + "Path"],
+ selector = options[type + "Selector"]
+
+ parent = $(parent)
+ if(path)
+ parent = parent.find(path)
+
+ return parent.children(selector)
+ },
+ _serialize: function (parent, isContainer) {
+ var that = this,
+ childType = isContainer ? "item" : "container",
+
+ children = this.$getChildren(parent, childType).not(this.options.exclude).map(function () {
+ return that._serialize($(this), !isContainer)
+ }).get()
+
+ return this.rootGroup.options.serialize(parent, children, isContainer)
+ },
+ traverse: function(callback) {
+ $.each(this.items || [], function(item){
+ var group = $.data(this, subContainerKey)
+ if(group)
+ group.traverse(callback)
+ });
+
+ callback(this)
+ },
+ _clearDimensions: function () {
+ this.itemDimensions = undefined
+ },
+ _destroy: function() {
+ var that = this;
+
+ this.target.off(eventNames.start, this.handle);
+ this.el.removeData(pluginName)
+
+ if(this.options.drop)
+ this.group.containers = $.grep(this.group.containers, function(val){
+ return val != that
+ })
+
+ $.each(this.items || [], function(){
+ $.removeData(this, subContainerKey)
+ })
+ }
+ }
+
+ var API = {
+ enable: function() {
+ this.traverse(function(object){
+ object.disabled = false
+ })
+ },
+ disable: function (){
+ this.traverse(function(object){
+ object.disabled = true
+ })
+ },
+ serialize: function () {
+ return this._serialize(this.el, true)
+ },
+ refresh: function() {
+ this.traverse(function(object){
+ object._clearDimensions()
+ })
+ },
+ destroy: function () {
+ this.traverse(function(object){
+ object._destroy();
+ })
+ }
+ }
+
+ $.extend(Container.prototype, API)
+
+ /**
+ * jQuery API
+ *
+ * Parameters are
+ * either options on init
+ * or a method name followed by arguments to pass to the method
+ */
+ $.fn[pluginName] = function(methodOrOptions) {
+ var args = Array.prototype.slice.call(arguments, 1)
+
+ return this.map(function(){
+ var $t = $(this),
+ object = $t.data(pluginName)
+
+ if(object && API[methodOrOptions])
+ return API[methodOrOptions].apply(object, args) || this
+ else if(!object && (methodOrOptions === undefined ||
+ typeof methodOrOptions === "object"))
+ $t.data(pluginName, new Container($t, methodOrOptions))
+
+ return this
+ });
+ };
+
+}(jQuery, window, 'sortable');
diff --git a/yaksh/static/yaksh/js/requesthandler.js b/yaksh/static/yaksh/js/requesthandler.js
index ec2391a..952de3a 100644
--- a/yaksh/static/yaksh/js/requesthandler.js
+++ b/yaksh/static/yaksh/js/requesthandler.js
@@ -75,6 +75,24 @@ function response_handler(method_type, content_type, data, uid){
var error_output = document.getElementById("error_panel");
error_output.innerHTML = res.error;
focus_on_error(error_output);
+ if(global_editor.editor){
+ err_lineno = $("#err_lineno").val();
+ if(marker){
+ marker.clear();
+ }
+ if(err_lineno){
+ var lineno = parseInt(err_lineno) - 1;
+ var editor = global_editor.editor;
+ var line_length = editor.getLine(lineno).length;
+ marker = editor.markText({line: lineno, ch: 0}, {line: lineno, ch: line_length},
+ {className: "activeline", clearOnEnter:true});
+ }
+ else{
+ if(marker){
+ marker.clear();
+ }
+ }
+ }
}
} else {
reset_values();
@@ -125,6 +143,8 @@ function ajax_check_code(url, method_type, data_type, data, uid)
var global_editor = {};
var csrftoken = jQuery("[name=csrfmiddlewaretoken]").val();
+var err_lineno;
+var marker;
$(document).ready(function(){
if(is_exercise == "True" && can_skip == "False"){
setTimeout(function() {show_solution();}, delay_time*1000);
@@ -148,6 +168,7 @@ $(document).ready(function(){
mode: mode_dict[lang],
gutter: true,
lineNumbers: true,
+ styleSelectedText: true,
onChange: function (instance, changes) {
render();
}
@@ -177,6 +198,13 @@ if (question_type == 'upload' || question_type == 'code') {
global_editor.editor.setValue(init_val);
global_editor.editor.clearHistory();
}
-
-
});
+function user_arranged_options(){
+ var temp_array = []
+ var add_array = document.getElementById("arrange_order");
+ var ans_array = order_array.children().get()
+ var answer_is = $.each(ans_array, function( index, value ) {
+ temp_array.push(value.id);
+ });
+ add_array.value = temp_array
+}
diff --git a/yaksh/templates/manage.html b/yaksh/templates/manage.html
index 17ce23e..c1f9da3 100644
--- a/yaksh/templates/manage.html
+++ b/yaksh/templates/manage.html
@@ -18,7 +18,7 @@
<li><a href="{{ URL_ROOT }}/exam/manage/courses">Courses</a></li>
<li><a href="{{ URL_ROOT }}/exam/manage/monitor">Monitor</a></li>
<li><a href="{{ URL_ROOT }}/exam/manage/gradeuser">Grade User</a></li>
- <li><a href="{{ URL_ROOT }}/exam/manage/grader"> Grader </a></li>
+ <li><a href="{{ url_root }}/exam/manage/grader"> Regrade </a></li>
<li><a href="{{ URL_ROOT }}/exam/reset/changepassword">Change Password</a></li>
<li><a href="{{ URL_ROOT }}/exam/viewprofile"> {{ user.get_full_name.title }} </a></li>
<li><a href="{{URL_ROOT}}/exam/logout/" id="logout">Logout</a></li>
diff --git a/yaksh/templates/yaksh/add_question.html b/yaksh/templates/yaksh/add_question.html
index ed69657..79c132c 100644
--- a/yaksh/templates/yaksh/add_question.html
+++ b/yaksh/templates/yaksh/add_question.html
@@ -64,6 +64,7 @@
<option value="integertestcase">Integer </option>
<option value="stringtestcase"> String </option>
<option value="floattestcase"> Float </option>
+ <option value="arrangetestcase">Arrange options </option>
</select></p>
<center>
<button class="btn" type="submit" name="save_question">Save</button>
diff --git a/yaksh/templates/yaksh/course_detail.html b/yaksh/templates/yaksh/course_detail.html
index a5d10a7..9fcae68 100644
--- a/yaksh/templates/yaksh/course_detail.html
+++ b/yaksh/templates/yaksh/course_detail.html
@@ -136,12 +136,14 @@
<th>Sr No.</th>
<th>Students</th>
<th>Total</th>
+ <th>Grade</th>
<th colspan="{{modules|length}}">Modules</th>
</tr>
<tr>
<th scope="row"></th>
<th></th>
<th></th>
+ <th></th>
{% if modules %}
{% for module in modules %}
<th>
@@ -171,6 +173,10 @@
{% course_completion_percent course student as c_percent %}
{{c_percent}} %
</td>
+ <td>
+ {% course_grade course student as grade %}
+ {{grade}}
+ </td>
{% if modules %}
{% for module in modules %}
<td>
diff --git a/yaksh/templates/yaksh/course_modules.html b/yaksh/templates/yaksh/course_modules.html
index afbae75..6c93e97 100644
--- a/yaksh/templates/yaksh/course_modules.html
+++ b/yaksh/templates/yaksh/course_modules.html
@@ -17,6 +17,7 @@
<center>{{ msg }}</center>
</div>
{% endif %}
+<b>Grade: {% if grade %} {{ grade }} {% else %} Will be available once the course is complete {% endif %}</b>
{% if learning_modules %}
<table class="table">
{% for module in learning_modules %}
diff --git a/yaksh/templates/yaksh/courses.html b/yaksh/templates/yaksh/courses.html
index dabf8eb..ba09c6d 100644
--- a/yaksh/templates/yaksh/courses.html
+++ b/yaksh/templates/yaksh/courses.html
@@ -56,6 +56,10 @@
<a href="{{URL_ROOT}}/exam/manage/courses/all_learning_module">
Add/View Modules</a>
</li>
+ <li>
+ <a href="{% url 'grades:grading_systems'%}">
+ Add/View Grading Systems </a>
+ </li>
</ul>
</div>
</div>
diff --git a/yaksh/templates/yaksh/error_template.html b/yaksh/templates/yaksh/error_template.html
index 61657ae..301020e 100644
--- a/yaksh/templates/yaksh/error_template.html
+++ b/yaksh/templates/yaksh/error_template.html
@@ -3,7 +3,6 @@
{% endblock %}
{% load custom_filters %}
-
{% if error_message %}
{% for error in error_message %}
@@ -35,6 +34,7 @@
</tr>
<tr>
{% if error.traceback %}
+ <input type="hidden" id="err_lineno" value="{{error.line_no}}">
<td><b>Full Traceback: </b></td>
<td><pre>{{error.traceback}}</pre></td>
{% endif %}
diff --git a/yaksh/templates/yaksh/grade_user.html b/yaksh/templates/yaksh/grade_user.html
index 93f00e0..8430e91 100644
--- a/yaksh/templates/yaksh/grade_user.html
+++ b/yaksh/templates/yaksh/grade_user.html
@@ -167,7 +167,7 @@ Status : <b style="color: red;"> Failed </b><br/>
{% endif %}
{% endfor %}
- {% elif question.type == "integer" or "string" or "float" %}
+ {% elif question.type == "integer" or question.type == "string" or question.type == "float" %}
<h5> <u>Correct Answer:</u></h5>
{% for testcase in question.get_test_cases %}
<strong>{{ testcase.correct|safe }}</strong>
@@ -175,6 +175,14 @@ Status : <b style="color: red;"> Failed </b><br/>
<strong>{{ testcase.error_margin|safe }}</strong>
{% endif %}
{% endfor %}
+ {% elif question.type == "arrange" %}
+ <h5> <u>Correct Order:</u></h5>
+ <div class="list-group" >
+ {% for testcase in question.get_test_cases %}
+ <li class="list-group-item"><strong>{{ testcase.options|safe }}</strong></li>
+ {% endfor %}
+ </div>
+
{% else %}
<h5> <u>Test cases: </u></h5>
{% for testcase in question.get_test_cases %}
@@ -307,6 +315,15 @@ Status : <b style="color: red;"> Failed </b><br/>
{% endif %}
{% endfor %}
</div>
+
+ {% elif question.type == "arrange"%}
+ <div class="well well-sm">
+ {% get_answer_for_arrange_options ans.answer.answer question as tc_list %}
+ {% for testcases in tc_list %}
+ <li>{{ testcases.options.strip|safe }}</li>
+ {% endfor %}
+ </div>
+
{% else %}
<div class="well well-sm">
{{ ans.answer.answer.strip|safe }}
diff --git a/yaksh/templates/yaksh/question.html b/yaksh/templates/yaksh/question.html
index 9d6ce48..ebfe066 100644
--- a/yaksh/templates/yaksh/question.html
+++ b/yaksh/templates/yaksh/question.html
@@ -11,6 +11,10 @@
.CodeMirror{
border-style: groove;
}
+ .activeline {
+ background: #FBC2C4 !important;
+ color: #8a1f11 !important;
+ }
</style>
{% endblock %}
@@ -21,6 +25,7 @@
<script src="{{ URL_ROOT }}/static/yaksh/js/codemirror/mode/clike/clike.js"></script>
<script src="{{ URL_ROOT }}/static/yaksh/js/codemirror/mode/shell/shell.js"></script>
<script src="{{ URL_ROOT }}/static/yaksh/js/mathjax/MathJax.js?config=TeX-MML-AM_CHTML"></script>
+<script src="{{ URL_ROOT }}/static/yaksh/js/jquery-sortable.js"></script>
<script>
init_val = '{{ last_attempt|escape_quotes|safe }}';
lang = "{{ question.language }}"
@@ -175,10 +180,12 @@ question_type = "{{ question.type }}"
{% else %}
<h5>(CASE SENSITIVE)</h5>
{% endif %}
-
{% elif question.type == "float" %}
(FILL IN THE BLANKS WITH FLOAT ANSWER)
+ {% elif question.type == "arrange" %}
+ (ARRANGE THE OPTIONS IN CORRECT ORDER)
{% endif %}
+
</u>
<font class=pull-right>(Marks : {{ question.points }}) </font>
</h4>
@@ -218,7 +225,7 @@ question_type = "{{ question.type }}"
{% if question.type == "integer" %}
Enter Integer:<br/>
- <input autofocus name="answer" type="number" id="integer" value={{ last_attempt|safe }} />
+ <input autofocus name="answer" type="number" id="integer" value="{{ last_attempt|safe }}" />
<br/><br/>
{% endif %}
@@ -230,7 +237,7 @@ question_type = "{{ question.type }}"
{% if question.type == "float" %}
Enter Decimal Value :<br/>
- <input autofocus name="answer" type="number" step="any" id="float" value={{ last_attempt|safe }} />
+ <input autofocus name="answer" type="number" step="any" id="float" value="{{ last_attempt|safe }}" />
<br/><br/>
{% endif %}
@@ -251,6 +258,27 @@ question_type = "{{ question.type }}"
<input type=file id="assignment" name="assignment" multiple="">
<hr>
{% endif %}
+
+ {% if question.type == "arrange" %}
+ {% if last_attempt %}
+ {% get_answer_for_arrange_options last_attempt question as test_cases %}
+ {% endif %}
+ <input name="answer" type="hidden" id='arrange_order'/>
+ <div class="list-group">
+ <ol class="arrange">
+ {% for test_case in test_cases %}
+ <li class="list-group-item" id={{test_case.id}}>{{test_case.options| safe }}</li>
+ {% endfor %}
+ </ol>
+ </div>
+
+ <script type="text/javascript">
+ var arrange = $("ol.arrange");
+ var order_array = $(arrange).sortable(['serialize']);
+ </script>
+ {% endif %}
+
+
{% if question.type == "code" %}
<div class="row">
<div class="col-md-9">
@@ -269,6 +297,9 @@ question_type = "{{ question.type }}"
<br><button class="btn btn-primary" type="submit" name="check" id="check">Submit Answer</button>&nbsp;&nbsp;
{% elif question.type == "upload" %}
<br><button class="btn btn-primary" type="submit" name="check" id="check" onClick="return validate();">Upload</button>&nbsp;&nbsp;
+ {% elif question.type == "arrange" %}
+ <br><button class="btn btn-primary" type="submit" name="check" id="check" onClick="return user_arranged_options();">Submit Answer</button>&nbsp;&nbsp;
+
{% else %}
{% if question in paper.get_questions_unanswered or quiz.is_exercise %}
diff --git a/yaksh/templates/yaksh/user_data.html b/yaksh/templates/yaksh/user_data.html
index ce2533e..9449fcc 100644
--- a/yaksh/templates/yaksh/user_data.html
+++ b/yaksh/templates/yaksh/user_data.html
@@ -80,6 +80,13 @@ User IP address: {{ paper.user_ip }}
<strong>{{ testcase.correct|safe }}</strong>
{% endfor %}
+ {% elif question.type == "arrange" %}
+ <h5> <u>Correct Order:</u></h5>
+ <div class="list-group" >
+ {% for testcase in question.get_test_cases %}
+ <li class="list-group-item"><strong>{{ testcase.options|safe }}</strong></li>
+ {% endfor %}
+ </div>
{% else %}
<h5> <u>Test cases: </u></h5>
@@ -99,6 +106,7 @@ User IP address: {{ paper.user_ip }}
{% endif %}
<div class="panel-body">
<h5><u>Student answer:</u></h5>
+
{% if question.type == "mcc"%}
<div class="well well-sm">
{% for testcases in question.get_test_cases %}
@@ -107,6 +115,7 @@ User IP address: {{ paper.user_ip }}
{% endif %}
{% endfor %}
</div>
+
{% elif question.type == "mcq"%}
<div class="well well-sm">
{% for testcases in question.get_test_cases %}
@@ -115,6 +124,15 @@ User IP address: {{ paper.user_ip }}
{% endif %}
{% endfor %}
</div>
+
+ {% elif question.type == "arrange"%}
+ <div class="well well-sm">
+ {% get_answer_for_arrange_options answers.0.answer question as tc_list %}
+ {% for testcases in tc_list %}
+ <li>{{ testcases.options.strip|safe }}</li>
+ {% endfor %}
+ </div>
+
{%else%}
<div class="well well-sm">
{{ answers.0.answer|safe }}
diff --git a/yaksh/templates/yaksh/view_answerpaper.html b/yaksh/templates/yaksh/view_answerpaper.html
index 971ef77..7cbec91 100644
--- a/yaksh/templates/yaksh/view_answerpaper.html
+++ b/yaksh/templates/yaksh/view_answerpaper.html
@@ -35,9 +35,9 @@
End time : {{ paper.end_time }} <br/>
Percentage obtained: {{ paper.percent }}% <br/>
{% if paper.passed %}
- Status : <b style="color: red;"> Failed </b><br/>
- {% else %}
Status : <b style="color: green;"> Passed </b><br/>
+ {% else %}
+ Status : <b style="color: red;"> Failed </b><br/>
{% endif %}
</p>
@@ -67,12 +67,20 @@
{% endif %}
{% endfor %}
- {% elif question.type == "integer" or "string" or "float" %}
+ {% elif question.type == "integer" or question.type == "string" or question.type == "float" %}
<h5> <u>Correct Answer:</u></h5>
{% for testcase in question.get_test_cases %}
<strong>{{ testcase.correct|safe }}</strong>
{% endfor %}
+ {% elif question.type == "arrange" %}
+ <h5> <u>Correct Order:</u></h5>
+ <div class="list-group">
+ {% for testcase in question.get_test_cases %}
+ <li class="list-group-item"><strong>{{ testcase.options|safe }}</strong></li>
+ {% endfor %}
+ </div>
+
{% else %}
<h5> <u>Test cases: </u></h5>
{% for testcase in question.get_test_cases %}
@@ -108,6 +116,13 @@
{% endif %}
{% endfor %}
</div>
+ {% elif question.type == "arrange"%}
+ <div class="well well-sm">
+ {% get_answer_for_arrange_options answers.0.answer question as tc_list %}
+ {% for testcases in tc_list %}
+ <li>{{ testcases.options.strip|safe }}</li>
+ {% endfor %}
+ </div>
{% elif question.type == "upload" and has_user_assignment %}
<a href="{{URL_ROOT}}/exam/download/user_assignment/{{question.id}}/{{data.user.id}}/{{paper.question_paper.quiz.id}}">
<div class="well well-sm">
diff --git a/yaksh/templatetags/custom_filters.py b/yaksh/templatetags/custom_filters.py
index fa0802f..0c5eb5a 100644
--- a/yaksh/templatetags/custom_filters.py
+++ b/yaksh/templatetags/custom_filters.py
@@ -1,5 +1,6 @@
from django import template
from django.template.defaultfilters import stringfilter
+from ast import literal_eval
import os
try:
from itertools import zip_longest
@@ -65,5 +66,23 @@ def course_completion_percent(course, user):
@register.simple_tag
+def course_grade(course, user):
+ return course.get_grade(user)
+
+
+@register.simple_tag
def get_ordered_testcases(question, answerpaper):
- return question.get_ordered_test_cases(answerpaper) \ No newline at end of file
+ return question.get_ordered_test_cases(answerpaper)
+
+@register.simple_tag
+def get_answer_for_arrange_options(ans, question):
+ if type(ans) == bytes:
+ ans = ans.decode("utf-8")
+ else:
+ ans = str(ans)
+ answer = literal_eval(ans)
+ testcases = []
+ for answer_id in answer:
+ tc = question.get_test_case(id=int(answer_id))
+ testcases.append(tc)
+ return testcases
diff --git a/yaksh/templatetags/test_custom_filters.py b/yaksh/templatetags/test_custom_filters.py
new file mode 100644
index 0000000..7cef957
--- /dev/null
+++ b/yaksh/templatetags/test_custom_filters.py
@@ -0,0 +1,152 @@
+import unittest
+from datetime import datetime, timedelta
+from django.utils import timezone
+import pytz
+
+# local imports
+from yaksh.models import (User, Profile, Question, Quiz, QuestionPaper,
+ QuestionSet, AnswerPaper, Answer, Course,
+ IntegerTestCase, FloatTestCase,
+ StringTestCase, McqTestCase, ArrangeTestCase,
+ TestCaseOrder
+ )
+
+from yaksh.templatetags.custom_filters import (completed, inprogress,
+ get_ordered_testcases,
+ get_answer_for_arrange_options
+ )
+
+
+def setUpModule():
+ # Create user profile
+ teacher = User.objects.create_user(username='teacher2000',
+ password='demo',
+ email='teacher2000@test.com')
+ Profile.objects.create(user=teacher, roll_number=2000, institute='IIT',
+ department='Chemical', position='Teacher')
+ # Create a course
+ course = Course.objects.create(name="Python Course 2000",
+ enrollment="Enroll Request",
+ creator=teacher)
+ # Create a quiz
+ quiz = Quiz.objects.create(start_date_time=datetime(
+ 2015, 10, 9, 10, 8, 15, 0,
+ tzinfo=pytz.utc),
+ end_date_time=datetime(
+ 2199, 10, 9, 10, 8, 15, 0,
+ tzinfo=pytz.utc),
+ duration=30, active=True,
+ attempts_allowed=1, time_between_attempts=0,
+ description='demo quiz 2000',
+ pass_criteria=0,
+ instructions="Demo Instructions",
+ creator=teacher
+ )
+ # Create a question paper
+ question_paper = QuestionPaper.objects.create(quiz=quiz,
+ total_marks=1.0)
+ # Create an answer paper
+ answerpaper = AnswerPaper.objects.create(user=teacher,
+ user_ip='101.0.0.1',
+ start_time=timezone.now(),
+ question_paper=question_paper,
+ end_time=timezone.now()
+ +timedelta(minutes=5),
+ attempt_number=1,
+ course=course
+ )
+def tearDownModule():
+ User.objects.get(username="teacher2000").delete()
+
+
+class CustomFiltersTestCases(unittest.TestCase):
+
+ @classmethod
+ def setUpClass(self):
+ self.course = Course.objects.get(name="Python Course 2000")
+ self.quiz = Quiz.objects.get(description="demo quiz 2000")
+ self.question_paper = QuestionPaper.objects.get(quiz=self.quiz)
+ self.user = User.objects.get(username='teacher2000')
+ self.question1 = Question.objects.create(summary='int1', points=1,
+ type='code', user=self.user)
+ self.question1.language = 'python'
+ self.question1.type = "arrange"
+ self.question1.description = "Arrange alphabets in ascending order"
+ self.question1.test_case_type = 'arrangetestcase'
+ self.question1.save()
+ self.question_paper.fixed_questions.add(self.question1)
+ self.question_paper.save()
+ #Creating answerpaper
+
+ self.answerpaper = AnswerPaper.objects.get(user=self.user,
+ course=self.course,
+ question_paper=self.question_paper
+ )
+ self.answerpaper.questions.add(self.question1)
+ self.answerpaper.save()
+ # For question
+ self.arrange_testcase_1 = ArrangeTestCase(question=self.question1,
+ options="A",
+ type = 'arrangetestcase',
+ )
+ self.arrange_testcase_1.save()
+ self.testcase_1_id = self.arrange_testcase_1.id
+ self.arrange_testcase_2 = ArrangeTestCase(question=self.question1,
+ options="B",
+ type = 'arrangetestcase',
+ )
+ self.arrange_testcase_2.save()
+ self.testcase_2_id = self.arrange_testcase_2.id
+ self.arrange_testcase_3 = ArrangeTestCase(question=self.question1,
+ options="C",
+ type = 'arrangetestcase',
+ )
+ self.arrange_testcase_3.save()
+ self.testcase_3_id = self.arrange_testcase_3.id
+
+ @classmethod
+ def tearDownClass(self):
+ self.question1.delete()
+ self.answerpaper.delete()
+
+ def test_completed_inprogress(self):
+ # Test in progress
+ answerpaper = AnswerPaper.objects.filter(id=self.answerpaper.id)
+
+ self.assertEqual(inprogress(answerpaper), 1)
+ self.assertEqual(completed(answerpaper), 0)
+ # Test completed
+ self.answerpaper.status='completed'
+ self.answerpaper.save()
+ self.assertEqual(inprogress(answerpaper), 0)
+ self.assertEqual(completed(answerpaper), 1)
+
+ def test_get_answer_for_arrange_options(self):
+ arrange_ans = [self.arrange_testcase_3,
+ self.arrange_testcase_2,
+ self.arrange_testcase_1,
+ ]
+ arrange_ans_id = [tc.id for tc in arrange_ans]
+ user_ans_order = get_answer_for_arrange_options(arrange_ans_id,
+ self.question1
+ )
+ self.assertSequenceEqual(arrange_ans, user_ans_order)
+
+ def test_get_ordered_testcases(self):
+ new_answerpaper = self.question_paper.make_answerpaper(self.user,
+ "101.0.0.1",2,
+ self.course.id
+ )
+ tc_order = TestCaseOrder.objects.get(answer_paper=new_answerpaper,
+ question=self.question1
+ )
+ testcases = [self.question1.get_test_case(id=ids)
+ for ids in tc_order.order.split(",")
+ ]
+
+ ordered_testcases = get_ordered_testcases(self.question1,
+ new_answerpaper
+ )
+ self.assertSequenceEqual(testcases, ordered_testcases)
+
+ new_answerpaper.delete()
diff --git a/yaksh/test_models.py b/yaksh/test_models.py
index e5645c2..bcd0434 100644
--- a/yaksh/test_models.py
+++ b/yaksh/test_models.py
@@ -631,6 +631,15 @@ class QuestionPaperTestCases(unittest.TestCase):
# All active questions
self.questions = Question.objects.filter(active=True, user=self.user)
self.quiz = Quiz.objects.get(description="demo quiz 1")
+ self.quiz_with_time_between_attempts = Quiz.objects.create(
+ description="demo quiz with time between attempts",
+ start_date_time=datetime(2015, 10, 9, 10, 8, 15, 0, tzinfo=pytz.utc),
+ end_date_time=datetime(2199, 10, 9, 10, 8, 15, 0, tzinfo=pytz.utc),
+ duration=30, active=True,
+ attempts_allowed=3, time_between_attempts=1.0,
+ pass_criteria=0,
+ instructions="Demo Instructions"
+ )
# create question paper with only fixed questions
self.question_paper_fixed_questions = QuestionPaper.objects.create(
@@ -658,6 +667,12 @@ class QuestionPaperTestCases(unittest.TestCase):
shuffle_questions=True
)
+ self.question_paper_with_time_between_attempts = QuestionPaper.objects.create(
+ quiz=self.quiz_with_time_between_attempts,
+ total_marks=0.0,
+ shuffle_questions=True
+ )
+
self.question_paper.fixed_question_order = "{0}, {1}".format(
self.questions[3].id, self.questions[5].id
)
@@ -788,8 +803,10 @@ class QuestionPaperTestCases(unittest.TestCase):
answerpaper.passed = True
answerpaper.save()
# test can_attempt_now(self):
- self.assertFalse(self.question_paper.can_attempt_now(self.user,
- self.course.id))
+ result = (False, u'You cannot attempt demo quiz 1 quiz more than 1 time(s)')
+ self.assertEquals(
+ self.question_paper.can_attempt_now(self.user, self.course.id), result
+ )
# trying to create an answerpaper with same parameters passed.
answerpaper2 = self.question_paper.make_answerpaper(self.user, self.ip,
attempt_num,
@@ -798,6 +815,46 @@ class QuestionPaperTestCases(unittest.TestCase):
self.assertEqual(answerpaper, answerpaper2)
+ def test_time_between_attempt(self):
+ """ Test make_answerpaper() method of Question Paper"""
+ already_attempted = self.attempted_papers.count()
+ attempt_num = 1
+
+ self.first_start_time = timezone.now()
+ self.first_end_time = self.first_start_time + timedelta(minutes=20)
+ self.second_start_time = self.first_start_time + timedelta(minutes=30)
+ self.second_end_time = self.second_start_time + timedelta(minutes=20)
+
+ # create answerpaper
+ self.first_answerpaper = AnswerPaper(
+ user=self.user,
+ question_paper=self.question_paper_with_time_between_attempts,
+ start_time=self.first_start_time,
+ end_time=self.first_end_time,
+ user_ip=self.ip,
+ course=self.course,
+ attempt_number=attempt_num
+ )
+ self.first_answerpaper.passed = True
+ self.first_answerpaper.save()
+
+ self.second_answerpaper = AnswerPaper(
+ user=self.user,
+ question_paper=self.question_paper_with_time_between_attempts,
+ start_time=self.second_start_time,
+ end_time=self.second_end_time,
+ user_ip=self.ip,
+ course=self.course,
+ attempt_number=attempt_num + 1
+ )
+ self.second_answerpaper.passed = True
+ self.second_answerpaper.save()
+
+ result = (False, u'You cannot start the next attempt for this quiz before 1.0 hour(s)')
+ self.assertEquals(
+ self.question_paper_with_time_between_attempts.can_attempt_now(self.user, self.course.id), result
+ )
+
def test_create_trial_paper_to_test_quiz(self):
qu_list = [str(self.questions_list[0]), str(self.questions_list[1])]
@@ -1377,12 +1434,20 @@ class AnswerPaperTestCases(unittest.TestCase):
def test_get_question_answer(self):
""" Test get_question_answer() method of Answer Paper"""
+ questions = self.answerpaper.questions.all()
answered = self.answerpaper.get_question_answers()
- first_answer = list(answered.values())[0][0]
- first_answer_obj = first_answer['answer']
- self.assertEqual(first_answer_obj.answer, 'Demo answer')
- self.assertTrue(first_answer_obj.correct)
- self.assertEqual(len(answered), 2)
+ for question in questions:
+ answers_saved = Answer.objects.filter(question=question)
+ error_list = [json.loads(ans.error) for ans in answers_saved]
+ if answers_saved:
+ self.assertEqual(len(answered[question]), len(answers_saved))
+ ans = []
+ err = []
+ for val in answered[question]:
+ ans.append(val.get('answer'))
+ err.append(val.get('error_list'))
+ self.assertEqual(set(ans), set(answers_saved))
+ self.assertEqual(error_list, err)
def test_is_answer_correct(self):
self.assertTrue(self.answerpaper.is_answer_correct(self.questions[0]))
@@ -1835,3 +1900,101 @@ class AssignmentUploadTestCases(unittest.TestCase):
actual_file_name = self.quiz.description.replace(" ", "_")
file_name = file_name.replace(" ", "_")
self.assertIn(actual_file_name, file_name)
+
+
+class CourseStatusTestCases(unittest.TestCase):
+ def setUp(self):
+ user = User.objects.get(username='creator')
+ self.course = Course.objects.create(name="Demo Course", creator=user,
+ enrollment="Enroll Request")
+ self.module = LearningModule.objects.create(name='M1', creator=user,
+ description='module one')
+ self.quiz1 = Quiz.objects.create(time_between_attempts=0, weightage=50,
+ description='qz1')
+ self.quiz2 = Quiz.objects.create(time_between_attempts=0, weightage=100,
+ description='qz2')
+ question = Question.objects.first()
+ self.qpaper1 = QuestionPaper.objects.create(quiz=self.quiz1)
+ self.qpaper2 = QuestionPaper.objects.create(quiz=self.quiz2)
+ self.qpaper1.fixed_questions.add(question)
+ self.qpaper2.fixed_questions.add(question)
+ self.qpaper1.update_total_marks()
+ self.qpaper2.update_total_marks()
+ self.qpaper1.save()
+ self.qpaper2.save()
+ self.unit_1_quiz = LearningUnit.objects.create(order=1, type='quiz',
+ quiz=self.quiz1)
+ self.unit_2_quiz = LearningUnit.objects.create(order=2, type='quiz',
+ quiz=self.quiz2)
+ self.module.learning_unit.add(self.unit_1_quiz)
+ self.module.learning_unit.add(self.unit_2_quiz)
+ self.module.save()
+ self.course.learning_module.add(self.module)
+ student = User.objects.get(username='course_user')
+ self.course.students.add(student)
+ self.course.save()
+
+ attempt = 1
+ ip = '127.0.0.1'
+ self.answerpaper1 = self.qpaper1.make_answerpaper(student, ip, attempt,
+ self.course.id)
+ self.answerpaper2 = self.qpaper2.make_answerpaper(student, ip, attempt,
+ self.course.id)
+
+ self.course_status = CourseStatus.objects.create(course=self.course,
+ user=student)
+
+ def tearDown(self):
+ self.course_status.delete()
+ self.answerpaper1.delete()
+ self.answerpaper2.delete()
+ self.qpaper1.delete()
+ self.qpaper2.delete()
+ self.quiz1.delete()
+ self.quiz2.delete()
+ self.unit_1_quiz.delete()
+ self.unit_2_quiz.delete()
+ self.module.delete()
+ self.course.delete()
+
+ def test_course_is_complete(self):
+ # When
+ self.course_status.completed_units.add(self.unit_1_quiz)
+ # Then
+ self.assertFalse(self.course_status.is_course_complete())
+
+ # When
+ self.course_status.completed_units.add(self.unit_2_quiz)
+ # Then
+ self.assertTrue(self.course_status.is_course_complete())
+
+ # Given
+ self.answerpaper1.marks_obtained = 1
+ self.answerpaper1.save()
+ self.answerpaper2.marks_obtained = 0
+ self.answerpaper2.save()
+ # When
+ self.course_status.calculate_percentage()
+ # Then
+ self.assertEqual(round(self.course_status.percentage, 2), 33.33)
+ # When
+ self.course_status.set_grade()
+ # Then
+ self.assertEqual(self.course_status.get_grade(), 'F')
+
+ # Given
+ self.answerpaper1.marks_obtained = 0
+ self.answerpaper1.save()
+ self.answerpaper2.marks_obtained = 1
+ self.answerpaper2.save()
+ # When
+ self.course_status.calculate_percentage()
+ # Then
+ self.assertEqual(round(self.course_status.percentage, 2), 66.67)
+ # When
+ self.course_status.set_grade()
+ # Then
+ self.assertEqual(self.course_status.get_grade(), 'B')
+
+ # Test get course grade after completion
+ self.assertEqual(self.course.get_grade(self.answerpaper1.user), 'B')
diff --git a/yaksh/test_views.py b/yaksh/test_views.py
index fd4f040..514f1cd 100644
--- a/yaksh/test_views.py
+++ b/yaksh/test_views.py
@@ -2556,7 +2556,7 @@ class TestCourseDetail(TestCase):
attachment_file = mail.outbox[0].attachments[0][0]
subject = mail.outbox[0].subject
body = mail.outbox[0].alternatives[0][0]
- recipients = mail.outbox[0].recipients()
+ recipients = mail.outbox[0].bcc
self.assertEqual(attachment_file, "file.txt")
self.assertEqual(subject, "test_bulk_mail")
self.assertEqual(body, "Test_Mail")
diff --git a/yaksh/tests/test_code_server.py b/yaksh/tests/test_code_server.py
index 1309624..e2781df 100644
--- a/yaksh/tests/test_code_server.py
+++ b/yaksh/tests/test_code_server.py
@@ -106,6 +106,40 @@ class TestCodeServer(unittest.TestCase):
self.assertFalse(data['success'])
self.assertTrue('AssertionError' in data['error'][0]['exception'])
+ def test_question_with_no_testcases(self):
+ # Given
+ testdata = {
+ 'metadata': {
+ 'user_answer': 'def f(): return 1',
+ 'language': 'python',
+ 'partial_grading': False
+ },
+ 'test_case_data': []
+ }
+
+ # When
+ submit(self.url, '0', json.dumps(testdata), '')
+ result = get_result(self.url, '0', block=True)
+
+ # Then
+ data = json.loads(result.get('result'))
+ self.assertFalse(data['success'])
+
+ # With correct answer and test case
+ testdata["metadata"]["user_answer"] = 'def f(): return 2'
+ testdata["test_case_data"] = [{'test_case': 'assert f() == 2',
+ 'test_case_type': 'standardtestcase',
+ 'weight': 0.0
+ }
+ ]
+ # When
+ submit(self.url, '0', json.dumps(testdata), '')
+ result = get_result(self.url, '0', block=True)
+
+ # Then
+ data = json.loads(result.get('result'))
+ self.assertTrue(data['success'])
+
def test_multiple_simultaneous_hits(self):
# Given
results = Queue()
diff --git a/yaksh/urls.py b/yaksh/urls.py
index dd450ba..3611573 100644
--- a/yaksh/urls.py
+++ b/yaksh/urls.py
@@ -1,4 +1,4 @@
-from django.conf.urls import patterns, url
+from django.conf.urls import url
from yaksh import views
urlpatterns = [
diff --git a/yaksh/urls_password_reset.py b/yaksh/urls_password_reset.py
index c1e36c6..4a7ddf3 100644
--- a/yaksh/urls_password_reset.py
+++ b/yaksh/urls_password_reset.py
@@ -1,4 +1,4 @@
-from django.conf.urls import patterns, url
+from django.conf.urls import url
from django.contrib.auth.views import password_reset, password_reset_confirm,\
password_reset_done, password_reset_complete, password_change,\
password_change_done
diff --git a/yaksh/views.py b/yaksh/views.py
index c22500d..e1c1889 100644
--- a/yaksh/views.py
+++ b/yaksh/views.py
@@ -7,7 +7,7 @@ import csv
from django.http import HttpResponse, JsonResponse
from django.core.urlresolvers import reverse
from django.contrib.auth import login, logout, authenticate
-from django.shortcuts import render_to_response, get_object_or_404, redirect
+from django.shortcuts import render, get_object_or_404, redirect
from django.template import RequestContext, Context, Template
from django.template.loader import get_template, render_to_string
from django.http import Http404
@@ -65,14 +65,14 @@ def my_redirect(url):
return redirect(URL_ROOT + url)
-def my_render_to_response(template, context=None, **kwargs):
+def my_render_to_response(request, template, context=None, **kwargs):
"""Overridden render_to_response.
"""
if context is None:
context = {'URL_ROOT': URL_ROOT}
else:
context['URL_ROOT'] = URL_ROOT
- return render_to_response(template, context, **kwargs)
+ return render(request, template, context, **kwargs)
def is_moderator(user):
@@ -115,7 +115,6 @@ def user_register(request):
Create a user and corresponding profile and store roll_number also."""
user = request.user
- ci = RequestContext(request)
if user.is_authenticated():
return my_redirect("/exam/quizzes/")
context = {}
@@ -130,16 +129,18 @@ def user_register(request):
success, msg = send_user_mail(user_email, key)
context = {'activation_msg': msg}
return my_render_to_response(
+ request,
'yaksh/activation_status.html', context
)
return index(request)
else:
- return my_render_to_response('yaksh/register.html', {'form': form},
- context_instance=ci)
+ return my_render_to_response(
+ request, 'yaksh/register.html', {'form': form}
+ )
else:
form = UserRegisterForm()
return my_render_to_response(
- 'yaksh/register.html', {'form': form}, context_instance=ci
+ request, 'yaksh/register.html', {'form': form}
)
@@ -147,7 +148,7 @@ def user_logout(request):
"""Show a page to inform user that the quiz has been compeleted."""
logout(request)
context = {'message': "You have been logged out successfully"}
- return my_render_to_response('yaksh/complete.html', context)
+ return my_render_to_response(request, 'yaksh/complete.html', context)
@login_required
@@ -156,7 +157,6 @@ def user_logout(request):
def quizlist_user(request, enrolled=None, msg=None):
"""Show All Quizzes that is available to logged-in user."""
user = request.user
- ci = RequestContext(request)
if request.method == "POST":
course_code = request.POST.get('course_code')
@@ -178,9 +178,7 @@ def quizlist_user(request, enrolled=None, msg=None):
context = {'user': user, 'courses': courses, 'title': title,
'msg': msg}
- return my_render_to_response(
- "yaksh/quizzes_user.html", context, context_instance=ci
- )
+ return my_render_to_response(request, "yaksh/quizzes_user.html", context)
@login_required
@@ -190,14 +188,13 @@ def results_user(request):
user = request.user
papers = AnswerPaper.objects.get_user_answerpapers(user)
context = {'papers': papers}
- return my_render_to_response("yaksh/results_user.html", context)
+ return my_render_to_response(request, "yaksh/results_user.html", context)
@login_required
@email_verified
def add_question(request, question_id=None):
user = request.user
- ci = RequestContext(request)
test_case_type = None
if question_id is None:
@@ -259,7 +256,7 @@ def add_question(request, question_id=None):
'uploaded_files': uploaded_files
}
return my_render_to_response(
- "yaksh/add_question.html", context, context_instance=ci
+ request, "yaksh/add_question.html", context
)
qform = QuestionForm(instance=question)
@@ -284,7 +281,7 @@ def add_question(request, question_id=None):
context = {'qform': qform, 'fileform': fileform, 'question': question,
'formsets': formsets, 'uploaded_files': uploaded_files}
return my_render_to_response(
- "yaksh/add_question.html", context, context_instance=ci
+ request, "yaksh/add_question.html", context
)
@@ -294,7 +291,6 @@ def add_quiz(request, quiz_id=None, course_id=None):
"""To add a new quiz in the database.
Create a new quiz and store it."""
user = request.user
- ci = RequestContext(request)
if not is_moderator(user):
raise Http404('You are not allowed to view this course !')
if quiz_id:
@@ -325,16 +321,13 @@ def add_quiz(request, quiz_id=None, course_id=None):
context["course_id"] = course_id
context["quiz"] = quiz
context["form"] = form
- return my_render_to_response(
- 'yaksh/add_quiz.html', context, context_instance=ci
- )
+ return my_render_to_response(request, 'yaksh/add_quiz.html', context)
@login_required
@email_verified
def add_exercise(request, quiz_id=None, course_id=None):
user = request.user
- ci = RequestContext(request)
if not is_moderator(user):
raise Http404('You are not allowed to view this course !')
if quiz_id:
@@ -374,9 +367,7 @@ def add_exercise(request, quiz_id=None, course_id=None):
context["exercise"] = quiz
context["course_id"] = course_id
context["form"] = form
- return my_render_to_response(
- 'yaksh/add_exercise.html', context, context_instance=ci
- )
+ return my_render_to_response(request, 'yaksh/add_exercise.html', context)
@login_required
@@ -386,7 +377,6 @@ def prof_manage(request, msg=None):
"""Take credentials of the user with professor/moderator
rights/permissions and log in."""
user = request.user
- ci = RequestContext(request)
if not user.is_authenticated():
return my_redirect('/exam/login')
if not is_moderator(user):
@@ -416,7 +406,7 @@ def prof_manage(request, msg=None):
'trial_paper': trial_paper, 'msg': msg
}
return my_render_to_response(
- 'yaksh/moderator_dashboard.html', context, context_instance=ci
+ request, 'yaksh/moderator_dashboard.html', context
)
@@ -424,7 +414,6 @@ def user_login(request):
"""Take the credentials of the user and log the user in."""
user = request.user
- ci = RequestContext(request)
context = {}
if user.is_authenticated():
return index(request)
@@ -444,8 +433,7 @@ def user_login(request):
form = UserLoginForm()
context = {"form": form}
- return my_render_to_response('yaksh/login.html', context,
- context_instance=ci)
+ return my_render_to_response(request, 'yaksh/login.html', context)
@login_required
@@ -455,7 +443,6 @@ def start(request, questionpaper_id=None, attempt_num=None, course_id=None,
"""Check the user cedentials and if any quiz is available,
start the exam."""
user = request.user
- ci = RequestContext(request)
# check conditions
try:
quest_paper = QuestionPaper.objects.get(id=questionpaper_id)
@@ -534,9 +521,8 @@ def start(request, questionpaper_id=None, attempt_num=None, course_id=None,
previous_question=last_attempt.current_question()
)
# allowed to start
- if not quest_paper.can_attempt_now(user, course_id):
- msg = "You cannot attempt {0} quiz more than {1} time(s)".format(
- quest_paper.quiz.description, quest_paper.quiz.attempts_allowed)
+ if not quest_paper.can_attempt_now(user, course_id)[0]:
+ msg = quest_paper.can_attempt_now(user, course_id)[1]
if is_moderator(user):
return prof_manage(request, msg=msg)
return view_module(request, module_id=module_id, course_id=course_id,
@@ -555,8 +541,7 @@ def start(request, questionpaper_id=None, attempt_num=None, course_id=None,
}
if is_moderator(user):
context["status"] = "moderator"
- return my_render_to_response('yaksh/intro.html', context,
- context_instance=ci)
+ return my_render_to_response(request, 'yaksh/intro.html', context)
else:
ip = request.META['REMOTE_ADDR']
if not hasattr(user, 'profile'):
@@ -565,10 +550,11 @@ def start(request, questionpaper_id=None, attempt_num=None, course_id=None,
new_paper = quest_paper.make_answerpaper(user, ip, attempt_number,
course_id)
if new_paper.status == 'inprogress':
- return show_question(request, new_paper.current_question(),
- new_paper, course_id=course_id,
- module_id=module_id, previous_question=None
- )
+ return show_question(
+ request, new_paper.current_question(),
+ new_paper, course_id=course_id,
+ module_id=module_id, previous_question=None
+ )
else:
msg = 'You have already finished the quiz!'
raise Http404(msg)
@@ -618,7 +604,7 @@ def show_question(request, question, paper, error_message=None, notification=Non
if question.type == "code" else
'You have already attempted this question'
)
- if question.type in ['mcc', 'mcq']:
+ if question.type in ['mcc', 'mcq', 'arrange']:
test_cases = question.get_ordered_test_cases(paper)
else:
test_cases = question.get_test_cases()
@@ -647,9 +633,7 @@ def show_question(request, question, paper, error_message=None, notification=Non
last_attempt = answers[0].answer
if last_attempt:
context['last_attempt'] = last_attempt.encode('unicode-escape')
- ci = RequestContext(request)
- return my_render_to_response('yaksh/question.html', context,
- context_instance=ci)
+ return my_render_to_response(request, 'yaksh/question.html', context)
@login_required
@@ -728,6 +712,9 @@ def check(request, q_id, attempt_num=None, questionpaper_id=None,
elif current_question.type == 'mcc':
user_answer = request.POST.getlist('answer')
+ elif current_question.type == 'arrange':
+ user_answer_ids = request.POST.get('answer').split(',')
+ user_answer = [int(ids) for ids in user_answer_ids]
elif current_question.type == 'upload':
# if time-up at upload question then the form is submitted without
# validation
@@ -900,8 +887,7 @@ def quit(request, reason=None, attempt_num=None, questionpaper_id=None,
course_id=course_id)
context = {'paper': paper, 'message': reason, 'course_id': course_id,
'module_id': module_id}
- return my_render_to_response('yaksh/quit.html', context,
- context_instance=RequestContext(request))
+ return my_render_to_response(request, 'yaksh/quit.html', context)
@login_required
@@ -914,7 +900,7 @@ def complete(request, reason=None, attempt_num=None, questionpaper_id=None,
message = reason or "An Unexpected Error occurred. Please contact your '\
'instructor/administrator.'"
context = {'message': message}
- return my_render_to_response('yaksh/complete.html', context)
+ return my_render_to_response(request, 'yaksh/complete.html', context)
else:
q_paper = QuestionPaper.objects.get(id=questionpaper_id)
paper = AnswerPaper.objects.get(
@@ -934,14 +920,13 @@ def complete(request, reason=None, attempt_num=None, questionpaper_id=None,
'course_id': course_id, 'learning_unit': learning_unit}
if is_moderator(user):
context['user'] = "moderator"
- return my_render_to_response('yaksh/complete.html', context)
+ return my_render_to_response(request, 'yaksh/complete.html', context)
@login_required
@email_verified
def add_course(request, course_id=None):
user = request.user
- ci = RequestContext(request)
if course_id:
course = Course.objects.get(id=course_id)
if not course.is_creator(user) and not course.is_teacher(user):
@@ -960,12 +945,12 @@ def add_course(request, course_id=None):
return my_redirect('/exam/manage/courses')
else:
return my_render_to_response(
- 'yaksh/add_course.html', {'form': form}, context_instance=ci
+ request, 'yaksh/add_course.html', {'form': form}
)
else:
form = CourseForm(instance=course)
return my_render_to_response(
- 'yaksh/add_course.html', {'form': form}, context_instance=ci
+ request, 'yaksh/add_course.html', {'form': form}
)
@@ -973,7 +958,6 @@ def add_course(request, course_id=None):
@email_verified
def enroll_request(request, course_id):
user = request.user
- ci = RequestContext(request)
course = get_object_or_404(Course, pk=course_id)
if not course.is_active_enrollment() and course.hidden:
msg = (
@@ -993,7 +977,6 @@ def enroll_request(request, course_id):
@email_verified
def self_enroll(request, course_id):
user = request.user
- ci = RequestContext(request)
course = get_object_or_404(Course, pk=course_id)
if course.is_self_enroll():
was_rejected = False
@@ -1008,7 +991,6 @@ def self_enroll(request, course_id):
@email_verified
def courses(request):
user = request.user
- ci = RequestContext(request)
if not is_moderator(user):
raise Http404('You are not allowed to view this page')
courses = Course.objects.filter(
@@ -1017,15 +999,13 @@ def courses(request):
teachers=user, is_trial=False).order_by('-active', '-id')
context = {'courses': courses, "allotted_courses": allotted_courses,
"type": "courses"}
- return my_render_to_response('yaksh/courses.html', context,
- context_instance=ci)
+ return my_render_to_response(request, 'yaksh/courses.html', context)
@login_required
@email_verified
def course_detail(request, course_id):
user = request.user
- ci = RequestContext(request)
if not is_moderator(user):
raise Http404('You are not allowed to view this page')
@@ -1035,7 +1015,7 @@ def course_detail(request, course_id):
raise Http404('This course does not belong to you')
return my_render_to_response(
- 'yaksh/course_detail.html', {'course': course}, context_instance=ci
+ request, 'yaksh/course_detail.html', {'course': course}
)
@@ -1043,7 +1023,6 @@ def course_detail(request, course_id):
@email_verified
def enroll(request, course_id, user_id=None, was_rejected=False):
user = request.user
- ci = RequestContext(request)
if not is_moderator(user):
raise Http404('You are not allowed to view this page')
@@ -1065,7 +1044,7 @@ def enroll(request, course_id, user_id=None, was_rejected=False):
enroll_ids = [user_id]
if not enroll_ids:
return my_render_to_response(
- 'yaksh/course_detail.html', {'course': course}, context_instance=ci
+ request, 'yaksh/course_detail.html', {'course': course}
)
users = User.objects.filter(id__in=enroll_ids)
course.enroll(was_rejected, *users)
@@ -1076,7 +1055,6 @@ def enroll(request, course_id, user_id=None, was_rejected=False):
@email_verified
def send_mail(request, course_id, user_id=None):
user = request.user
- ci = RequestContext(request)
if not is_moderator(user):
raise Http404('You are not allowed to view this page')
@@ -1100,16 +1078,13 @@ def send_mail(request, course_id, user_id=None):
'course': course, 'message': message,
'state': 'mail'
}
- return my_render_to_response(
- 'yaksh/course_detail.html', context, context_instance=ci
- )
+ return my_render_to_response(request, 'yaksh/course_detail.html', context)
@login_required
@email_verified
def reject(request, course_id, user_id=None, was_enrolled=False):
user = request.user
- ci = RequestContext(request)
if not is_moderator(user):
raise Http404('You are not allowed to view this page')
@@ -1124,8 +1099,8 @@ def reject(request, course_id, user_id=None, was_enrolled=False):
if not reject_ids:
message = "Please select atleast one User"
return my_render_to_response(
- 'yaksh/course_detail.html', {'course': course, 'message': message},
- context_instance=ci
+ request, 'yaksh/course_detail.html',
+ {'course': course, 'message': message},
)
users = User.objects.filter(id__in=reject_ids)
course.reject(was_enrolled, *users)
@@ -1165,8 +1140,9 @@ def show_statistics(request, questionpaper_id, attempt_number=None,
context = {'quiz': quiz, 'attempts': attempt_numbers,
'questionpaper_id': questionpaper_id,
'course_id': course_id}
- return my_render_to_response('yaksh/statistics_question.html', context,
- context_instance=RequestContext(request))
+ return my_render_to_response(
+ request, 'yaksh/statistics_question.html', context
+ )
total_attempt = AnswerPaper.objects.get_count(questionpaper_id,
attempt_number,
course_id)
@@ -1180,8 +1156,9 @@ def show_statistics(request, questionpaper_id, attempt_number=None,
'questionpaper_id': questionpaper_id,
'attempts': attempt_numbers, 'total': total_attempt,
'course_id': course_id}
- return my_render_to_response('yaksh/statistics_question.html', context,
- context_instance=RequestContext(request))
+ return my_render_to_response(
+ request, 'yaksh/statistics_question.html', context
+ )
@login_required
@@ -1190,7 +1167,6 @@ def monitor(request, quiz_id=None, course_id=None):
"""Monitor the progress of the papers taken so far."""
user = request.user
- ci = RequestContext(request)
if not user.is_authenticated() or not is_moderator(user):
raise Http404('You are not allowed to view this page!')
@@ -1203,9 +1179,7 @@ def monitor(request, quiz_id=None, course_id=None):
"papers": [], "course_details": course_details,
"msg": "Monitor"
}
- return my_render_to_response(
- 'yaksh/monitor.html', context, context_instance=ci
- )
+ return my_render_to_response(request, 'yaksh/monitor.html', context)
# quiz_id is not None.
try:
quiz = get_object_or_404(Quiz, id=quiz_id)
@@ -1251,8 +1225,7 @@ def monitor(request, quiz_id=None, course_id=None):
"attempt_numbers": attempt_numbers,
"course": course
}
- return my_render_to_response('yaksh/monitor.html', context,
- context_instance=ci)
+ return my_render_to_response(request, 'yaksh/monitor.html', context)
@csrf_exempt
@@ -1276,7 +1249,7 @@ def ajax_questions_filter(request):
questions = list(Question.objects.filter(**filter_dict))
return my_render_to_response(
- 'yaksh/ajax_question_filter.html', {'questions': questions}
+ request, 'yaksh/ajax_question_filter.html', {'questions': questions}
)
@@ -1405,9 +1378,7 @@ def design_questionpaper(request, quiz_id, questionpaper_id=None,
'course_id': course_id
}
return my_render_to_response(
- 'yaksh/design_questionpaper.html',
- context,
- context_instance=RequestContext(request)
+ request, 'yaksh/design_questionpaper.html', context
)
@@ -1417,7 +1388,6 @@ def show_all_questions(request):
"""Show a list of all the questions currently in the database."""
user = request.user
- ci = RequestContext(request)
context = {}
if not is_moderator(user):
raise Http404("You are not allowed to view this page !")
@@ -1494,8 +1464,7 @@ def show_all_questions(request):
user=user).distinct()
context['questions'] = search_result
- return my_render_to_response('yaksh/showquestions.html', context,
- context_instance=ci)
+ return my_render_to_response(request, 'yaksh/showquestions.html', context)
@login_required
@@ -1509,8 +1478,7 @@ def user_data(request, user_id, questionpaper_id=None, course_id=None):
data = AnswerPaper.objects.get_user_data(user, questionpaper_id, course_id)
context = {'data': data, 'course_id': course_id}
- return my_render_to_response('yaksh/user_data.html', context,
- context_instance=RequestContext(request))
+ return my_render_to_response(request, 'yaksh/user_data.html', context)
def _expand_questions(questions, field_list):
@@ -1599,7 +1567,6 @@ def grade_user(request, quiz_id=None, user_id=None, attempt_number=None,
and update all their marks and also give comments for each paper.
"""
current_user = request.user
- ci = RequestContext(request)
if not current_user.is_authenticated() or not is_moderator(current_user):
raise Http404('You are not allowed to view this page!')
course_details = Course.objects.filter(Q(creator=current_user) |
@@ -1669,9 +1636,11 @@ def grade_user(request, quiz_id=None, user_id=None, attempt_number=None,
'comments_%d' % paper.question_paper.id, 'No comments')
paper.save()
- return my_render_to_response(
- 'yaksh/grade_user.html', context, context_instance=ci
- )
+ course_status = CourseStatus.objects.filter(course=course, user=user)
+ if course_status.exists():
+ course_status.first().set_grade()
+
+ return my_render_to_response(request, 'yaksh/grade_user.html', context)
@login_required
@@ -1680,13 +1649,12 @@ def grade_user(request, quiz_id=None, user_id=None, attempt_number=None,
def view_profile(request):
""" view moderators and users profile """
user = request.user
- ci = RequestContext(request)
if is_moderator(user):
template = 'manage.html'
else:
template = 'user.html'
context = {'template': template, 'user': user}
- return my_render_to_response('yaksh/view_profile.html', context)
+ return my_render_to_response(request, 'yaksh/view_profile.html', context)
@login_required
@@ -1695,7 +1663,6 @@ def edit_profile(request):
""" edit profile details facility for moderator and students """
user = request.user
- ci = RequestContext(request)
if is_moderator(user):
template = 'manage.html'
else:
@@ -1715,19 +1682,17 @@ def edit_profile(request):
form_data.user.last_name = request.POST['last_name']
form_data.user.save()
form_data.save()
- return my_render_to_response(
- 'yaksh/profile_updated.html', context_instance=ci
- )
+ return my_render_to_response(request, 'yaksh/profile_updated.html')
else:
context['form'] = form
return my_render_to_response(
- 'yaksh/editprofile.html', context, context_instance=ci
+ request, 'yaksh/editprofile.html', context
)
else:
form = ProfileForm(user=user, instance=profile)
context['form'] = form
return my_render_to_response(
- 'yaksh/editprofile.html', context, context_instance=ci
+ request, 'yaksh/editprofile.html', context
)
@@ -1736,7 +1701,6 @@ def edit_profile(request):
def search_teacher(request, course_id):
""" search teachers for the course """
user = request.user
- ci = RequestContext(request)
if not is_moderator(user):
raise Http404('You are not allowed to view this page!')
@@ -1763,9 +1727,7 @@ def search_teacher(request, course_id):
)
context['success'] = True
context['teachers'] = teachers
- return my_render_to_response(
- 'yaksh/addteacher.html', context, context_instance=ci
- )
+ return my_render_to_response(request, 'yaksh/addteacher.html', context)
@login_required
@@ -1774,7 +1736,6 @@ def add_teacher(request, course_id):
""" add teachers to the course """
user = request.user
- ci = RequestContext(request)
if not is_moderator(user):
raise Http404('You are not allowed to view this page!')
@@ -1793,9 +1754,7 @@ def add_teacher(request, course_id):
course.add_teachers(*teachers)
context['status'] = True
context['teachers_added'] = teachers
- return my_render_to_response(
- 'yaksh/addteacher.html', context, context_instance=ci
- )
+ return my_render_to_response(request, 'yaksh/addteacher.html', context)
@login_required
@@ -1875,7 +1834,9 @@ def view_answerpaper(request, questionpaper_id, course_id):
).exists()
context = {'data': data, 'quiz': quiz,
"has_user_assignment": has_user_assignment}
- return my_render_to_response('yaksh/view_answerpaper.html', context)
+ return my_render_to_response(
+ request, 'yaksh/view_answerpaper.html', context
+ )
else:
return my_redirect('/exam/quizzes/')
@@ -1885,7 +1846,6 @@ def view_answerpaper(request, questionpaper_id, course_id):
def create_demo_course(request):
""" creates a demo course for user """
user = request.user
- ci = RequestContext(request)
if not is_moderator(user):
raise Http404("You are not allowed to view this page")
demo_course = Course()
@@ -1908,7 +1868,7 @@ def grader(request, extra_context=None):
context = {'courses': user_courses}
if extra_context:
context.update(extra_context)
- return my_render_to_response('yaksh/regrade.html', context)
+ return my_render_to_response(request, 'yaksh/regrade.html', context)
@login_required
@@ -1924,14 +1884,27 @@ def regrade(request, course_id, question_id=None, answerpaper_id=None,
answerpaper = get_object_or_404(AnswerPaper, pk=answerpaper_id)
for question in answerpaper.questions.all():
details.append(answerpaper.regrade(question.id))
+ course_status = CourseStatus.objects.filter(user=answerpaper.user,
+ course=answerpaper.course)
+ if course_status.exists():
+ course_status.first().set_grade()
if questionpaper_id is not None and question_id is not None:
answerpapers = AnswerPaper.objects.filter(questions=question_id,
question_paper_id=questionpaper_id, course_id=course_id)
for answerpaper in answerpapers:
details.append(answerpaper.regrade(question_id))
+ course_status = CourseStatus.objects.filter(user=answerpaper.user,
+ course=answerpaper.course)
+ if course_status.exists():
+ course_status.first().set_grade()
if answerpaper_id is not None and question_id is not None:
answerpaper = get_object_or_404(AnswerPaper, pk=answerpaper_id)
details.append(answerpaper.regrade(question_id))
+ course_status = CourseStatus.objects.filter(user=answerpaper.user,
+ course=answerpaper.course)
+ if course_status.exists():
+ course_status.first().set_grade()
+
return grader(request, extra_context={'details': details})
@@ -1979,14 +1952,13 @@ def download_course_csv(request, course_id):
def activate_user(request, key):
- ci = RequestContext(request)
profile = get_object_or_404(Profile, activation_key=key)
context = {}
context['success'] = False
if profile.is_email_verified:
context['activation_msg'] = "Your account is already verified"
return my_render_to_response(
- 'yaksh/activation_status.html', context, context_instance=ci
+ request, 'yaksh/activation_status.html', context
)
if timezone.now() > profile.key_expiry_time:
@@ -2000,12 +1972,11 @@ def activate_user(request, key):
profile.save()
context['msg'] = "Your account is activated"
return my_render_to_response(
- 'yaksh/activation_status.html', context, context_instance=ci
+ request, 'yaksh/activation_status.html', context
)
def new_activation(request, email=None):
- ci = RequestContext(request)
context = {}
if request.method == "POST":
email = request.POST.get('email')
@@ -2016,15 +1987,13 @@ def new_activation(request, email=None):
context['email_err_msg'] = "Multiple entries found for this email"\
"Please change your email"
return my_render_to_response(
- 'yaksh/activation_status.html', context, context_instance=ci
+ request, 'yaksh/activation_status.html', context
)
except ObjectDoesNotExist:
context['success'] = False
context['msg'] = "Your account is not verified. \
Please verify your account"
- return render_to_response(
- 'yaksh/activation_status.html', context, context_instance=ci
- )
+ return render_to_response('yaksh/activation_status.html', context)
if not user.profile.is_email_verified:
user.profile.activation_key = generate_activation_key(user.username)
@@ -2043,13 +2012,12 @@ def new_activation(request, email=None):
context['activation_msg'] = "Your account is already verified"
return my_render_to_response(
- 'yaksh/activation_status.html', context, context_instance=ci
+ request, 'yaksh/activation_status.html', context
)
def update_email(request):
context = {}
- ci = RequestContext(request)
if request.method == "POST":
email = request.POST.get('email')
username = request.POST.get('username')
@@ -2060,7 +2028,7 @@ def update_email(request):
else:
context['email_err_msg'] = "Please Update your email"
return my_render_to_response(
- 'yaksh/activation_status.html', context, context_instance=ci
+ request, 'yaksh/activation_status.html', context
)
@@ -2099,7 +2067,6 @@ def download_assignment_file(request, quiz_id, question_id=None, user_id=None):
@email_verified
def upload_users(request, course_id):
user = request.user
- ci = RequestContext(request)
course = get_object_or_404(Course, pk=course_id)
context = {'course': course}
@@ -2109,32 +2076,35 @@ def upload_users(request, course_id):
if request.method == 'POST':
if 'csv_file' not in request.FILES:
context['message'] = "Please upload a CSV file."
- return my_render_to_response('yaksh/course_detail.html', context,
- context_instance=ci)
+ return my_render_to_response(
+ request, 'yaksh/course_detail.html', context
+ )
csv_file = request.FILES['csv_file']
is_csv_file, dialect = is_csv(csv_file)
if not is_csv_file:
context['message'] = "The file uploaded is not a CSV file."
- return my_render_to_response('yaksh/course_detail.html', context,
- context_instance=ci)
+ return my_render_to_response(
+ request, 'yaksh/course_detail.html', context
+ )
required_fields = ['firstname', 'lastname', 'email']
try:
reader = csv.DictReader(csv_file.read().decode('utf-8').splitlines(),
dialect=dialect)
except TypeError:
context['message'] = "Bad CSV file"
- return my_render_to_response('yaksh/course_detail.html', context,
- context_instance=ci)
+ return my_render_to_response(
+ request, 'yaksh/course_detail.html', context
+ )
stripped_fieldnames = [field.strip().lower() for field in reader.fieldnames]
for field in required_fields:
if field not in stripped_fieldnames:
context['message'] = "The CSV file does not contain the required headers"
- return my_render_to_response('yaksh/course_detail.html', context,
- context_instance=ci)
+ return my_render_to_response(
+ request, 'yaksh/course_detail.html', context
+ )
reader.fieldnames = stripped_fieldnames
context['upload_details'] = _read_user_csv(reader, course)
- return my_render_to_response('yaksh/course_detail.html', context,
- context_instance=ci)
+ return my_render_to_response(request, 'yaksh/course_detail.html', context)
def _read_user_csv(reader, course):
@@ -2292,7 +2262,6 @@ def download_yaml_template(request):
@email_verified
def edit_lesson(request, lesson_id=None, course_id=None):
user = request.user
- ci = RequestContext(request)
if not is_moderator(user):
raise Http404('You are not allowed to view this page!')
if lesson_id:
@@ -2345,9 +2314,7 @@ def edit_lesson(request, lesson_id=None, course_id=None):
context['lesson_file_form'] = lesson_files_form
context['lesson_files'] = lesson_files
context['course_id'] = course_id
- return my_render_to_response(
- 'yaksh/add_lesson.html', context, context_instance=ci
- )
+ return my_render_to_response(request, 'yaksh/add_lesson.html', context)
@login_required
@@ -2388,14 +2355,13 @@ def show_lesson(request, lesson_id, module_id, course_id):
'course': course, 'state': "lesson", "all_modules": all_modules,
'learning_units': learning_units, "current_unit": learn_unit,
'learning_module': learn_module}
- return my_render_to_response('yaksh/show_video.html', context)
+ return my_render_to_response(request, 'yaksh/show_video.html', context)
@login_required
@email_verified
def design_module(request, module_id, course_id=None):
user = request.user
- ci = RequestContext(request)
if not is_moderator(user):
raise Http404('You are not allowed to view this page!')
context = {}
@@ -2461,15 +2427,13 @@ def design_module(request, module_id, course_id=None):
context['status'] = 'design'
context['module_id'] = module_id
context['course_id'] = course_id
- return my_render_to_response('yaksh/add_module.html', context,
- context_instance=ci)
+ return my_render_to_response(request, 'yaksh/add_module.html', context)
@login_required
@email_verified
def add_module(request, module_id=None, course_id=None):
user = request.user
- ci = RequestContext(request)
if not is_moderator(user):
raise Http404('You are not allowed to view this page!')
redirect_url = "/exam/manage/courses/all_learning_module/"
@@ -2502,8 +2466,7 @@ def add_module(request, module_id=None, course_id=None):
context['module_form'] = module_form
context['course_id'] = course_id
context['status'] = "add"
- return my_render_to_response("yaksh/add_module.html",
- context, context_instance=ci)
+ return my_render_to_response(request, "yaksh/add_module.html", context)
@login_required
@@ -2514,7 +2477,7 @@ def show_all_quizzes(request):
raise Http404('You are not allowed to view this page!')
quizzes = Quiz.objects.filter(creator=user, is_trial=False)
context = {"quizzes": quizzes, "type": "quiz"}
- return my_render_to_response('yaksh/courses.html', context)
+ return my_render_to_response(request, 'yaksh/courses.html', context)
@login_required
@@ -2525,7 +2488,7 @@ def show_all_lessons(request):
raise Http404('You are not allowed to view this page!')
lessons = Lesson.objects.filter(creator=user)
context = {"lessons": lessons, "type": "lesson"}
- return my_render_to_response('yaksh/courses.html', context)
+ return my_render_to_response(request, 'yaksh/courses.html', context)
@login_required
@@ -2537,7 +2500,7 @@ def show_all_modules(request):
learning_modules = LearningModule.objects.filter(
creator=user, is_trial=False)
context = {"learning_modules": learning_modules, "type": "learning_module"}
- return my_render_to_response('yaksh/courses.html', context)
+ return my_render_to_response(request, 'yaksh/courses.html', context)
@login_required
@@ -2612,7 +2575,6 @@ def get_next_unit(request, course_id, module_id, current_unit_id=None,
@email_verified
def design_course(request, course_id):
user = request.user
- ci = RequestContext(request)
if not is_moderator(user):
raise Http404('You are not allowed to view this page!')
course = Course.objects.get(id=course_id)
@@ -2666,8 +2628,9 @@ def design_course(request, course_id):
context['added_learning_modules'] = added_learning_modules
context['learning_modules'] = learning_modules
context['course_id'] = course_id
- return my_render_to_response('yaksh/design_course_session.html', context,
- context_instance=ci)
+ return my_render_to_response(
+ request, 'yaksh/design_course_session.html', context
+ )
@login_required
@@ -2702,7 +2665,7 @@ def view_module(request, module_id, course_id, msg=None):
context['course'] = course
context['state'] = "module"
context['msg'] = msg
- return my_render_to_response('yaksh/show_video.html', context)
+ return my_render_to_response(request, 'yaksh/show_video.html', context)
@login_required
@@ -2720,7 +2683,13 @@ def course_modules(request, course_id, msg=None):
learning_modules = course.get_learning_modules()
context = {"course": course, "learning_modules": learning_modules,
"user": user, "msg": msg}
- return my_render_to_response('yaksh/course_modules.html', context)
+ course_status = CourseStatus.objects.filter(course=course, user=user)
+ if course_status.exists():
+ course_status = course_status.first()
+ if not course_status.grade:
+ course_status.set_grade()
+ context['grade'] = course_status.get_grade()
+ return my_render_to_response(request, 'yaksh/course_modules.html', context)
@login_required
@@ -2737,7 +2706,7 @@ def course_status(request, course_id):
'course': course, 'students': students,
'state': 'course_status', 'modules': course.get_learning_modules()
}
- return my_render_to_response('yaksh/course_detail.html', context)
+ return my_render_to_response(request, 'yaksh/course_detail.html', context)
def _update_unit_status(course_id, user, unit):
@@ -2771,5 +2740,5 @@ def preview_questionpaper(request, questionpaper_id):
}
return my_render_to_response(
- 'yaksh/preview_questionpaper.html', context
+ request, 'yaksh/preview_questionpaper.html', context
)