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.py1
-rw-r--r--setup.py2
-rw-r--r--yaksh/evaluator_tests/test_simple_question_types.py342
-rw-r--r--yaksh/forms.py3
-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.py101
-rw-r--r--yaksh/static/yaksh/js/jquery-sortable.js693
-rw-r--r--yaksh/static/yaksh/js/requesthandler.js11
-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/grade_user.html19
-rw-r--r--yaksh/templates/yaksh/question.html29
-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.py116
-rw-r--r--yaksh/views.py28
44 files changed, 2235 insertions, 107 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..49276ba9
--- /dev/null
+++ b/grades/urls.py
@@ -0,0 +1,10 @@
+from django.conf.urls import url, patterns
+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..3e62fd6 100644
--- a/online_test/urls.py
+++ b/online_test/urls.py
@@ -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/setup.py b/setup.py
index 9fe5be5..83a5e42 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 = {}
@@ -24,6 +25,7 @@ install_requires = [
'invoke==0.21.0',
'six',
'requests',
+ 'markdown==2.6.9',
]
setup(
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/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 d9eea57..d011bb0 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 = (
@@ -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)
@@ -602,6 +606,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):
@@ -772,6 +778,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:
@@ -793,7 +807,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
###############################################################################
@@ -1276,8 +1327,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]
@@ -1849,6 +1900,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)
@@ -1870,13 +1930,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) \
@@ -2069,6 +2137,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
@@ -2083,3 +2163,6 @@ class TestCaseOrder(models.Model):
#Order of the test case for a question.
order = models.TextField()
+
+
+##############################################################################
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..a215ce4 100644
--- a/yaksh/static/yaksh/js/requesthandler.js
+++ b/yaksh/static/yaksh/js/requesthandler.js
@@ -177,6 +177,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/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..b65073a 100644
--- a/yaksh/templates/yaksh/question.html
+++ b/yaksh/templates/yaksh/question.html
@@ -21,6 +21,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 +176,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>
@@ -251,6 +254,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 +293,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 49bba00..41730c3 100644
--- a/yaksh/test_models.py
+++ b/yaksh/test_models.py
@@ -1372,12 +1372,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]))
@@ -1830,3 +1838,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/views.py b/yaksh/views.py
index c22500d..1cb77fc 100644
--- a/yaksh/views.py
+++ b/yaksh/views.py
@@ -618,7 +618,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()
@@ -728,6 +728,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
@@ -1669,6 +1672,10 @@ def grade_user(request, quiz_id=None, user_id=None, attempt_number=None,
'comments_%d' % paper.question_paper.id, 'No comments')
paper.save()
+ course_status = CourseStatus.objects.filter(course=course, user=user)
+ if course_status.exists():
+ course_status.first().set_grade()
+
return my_render_to_response(
'yaksh/grade_user.html', context, context_instance=ci
)
@@ -1924,14 +1931,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})
@@ -2720,6 +2740,12 @@ 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}
+ 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('yaksh/course_modules.html', context)