diff options
132 files changed, 6637 insertions, 3112 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 e259117..d603d34 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,3 +1,46 @@ +=== 0.8.1 (18-06-2018) === + +* Fixed a bug where quiz completion shows inprogress even after completing. +* Changed send bulk mail to add recipients to BCC list instead of TO list. +* Changed course status feature to view student progress more easily and quickly. +* Changed student course dashboard to view completion percentage per module and + overall course. +* Added support for nose asserts for python assertion based evaluation. +* Added a new feature to show error line number in student code for python + based questions only. +* Replaced django render_to_response function with render function in views. +* Upgraded Django version from 1.9.5 to 1.10. +* Fixed pep8 for the code base. +* Fixed a bug that allows answer to be true if there are no test cases to a code + question. + +=== 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. + + === 0.7.0 (15-11-2017) === * Simplified Standard I/O type question error output. @@ -18,8 +61,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. @@ -42,7 +85,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. @@ -65,4 +108,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..a508eb3 --- /dev/null +++ b/grades/tests/test_models.py @@ -0,0 +1,28 @@ +from django.test import TestCase +from grades.models import GradingSystem + + +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..6b76565 --- /dev/null +++ b/grades/tests/test_views.py @@ -0,0 +1,104 @@ +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.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 + 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 + self.client.post(reverse('grades:edit_grade', + kwargs={'system_id': 2}), data) + # Then + ranges = grading_system.graderange_set.all() + self.assertEqual(len(ranges), 1) diff --git a/grades/urls.py b/grades/urls.py new file mode 100644 index 0000000..32a7e4d --- /dev/null +++ b/grades/urls.py @@ -0,0 +1,10 @@ +from django.conf.urls import url +from grades import views + +urlpatterns = [ + url(r'^$', views.grading_systems, name="grading_systems_home"), + url(r'^grading_systems/$', views.grading_systems, name="grading_systems"), + url(r'^add_grade/$', views.add_grading_system, name="add_grade"), + url(r'^add_grade/(?P<system_id>\d+)/$', views.add_grading_system, + name="edit_grade"), +] diff --git a/grades/views.py b/grades/views.py new file mode 100644 index 0000000..67844bd --- /dev/null +++ b/grades/views.py @@ -0,0 +1,49 @@ +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..ef72cc0 100644 --- a/online_test/__init__.py +++ b/online_test/__init__.py @@ -1 +1 @@ -__version__ = '0.7.0' +__version__ = '0.8.1' diff --git a/online_test/settings.py b/online_test/settings.py index c55a056..b9a7a2c 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 = ( @@ -70,11 +71,14 @@ DATABASES = { 'ENGINE': 'django.db.backends.{0}'.format( config('DB_ENGINE', default='sqlite3') ), - 'NAME': config('DB_NAME', default=os.path.join(BASE_DIR, 'db.sqlite3')), + 'NAME': config('DB_NAME', + default=os.path.join(BASE_DIR, 'db.sqlite3') + ), # The following settings are not used with sqlite3: 'USER': config('DB_USER', default=''), 'PASSWORD': config('DB_PASSWORD', default=''), - 'HOST': config('DB_HOST', default='localhost'), # Empty for localhost through domain sockets or '1$ + # Empty for localhost through domain sockets or '1$ + 'HOST': config('DB_HOST', default='localhost'), 'PORT': config('DB_PORT', default=''), }, } @@ -107,7 +111,7 @@ MEDIA_URL = "/data/" MEDIA_ROOT = os.path.join(BASE_DIR, "yaksh_data", "data") -STATIC_ROOT='yaksh/static/' +STATIC_ROOT = 'yaksh/static/' # Set this varable to <True> if smtp-server is not allowing to send email. EMAIL_USE_TLS = False diff --git a/online_test/urls.py b/online_test/urls.py index ce0de41..28c2a26 100644 --- a/online_test/urls.py +++ b/online_test/urls.py @@ -1,4 +1,4 @@ -from django.conf.urls import patterns, include, url +from django.conf.urls import include, url from django.conf import settings from django.conf.urls.static import static from django.contrib import admin @@ -13,5 +13,7 @@ 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/online_test/wsgi.py b/online_test/wsgi.py index 2dd7ae3..21618b7 100644 --- a/online_test/wsgi.py +++ b/online_test/wsgi.py @@ -8,7 +8,7 @@ https://docs.djangoproject.com/en/1.6/howto/deployment/wsgi/ """ import os +from django.core.wsgi import get_wsgi_application os.environ.setdefault("DJANGO_SETTINGS_MODULE", "online_test.settings") -from django.core.wsgi import get_wsgi_application application = get_wsgi_application() diff --git a/requirements/requirements-codeserver.txt b/requirements/requirements-codeserver.txt index e9585fa..11bc0a2 100644 --- a/requirements/requirements-codeserver.txt +++ b/requirements/requirements-codeserver.txt @@ -2,5 +2,6 @@ pytest python-decouple six requests -tornado +tornado==4.5.3 psutil +nose==1.3.7 diff --git a/requirements/requirements-common.txt b/requirements/requirements-common.txt index b170694..484111e 100644 --- a/requirements/requirements-common.txt +++ b/requirements/requirements-common.txt @@ -1,6 +1,6 @@ -r requirements-codeserver.txt invoke==0.21.0 -django==1.9.5 +django==1.10 django-taggit==0.18.1 pytz==2016.4 python-social-auth==0.2.19 @@ -1,2 +1,2 @@ [metadata] -description-file = README.md +description-file = README.rst @@ -6,6 +6,7 @@ README = open(os.path.join(os.path.dirname(__file__), 'README.rst')).read() # allow setup.py to be run from any path os.chdir(os.path.normpath(os.path.join(os.path.abspath(__file__), os.pardir))) + def get_version(): import os data = {} @@ -14,7 +15,7 @@ def get_version(): return data.get('__version__') install_requires = [ - 'django==1.9.5', + 'django==1.10', 'django-taggit==0.18.1', 'pytz==2016.4', 'python-social-auth==0.2.19', @@ -24,6 +25,7 @@ install_requires = [ 'invoke==0.21.0', 'six', 'requests', + 'markdown==2.6.9', ] setup( @@ -22,26 +22,32 @@ def create_dir(path): if not os.path.exists(path): os.makedirs(path) + def remove_check_file(path): if os.path.isfile(path): os.remove(path) + def remove_dir(path): if os.path.isdir(path): shutil.rmtree(path) + def run_as(os_name): - if os_name.startswith('linux') or os_name == 'darwin' or os_name.startswith('freebsd'): + if (os_name.startswith('linux') or os_name == 'darwin' or + os_name.startswith('freebsd')): return 'sudo' - else: # For os_name = 'Win32' + else: # For os_name = 'Win32' return None + def get_cmd(run_as_cmd, base_cmd): if run_as_cmd: return '{0} {1}'.format(run_as_cmd, base_cmd) else: return base_cmd + @task def setupdb(ctx): print("** Setting up & migrating database **") @@ -49,22 +55,26 @@ def setupdb(ctx): ctx.run("python manage.py migrate") print("** Done! Migrations complete **") + @task def loadfixtures(ctx): print("** Loading fixtures into database **") ctx.run("python manage.py loaddata demo_fixtures.json") print("** Done! Loaded fixtures into database **") + @task(setupdb, loadfixtures) def serve(ctx): print("** Running the Django web server. Press Ctrl-C to Exit **") ctx.run("python manage.py runserver") + @task def clean(ctx): print("** Discarding database **") remove_check_file(os.path.join(SCRIPT_DIR, 'db.sqlite3')) + @task def getimage(ctx, image=SRC_IMAGE_NAME): try: @@ -74,16 +84,21 @@ def getimage(ctx, image=SRC_IMAGE_NAME): ctx.run(cmd, hide=True) except invoke.exceptions.Failure: print("The docker image {0} does not exist locally".format(image)) - print("\n** Pulling latest image <{0}> from docker hub **".format(image)) + print("\n** Pulling latest image <{0}> from docker hub **".format( + image) + ) base_cmd = "docker pull {0}".format(image) run_as_cmd = run_as(OS_NAME) cmd = get_cmd(run_as_cmd, base_cmd) ctx.run(cmd) - print("\n** Done! Successfully pulled latest image <{0}> **".format(image)) + print("\n** Done! Successfully pulled latest image <{0}> **".format( + image) + ) + @task def start(ctx, ports=SERVER_POOL_PORT, image=SRC_IMAGE_NAME, unsafe=False, - version=3): + version=3): if unsafe: with ctx.cd(SCRIPT_DIR): print("** Initializing local code server **") @@ -94,15 +109,16 @@ def start(ctx, ports=SERVER_POOL_PORT, image=SRC_IMAGE_NAME, unsafe=False, cmd = get_cmd(run_as_cmd, base_cmd) ctx.run(cmd) else: - cmd_params = {'ports': ports, - 'image': SRC_IMAGE_NAME, + cmd_params = { + 'ports': ports, 'image': SRC_IMAGE_NAME, 'name': TARGET_CONTAINER_NAME, 'vol_mount': os.path.join(SCRIPT_DIR, 'yaksh_data'), 'command': 'sh {0}'.format( - os.path.join(SCRIPT_DIR, - 'yaksh_data', 'yaksh', 'scripts', 'yaksh_script.sh') - ) - } + os.path.join( + SCRIPT_DIR, + 'yaksh_data', 'yaksh', 'scripts', 'yaksh_script.sh') + ) + } remove_check_file(CHECK_FILE_PATH) getimage(ctx, image=SRC_IMAGE_NAME) @@ -117,7 +133,8 @@ def start(ctx, ports=SERVER_POOL_PORT, image=SRC_IMAGE_NAME, unsafe=False, ) copy_file( - os.path.join(SCRIPT_DIR, 'requirements', 'requirements-codeserver.txt'), + os.path.join(SCRIPT_DIR, 'requirements', + 'requirements-codeserver.txt'), os.path.join(SCRIPT_DIR, 'yaksh_data') ) @@ -132,7 +149,8 @@ def start(ctx, ports=SERVER_POOL_PORT, image=SRC_IMAGE_NAME, unsafe=False, ctx.run(cmd) while not os.path.isfile(CHECK_FILE_PATH): - print("** Checking code server status. Press Ctrl-C to exit **\r", end="") + print("** Checking code server status. Press Ctrl-C to exit **\r", + end="") print("\n** Code server is up and running successfully **") @@ -145,20 +163,23 @@ def stop(ctx, container=TARGET_CONTAINER_NAME, hide=True): remove_check_file(CHECK_FILE_PATH) if result.stdout: - print ("** Stopping the docker container <{0}> **".format(container)) + print("** Stopping the docker container <{0}> **".format(container)) base_stop_cmd = "docker stop {0}".format(container) cmd = get_cmd(run_as_cmd, base_stop_cmd) ctx.run(cmd) - print ("** Done! Stopped the docker container <{0}> **".format(container)) + print("** Done! Stopped the docker container <{0}> **".format( + container)) - print ("** Discarding the docker container <{0}> **".format(container)) + print("** Discarding the docker container <{0}> **".format(container)) base_rm_cmd = "docker rm {0}".format(container) cmd = get_cmd(run_as_cmd, base_rm_cmd) ctx.run(cmd) - print ("** Done! Discarded the docker container <{0}> **".format(container)) + print("** Done! Discarded the docker container <{0}> **".format( + container)) else: print("** Docker container <{0}> not found **".format(container)) + # Docker compose based deployment @task def build(ctx): @@ -171,15 +192,16 @@ def build(ctx): base_build_cmd = "docker-compose build --no-cache" cmd = get_cmd(run_as_cmd, base_build_cmd) - print ("** Building docker images **") + print("** Building docker images **") ctx.run(cmd) - print ("** Done! Built the docker images **") + print("** Done! Built the docker images **") base_build_cmd = "docker pull mariadb:10.2 " cmd = get_cmd(run_as_cmd, base_build_cmd) - print ("** Pulling maria-db base image **") + print("** Pulling maria-db base image **") ctx.run(cmd) - print ("** Done! Pulled maria-db base image **") + print("** Done! Pulled maria-db base image **") + @task def begin(ctx): @@ -188,7 +210,8 @@ def begin(ctx): run_as_cmd = run_as(OS_NAME) cmd = get_cmd(run_as_cmd, base_cmd) ctx.run(cmd) - print ("** Done! Initialized the docker containers **") + print("** Done! Initialized the docker containers **") + @task def deploy(ctx, fixtures=False, static=True): @@ -215,37 +238,42 @@ def deploy(ctx, fixtures=False, static=True): print("** Done! Loaded fixtures into database **") if static: - base_static_cmd = "docker exec -i yaksh_django python3 manage.py collectstatic" + base_static_cmd = "docker exec -i yaksh_django " \ + "python3 manage.py collectstatic" cmd = get_cmd(run_as_cmd, base_static_cmd) - print ("** Setting up static assets **") + print("** Setting up static assets **") ctx.run(cmd) - print ("** Done! Set up static assets **") + print("** Done! Set up static assets **") + @task def status(ctx): run_as_cmd = run_as(OS_NAME) base_cmd = "docker-compose ps" cmd = get_cmd(run_as_cmd, base_cmd) - print ("** Fetching container status **") + print("** Fetching container status **") ctx.run(cmd) + @task def halt(ctx): run_as_cmd = run_as(OS_NAME) base_cmd = "docker-compose stop" cmd = get_cmd(run_as_cmd, base_cmd) - print ("** Stopping containers **") + print("** Stopping containers **") ctx.run(cmd) - print ("** Done! Stopped containers **") + print("** Done! Stopped containers **") + @task def restart(ctx): run_as_cmd = run_as(OS_NAME) base_cmd = "docker-compose restart" cmd = get_cmd(run_as_cmd, base_cmd) - print ("** Restarting containers **") + print("** Restarting containers **") ctx.run(cmd) - print ("** Done! Restarted containers **") + print("** Done! Restarted containers **") + @task(halt) def remove(ctx): @@ -253,7 +281,7 @@ def remove(ctx): base_cmd = "docker-compose rm --force" cmd = get_cmd(run_as_cmd, base_cmd) sql_dir = os.path.join(SCRIPT_DIR, 'docker', 'mysql') - print ("** Removing containers **") + print("** Removing containers **") remove_dir(sql_dir) ctx.run(cmd) - print ("** Done! Removed containers **") + print("** Done! Removed containers **") diff --git a/yaksh/admin.py b/yaksh/admin.py index 199fb56..7ea8ed6 100644 --- a/yaksh/admin.py +++ b/yaksh/admin.py @@ -1,16 +1,18 @@ from yaksh.models import Question, Quiz, QuestionPaper, Profile -from yaksh.models import TestCase, StandardTestCase, StdIOBasedTestCase, Course, AnswerPaper +from yaksh.models import (TestCase, StandardTestCase, StdIOBasedTestCase, + Course, AnswerPaper) from django.contrib import admin - class AnswerPaperAdmin(admin.ModelAdmin): - search_fields = ['user__first_name', 'user__last_name','user__username', - "question_paper__quiz__description","user_ip" ] + search_fields = ['user__first_name', 'user__last_name', 'user__username', + "question_paper__quiz__description", "user_ip"] + class ProfileAdmin(admin.ModelAdmin): - search_fields = ['user__first_name', 'user__last_name','user__username', - "roll_number", "institute","department"] + search_fields = ['user__first_name', 'user__last_name', 'user__username', + "roll_number", "institute", "department"] + admin.site.register(Profile, ProfileAdmin) admin.site.register(Question) diff --git a/yaksh/base_evaluator.py b/yaksh/base_evaluator.py index e702f68..567d5ad 100644 --- a/yaksh/base_evaluator.py +++ b/yaksh/base_evaluator.py @@ -1,20 +1,19 @@ #!/usr/bin/env python from __future__ import unicode_literals -import traceback -import pwd import os -from os.path import join, isfile -from os.path import isdir, dirname, abspath, join, isfile, exists +from os.path import abspath, exists import subprocess import stat import signal # Local imports -from .grader import MY_DIR, TimeoutException +from .grader import TimeoutException + class BaseEvaluator(object): - """Base Evaluator class containing generic attributes and callable methods""" + """Base Evaluator class containing generic attributes + and callable methods""" def __init__(self): pass @@ -31,7 +30,8 @@ class BaseEvaluator(object): stdout and stderr. """ try: - proc = subprocess.Popen(cmd_args,preexec_fn=os.setpgrp, *args, **kw) + proc = subprocess.Popen(cmd_args, + preexec_fn=os.setpgrp, *args, **kw) stdout, stderr = proc.communicate() except TimeoutException: # Runaway code, so kill it. @@ -64,6 +64,6 @@ class BaseEvaluator(object): submit_f.close() def _set_file_as_executable(self, fname): - os.chmod(fname, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR - | stat.S_IRGRP | stat.S_IWGRP | stat.S_IXGRP - | stat.S_IROTH | stat.S_IWOTH | stat.S_IXOTH) + os.chmod(fname, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR | + stat.S_IRGRP | stat.S_IWGRP | stat.S_IXGRP | stat.S_IROTH | + stat.S_IWOTH | stat.S_IXOTH) diff --git a/yaksh/bash_code_evaluator.py b/yaksh/bash_code_evaluator.py index 288a744..61cb9fa 100644 --- a/yaksh/bash_code_evaluator.py +++ b/yaksh/bash_code_evaluator.py @@ -1,12 +1,8 @@ #!/usr/bin/env python from __future__ import unicode_literals -import traceback -import pwd import os -from os.path import join, isfile -import sys +from os.path import isfile import subprocess -import importlib # local imports from .base_evaluator import BaseEvaluator @@ -19,7 +15,7 @@ class BashCodeEvaluator(BaseEvaluator): self.files = [] self.submit_code_path = "" self.test_code_path = "" - self.tc_args_path= "" + self.tc_args_path = "" # Set metadata values self.user_answer = metadata.get('user_answer') @@ -55,20 +51,20 @@ class BashCodeEvaluator(BaseEvaluator): Returns -------- - success - Boolean, indicating if code was executed successfully, correctly + success - Boolean, indicating if code was executed successfully, + correctly weight - Float, indicating total weight of all successful test cases error - String, error message if success is false - returns (True, "Correct answer", 1.0) : If the student script passes all - test cases/have same output, when compared to the instructor script + returns (True, "Correct answer", 1.0) : If the student script passes + all test cases/have same output, when compared to the instructor script returns (False, error_msg, 0.0): If the student script fails a single test/have dissimilar output, when compared to the instructor script. - Returns (False, error_msg, 0.0): If mandatory arguments are not files or if - the required permissions are not given to the file(s). + Returns (False, error_msg, 0.0): If mandatory arguments are not files + or if the required permissions are not given to the file(s). """ - success = False mark_fraction = 0.0 self.submit_code_path = self.create_submit_code_file('submit.sh') self._set_file_as_executable(self.submit_code_path) @@ -76,13 +72,15 @@ class BashCodeEvaluator(BaseEvaluator): self._set_file_as_executable(self.test_code_path) if self.test_case_args: self.tc_args_path = self.create_submit_code_file('main.args') - self.write_to_submit_code_file(self.tc_args_path, self.test_case_args) + self.write_to_submit_code_file(self.tc_args_path, + self.test_case_args) shebang = "#!/bin/bash\n" self.user_answer = shebang + self.user_answer.replace("\r", "") self.test_case = self.test_case.replace("\r", "") self.write_to_submit_code_file(self.submit_code_path, self.user_answer) self.write_to_submit_code_file(self.test_code_path, self.test_case) - clean_ref_code_path, clean_test_case_path = self.test_code_path, self.tc_args_path + clean_ref_code_path, clean_test_case_path = \ + self.test_code_path, self.tc_args_path if self.file_paths: self.files = copy_files(self.file_paths) @@ -101,22 +99,23 @@ class BashCodeEvaluator(BaseEvaluator): if not clean_test_case_path: ret = self._run_command(["bash", clean_ref_code_path], - stdin=None, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE - ) + stdin=None, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE + ) proc, inst_stdout, inst_stderr = ret ret = self._run_command(["bash", self.submit_code_path], - stdin=None, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE - ) + stdin=None, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE + ) proc, stdnt_stdout, stdnt_stderr = ret if inst_stdout == stdnt_stdout: mark_fraction = 1.0 if self.partial_grading else 0.0 return True, None, mark_fraction else: - err = "Error: expected %s, got %s" % (inst_stdout + inst_stderr, + err = "Error: expected %s, got %s" % ( + inst_stdout + inst_stderr, stdnt_stdout + stdnt_stderr ) return False, err, 0.0 @@ -140,27 +139,28 @@ class BashCodeEvaluator(BaseEvaluator): args = ["bash", clean_ref_code_path] + \ [x for x in tc.split()] ret = self._run_command(args, - stdin=None, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE - ) + stdin=None, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE + ) proc, inst_stdout, inst_stderr = ret if self.file_paths: self.files = copy_files(self.file_paths) args = ["bash", self.submit_code_path] + \ [x for x in tc.split()] ret = self._run_command(args, - stdin=None, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE) + stdin=None, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE + ) proc, stdnt_stdout, stdnt_stderr = ret valid_answer = inst_stdout == stdnt_stdout if valid_answer and (num_lines == loop_count): mark_fraction = 1.0 if self.partial_grading else 0.0 return True, None, mark_fraction else: - err = ("Error:expected" - " {0}, got {1}").format(inst_stdout+inst_stderr, + err = ("Error:expected {0}, got {1}").format( + inst_stdout+inst_stderr, stdnt_stdout+stdnt_stderr ) return False, err, 0.0 diff --git a/yaksh/bash_stdio_evaluator.py b/yaksh/bash_stdio_evaluator.py index 1ce729a..f445e09 100644 --- a/yaksh/bash_stdio_evaluator.py +++ b/yaksh/bash_stdio_evaluator.py @@ -4,7 +4,7 @@ import subprocess import os from os.path import isfile -#local imports +# local imports from .stdio_evaluator import StdIOEvaluator from .file_utils import copy_files, delete_files @@ -22,7 +22,7 @@ class BashStdIOEvaluator(StdIOEvaluator): # Set test case data values self.expected_input = test_case_data.get('expected_input') self.expected_output = test_case_data.get('expected_output') - self.weight = test_case_data.get('weight') + self.weight = test_case_data.get('weight') def teardown(self): os.remove(self.submit_code_path) @@ -36,7 +36,6 @@ class BashStdIOEvaluator(StdIOEvaluator): if not isfile(self.submit_code_path): msg = "No file at %s or Incorrect path" % self.submit_code_path return False, msg - user_code_directory = os.getcwd() + '/' self.user_answer = self.user_answer.replace("\r", "") self.write_to_submit_code_file(self.submit_code_path, self.user_answer) diff --git a/yaksh/cpp_code_evaluator.py b/yaksh/cpp_code_evaluator.py index 4c8e938..8bd3beb 100644 --- a/yaksh/cpp_code_evaluator.py +++ b/yaksh/cpp_code_evaluator.py @@ -1,9 +1,7 @@ #!/usr/bin/env python from __future__ import unicode_literals -import traceback -import pwd import os -from os.path import join, isfile +from os.path import isfile import subprocess # Local imports @@ -51,12 +49,13 @@ class CppCodeEvaluator(BaseEvaluator): return user_output_path, ref_output_path def get_commands(self, clean_ref_code_path, user_output_path, - ref_output_path): - compile_command = 'g++ {0} -c -o {1}'.format(self.submit_code_path, - user_output_path) - compile_main = 'g++ {0} {1} -o {2}'.format(clean_ref_code_path, - user_output_path, - ref_output_path) + ref_output_path): + compile_command = 'g++ {0} -c -o {1}'.format( + self.submit_code_path, user_output_path) + compile_main = 'g++ {0} {1} -o {2}'.format( + clean_ref_code_path, user_output_path, + ref_output_path + ) return compile_command, compile_main def compile_code(self): @@ -65,7 +64,8 @@ class CppCodeEvaluator(BaseEvaluator): else: self.submit_code_path = self.create_submit_code_file('submit.c') self.test_code_path = self.create_submit_code_file('main.c') - self.write_to_submit_code_file(self.submit_code_path, self.user_answer) + self.write_to_submit_code_file(self.submit_code_path, + self.user_answer) self.write_to_submit_code_file(self.test_code_path, self.test_case) clean_ref_code_path = self.test_code_path if self.file_paths: @@ -129,13 +129,12 @@ class CppCodeEvaluator(BaseEvaluator): if stdnt_stderr == '': proc, main_out, main_err = self.compiled_test_code main_err = self._remove_null_substitute_char(main_err) - if main_err == '': ret = self._run_command([self.ref_output_path], - stdin=None, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE - ) + stdin=None, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE + ) proc, stdout, stderr = ret if proc.returncode == 0: success, err = True, None @@ -143,7 +142,7 @@ class CppCodeEvaluator(BaseEvaluator): else: err = "{0} \n {1}".format(stdout, stderr) else: - err = "Error:" + err = "Test case Error:" try: error_lines = main_err.splitlines() for e in error_lines: @@ -151,7 +150,7 @@ class CppCodeEvaluator(BaseEvaluator): err = "{0} \n {1}".format(err, e.split(":", 1)[1]) else: err = "{0} \n {1}".format(err, e) - except: + except Exception: err = "{0} \n {1}".format(err, main_err) else: err = "Compilation Error:" @@ -162,7 +161,7 @@ class CppCodeEvaluator(BaseEvaluator): err = "{0} \n {1}".format(err, e.split(":", 1)[1]) else: err = "{0} \n {1}".format(err, e) - except: + except Exception: err = "{0} \n {1}".format(err, stdnt_stderr) return success, err, mark_fraction diff --git a/yaksh/cpp_stdio_evaluator.py b/yaksh/cpp_stdio_evaluator.py index d211bb7..4e8f8df 100644 --- a/yaksh/cpp_stdio_evaluator.py +++ b/yaksh/cpp_stdio_evaluator.py @@ -4,7 +4,7 @@ import subprocess import os from os.path import isfile -#Local imports +# Local imports from .stdio_evaluator import StdIOEvaluator from .file_utils import copy_files, delete_files @@ -22,12 +22,17 @@ class CppStdIOEvaluator(StdIOEvaluator): # Set test case data values self.expected_input = test_case_data.get('expected_input') self.expected_output = test_case_data.get('expected_output') - self.weight = test_case_data.get('weight') + self.weight = test_case_data.get('weight') def teardown(self): - os.remove(self.submit_code_path) + if os.path.exists(self.submit_code_path): + os.remove(self.submit_code_path) if self.files: delete_files(self.files) + if os.path.exists(self.ref_output_path): + os.remove(self.ref_output_path) + if os.path.exists(self.user_output_path): + os.remove(self.user_output_path) def set_file_paths(self): user_output_path = os.getcwd() + '/output_file' @@ -44,7 +49,7 @@ class CppStdIOEvaluator(StdIOEvaluator): def compile_code(self): self.submit_code_path = self.create_submit_code_file('submit.c') if self.file_paths: - self.files = copy_files(file_paths) + self.files = copy_files(self.file_paths) if not isfile(self.submit_code_path): msg = "No file at %s or Incorrect path" % self.submit_code_path return False, msg @@ -77,31 +82,17 @@ class CppStdIOEvaluator(StdIOEvaluator): if stdnt_stderr == '': proc, main_out, main_err = self.compiled_test_code main_err = self._remove_null_substitute_char(main_err) - if main_err == '': - proc = subprocess.Popen("./executable", - shell=True, - stdin=subprocess.PIPE, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - preexec_fn=os.setpgrp - ) - success, err = self.evaluate_stdio(self.user_answer, proc, - self.expected_input, - self.expected_output - ) - os.remove(self.ref_output_path) - else: - err = "Error:" - try: - error_lines = main_err.splitlines() - for e in error_lines: - if ':' in e: - err = err + "\n" + e.split(":", 1)[1] - else: - err = err + "\n" + e - except: - err = err + "\n" + main_err - os.remove(self.user_output_path) + proc = subprocess.Popen("./executable", + shell=True, + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + preexec_fn=os.setpgrp + ) + success, err = self.evaluate_stdio(self.user_answer, proc, + self.expected_input, + self.expected_output + ) else: err = "Compilation Error:" try: @@ -111,7 +102,7 @@ class CppStdIOEvaluator(StdIOEvaluator): err = err + "\n" + e.split(":", 1)[1] else: err = err + "\n" + e - except: + except Exception: err = err + "\n" + stdnt_stderr mark_fraction = 1.0 if self.partial_grading and success else 0.0 return success, err, mark_fraction diff --git a/yaksh/decorators.py b/yaksh/decorators.py index 9e9bc6d..81912f0 100644 --- a/yaksh/decorators.py +++ b/yaksh/decorators.py @@ -1,6 +1,5 @@ -from django.shortcuts import render_to_response, redirect +from django.shortcuts import render from django.conf import settings -from django.template import RequestContext # Local imports from yaksh.forms import ProfileForm @@ -20,15 +19,13 @@ def has_profile(func): def _wrapped_view(request, *args, **kwargs): if user_has_profile(request.user): return func(request, *args, **kwargs) - ci = RequestContext(request) if request.user.groups.filter(name='moderator').exists(): template = 'manage.html' else: template = 'user.html' form = ProfileForm(user=request.user, instance=None) context = {'template': template, 'form': form} - return render_to_response('yaksh/editprofile.html', context, - context_instance=ci) + return render(request, 'yaksh/editprofile.html', context) return _wrapped_view @@ -40,7 +37,6 @@ def email_verified(func): """ def is_email_verified(request, *args, **kwargs): - ci = RequestContext(request) user = request.user context = {} if not settings.IS_DEVELOPMENT: @@ -49,7 +45,8 @@ def email_verified(func): context['success'] = False context['msg'] = "Your account is not verified. \ Please verify your account" - return render_to_response('yaksh/activation_status.html', - context, context_instance=ci) + return render( + request, 'yaksh/activation_status.html', context + ) return func(request, *args, **kwargs) - return is_email_verified
\ No newline at end of file + return is_email_verified diff --git a/yaksh/documentation/conf.py b/yaksh/documentation/conf.py index 10ad210..39481c7 100644 --- a/yaksh/documentation/conf.py +++ b/yaksh/documentation/conf.py @@ -38,7 +38,7 @@ extensions = [ 'sphinx.ext.mathjax', 'sphinx.ext.ifconfig', 'sphinx.ext.viewcode', -# 'sphinx.ext.githubpages', + # 'sphinx.ext.githubpages', ] # Add any paths that contain templates here, relative to this directory. @@ -59,7 +59,7 @@ master_doc = 'index' # General information about the project. project = u'Yaksh' -copyright = u'2016, FOSSEE' +copyright = u'2018, FOSSEE' author = u'FOSSEE' # The version info for the project you're documenting, acts as replacement for @@ -67,9 +67,9 @@ author = u'FOSSEE' # built documents. # # The short X.Y version. -version = u'0.1.2' +version = u'0.7' # The full version, including alpha/beta/rc tags. -release = u'July 2016' +release = u'Feb 2018' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. @@ -129,7 +129,7 @@ todo_include_todos = True # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. # -html_theme = 'default' #'sphinx_rtd_theme' +html_theme = 'default' # 'sphinx_rtd_theme' # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the @@ -155,8 +155,8 @@ html_theme = 'default' #'sphinx_rtd_theme' # html_logo = None # The name of an image file (relative to this directory) to use as a favicon of -# the docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 -# pixels large. +# the docs. This file should be a Windows icon file (.ico) being 16x16 or +# 32x32 pixels large. # # html_favicon = None diff --git a/yaksh/documentation/images/add_exercise.jpg b/yaksh/documentation/images/add_exercise.jpg Binary files differnew file mode 100644 index 0000000..2512f1a --- /dev/null +++ b/yaksh/documentation/images/add_exercise.jpg diff --git a/yaksh/documentation/images/add_lesson.jpg b/yaksh/documentation/images/add_lesson.jpg Binary files differnew file mode 100644 index 0000000..6de272c --- /dev/null +++ b/yaksh/documentation/images/add_lesson.jpg diff --git a/yaksh/documentation/images/add_question.jpg b/yaksh/documentation/images/add_question.jpg Binary files differindex 791ba79..b9b5bc7 100644 --- a/yaksh/documentation/images/add_question.jpg +++ b/yaksh/documentation/images/add_question.jpg diff --git a/yaksh/documentation/images/add_quiz.jpg b/yaksh/documentation/images/add_quiz.jpg Binary files differindex 780a179..3264684 100644 --- a/yaksh/documentation/images/add_quiz.jpg +++ b/yaksh/documentation/images/add_quiz.jpg diff --git a/yaksh/documentation/images/course_details_features.jpg b/yaksh/documentation/images/course_details_features.jpg Binary files differindex 5e820f9..63b4b2e 100644 --- a/yaksh/documentation/images/course_details_features.jpg +++ b/yaksh/documentation/images/course_details_features.jpg diff --git a/yaksh/documentation/images/course_features.jpg b/yaksh/documentation/images/course_features.jpg Binary files differindex 12853af..2da356e 100644 --- a/yaksh/documentation/images/course_features.jpg +++ b/yaksh/documentation/images/course_features.jpg diff --git a/yaksh/documentation/images/cpp_standard_testcase.jpg b/yaksh/documentation/images/cpp_standard_testcase.jpg Binary files differindex 8d3161f..cfb1d89 100644 --- a/yaksh/documentation/images/cpp_standard_testcase.jpg +++ b/yaksh/documentation/images/cpp_standard_testcase.jpg diff --git a/yaksh/documentation/images/create_course.jpg b/yaksh/documentation/images/create_course.jpg Binary files differnew file mode 100644 index 0000000..bcf1eff --- /dev/null +++ b/yaksh/documentation/images/create_course.jpg diff --git a/yaksh/documentation/images/design_course.jpg b/yaksh/documentation/images/design_course.jpg Binary files differnew file mode 100644 index 0000000..287ebea --- /dev/null +++ b/yaksh/documentation/images/design_course.jpg diff --git a/yaksh/documentation/images/design_module.jpg b/yaksh/documentation/images/design_module.jpg Binary files differnew file mode 100644 index 0000000..eda8825 --- /dev/null +++ b/yaksh/documentation/images/design_module.jpg diff --git a/yaksh/documentation/images/design_questionpaper.jpg b/yaksh/documentation/images/design_questionpaper.jpg Binary files differindex fdadcdb..05da597 100644 --- a/yaksh/documentation/images/design_questionpaper.jpg +++ b/yaksh/documentation/images/design_questionpaper.jpg diff --git a/yaksh/documentation/images/embed_video.jpg b/yaksh/documentation/images/embed_video.jpg Binary files differnew file mode 100644 index 0000000..84d18a2 --- /dev/null +++ b/yaksh/documentation/images/embed_video.jpg diff --git a/yaksh/documentation/images/float_testcase.jpg b/yaksh/documentation/images/float_testcase.jpg Binary files differindex 2b6827c..70b8a9f 100644 --- a/yaksh/documentation/images/float_testcase.jpg +++ b/yaksh/documentation/images/float_testcase.jpg diff --git a/yaksh/documentation/images/hook_testcase.jpg b/yaksh/documentation/images/hook_testcase.jpg Binary files differindex 3018050..4d404f6 100644 --- a/yaksh/documentation/images/hook_testcase.jpg +++ b/yaksh/documentation/images/hook_testcase.jpg diff --git a/yaksh/documentation/images/integer_testcase.jpg b/yaksh/documentation/images/integer_testcase.jpg Binary files differindex ca70a41..58ff6be 100644 --- a/yaksh/documentation/images/integer_testcase.jpg +++ b/yaksh/documentation/images/integer_testcase.jpg diff --git a/yaksh/documentation/images/java_standard_testcase.jpg b/yaksh/documentation/images/java_standard_testcase.jpg Binary files differindex a5af3fc..c13f618 100644 --- a/yaksh/documentation/images/java_standard_testcase.jpg +++ b/yaksh/documentation/images/java_standard_testcase.jpg diff --git a/yaksh/documentation/images/moderator_dashboard.jpg b/yaksh/documentation/images/moderator_dashboard.jpg Binary files differindex 13ce524..739d221 100644 --- a/yaksh/documentation/images/moderator_dashboard.jpg +++ b/yaksh/documentation/images/moderator_dashboard.jpg diff --git a/yaksh/documentation/images/python_standard_testcase.jpg b/yaksh/documentation/images/python_standard_testcase.jpg Binary files differindex 992e805..a4d2ac8 100644 --- a/yaksh/documentation/images/python_standard_testcase.jpg +++ b/yaksh/documentation/images/python_standard_testcase.jpg diff --git a/yaksh/documentation/images/questions.jpg b/yaksh/documentation/images/questions.jpg Binary files differindex 780d729..1935541 100644 --- a/yaksh/documentation/images/questions.jpg +++ b/yaksh/documentation/images/questions.jpg diff --git a/yaksh/documentation/images/stdio_testcase.jpg b/yaksh/documentation/images/stdio_testcase.jpg Binary files differindex 41a1694..9a041ba 100644 --- a/yaksh/documentation/images/stdio_testcase.jpg +++ b/yaksh/documentation/images/stdio_testcase.jpg diff --git a/yaksh/documentation/images/string_testcase.jpg b/yaksh/documentation/images/string_testcase.jpg Binary files differindex 7286eff..6cd2b72 100644 --- a/yaksh/documentation/images/string_testcase.jpg +++ b/yaksh/documentation/images/string_testcase.jpg diff --git a/yaksh/documentation/images/view_lessons.jpg b/yaksh/documentation/images/view_lessons.jpg Binary files differnew file mode 100644 index 0000000..4229afe --- /dev/null +++ b/yaksh/documentation/images/view_lessons.jpg diff --git a/yaksh/documentation/images/view_modules.jpg b/yaksh/documentation/images/view_modules.jpg Binary files differnew file mode 100644 index 0000000..5b535d3 --- /dev/null +++ b/yaksh/documentation/images/view_modules.jpg diff --git a/yaksh/documentation/images/view_quizzes.jpg b/yaksh/documentation/images/view_quizzes.jpg Binary files differnew file mode 100644 index 0000000..43bb36f --- /dev/null +++ b/yaksh/documentation/images/view_quizzes.jpg diff --git a/yaksh/documentation/index.rst b/yaksh/documentation/index.rst index a790357..db48544 100644 --- a/yaksh/documentation/index.rst +++ b/yaksh/documentation/index.rst @@ -2,10 +2,6 @@ Welcome to Yaksh's documentation! ================================= Yaksh lets user create and take online programming quiz. Yaksh is an open source project developed by FOSSEE. The code is available on `github <https://github.com/fossee/online_test>`_. - -.. note:: - - This is a basic documentation for users to get comfortable with the interface. The documentation is still under progress. The user documentation for the site is organized into a few sections: diff --git a/yaksh/documentation/installation.rst b/yaksh/documentation/installation.rst index e4ec581..1c90997 100644 --- a/yaksh/documentation/installation.rst +++ b/yaksh/documentation/installation.rst @@ -12,67 +12,311 @@ Installing Yaksh **For installing Yaksh** - 1. **Clone the repository**:: + 1. **Clone the repository**:: $ git clone https://github.com/FOSSEE/online_test.git - 2. **Go to the online_test directory**:: + 2. **Go to the online_test directory**:: - $ cd ./online_test + $ cd ./online_test - 3. **Install the dependencies** - - * For Python 2 use:: + 3. **Install the dependencies** - + * For Python 2 use:: - $ pip install -r ./requirements/requirements-py2.txt + $ pip install -r ./requirements/requirements-py2.txt - * For Python 3 (recommended) use:: + * For Python 3 (recommended) use:: - $ pip install -r ./requirements/requirements-py3.txt + $ pip install -r ./requirements/requirements-py3.txt Quick Start ----------- 1. **Start up the code server that executes the user code safely**: - * To run the code server in a sandboxed docker environment, run the command:: + * To run the code server in a sandboxed docker environment, run the command:: - $ invoke start + $ invoke start - .. note:: + .. note:: - Make sure that you have Docker installed on your system beforehand. - Find docker installation guide `here <https://docs.docker.com/engine/installation/#desktop>`_. + Make sure that you have Docker installed on your system beforehand. + Find docker installation guide `here <https://docs.docker.com/engine/installation/#desktop>`_. - * To run the code server without docker, locally use:: + * To run the code server without docker, locally use:: - $ invoke start --unsafe + $ invoke start --unsafe - .. note:: + .. note:: - Note this command will run the yaksh code server locally on your machine and is susceptible to malicious code. You will have to install the code server requirements in sudo mode. + Note this command will run the yaksh code server locally on your machine and is susceptible to malicious code. You will have to install the code server requirements in sudo mode. 2. **On another terminal, run the application using the following command** - * To start the django server:: + * To start the django server:: - $ invoke serve + $ invoke serve - .. note:: + .. note:: - The serve command will run the django application server on the 8000 port and hence this port will be unavailable to other processes. + The serve command will run the django application server on the 8000 port and hence this port will be unavailable to other processes. 3. **Open your browser and open the URL** - ``http://localhost:8000/exam`` 4. **Login as a teacher to edit the quiz or as a student to take the quiz** - - * Credentials: - For Student: - * Username: student - * Password: student - For Teacher: - * Username: teacher - * Password: teacher + + * Credentials: + For Student: + * Username: student + * Password: student + For Teacher: + * Username: teacher + * Password: teacher 5. **User can also login to the Default Django admin by going to URL and entering the following admin credentials** ``http://localhost:8000/admin`` - For admin: - * Username: admin - * Password: admin + For admin: + * Username: admin + * Password: admin + + +Production Deployment +--------------------- + +* **Deploying Locally** + + Follow these steps to deploy locally on the server. + + * **Pre-Requisite** + + 1. Ensure `pip <https://pip.pypa.io/en/latest/installing.html>`__ is + installed + 2. Install dependencies, Run; + + :: + + pip install -r requirements/requirements-py2.txt # For Python 2 + + pip3 install -r requirements/requirements-py3.txt # For Python 3 + + 3. Install MySql Server + 4. Install Python MySql support + 5. Install Apache Server for deployment + + 6. Create a database named ``yaksh`` by following the steps below + + :: + + $> mysql -u root -p + $> mysql> create database yaksh + + 7. Add a user named ``yaksh_user`` and give access to it on the database + ``yaksh`` by following the steps below + + :: + + mysql> grant usage on yaksh to yaksh_user@localhost identified + by 'mysecretpassword'; + + mysql> grant all privileges on yaksh to yaksh_user@localhost; + + 8. Add ``DATABASE_PASSWORD = 'mysecretpassword'`` and + ``DATABASE_USER = 'yaksh_user'`` to online\_test/settings.py + + + * **Installation & Usage** + + To install this app follow the steps below: + + 1. Clone this repository and cd to the cloned repo. + + :: + + $ git clone https://github.com/FOSSEE/online_test.git + + 2. Rename the ``.sampleenv`` to ``.env`` + + 3. In the ``.env`` file, uncomment the following and replace the values (please keep the remaining settings as is); + + :: + + DB_ENGINE=mysql # Or psycopg (postgresql), sqlite3 (SQLite) + DB_NAME=yaksh + DB_USER=root + DB_PASSWORD=mypassword # Or the password used while creating a Database + DB_PORT=3306 + + 4. Run: + + :: + + $ python manage.py makemigrations yaksh + + $ python manage.py migrate yaksh + + 5. Run the python server provided. This ensures that the code is + executed in a safe environment. Do this like so: + + :: + + $ sudo python -m yaksh.code_server # For Python 2.x + + + $ sudo python3 -m yaksh.code_server # For Python 3.x + + Put this in the background once it has started since this will not + return back the prompt. It is important that the server be running + *before* students start attempting the exam. Using sudo is necessary + since the server is run as the user "nobody". This runs the number + ports configured in the settings.py file in the variable + "N\_CODE\_SERVERS". The "SERVER\_TIMEOUT" also can be changed there. + This is the maximum time allowed to execute the submitted code. Note + that this will likely spawn multiple processes as "nobody" depending + on the number of server ports specified. + + You can also use a Dockerized code server, see `Dockerized Code Server` + + + 6. The ``wsgi.py`` script should make it easy to deploy this using + mod\_wsgi. You will need to add a line of the form: + + :: + + WSGIScriptAlias / "/online_test/wsgi.py" + + to your apache.conf. For more details see the Django docs here: + + https://docs.djangoproject.com/en/2.0/howto/deployment/wsgi/ + + 7. Create a Superuser/Administrator: + + :: + + python manage.py createsuperuser + + 8. Go to http://desired\_host\_or\_ip:desired\_port/exam + + And you should be all set. + + 9. Note that the directory "output" will contain directories, one for + each user. Users can potentially write output into these that can be + used for checking later. + + 10. As a moderator you can visit http://desired\_host\_or\_ip/exam/monitor to view results and user data interactively. You could also "grade" the papers manually if needed. + +.. _dockerized-code-server: + +* **Using Dockerized Code Server** + + 1. Install + `Docker <https://docs.docker.com/engine/installation/>`__ + + 2. Go to the directory where the project is located + + :: + + cd /path/to/online_test + + 3. Create a docker image. This may take a few minutes, + + :: + + docker build -t yaksh_code_server -f ./docker/Dockerfile_codeserver + + 4. Check if the image has been created using the output of ``docker + images`` + + 5. Run the invoke script using the command ``invoke start`` The command + will create and run a new docker container (that is running the + code\_server.py within it), it will also bind the ports of the host + with those of the container + + 6. You can use ``invoke --list`` to get a list of all the available commands + + + .. _deploying-multiple-dockers: + + +* **Deploying Multiple Dockers** + + Follow these steps to deploy and run the Django Server, MySQL instance and Code Server in seperate Docker instances. + + 1. Install `Docker <https://docs.docker.com/engine/installation/>`__ + + 2. Install `Docker Compose <https://docs.docker.com/compose/install/>`__ + + 3. Rename the ``.sampleenv`` to ``.env`` + + 4. In the ``.env`` file, uncomment all the values and keep the default values as is. + + 5. Go to the ``docker`` directory where the project is located: + + :: + + cd /path/to/online_test/docker + + 6. Build the docker images + + :: + + invoke build + + 7. Run the containers and scripts necessary to deploy the web + application + + :: + + invoke begin + + 8. Make sure that all the containers are ``Up`` and stable + + :: + + invoke status + + 8. Run the containers and scripts necessary to deploy the web + application, ``--fixtures`` allows you to load fixtures. + + :: + + invoke deploy --fixtures + + 10. Stop the containers, you can use ``invoke restart`` to restart the containers without removing them + + :: + + invoke halt + + 11. Remove the containers + + :: + + 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/yaksh/documentation/moderator_dashboard.rst b/yaksh/documentation/moderator_dashboard.rst index a93ea3c..4b5bfea 100644 --- a/yaksh/documentation/moderator_dashboard.rst +++ b/yaksh/documentation/moderator_dashboard.rst @@ -14,4 +14,5 @@ The following pages explain the various functions available for moderators moderator_docs/creating_course.rst moderator_docs/creating_quiz.rst moderator_docs/creating_question.rst + moderator_docs/creating_lessons_modules.rst moderator_docs/other_features.rst
\ No newline at end of file diff --git a/yaksh/documentation/moderator_docs/creating_course.rst b/yaksh/documentation/moderator_docs/creating_course.rst index d4dc5f8..5aaddf5 100644 --- a/yaksh/documentation/moderator_docs/creating_course.rst +++ b/yaksh/documentation/moderator_docs/creating_course.rst @@ -3,26 +3,30 @@ Courses ======= For students to take a quiz, it is imperative for the moderator to create a course first. +A course can contain several modules and a module can contain several lessons and/or quizzes. + +To create modules, lessons and quizzes go to the :doc:`creating_lessons_modules` +and :doc:`creating_quiz` section of the documentation. Setting up a new course ----------------------- To create a course, click on the Add New Course button on the moderator's dashboard. This will lead you to a create add course page, where you need to fill in the following fields. - .. image:: ../images/create_course.png + .. image:: ../images/create_course.jpg - * Name + * **Name** Name of the Course - * Enrollment + * **Enrollment** Open enrollment is open to all students. Enroll Request requires students to send a request which the moderator can accept or reject. - * Active + * **Active** If the course should be active for students to take the quiz. The status of the course can be edited later. - * Code + * **Code** If the course should be hidden and only accessible to students possessing the correct course code. - * Instructions + * **Instructions** Instructions for the course - * Start Date and Time for enrollment of course + * **Start Date and Time for enrollment of course** If the enrollment of the course should be available only after a set date and time - * End Date and Time for enrollment of course + * **End Date and Time for enrollment of course** If the enrollment of the course should be available only before a set date and time @@ -37,21 +41,77 @@ Features in Courses The following features are available for courses - * Course Name - Clicking on course name link will display all the enrolled, rejected and requested students list. Moderator can accept or reject the student. - * Quiz Name - Clicking on the quiz name will let you edit the quiz. - * Question Paper - Click on the **Add** link to create a Question Paper for associated Quiz. - If a question paper is already created, click on the Question Paper link to edit question paper. - * Add Teacher - Clicking on Add teacher can let you add teachers for the course. The teachers can edit and modify only the specific course that are allotted to them. - * Teachers added to the course + * **Course Name** + Click on course name link to view all the enrolled, rejected and requested students list. Moderator can accept or reject the student. + * **Module Name** + Click to edit a module added to the course + * **Lesson or Quiz Name** + Click to edit a Lesson or Quiz added to the course + + In edit quiz you can also attempt the quiz in two modes - + * **God Mode** - In God mode you can attempt quiz without any time or eligibilty constraints. + * **User Mode** - In user mode you can attempt quiz the way normal users will attempt i.e. + + * Quiz will have the same duration as that of the original quiz. + * Quiz won't start if the course is inactive or the quiz time has expired. + * **Add Quizzes/Lessons for <module-name>** + Click to add/delete lessons or quizzes. + * **Design Course** + Click to add/delete modules of a course. + * **Add Teacher** + Click to add teachers for the course. The teachers can edit and modify only the specific course that are allotted to them. + * **Clone Course** + Click to create a copy of a course along with its modules, lessons and quizzes. + * **Teachers added to the course** This shows all the teachers added to a particular course. - * Download CSV for the entire course + * **Download CSV for the entire course** This downloads the CSV file containing the performance of all students in every quiz for a given course. - * Edit Course - Clicking on the edit course button will let you edit the details of an existing course. + * **Edit Course** + Click to edit the details of an existing course. + * **Deactivate/Activate Course** + Click to deactivate or activate the course. + * **My Courses** + Click to show all the courses created by you. + * **Allotted courses** + Click to view all the courses allotted to you. + * **Add New Course** + Click to open course form to create new course. + * **Add/View Quizzes** + Click to view all the quizzes created by you or add new quiz. + * **Add/View Lessons** + Click to view all the lessons created by you or add new lesson. + * **Add/View Modules** + Click to view all the modules created by you or add new module. + + +Design a Course +--------------- + + Clicking on **Design Course** will show the below page. + + .. image:: ../images/design_course.jpg + + **Available Modules** contains all the modules that are not added to a course. + + To add a module to the course select the checkbox besides the desired module to be added and click **Add to course** button. + + **Chosen Modules** contains all the modules that are added to a course. + + Following parameters can be changed while designing a course: + + **Order** - Order in which modules are shown to a student. + + To change a module's order change the value to a desired order in the textbox under **Order** column and click **Change order**. + + **Check Prerequisite** - Check if previous module is completed. Default value is **Yes**. + For e.g., Assuming a course contains modules **Demo Module** and **Python module** in the given order; a student has to first complete **Demo module** to attempt **Python Module** if the **Check Prerequisite** value for **Python Module** is checked **Yes**. + + **Currently** column shows the current value of **Check Prerequisite** which in this case is **Yes**. + + Select the checkbox from **Change** column under **Check Prerequisite** and click **Change Prerequisite** button to change the value. + + To remove a module from the course select the checkbox beside every module and click **Remove from course** button. + Features in Course Details -------------------------- @@ -62,17 +122,15 @@ Features in Course Details Following are the features for course details - - * Requests + * **Requests** This is a list of students who have requested to be enrolled in the course. Moderator can enroll or reject selected students. - * Enrolled + * **Enrolled** This is a list of students who have been enrolled in the course. Moderator can reject enrolled students. - * Rejected + * **Rejected** This is a list of students who have been rejected for enrollment in a course. Moderator can enroll rejected students. - * Deactivate/Activate Course - Clicking on this will deactivate or activate the course. - * Upload Users + * **Upload Users** Create and enroll users automatically by uploading a csv of the users. The mandatory fields for this csv are - **firstname, lastname, email**. Other fields like **username, password, institute, roll_no, department, remove** fields are optionals. - * Clone Course - This will create a clone of the course for the moderator. - * Send Mail + * **Send Mail** Moderator can send mail to all enrolled students or selected students. + * **View Course Status** + View students' progress through the course. diff --git a/yaksh/documentation/moderator_docs/creating_lessons_modules.rst b/yaksh/documentation/moderator_docs/creating_lessons_modules.rst new file mode 100644 index 0000000..5131dd1 --- /dev/null +++ b/yaksh/documentation/moderator_docs/creating_lessons_modules.rst @@ -0,0 +1,91 @@ +.. _creating_lessons_modules: + +=================== +Lessons and Modules +=================== + +Courses can have lessons and quizzes encapsulated using a module. + + * **What is a lesson?** + A lesson can be any markdown text with/or an embedded video of a particular topic. + + * **What is a module?** + A Module is a collection of lessons and courses clubbed together by similar idea/content. A module can have its own description as a markdown text with/or an embedded video. + + +Setting up a Lesson +----------------------- + + To create a new lesson or edit any existing lesson click on **Add/View Lessons** from courses page. + + .. image:: ../images/view_lessons.jpg + + This page shows all the lessons created by you. + + Click on **Add new Lesson** to add new lesson. Click on the **lesson name** to edit a lesson. + + .. image:: ../images/add_lesson.jpg + + * **Name** - Name of the lesson. + * **Description** - Description can be any markdown text or embedded video link. + * **Active** - Activate/Deactivate a lesson + * **Lesson files** - Add files to the lesson which will be available for students to view and download. All the uploaded files will be shown below. + + Click on **Save** to save a lesson. + + Click on **Preview Lesson Description** to preview lesson description. Markdown text from the description is converted to html and is displayed below. + + Select the checkbox beside each uploaded file and click on **Delete files** to remove files from the lesson. + + Click on **Embed Video Link** to embed a video. On clicking a pop-up will be shown. + + .. image:: ../images/embed_video.jpg + + Enter the url and click on **Submit** a html div is generated in the text area below. + Click on the button below the textarea to copy the textarea content. This html div can then be added in the lesson description. + + +Setting up a Module +----------------------- + + To create a new module or edit any existing module click on **Add/View Modules** from courses page. + + .. image:: ../images/view_modules.jpg + + This page shows all the modules created by you. + + Creating a new module or editing an existing module is similar to a lesson creation with a difference that a module has no option to upload files. + + +Design a Module +--------------- + + To add lessons or quizzes to a module click on **Add Quizzes/Lessons for <module-name>**. + + .. image:: ../images/design_module.jpg + + **Available Lessons and quizzes** contains all the lessons and quizzes that are not added to a module. + + To add a lesson or a quiz to the module select the checkbox beside every lesson or quiz and click **Add to Module** button. + + **Choosen Lesson and quizzes** contains all the lessons and quizzes that are added to a module. + + A lesson or quiz added to a module becomes a unit. A unit has following parameters to change: + + **Order** - Order in which units are shown to a student. + + To change a unit's order change the value in the textbox under **Order** column and click **Change order**. + + **Check Prerequisite** - Check if previous unit is completed. Default value is **Yes**. + For e.g. A student has to first complete **Yaksh Demo quiz** to attempt **Demo Lesson** if the **Check Prerequisite** value for **Demo Lesson** is checked **Yes**. + + **Currently** column shows the current value of **Check Prerequisite** which in this case is **Yes**. + + Select the checkbox from **Change** column under **Check Prerequisite** and click **Change Prerequisite** button to change the value. + + To remove a lesson or a quiz from the module select the checkbox beside every lesson or quiz and click **Remove from Module** button. + + + + + diff --git a/yaksh/documentation/moderator_docs/creating_question.rst b/yaksh/documentation/moderator_docs/creating_question.rst index 78b6f2c..82bb6e5 100644 --- a/yaksh/documentation/moderator_docs/creating_question.rst +++ b/yaksh/documentation/moderator_docs/creating_question.rst @@ -5,315 +5,356 @@ Questions Setting up questions -------------------- - Setting up questions is the most important part of the Yaksh experience. Questions can be of multiple types i.e Multiple choice questions (MCQ), Multiple correct choices (MCC), Coding questions and assignment upload types. + Setting up questions is the most important part of the Yaksh experience. Questions can be of multiple types i.e Multiple choice questions (MCQ), Multiple correct choices (MCC), Coding questions, assignment upload, fill in the blanks. - To set up a question click on the questions link in the navigation bar. + To set up a question click on the questions link in the navigation bar. - .. image:: ../images/questions.jpg - - To add a question click on the **Add Question** button + .. image:: ../images/questions.jpg + + To add a question click on the **Add Question** button - .. image:: ../images/add_question.jpg + .. image:: ../images/add_question.jpg - * **Summary**- Summary or the name of the question. + * **Summary**- Summary or the name of the question. - * **Language** - Programming language on which the question is based. + * **Language** - Programming language on which the question is based. - * **Type** - Type of the question. i.e Multiple Choice, Multiple Correct Choice, Code and Assignment Upload. + * **Type** - Type of the question. i.e Multiple Choice, Multiple Correct Choice, Code, Assignment Upload etc. - * **Points** - Points is the marks for a question. + * **Points** - Points is the marks for a question. - * **Description** - The actual question description is to be written. + * **Description** - The actual question description in HTML format. - .. note:: To add code snippets in questions please use html <code> and <br> tags. + .. note:: To add code snippets in questions please use html <code> and <br> tags. - * **Tags** - Type of label or metadata tag making it easier to find specific type of questions. + * **Tags** - Type of label or metadata tag making it easier to find specific type of questions. - * **Snippet** - Snippet is used to give any default value or default code or command. This will be displayed in the students answer form. This is used only for code questions. + * **Solution** - Add solution for the question. - * **Partial Grading** - Click this checkbox to enable partial grading feature. + * **Snippet** - Snippet is used to give any default value or default code or command. This will be displayed in the students answer form. This is used only for code questions. - * **File** - File field is used to upload files if there is any file based question. - For e.g. The question is reading a file say **dummy.txt** and print its content. - You can then upload a file **dummy.txt** which will be available to the student while attempting the quiz. + * **Minimum time(in minutes)** - This value can be set for questions which will be added to a Exercise. Exercise time will depend on this time. - * Some file features: - 1. To delete a file click the delete checkbox and click on Delete Selected Files button. - 2. To extract a file for e.g. say **dummy.zip** click the extract checkbox and click on Save button. - If **extract** is selected, the file will be extracted while checking - the student submitted code. - 3. To hide any file from student click the hide checkbox and click on Save button. + * **Partial Grading** - Click this checkbox to enable partial grading feature. - .. Note:: We only support **zip** extension for **extract file** feature. + * **Grade Assignment Upload** - Click this checkbox if the assignment upload based question needs evaluation. Evaluation is done with **Hook based TestCase** only. + + * **File** - File field is used to upload files if there is any file based question. + For e.g. The question is reading a file say **dummy.txt** and print its content. + You can then upload a file **dummy.txt** which will be available to the student while attempting the quiz. + + * Some file features: + 1. To delete a file click the delete checkbox and click on **Delete Selected Files button**. + 2. To extract a file for e.g. say **dummy.zip** click the extract checkbox and click on Save button. + If **extract** is selected, the file will be extracted while checking + the student submitted code. + 3. To hide any file from student click the hide checkbox and click on Save button. + + .. Note:: We only support **zip** extension for **extract file** feature. How to write Test cases ----------------------- - - The following explains different methods to write test cases. - - * **Create Standard Test Case** - - Select Standard from Add Test Case field. - - * For Python: - .. image:: ../images/python_standard_testcase.jpg - :width: 80% - - In the test case field write a python assert to check the user code. - For e.g. :: - - assert add(1, 2) == 3 - - for program of addition. - - * For C, C++, Java and Bash: - Sample Moderator code - - For C and C++: - .. image:: ../images/cpp_standard_testcase.jpg - :width: 80% - - Consider a Program to add three numbers. - The code in the Test case field should be as follows: :: - - #include <stdio.h> - #include <stdlib.h> - - extern int add(int, int, int); - - template <class T> - void check(T expect,T result) - { - if (expect == result) - { - printf("\nCorrect:\n Expected %d got %d \n",expect,result); - } - else - { - printf("\nIncorrect:\n Expected %d got %d \n",expect,result); - exit (1); - } - } - - int main(void) - { - int result; - result = add(0,0,0); - printf("Input submitted to the function: 0, 0, 0"); - check(0, result); - result = add(2,3,3); - printf("Input submitted to the function: 2, 3, 3"); - check(8,result); - printf("All Correct\n"); - } - - Assuming Students answer to be as below: :: - - int add(int a, int b, int c) - { - return a+b+c; - } - - .. Note:: 1. In the above example, **add** in the main function is obtained from student code. - 2. Please make sure that the student code function and testcase calling function should be same which in this case is **add**. - - For Java: - .. image:: ../images/java_standard_testcase.jpg - :width: 80% - - Consider a Program to find square of a number. - The code in the Test case Field should be as follows: :: - class main - { - public static <E> void check(E expect, E result) - { - if(result.equals(expect)) - { - System.out.println("Correct:\nOutput expected "+expect+" and got "+result); - } - else - { - System.out.println("Incorrect:\nOutput expected "+expect+" but got "+result); - System.exit(1); - } - } - public static void main(String arg[]) - { - Test t = new Test(); - int result, input, output; - input = 0; output = 0; - result = t.square_num(input); - System.out.println("Input submitted to the function: "+input); - check(output, result); - input = 5; output = 25; - result = t.square_num(input); - System.out.println("Input submitted to the function: "+input); - check(output, result); - input = 6; output = 36; - result = t.square_num(input); - System.out.println("Input submitted to the function: "+input); - check(output, result); - } - } - - Assuming Students answer to be as below: :: - - class Test - { - int square_num(int num) - { - return num*num; - } - } - - .. Note:: 1. For Java, class name should always be **main** in testcase. - - 2. In the above example, **Test** is the class of student's code. - 3. Please make sure that the student's code class and calling class in testcase is always **Test**. (square_num is the function inside Test class.) - - For Bash: - .. image:: ../images/bash_standard_testcase.jpg - :width: 80% - - In **Test case** Field write your bash script. - For e.g. the question is to move to a particular directory and read a file - **test.txt** - The Test case code shown is: :: - - #!/bin/bash - cd $1 - cat $2 - - In **Test case args** Field type your Command line arguments. - - In this case the test case args are: :: - - somedata/ test.txt - - .. Note:: 1. **Test case args** field is used only for bash. - 2. Each argument should be separated by **space**. - 3. This field can be left blank. - - - Check Delete Field if a test case is to be removed. - - Finally click on Save to save the test case. - - - * **Create Standard Input/Output Based Test Case** + + The following explains different methods to write test cases. + + * **Create Standard Test Case** + + Select Standard from Add Test Case field. Sample Testcases are given for all + languages. + + * **For Python:** + .. image:: ../images/python_standard_testcase.jpg + :width: 80% + + In the test case field write a python assert to check the user code. + For e.g. :: + + assert add(1, 2) == 3 + + for program of addition. + + * **For C, C++:** + + .. image:: ../images/cpp_standard_testcase.jpg + :width: 80% + + Consider a Program to add three numbers. + The code in the Test case field should be as follows: :: + + #include <stdio.h> + #include <stdlib.h> + + extern int add(int, int, int); + + template <class T> + void check(T expect,T result) + { + if (expect == result) + { + printf("\nCorrect:\n Expected %d got %d \n",expect,result); + } + else + { + printf("\nIncorrect:\n Expected %d got %d \n",expect,result); + exit (1); + } + } + + int main(void) + { + int result; + result = add(0,0,0); + printf("Input submitted to the function: 0, 0, 0"); + check(0, result); + result = add(2,3,3); + printf("Input submitted to the function: 2, 3, 3"); + check(8,result); + printf("All Correct\n"); + } + + Assuming Students answer to be as below: :: + + int add(int a, int b, int c) + { + return a+b+c; + } + + .. Note:: 1. In the above example, **add** in the main function is obtained from student code. + 2. Please make sure that the student code function and testcase calling function should be same which in this case is **add**. + + * **For Java:** + .. image:: ../images/java_standard_testcase.jpg + :width: 80% + + Consider a Program to find square of a number. + The code in the Test case Field should be as follows: :: + + class main + { + public static <E> void check(E expect, E result) + { + if(result.equals(expect)) + { + System.out.println("Correct:\nOutput expected "+expect+" and got "+result); + } + else + { + System.out.println("Incorrect:\nOutput expected "+expect+" but got "+result); + System.exit(1); + } + } + public static void main(String arg[]) + { + Test t = new Test(); + int result, input, output; + input = 0; output = 0; + result = t.square_num(input); + System.out.println("Input submitted to the function: "+input); + check(output, result); + input = 5; output = 25; + result = t.square_num(input); + System.out.println("Input submitted to the function: "+input); + check(output, result); + input = 6; output = 36; + result = t.square_num(input); + System.out.println("Input submitted to the function: "+input); + check(output, result); + } + } + + Assuming Students answer to be as below: :: + + class Test + { + int square_num(int num) + { + return num*num; + } + } + + .. Note:: 1. For Java, class name should always be **main** in testcase. + + 2. In the above example, **Test** is the class of student's code. + 3. Please make sure that the student's code class and calling class in testcase is always **Test**. (square_num is the function inside Test class.) + + * **For Bash:** + .. image:: ../images/bash_standard_testcase.jpg + :width: 80% + + In **Test case** Field write your bash script. + For e.g. the question is to move to a particular directory and read a file + **test.txt** + The Test case code shown is: :: + + cd $1 + cat $2 + + In **Test case args** Field type your Command line arguments. + + In this case the test case args are: :: + + somedata/ test.txt + + .. Note:: 1. **Test case args** field is used only for bash. + 2. Each argument should be separated by **space**. + 3. This field can be left blank. + + + Check Delete Field if a test case is to be removed. + + Finally click on Save to save the test case. + + + * **Create Standard Input/Output Based Test Case** - Select StdIO from Add Test Case field. + Select StdIO from Add Test Case field. + + .. image:: ../images/stdio_testcase.jpg + :width: 80% + + In Expected input field, enter the value(s) that will be passed to the students' code through a standard I/O stream. + + .. note:: If there are multiple input values in a test case, enter the values in new line. + + In Expected Output Field, enter the expected output for that test case. For e.g type 3 if the output of the user code is 3. + + Setting up Standard Input/Output Based questions is same for all languages. - .. image:: ../images/stdio_testcase.jpg - :width: 80% + * **Create MCQ or MCC Based Test Case** - In Expected input field, enter the value(s) that will be passed to the students' code through a standard I/O stream. + Select MCQ/MCC from Add Test Case field. - .. note:: If there are multiple input values in a test case, enter the values in new line. + Fig (a) showing MCQ based testcase - In Expected Output Field, enter the expected output for that test case. For e.g type 3 if the output of the user code is 3. + .. image:: ../images/mcq_testcase.jpg + :width: 80% - Setting up Standard Input/Output Based questions is same for all languages. + Fig (b) showing MCC based testcase - * **Create MCQ or MCC Based Test Case** + .. image:: ../images/mcc_testcase.jpg + :width: 80% - Select MCQ/MCC from Add Test Case field. + In Options Field type the option check the correct checkbox if the current option is correct and click on Save button to save each option. - Fig (a) showing MCQ based testcase + For MCC based question, check the correct checkbox for multiple correct options. - .. image:: ../images/mcq_testcase.jpg - :width: 80% + * **Create Hook based Test Case** - Fig (b) showing MCC based testcase + Select Hook from Add Test Case field. - .. image:: ../images/mcc_testcase.jpg - :width: 80% + In Hook based test case type, moderator is provided with a evaluator function + called **check_answer** which is provided with a parameter called **user_answer**. - In Options Field type the option check the correct checkbox if the current option is correct and click on Save button to save each option. + **user_answer** is the code of the student in string format. - For MCC based question, check the correct checkbox for multiple correct options. + .. note :: For assignment upload type question there will be no **user answer** File uploaded by student will be the answer. - * **Create Hook based Test Case** + Suppose the student needs to upload a file say **new.txt** as assignment. + Sample Hook code for this will be as shown below. :: - Select Hook from Add Test Case field. + def check_answer(user_answer): + ''' Evaluates user answer to return - + success - Boolean, indicating if code was executed correctly + mark_fraction - Float, indicating fraction of the weight to a test case + error - String, error message if success is false - In Hook based test case type, moderator is provided with a evaluator function - called **check_answer** which is provided with a parameter called **user_answer**. + In case of assignment upload there will be no user answer ''' - **user_answer** is the code of the student in string format. + success = False + err = "Incorrect Answer" # Please make this more specific + mark_fraction = 0.0 - A moderator can check the string for specific words in the user answer - and/or compile and execute the user answer (using standard python libraries) to - evaluate and hence return the mark fraction. + try: + with open('new.txt', 'r') as f: + if "Hello, World!" in f.read(): + success = True + err = "Correct Answer" + mark_fraction = 1.0 + else: + err = "Did not found string Hello, World! in file." + except IOError: + err = "File new.txt not found." + return success, err, mark_fraction - .. image:: ../images/hook_testcase.jpg - :width: 80% + A moderator can check the string for specific words in the user answer + and/or compile and execute the user answer (using standard python libraries) to + evaluate and hence return the mark fraction. - * **Create Integer Based Test Case** - Select **Answer in Integer** from Type field. + .. image:: ../images/hook_testcase.jpg + :width: 80% - Select Integer from Add Test Case field. + * **Create Integer Based Test Case** - In the Correct field, add the correct integer value for the question. + Select **Answer in Integer** from Type field. - .. image:: ../images/integer_testcase.jpg - :width: 80% + Select Integer from Add Test Case field. - * **Create String Based Test Case** + In the Correct field, add the correct integer value for the question. - Select **Answer in String** from Type field. + .. image:: ../images/integer_testcase.jpg + :width: 80% - Select **String** from Add Test Case field. + * **Create String Based Test Case** - In the **Correct** field, add the exact string answer for the question. + Select **Answer in String** from Type field. - In **String Check** field, select if the checking of the string answer - should be case sensitive or not. + Select **String** from Add Test Case field. - .. image:: ../images/string_testcase.jpg - :width: 80% + In the **Correct** field, add the exact string answer for the question. - * **Create Float Based Test Case** + In **String Check** field, select if the checking of the string answer + should be case sensitive or not. - Select **Answer in Float** from Type field. + .. image:: ../images/string_testcase.jpg + :width: 80% - Select **Float** from Add Test Case field. + * **Create Float Based Test Case** - In the **Correct** field, add the correct float value for the question. + Select **Answer in Float** from Type field. - In the **Error Margin** field, add the margin of error that will be allowed. + Select **Float** from Add Test Case field. - .. image:: ../images/float_testcase.jpg - :width: 80% + In the **Correct** field, add the correct float value for the question. + + In the **Error Margin** field, add the margin of error that will be allowed. + + .. image:: ../images/float_testcase.jpg + :width: 80% Features in Question -------------------- - - * **Download Questions** + + * **Download Questions** - Select questions from the list of questions displayed on the Questions page. Click on the Download Selected button to download the questions. This will create a zip file of the Questions selected. + Select questions from the list of questions displayed on the Questions page. Click on the Download Selected button to download the questions. This will create a zip file of the Questions selected. - * **Upload Questions** - - Click on the browse button. This will open up a window. Select the zip file of questions and click Ok and then click on Upload file button, questions will be uploaded and displayed on the Questions page. + * **Upload Questions** + + Click on the browse button. This will open up a window. Select the zip file of questions and click Ok and then click on Upload file button, questions will be uploaded and displayed on the Questions page. - Zip file should contain **questions_dump.yaml** from which questions will be loaded. - Zip file can contain files related to questions. + Zip file should contain **questions_dump.yaml** from which questions will be loaded. + Zip file can contain files related to questions. - * **Test Questions** - - Select questions from the list of question displayed on the Questions page. Click on Test selected button. This will take you to a quiz with the selected questions. + * **Test Questions** + + Select questions from the list of question displayed on the Questions page. Click on Test selected button. This will take you to a quiz with the selected questions. + + .. Note:: This will not create an actual quiz but a trial quiz. This quiz is hidden from the students and only for moderator to view. You can delete the quiz from moderator's dashboard. + + * **Filter Questions** + + You can filter questions based on type of question, language of question or marks of question. + 1. Click Select Question Type to filter question based on type of the question. + 2. Click Select Language to filter question based on language of the question. + 3. Click Select marks to filter question based on mark of the question. - .. Note:: This will not create an actual quiz but a trial quiz. This quiz is hidden from the students and only for moderator to view. You can delete the quiz from moderator's dashboard. + * **Search by tags** - * **Filter Questions** - - You can filter questions based on type of question, language of question or marks of question. - 1. Click Select Question Type to filter question based on type of the question. - 2. Click Select Language to filter question based on language of the question. - 3. Click Select marks to filter question based on mark of the question. + 1. You can search the questions by tags added during question creation. + 2. Click on the Available tags to view all the available tags. Select any tag from available tags and click **Search**. + 3. Enter the tag in the search bar and click on **Search** respective questions will be displayed. diff --git a/yaksh/documentation/moderator_docs/creating_quiz.rst b/yaksh/documentation/moderator_docs/creating_quiz.rst index 3f227ef..8b93188 100644 --- a/yaksh/documentation/moderator_docs/creating_quiz.rst +++ b/yaksh/documentation/moderator_docs/creating_quiz.rst @@ -1,44 +1,68 @@ +.. _creating_quiz: + ======= Quizzes ======= Quizzes are intrinsically associated with a course, hence to view and/or edit a quiz, we need to navigate to the courses page. -In courses page click on **Add Quiz** button to create a new quiz. +Clicking on Add/View Quizzes from courses page will open the page as shown below + +.. image:: ../images/view_quizzes.jpg +This page shows all the quizzes and exercise created. Creating a Quiz --------------- + + Click on **Add New Quiz** button to add a quiz. + .. image:: ../images/add_quiz.jpg .. note :: It is important to have created or uploaded questions before creating a quiz. - In courses click on **Add Quiz** button to add a quiz. - - * **Course** - Select a course from Course field. This field is mandatory. * **Start Date and Time of quiz** - The date and time after which the quiz can be taken. * **End Date and Time of quiz** - The date and time after which the quiz is deactivated and cannot be attempted. * **Duration** - Duration of quiz to be written in minutes. - * **Active** - If the quiz is active or not. + * **Active** - Check the checkbox to activate/deactivate quiz. * **Description** - Description or name of the quiz. * **Passing Percentage** - Minimum percentage required to pass the test. - * **Prerequisite** - Set a prerequisite quiz to be passed before attempting the current quiz. - * **Language** - Programming language on which the quiz is based. * **Attempts allowed** - Number of attempts that a student can take of the current quiz. - * **Number of Days** - Number of days between attempts. + * **Time Between Quiz Attempts in hours** - For a quiz with multiple attempts this value can be set so that student can attempt again after the specified time. * **Instructions for students** - Additional instructions for students can be added. Some default instructions are already provided. * **Allow student to view answer paper** - Click on this checkbox to allow student to view their answer paper. + * **Allow student to skip questions** - Click on this checkbox to allow/disallow student to skip questions for a quiz. Value defaults to allow skipping questions. + * **Weightage** - Every quiz will have weightage depending on which grades will be calculated. Once a quiz parameters have been set click on **Save** button to save the quiz. -To create a Question paper, Click on **Add** link located besides the created quiz in courses page. +To create a Question paper, Click on **Add** link located besides the created quiz. + +Creating a Exercise +------------------- + + Click on **Add New Exercise** button to add a exercise. + + .. image:: ../images/add_exercise.jpg + + Exercise is similar to quiz with a difference that exercise has infinite attempts and + infinite time. It also does not allow a student to skip the question. + Each question in an exercise can be timed i.e. time to solve a particular question. + Once the question time expires, question solution is shown to the student. + + All the parameters are set by default only below parameters can be changed. + + * **Description** - Description or name of the exercise. + * **Allow student to view answer paper** - Click on this checkbox to allow student to view their answer paper. + * **Active** - Select the checkbox to activate/deactivate exercise. Default value is active. + Designing Question Paper ------------------------ .. image:: ../images/design_questionpaper.jpg - A quiz can have fixed as well as random questions. Fixed questions are those question that are bound to appear for every student taking the quiz. In random questions a pool of questions is given and number of questions to be picked from the pool is set. Hence for different students, different questions from the pool will appear. + A quiz/exercise can have fixed as well as random questions. Fixed questions are those question that are bound to appear for every student taking the quiz. In random questions a pool of questions is given and number of questions to be picked from the pool is set. Hence for different students, different questions from the pool will appear. To add questions to a questionpaper @@ -48,25 +72,17 @@ Designing Question Paper * Click on save question paper to save it or preview question paper to preview it. -Editing a Quiz --------------- - - In Courses page, click on the quiz link to edit the quiz. Then change the parameters and click on design question paper to save it. This will redirect you to the moderator dashboard. +Editing a Quiz/Exercise +----------------------- - In edit quiz you can also attempt the quiz in two modes - - * **God Mode** - In God mode you can attempt quiz without any time or eligibilty constraints. - * **User Mode** - In user mode you can attempt quiz the way normal users will attempt i.e. - - * Quiz will have the same duration as that of the original quiz. - * Quiz won't start if the course is inactive or the quiz time has expired. - * You will be notified about quiz prerequisites.(You can still attempt the quiz though) + Click on the quiz/exercise link to edit, change the parameters and click on Save. Editing a QuestionPaper ----------------------- - Click on the Question Paper for a Quiz link besides Quiz in courses page and follow steps from Design Question Paper. + Click on the Question Paper for a <Quiz-name/Exercise-name> besides Quiz/Exercise and follow steps from Design Question Paper. If the questions are already added to a Question Paper then they are shown in the **Fixed Questions currently in the paper** section. diff --git a/yaksh/error_messages.py b/yaksh/error_messages.py index 7ea8618..512d664 100644 --- a/yaksh/error_messages.py +++ b/yaksh/error_messages.py @@ -3,7 +3,9 @@ try: except ImportError: from itertools import izip_longest as zip_longest -def prettify_exceptions(exception, message, traceback=None, testcase=None): + +def prettify_exceptions(exception, message, traceback=None, + testcase=None, line_no=None): err = {"type": "assertion", "exception": exception, "traceback": traceback, @@ -13,23 +15,28 @@ def prettify_exceptions(exception, message, traceback=None, testcase=None): err["traceback"] = None if exception == 'AssertionError': - value = ("Expected answer from the" - + " test case did not match the output") - err["message"] = value + value = ("Expected answer from the" + + " test case did not match the output") + if message: + err["message"] = message + else: + err["message"] = value err["traceback"] = None - if testcase: - err["test_case"] = testcase + err["test_case"] = testcase + err["line_no"] = line_no return err + def _get_incorrect_user_lines(exp_lines, user_lines): err_line_numbers = [] for line_no, (expected_line, user_line) in \ - enumerate(zip_longest(exp_lines, user_lines)): - if not user_line or not expected_line or \ - user_line.strip() != expected_line.strip(): + enumerate(zip_longest(exp_lines, user_lines)): + if (not user_line or not expected_line or + user_line.strip() != expected_line.strip()): err_line_numbers.append(line_no) return err_line_numbers - + + def compare_outputs(expected_output, user_output, given_input=None): given_lines = user_output.splitlines() exp_lines = expected_output.splitlines() @@ -43,19 +50,18 @@ def compare_outputs(expected_output, user_output, given_input=None): err_line_numbers = _get_incorrect_user_lines(exp_lines, given_lines) msg["error_line_numbers"] = err_line_numbers if ng != ne: - msg["error_msg"] = ("Incorrect Answer: " - + "We had expected {} number of lines. "\ - .format(ne) - + "We got {} number of lines.".format(ng) + msg["error_msg"] = ("Incorrect Answer: " + + "We had expected {} number of lines. ".format(ne) + + "We got {} number of lines.".format(ng) ) return False, msg else: if err_line_numbers: - msg["error_msg"] = ("Incorrect Answer: " - + "Line number(s) {0} did not match." - .format(", ".join(map( - str,[x+1 for x in err_line_numbers] - )))) + msg["error_msg"] = ("Incorrect Answer: " + + "Line number(s) {0} did not match." + .format(", ".join( + map(str, [x+1 for x in err_line_numbers]) + ))) return False, msg else: msg["error_msg"] = "Correct Answer" diff --git a/yaksh/evaluator_tests/test_bash_evaluation.py b/yaksh/evaluator_tests/test_bash_evaluation.py index 5542710..f86bf24 100644 --- a/yaksh/evaluator_tests/test_bash_evaluation.py +++ b/yaksh/evaluator_tests/test_bash_evaluation.py @@ -3,11 +3,9 @@ import unittest import os import shutil import tempfile -from psutil import Process, pid_exists +from psutil import Process # Local Imports from yaksh.grader import Grader -from yaksh.bash_code_evaluator import BashCodeEvaluator -from yaksh.bash_stdio_evaluator import BashStdIOEvaluator from yaksh.evaluator_tests.test_python_evaluation import EvaluatorBaseTest from yaksh.settings import SERVER_TIMEOUT from textwrap import dedent @@ -25,15 +23,15 @@ class BashAssertionEvaluationTestCases(EvaluatorBaseTest): self.tc_data_args = "1 2\n2 1" self.test_case_data = [ {"test_case": self.tc_data, - "test_case_args": self.tc_data_args, - "test_case_type": "standardtestcase", - "weight": 0.0 - } + "test_case_args": self.tc_data_args, + "test_case_type": "standardtestcase", + "weight": 0.0 + } ] self.in_dir = tempfile.mkdtemp() self.timeout_msg = ("Code took more than {0} seconds to run. " - "You probably have an infinite loop in your" - " code.").format(SERVER_TIMEOUT) + "You probably have an infinite loop in your" + " code.").format(SERVER_TIMEOUT) self.file_paths = None def tearDown(self): @@ -43,16 +41,14 @@ class BashAssertionEvaluationTestCases(EvaluatorBaseTest): def test_correct_answer(self): # Given user_answer = ("#!/bin/bash\n[[ $# -eq 2 ]]" - " && echo $(( $1 + $2 )) && exit $(( $1 + $2 ))" - ) - kwargs = { - 'metadata': { - 'user_answer': user_answer, - 'file_paths': self.file_paths, - 'partial_grading': False, - 'language': 'bash' - }, - 'test_case_data': self.test_case_data, + " && echo $(( $1 + $2 )) && exit $(( $1 + $2 ))" + ) + kwargs = {'metadata': { + 'user_answer': user_answer, + 'file_paths': self.file_paths, + 'partial_grading': False, + 'language': 'bash' + }, 'test_case_data': self.test_case_data, } # When @@ -62,18 +58,69 @@ class BashAssertionEvaluationTestCases(EvaluatorBaseTest): # Then self.assertTrue(result.get('success')) + def test_correct_answer_without_test_case_args(self): + # Given + user_answer = "echo 'hello'" + tc_data = "echo 'hello'" + self.test_case_data = [ + {"test_case": tc_data, + "test_case_args": "", + "test_case_type": "standardtestcase", + "weight": 0.0 + } + ] + kwargs = {'metadata': { + 'user_answer': user_answer, + 'file_paths': self.file_paths, + 'partial_grading': False, + 'language': 'bash' + }, 'test_case_data': self.test_case_data, + } + + # When + grader = Grader(self.in_dir) + result = grader.evaluate(kwargs) + + # Then + self.assertTrue(result.get('success')) + + def test_incorrect_answer_without_test_case_args(self): + # Given + user_answer = "echo 'hello'" + tc_data = "echo 'hello world'" + self.test_case_data = [ + {"test_case": tc_data, + "test_case_args": "", + "test_case_type": "standardtestcase", + "weight": 0.0 + } + ] + kwargs = {'metadata': { + 'user_answer': user_answer, + 'file_paths': self.file_paths, + 'partial_grading': False, + 'language': 'bash' + }, 'test_case_data': self.test_case_data, + } + + # When + grader = Grader(self.in_dir) + result = grader.evaluate(kwargs) + + # Then + self.assertFalse(result.get('success')) + def test_error(self): # Given user_answer = ("#!/bin/bash\n[[ $# -eq 2 ]] " - "&& echo $(( $1 - $2 )) && exit $(( $1 - $2 ))") + "&& echo $(( $1 - $2 )) && exit $(( $1 - $2 ))") kwargs = { 'metadata': { 'user_answer': user_answer, 'file_paths': self.file_paths, 'partial_grading': False, 'language': 'bash' - }, - 'test_case_data': self.test_case_data, + }, 'test_case_data': self.test_case_data, } # When @@ -87,15 +134,14 @@ class BashAssertionEvaluationTestCases(EvaluatorBaseTest): def test_infinite_loop(self): # Given user_answer = ("#!/bin/bash\nwhile [ 1 ] ;" - " do echo "" > /dev/null ; done") + " do echo "" > /dev/null ; done") kwargs = { 'metadata': { 'user_answer': user_answer, 'file_paths': self.file_paths, 'partial_grading': False, 'language': 'bash' - }, - 'test_case_data': self.test_case_data, + }, 'test_case_data': self.test_case_data, } # When @@ -120,22 +166,19 @@ class BashAssertionEvaluationTestCases(EvaluatorBaseTest): cat $1 """) self.tc_data_args = "test.txt" - self.test_case_data = [ - {"test_case": self.tc_data, - "test_case_args": self.tc_data_args, - "test_case_type": "standardtestcase", - "weight": 0.0 - } - ] + self.test_case_data = [{ + "test_case": self.tc_data, + "test_case_args": self.tc_data_args, + "test_case_type": "standardtestcase", + "weight": 0.0 + }] user_answer = ("#!/bin/bash\ncat $1") - kwargs = { - 'metadata': { + kwargs = {'metadata': { 'user_answer': user_answer, 'file_paths': self.file_paths, 'partial_grading': False, 'language': 'bash' - }, - 'test_case_data': self.test_case_data, + }, 'test_case_data': self.test_case_data, } # When @@ -145,6 +188,7 @@ class BashAssertionEvaluationTestCases(EvaluatorBaseTest): # Then self.assertTrue(result.get("success")) + class BashStdIOEvaluationTestCases(EvaluatorBaseTest): def setUp(self): self.in_dir = tempfile.mkdtemp() @@ -153,7 +197,6 @@ class BashStdIOEvaluationTestCases(EvaluatorBaseTest): " code.").format(SERVER_TIMEOUT) self.file_paths = None - def test_correct_answer(self): # Given user_answer = dedent(""" #!/bin/bash @@ -162,7 +205,8 @@ class BashStdIOEvaluationTestCases(EvaluatorBaseTest): echo -n `expr $A + $B` """ ) - test_case_data = [{'expected_output': '11', + test_case_data = [{ + 'expected_output': '11', 'expected_input': '5\n6', 'test_case_type': 'stdiobasedtestcase', 'weight': 0.0 @@ -173,8 +217,7 @@ class BashStdIOEvaluationTestCases(EvaluatorBaseTest): 'file_paths': self.file_paths, 'partial_grading': False, 'language': 'bash' - }, - 'test_case_data': test_case_data, + }, 'test_case_data': test_case_data, } # When @@ -190,14 +233,14 @@ class BashStdIOEvaluationTestCases(EvaluatorBaseTest): COUNTER=0 while [ $COUNTER -lt 3 ]; do echo -n "${arr[$COUNTER]}" - let COUNTER=COUNTER+1 + let COUNTER=COUNTER+1 done """ ) test_case_data = [{'expected_output': '1 2 3\n4 5 6\n7 8 9\n', - 'expected_input': '1,2,3\n4,5,6\n7,8,9', - 'test_case_type': 'stdiobasedtestcase', - 'weight': 0.0 + 'expected_input': '1,2,3\n4,5,6\n7,8,9', + 'test_case_type': 'stdiobasedtestcase', + 'weight': 0.0 }] kwargs = { 'metadata': { @@ -205,8 +248,7 @@ class BashStdIOEvaluationTestCases(EvaluatorBaseTest): 'file_paths': self.file_paths, 'partial_grading': False, 'language': 'bash' - }, - 'test_case_data': test_case_data, + }, 'test_case_data': test_case_data, } # When @@ -224,7 +266,8 @@ class BashStdIOEvaluationTestCases(EvaluatorBaseTest): echo -n `expr $A - $B` """ ) - test_case_data = [{'expected_output': '11', + test_case_data = [{ + 'expected_output': '11', 'expected_input': '5\n6', 'test_case_type': 'stdiobasedtestcase', 'weight': 0.0 @@ -235,8 +278,7 @@ class BashStdIOEvaluationTestCases(EvaluatorBaseTest): 'file_paths': self.file_paths, 'partial_grading': False, 'language': 'bash' - }, - 'test_case_data': test_case_data, + }, 'test_case_data': test_case_data, } # When @@ -266,8 +308,7 @@ class BashStdIOEvaluationTestCases(EvaluatorBaseTest): 'file_paths': self.file_paths, 'partial_grading': False, 'language': 'bash' - }, - 'test_case_data': test_case_data, + }, 'test_case_data': test_case_data, } # When @@ -286,8 +327,8 @@ class BashHookEvaluationTestCases(EvaluatorBaseTest): f.write('2'.encode('ascii')) self.in_dir = tempfile.mkdtemp() self.timeout_msg = ("Code took more than {0} seconds to run. " - "You probably have an infinite loop in your" - " code.").format(SERVER_TIMEOUT) + "You probably have an infinite loop in your" + " code.").format(SERVER_TIMEOUT) self.file_paths = None def tearDown(self): @@ -306,28 +347,26 @@ class BashHookEvaluationTestCases(EvaluatorBaseTest): success = False err = "Incorrect Answer" mark_fraction = 0.0 - proc = subprocess.Popen(user_answer, shell=True, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE - ) + proc = subprocess.Popen( + user_answer, shell=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE + ) stdout,stderr = proc.communicate() if stdout.decode("utf-8") == "Hello, world!": success, err, mark_fraction = True, "", 1.0 return success, err, mark_fraction - """ - ) + """) test_case_data = [{"test_case_type": "hooktestcase", - "hook_code": hook_code,"weight": 1.0 - }] + "hook_code": hook_code, "weight": 1.0}] kwargs = { 'metadata': { 'user_answer': user_answer, 'file_paths': self.file_paths, 'partial_grading': False, 'language': 'bash' - }, - 'test_case_data': test_case_data, + }, 'test_case_data': test_case_data, } # When @@ -349,20 +388,18 @@ class BashHookEvaluationTestCases(EvaluatorBaseTest): success = False err = "Incorrect Answer" mark_fraction = 0.0 - proc = subprocess.Popen(user_answer, shell=True, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE - ) + proc = subprocess.Popen( + user_answer, shell=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE + ) stdout,stderr = proc.communicate() if stdout.decode("utf-8") == "Hello, world!": success, err, mark_fraction = True, "", 1.0 return success, err, mark_fraction - """ - ) - + """) test_case_data = [{"test_case_type": "hooktestcase", - "hook_code": hook_code,"weight": 1.0 - }] + "hook_code": hook_code, "weight": 1.0}] kwargs = { 'metadata': { @@ -370,8 +407,7 @@ class BashHookEvaluationTestCases(EvaluatorBaseTest): 'file_paths': self.file_paths, 'partial_grading': False, 'language': 'bash' - }, - 'test_case_data': test_case_data, + }, 'test_case_data': test_case_data, } # When @@ -381,7 +417,7 @@ class BashHookEvaluationTestCases(EvaluatorBaseTest): # Then self.assertFalse(result.get('success')) self.assert_correct_output('Incorrect Answer', result.get('error')) - + def test_assert_with_hook(self): # Given user_answer = ("#!/bin/bash\n[[ $# -eq 2 ]]" @@ -393,7 +429,7 @@ class BashHookEvaluationTestCases(EvaluatorBaseTest): """) assert_test_case_args = "1 2\n2 1" - + hook_code = dedent("""\ def check_answer(user_answer): success = False @@ -402,13 +438,11 @@ class BashHookEvaluationTestCases(EvaluatorBaseTest): if "echo $(( $1 + $2 ))" in user_answer: success, err, mark_fraction = True, "", 1.0 return success, err, mark_fraction - """ - ) - + """) test_case_data = [{"test_case_type": "standardtestcase", "test_case": assert_test_case, - "test_case_args":assert_test_case_args, + "test_case_args": assert_test_case_args, 'weight': 1.0 }, {"test_case_type": "hooktestcase", @@ -420,8 +454,7 @@ class BashHookEvaluationTestCases(EvaluatorBaseTest): 'file_paths': self.file_paths, 'partial_grading': True, 'language': 'bash' - }, - 'test_case_data': test_case_data, + }, 'test_case_data': test_case_data, } # When @@ -438,7 +471,7 @@ class BashHookEvaluationTestCases(EvaluatorBaseTest): echo -n Hello, world! """ ) - + hook_code_1 = dedent("""\ def check_answer(user_answer): success = False @@ -447,8 +480,7 @@ class BashHookEvaluationTestCases(EvaluatorBaseTest): if "echo -n Hello, world!" in user_answer: success, err, mark_fraction = True, "", 0.5 return success, err, mark_fraction - """ - ) + """) hook_code_2 = dedent("""\ def check_answer(user_answer): import subprocess @@ -465,9 +497,7 @@ class BashHookEvaluationTestCases(EvaluatorBaseTest): if stdout.decode('utf-8') == "Hello, world!": success, err, mark_fraction = True, "", 1.0 return success, err, mark_fraction - """ - ) - + """) test_case_data = [{"test_case_type": "hooktestcase", "hook_code": hook_code_1, 'weight': 1.0}, @@ -480,8 +510,7 @@ class BashHookEvaluationTestCases(EvaluatorBaseTest): 'file_paths': self.file_paths, 'partial_grading': True, 'language': 'bash' - }, - 'test_case_data': test_case_data, + }, 'test_case_data': test_case_data, } # When @@ -491,7 +520,7 @@ class BashHookEvaluationTestCases(EvaluatorBaseTest): # Then self.assertTrue(result.get('success')) self.assertEqual(result.get("weight"), 1.5) - + def test_infinite_loop(self): # Given user_answer = ("#!/bin/bash\nwhile [ 1 ] ;" @@ -503,21 +532,19 @@ class BashHookEvaluationTestCases(EvaluatorBaseTest): success = False err = "Incorrect Answer" mark_fraction = 0.0 - proc = subprocess.Popen(user_answer, shell=True, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE - ) + proc = subprocess.Popen( + user_answer, shell=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE + ) stdout,stderr = proc.communicate() if stdout.decode("utf-8") == "Hello, world!": success, err, mark_fraction = True, "", 1.0 return success, err, mark_fraction - """ - ) + """) - test_case_data = [{"test_case_type": "hooktestcase", - "hook_code": hook_code,"weight": 1.0 - }] + "hook_code": hook_code, "weight": 1.0}] kwargs = { 'metadata': { @@ -525,8 +552,7 @@ class BashHookEvaluationTestCases(EvaluatorBaseTest): 'file_paths': self.file_paths, 'partial_grading': False, 'language': 'bash' - }, - 'test_case_data': test_case_data, + }, 'test_case_data': test_case_data, } # When diff --git a/yaksh/evaluator_tests/test_c_cpp_evaluation.py b/yaksh/evaluator_tests/test_c_cpp_evaluation.py index 162d90c..5b49671 100644 --- a/yaksh/evaluator_tests/test_c_cpp_evaluation.py +++ b/yaksh/evaluator_tests/test_c_cpp_evaluation.py @@ -8,8 +8,6 @@ from psutil import Process # Local import from yaksh.grader import Grader -from yaksh.cpp_code_evaluator import CppCodeEvaluator -from yaksh.cpp_stdio_evaluator import CppStdIOEvaluator from yaksh.evaluator_tests.test_python_evaluation import EvaluatorBaseTest from yaksh.settings import SERVER_TIMEOUT @@ -60,8 +58,8 @@ class CAssertionEvaluationTestCases(EvaluatorBaseTest): }] self.in_dir = tmp_in_dir_path self.timeout_msg = ("Code took more than {0} seconds to run. " - "You probably have an infinite loop in your" - " code.").format(SERVER_TIMEOUT) + "You probably have an infinite loop in your" + " code.").format(SERVER_TIMEOUT) self.file_paths = None def tearDown(self): @@ -77,8 +75,7 @@ class CAssertionEvaluationTestCases(EvaluatorBaseTest): 'file_paths': self.file_paths, 'partial_grading': False, 'language': 'cpp' - }, - 'test_case_data': self.test_case_data, + }, 'test_case_data': self.test_case_data, } # When @@ -97,8 +94,7 @@ class CAssertionEvaluationTestCases(EvaluatorBaseTest): 'file_paths': self.file_paths, 'partial_grading': False, 'language': 'cpp' - }, - 'test_case_data': self.test_case_data, + }, 'test_case_data': self.test_case_data, } # When @@ -120,8 +116,7 @@ class CAssertionEvaluationTestCases(EvaluatorBaseTest): 'file_paths': self.file_paths, 'partial_grading': False, 'language': 'cpp' - }, - 'test_case_data': self.test_case_data, + }, 'test_case_data': self.test_case_data, } # When @@ -141,8 +136,7 @@ class CAssertionEvaluationTestCases(EvaluatorBaseTest): 'file_paths': self.file_paths, 'partial_grading': False, 'language': 'cpp' - }, - 'test_case_data': self.test_case_data, + }, 'test_case_data': self.test_case_data, } # When @@ -211,8 +205,7 @@ class CAssertionEvaluationTestCases(EvaluatorBaseTest): 'file_paths': self.file_paths, 'partial_grading': False, 'language': 'cpp' - }, - 'test_case_data': self.test_case_data, + }, 'test_case_data': self.test_case_data, } # When @@ -222,6 +215,71 @@ class CAssertionEvaluationTestCases(EvaluatorBaseTest): # Then self.assertTrue(result.get('success')) + def test_incorrect_testcase(self): + # Given + self.tc_data = dedent(""" + #include <stdio.h> + #include <stdlib.h> + + extern int add(int, int); + + template <class T> + + void check(T expect, T result) + { + if (expect == result) + { + printf("Correct: Expected %d got %d ",expect,result); + } + else + { + printf("Incorrect: Expected %d got %d ",expect,result); + exit (1); + } + } + + int main(void) + { + int result; + result = add(0,0); + printf("Input submitted to the function: 0, 0"); + check(0, result); + result = add(2,3); + printf("Input submitted to the function: 2 3"); + check(5,result) + printf("All Correct"); + return 0; + } + """) + user_answer = dedent("""\ + int add(int a, int b) + { + return a+b; + }""") + self.test_case_data = [{"test_case": self.tc_data, + "test_case_type": "standardtestcase", + "weight": 0.0 + }] + kwargs = { + 'metadata': { + 'user_answer': user_answer, + 'file_paths': self.file_paths, + 'partial_grading': False, + 'language': 'cpp' + }, 'test_case_data': self.test_case_data, + } + + # When + grader = Grader(self.in_dir) + result = grader.evaluate(kwargs) + + # Then + err = result.get('error')[0] + lines_of_error = len(err.splitlines()) + self.assertFalse(result.get('success')) + self.assertTrue(lines_of_error > 1) + self.assertIn("Test case Error", err) + class CppStdIOEvaluationTestCases(EvaluatorBaseTest): def setUp(self): @@ -254,8 +312,7 @@ class CppStdIOEvaluationTestCases(EvaluatorBaseTest): 'file_paths': self.file_paths, 'partial_grading': False, 'language': 'cpp' - }, - 'test_case_data': self.test_case_data, + }, 'test_case_data': self.test_case_data, } # When @@ -287,8 +344,7 @@ class CppStdIOEvaluationTestCases(EvaluatorBaseTest): 'file_paths': self.file_paths, 'partial_grading': False, 'language': 'cpp' - }, - 'test_case_data': self.test_case_data, + }, 'test_case_data': self.test_case_data, } # When @@ -318,8 +374,7 @@ class CppStdIOEvaluationTestCases(EvaluatorBaseTest): 'file_paths': self.file_paths, 'partial_grading': False, 'language': 'cpp' - }, - 'test_case_data': self.test_case_data, + }, 'test_case_data': self.test_case_data, } # When @@ -343,8 +398,7 @@ class CppStdIOEvaluationTestCases(EvaluatorBaseTest): 'file_paths': self.file_paths, 'partial_grading': False, 'language': 'cpp' - }, - 'test_case_data': self.test_case_data, + }, 'test_case_data': self.test_case_data, } # When @@ -372,8 +426,7 @@ class CppStdIOEvaluationTestCases(EvaluatorBaseTest): 'file_paths': self.file_paths, 'partial_grading': False, 'language': 'cpp' - }, - 'test_case_data': self.test_case_data, + }, 'test_case_data': self.test_case_data, } # When @@ -398,8 +451,7 @@ class CppStdIOEvaluationTestCases(EvaluatorBaseTest): 'file_paths': self.file_paths, 'partial_grading': False, 'language': 'cpp' - }, - 'test_case_data': self.test_case_data, + }, 'test_case_data': self.test_case_data, } # When @@ -434,8 +486,7 @@ class CppStdIOEvaluationTestCases(EvaluatorBaseTest): 'file_paths': self.file_paths, 'partial_grading': False, 'language': 'cpp' - }, - 'test_case_data': self.test_case_data, + }, 'test_case_data': self.test_case_data, } # When @@ -461,8 +512,7 @@ class CppStdIOEvaluationTestCases(EvaluatorBaseTest): 'file_paths': self.file_paths, 'partial_grading': False, 'language': 'cpp' - }, - 'test_case_data': self.test_case_data, + }, 'test_case_data': self.test_case_data, } # When @@ -495,8 +545,7 @@ class CppStdIOEvaluationTestCases(EvaluatorBaseTest): 'file_paths': self.file_paths, 'partial_grading': False, 'language': 'cpp' - }, - 'test_case_data': self.test_case_data, + }, 'test_case_data': self.test_case_data, } # When @@ -527,8 +576,7 @@ class CppStdIOEvaluationTestCases(EvaluatorBaseTest): 'file_paths': self.file_paths, 'partial_grading': False, 'language': 'cpp' - }, - 'test_case_data': self.test_case_data, + }, 'test_case_data': self.test_case_data, } # When @@ -553,8 +601,7 @@ class CppStdIOEvaluationTestCases(EvaluatorBaseTest): 'file_paths': self.file_paths, 'partial_grading': False, 'language': 'cpp' - }, - 'test_case_data': self.test_case_data, + }, 'test_case_data': self.test_case_data, } # When @@ -583,8 +630,7 @@ class CppStdIOEvaluationTestCases(EvaluatorBaseTest): 'file_paths': self.file_paths, 'partial_grading': False, 'language': 'cpp' - }, - 'test_case_data': self.test_case_data, + }, 'test_case_data': self.test_case_data, } # When @@ -610,8 +656,7 @@ class CppStdIOEvaluationTestCases(EvaluatorBaseTest): 'file_paths': self.file_paths, 'partial_grading': False, 'language': 'cpp' - }, - 'test_case_data': self.test_case_data, + }, 'test_case_data': self.test_case_data, } # When @@ -644,8 +689,7 @@ class CppStdIOEvaluationTestCases(EvaluatorBaseTest): 'file_paths': self.file_paths, 'partial_grading': False, 'language': 'cpp' - }, - 'test_case_data': self.test_case_data, + }, 'test_case_data': self.test_case_data, } # When @@ -655,6 +699,7 @@ class CppStdIOEvaluationTestCases(EvaluatorBaseTest): # Then self.assertTrue(result.get('success')) + class CppHookEvaluationTestCases(EvaluatorBaseTest): def setUp(self): @@ -664,8 +709,8 @@ class CppHookEvaluationTestCases(EvaluatorBaseTest): tmp_in_dir_path = tempfile.mkdtemp() self.in_dir = tmp_in_dir_path self.timeout_msg = ("Code took more than {0} seconds to run. " - "You probably have an infinite loop in your" - " code.").format(SERVER_TIMEOUT) + "You probably have an infinite loop in your" + " code.").format(SERVER_TIMEOUT) self.file_paths = None def tearDown(self): @@ -703,20 +748,17 @@ class CppHookEvaluationTestCases(EvaluatorBaseTest): if stdout.decode("utf-8") == "Hello, world!": success, err, mark_fraction = True, "", 1.0 return success, err, mark_fraction - """ - ) + """) test_case_data = [{"test_case_type": "hooktestcase", - "hook_code": hook_code,"weight": 1.0 - }] + "hook_code": hook_code, "weight": 1.0}] kwargs = { 'metadata': { 'user_answer': user_answer, 'file_paths': self.file_paths, 'partial_grading': False, 'language': 'cpp' - }, - 'test_case_data': test_case_data, + }, 'test_case_data': test_case_data, } # When @@ -744,11 +786,11 @@ class CppHookEvaluationTestCases(EvaluatorBaseTest): err = "Incorrect Answer" mark_fraction = 0.0 def _run_command(cmd): - proc = subprocess.Popen("{}".format(cmd), - shell=True, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE - ) + proc = subprocess.Popen( + "{}".format(cmd), shell=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE + ) stdout,stderr = proc.communicate() return stdout,stderr cmds = ["gcc Test.c", "./a.out"] @@ -757,20 +799,17 @@ class CppHookEvaluationTestCases(EvaluatorBaseTest): if stdout.decode("utf-8") == "Hello, world!": success, err, mark_fraction = True, "", 1.0 return success, err, mark_fraction - """ - ) + """) test_case_data = [{"test_case_type": "hooktestcase", - "hook_code": hook_code,"weight": 1.0 - }] + "hook_code": hook_code, "weight": 1.0}] kwargs = { 'metadata': { 'user_answer': user_answer, 'file_paths': self.file_paths, 'partial_grading': False, 'language': 'cpp' - }, - 'test_case_data': test_case_data, + }, 'test_case_data': test_case_data, } # When @@ -780,46 +819,47 @@ class CppHookEvaluationTestCases(EvaluatorBaseTest): # Then self.assertFalse(result.get('success')) self.assert_correct_output('Incorrect Answer', result.get('error')) - + def test_assert_with_hook(self): # Given user_answer = "int add(int a, int b)\n{return a+b;}" - assert_test_case = dedent("""\ - #include <stdio.h> - #include <stdlib.h> - - extern int add(int, int); - - template <class T> - - void check(T expect, T result) - { - if (expect == result) - { - printf("Correct: Expected %d got %d ",expect,result); - } - else - { - printf("Incorrect: Expected %d got %d ",expect,result); - exit (1); - } - } - - int main(void) - { - int result; - result = add(0,0); - printf("Input submitted to the function: 0, 0"); - check(0, result); - result = add(2,3); - printf("Input submitted to the function: 2 3"); - check(5,result); - printf("All Correct"); - return 0; - } - """) + #include <stdio.h> + #include <stdlib.h> + + extern int add(int, int); + + template <class T> + + void check(T expect, T result) + { + if (expect == result) + { + printf("Correct: Expected %d got %d ", + expect,result); + } + else + { + printf("Incorrect: Expected %d got %d ", + expect,result); + exit (1); + } + } + + int main(void) + { + int result; + result = add(0,0); + printf("Input submitted 0, 0"); + check(0, result); + result = add(2,3); + printf("Input submitted 2 3"); + check(5,result); + printf("All Correct"); + return 0; + } + """) hook_code = dedent("""\ def check_answer(user_answer): @@ -829,9 +869,7 @@ class CppHookEvaluationTestCases(EvaluatorBaseTest): if "return a+b;" in user_answer: success, err, mark_fraction = True, "", 1.0 return success, err, mark_fraction - """ - ) - + """) test_case_data = [{"test_case_type": "standardtestcase", "test_case": assert_test_case, @@ -846,8 +884,7 @@ class CppHookEvaluationTestCases(EvaluatorBaseTest): 'file_paths': self.file_paths, 'partial_grading': True, 'language': 'cpp' - }, - 'test_case_data': test_case_data, + }, 'test_case_data': test_case_data, } # When @@ -867,7 +904,7 @@ class CppHookEvaluationTestCases(EvaluatorBaseTest): printf("Hello, world!"); } """) - + hook_code_1 = dedent("""\ def check_answer(user_answer): with open("Test.c", "w+") as f: @@ -877,11 +914,11 @@ class CppHookEvaluationTestCases(EvaluatorBaseTest): err = "Incorrect Answer" mark_fraction = 0.0 def _run_command(cmd): - proc = subprocess.Popen("{}".format(cmd), - shell=True, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE - ) + proc = subprocess.Popen( + "{}".format(cmd), shell=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE + ) stdout,stderr = proc.communicate() return stdout,stderr cmds = ["gcc Test.c", "./a.out"] @@ -890,8 +927,8 @@ class CppHookEvaluationTestCases(EvaluatorBaseTest): if stdout.decode("utf-8") == "Hello, world!": success, err, mark_fraction = True, "", 1.0 return success, err, mark_fraction - """ - ) + """) + hook_code_2 = dedent("""\ def check_answer(user_answer): success = False @@ -900,10 +937,7 @@ class CppHookEvaluationTestCases(EvaluatorBaseTest): if 'printf("Hello, world!");' in user_answer: success, err, mark_fraction = True, "", 0.5 return success, err, mark_fraction - """ - ) - - + """) test_case_data = [{"test_case_type": "hooktestcase", "hook_code": hook_code_1, 'weight': 1.0}, @@ -916,8 +950,7 @@ class CppHookEvaluationTestCases(EvaluatorBaseTest): 'file_paths': self.file_paths, 'partial_grading': True, 'language': 'cpp' - }, - 'test_case_data': test_case_data, + }, 'test_case_data': test_case_data, } # When @@ -927,7 +960,7 @@ class CppHookEvaluationTestCases(EvaluatorBaseTest): # Then self.assertTrue(result.get('success')) self.assertEqual(result.get("weight"), 1.5) - + def test_infinite_loop(self): # Given user_answer = dedent("""\ @@ -937,7 +970,7 @@ class CppHookEvaluationTestCases(EvaluatorBaseTest): printf("abc");} }""") - hook_code= dedent("""\ + hook_code = dedent("""\ def check_answer(user_answer): with open("Test.c", "w+") as f: f.write(user_answer) @@ -946,11 +979,11 @@ class CppHookEvaluationTestCases(EvaluatorBaseTest): err = "Incorrect Answer" mark_fraction = 0.0 def _run_command(cmd): - proc = subprocess.Popen("{}".format(cmd), - shell=True, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE - ) + proc = subprocess.Popen( + "{}".format(cmd), shell=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE + ) stdout,stderr = proc.communicate() return stdout,stderr cmds = ["gcc Test.c", "./a.out"] @@ -959,12 +992,10 @@ class CppHookEvaluationTestCases(EvaluatorBaseTest): if stdout.decode("utf-8") == "Hello, world!": success, err, mark_fraction = True, "", 1.0 return success, err, mark_fraction - """ - ) - + """) + test_case_data = [{"test_case_type": "hooktestcase", - "hook_code": hook_code,"weight": 1.0 - }] + "hook_code": hook_code, "weight": 1.0}] kwargs = { 'metadata': { @@ -972,8 +1003,7 @@ class CppHookEvaluationTestCases(EvaluatorBaseTest): 'file_paths': self.file_paths, 'partial_grading': False, 'language': 'cpp' - }, - 'test_case_data': test_case_data, + }, 'test_case_data': test_case_data, } # When diff --git a/yaksh/evaluator_tests/test_grader_evaluation.py b/yaksh/evaluator_tests/test_grader_evaluation.py index d11f4a0..6a05e19 100644 --- a/yaksh/evaluator_tests/test_grader_evaluation.py +++ b/yaksh/evaluator_tests/test_grader_evaluation.py @@ -1,9 +1,8 @@ from __future__ import unicode_literals import unittest -import os from yaksh import python_assertion_evaluator from yaksh.language_registry import _LanguageRegistry, get_registry -from yaksh.settings import SERVER_TIMEOUT, code_evaluators +from yaksh.settings import code_evaluators class RegistryTestCase(unittest.TestCase): @@ -11,32 +10,33 @@ class RegistryTestCase(unittest.TestCase): self.registry_object = get_registry() self.language_registry = _LanguageRegistry() assertion_evaluator_path = ("yaksh.python_assertion_evaluator" - ".PythonAssertionEvaluator" - ) + ".PythonAssertionEvaluator" + ) stdio_evaluator_path = ("yaksh.python_stdio_evaluator." - "PythonStdIOEvaluator" - ) + "PythonStdIOEvaluator" + ) hook_evaluator_path = ("yaksh.hook_evaluator." - "HookEvaluator" - ) + "HookEvaluator" + ) code_evaluators['python'] = \ - {"standardtestcase": assertion_evaluator_path, - "stdiobasedtestcase": stdio_evaluator_path, - "hooktestcase": hook_evaluator_path - } + {"standardtestcase": assertion_evaluator_path, + "stdiobasedtestcase": stdio_evaluator_path, + "hooktestcase": hook_evaluator_path + } def test_set_register(self): - evaluator_class = self.registry_object.get_class("python", - "standardtestcase" + evaluator_class = self.registry_object.get_class( + "python", "standardtestcase" ) - class_name = getattr(python_assertion_evaluator, - 'PythonAssertionEvaluator' + class_name = getattr( + python_assertion_evaluator, 'PythonAssertionEvaluator' ) self.assertEqual(evaluator_class, class_name) def tearDown(self): self.registry_object = None + if __name__ == '__main__': unittest.main() diff --git a/yaksh/evaluator_tests/test_java_evaluation.py b/yaksh/evaluator_tests/test_java_evaluation.py index 35b64d0..ab86dec 100644 --- a/yaksh/evaluator_tests/test_java_evaluation.py +++ b/yaksh/evaluator_tests/test_java_evaluation.py @@ -4,15 +4,12 @@ import os import shutil import tempfile from textwrap import dedent -from psutil import Process, pid_exists -import time +from psutil import Process # Local Import from yaksh import grader as gd from yaksh.grader import Grader -from yaksh.java_code_evaluator import JavaCodeEvaluator -from yaksh.java_stdio_evaluator import JavaStdIOEvaluator from yaksh.evaluator_tests.test_python_evaluation import EvaluatorBaseTest @@ -29,11 +26,13 @@ class JavaAssertionEvaluationTestCases(EvaluatorBaseTest): { if(result.equals(expect)) { - System.out.println("Correct:Output expected "+expect+" and got "+result); + System.out.println("Correct:Output expected "+expect+ + "and got "+result); } else { - System.out.println("Incorrect:Output expected "+expect+" but got "+result); + System.out.println("Incorrect:Output expected "+expect+ + "but got "+result); System.exit(1); } } @@ -43,15 +42,18 @@ class JavaAssertionEvaluationTestCases(EvaluatorBaseTest): int result, input, output; input = 0; output = 0; result = t.square_num(input); - System.out.println("Input submitted to the function: "+input); + System.out.println("Input submitted to the function: "+ + input); check(output, result); input = 5; output = 25; result = t.square_num(input); - System.out.println("Input submitted to the function: "+input); + System.out.println("Input submitted to the function: "+ + input); check(output, result); input = 6; output = 36; result = t.square_num(input); - System.out.println("Input submitted to the function: "+input); + System.out.println("Input submitted to the function: "+ + input); check(output, result); } } @@ -59,17 +61,16 @@ class JavaAssertionEvaluationTestCases(EvaluatorBaseTest): self.test_case_data = [ {"test_case": self.tc_data, - "test_case_type": "standardtestcase", - "weight": 0.0 - } + "test_case_type": "standardtestcase", + "weight": 0.0 + } ] self.in_dir = tmp_in_dir_path self.file_paths = None gd.SERVER_TIMEOUT = 9 self.timeout_msg = ("Code took more than {0} seconds to run. " - "You probably have an infinite loop in" - " your code.").format(gd.SERVER_TIMEOUT) - + "You probably have an infinite loop in" + " your code.").format(gd.SERVER_TIMEOUT) def tearDown(self): gd.SERVER_TIMEOUT = 4 @@ -78,15 +79,15 @@ class JavaAssertionEvaluationTestCases(EvaluatorBaseTest): def test_correct_answer(self): # Given - user_answer = "class Test {\n\tint square_num(int a) {\n\treturn a*a;\n\t}\n}" + user_answer = ("class Test {\n\tint square_num(int a)" + " {\n\treturn a*a;\n\t}\n}") kwargs = { 'metadata': { 'user_answer': user_answer, 'file_paths': self.file_paths, 'partial_grading': False, 'language': 'java' - }, - 'test_case_data': self.test_case_data, + }, 'test_case_data': self.test_case_data, } # When @@ -98,15 +99,15 @@ class JavaAssertionEvaluationTestCases(EvaluatorBaseTest): def test_incorrect_answer(self): # Given - user_answer = "class Test {\n\tint square_num(int a) {\n\treturn a;\n\t}\n}" + user_answer = ("class Test {\n\tint square_num(int a) " + "{\n\treturn a;\n\t}\n}") kwargs = { 'metadata': { 'user_answer': user_answer, 'file_paths': self.file_paths, 'partial_grading': False, 'language': 'java' - }, - 'test_case_data': self.test_case_data, + }, 'test_case_data': self.test_case_data, } # When @@ -122,15 +123,14 @@ class JavaAssertionEvaluationTestCases(EvaluatorBaseTest): def test_error(self): # Given - user_answer = "class Test {\n\tint square_num(int a) {\n\treturn a*a" + user_answer = "class Test {\n\tint square_num(int a) {\n\treturn a*a}" kwargs = { 'metadata': { 'user_answer': user_answer, 'file_paths': self.file_paths, 'partial_grading': False, 'language': 'java' - }, - 'test_case_data': self.test_case_data, + }, 'test_case_data': self.test_case_data, } # When @@ -143,15 +143,15 @@ class JavaAssertionEvaluationTestCases(EvaluatorBaseTest): def test_infinite_loop(self): # Given - user_answer = "class Test {\n\tint square_num(int a) {\n\t\twhile(0==0){\n\t\t}\n\t}\n}" + user_answer = ("class Test {\n\tint square_num(int a)" + " {\n\t\twhile(0==0){\n\t\t}\n\t}\n}") kwargs = { 'metadata': { 'user_answer': user_answer, 'file_paths': self.file_paths, 'partial_grading': False, 'language': 'java' - }, - 'test_case_data': self.test_case_data, + }, 'test_case_data': self.test_case_data, } # When @@ -178,11 +178,13 @@ class JavaAssertionEvaluationTestCases(EvaluatorBaseTest): { if(result.equals(expect)) { - System.out.println("Correct:Output expected "+expect+" and got "+result); + System.out.println("Correct:Output expected "+expect+ + " and got "+result); } else { - System.out.println("Incorrect:Output expected "+expect+" but got "+result); + System.out.println("Incorrect:Output expected "+expect+ + " but got "+result); System.exit(1); } } @@ -201,9 +203,9 @@ class JavaAssertionEvaluationTestCases(EvaluatorBaseTest): """) self.test_case_data = [ {"test_case": self.tc_data, - "test_case_type": "standardtestcase", - "weight": 0.0 - } + "test_case_type": "standardtestcase", + "weight": 0.0 + } ] user_answer = dedent(""" import java.io.BufferedReader; @@ -229,8 +231,7 @@ class JavaAssertionEvaluationTestCases(EvaluatorBaseTest): 'file_paths': self.file_paths, 'partial_grading': False, 'language': 'java' - }, - 'test_case_data': self.test_case_data, + }, 'test_case_data': self.test_case_data, } # When @@ -240,6 +241,64 @@ class JavaAssertionEvaluationTestCases(EvaluatorBaseTest): # Then self.assertTrue(result.get("success")) + def test_incorrect_testcase(self): + # Given + self.tc_data = dedent(""" + class main + { + public static <E> void check(E expect, E result) + { + if(result.equals(expect)) + { + System.out.println("Correct:Output expected "+expect+ + "and got "+result); + } + else + { + System.out.println("Incorrect:Output expected "+expect+ + "but got "+result); + System.exit(1); + } + } + public static void main(String arg[]) + { + Test t = new Test(); + int result, input, output; + input = 0; output = 0; + result = t.square_num(input); + System.out.println("Input submitted to the function: "+ + input); + check(output, result) + } + } + """) + user_answer = ("class Test {\n\tint square_num(int a) " + "{\n\treturn a;\n\t}\n}") + self.test_case_data = [{"test_case": self.tc_data, + "test_case_type": "standardtestcase", + "weight": 0.0 + }] + kwargs = { + 'metadata': { + 'user_answer': user_answer, + 'file_paths': self.file_paths, + 'partial_grading': False, + 'language': 'java' + }, 'test_case_data': self.test_case_data, + } + + # When + grader = Grader(self.in_dir) + result = grader.evaluate(kwargs) + + # Then + err = result.get('error')[0] + lines_of_error = len(err.splitlines()) + self.assertFalse(result.get('success')) + self.assertTrue(lines_of_error > 1) + self.assertIn("Test case Error", err) + + class JavaStdIOEvaluationTestCases(EvaluatorBaseTest): def setUp(self): self.f_path = os.path.join(tempfile.gettempdir(), "test.txt") @@ -251,7 +310,7 @@ class JavaStdIOEvaluationTestCases(EvaluatorBaseTest): 'expected_input': '5\n6', 'test_case_type': 'stdiobasedtestcase', 'weight': 0.0 - }] + }] self.file_paths = None gd.SERVER_TIMEOUT = 9 self.timeout_msg = ("Code took more than {0} seconds to run. " @@ -280,8 +339,7 @@ class JavaStdIOEvaluationTestCases(EvaluatorBaseTest): 'file_paths': self.file_paths, 'partial_grading': False, 'language': 'java' - }, - 'test_case_data': self.test_case_data, + }, 'test_case_data': self.test_case_data, } # When @@ -314,8 +372,7 @@ class JavaStdIOEvaluationTestCases(EvaluatorBaseTest): 'file_paths': self.file_paths, 'partial_grading': False, 'language': 'java' - }, - 'test_case_data': self.test_case_data, + }, 'test_case_data': self.test_case_data, } # When @@ -342,8 +399,7 @@ class JavaStdIOEvaluationTestCases(EvaluatorBaseTest): 'file_paths': self.file_paths, 'partial_grading': False, 'language': 'java' - }, - 'test_case_data': self.test_case_data, + }, 'test_case_data': self.test_case_data, } # When @@ -370,8 +426,7 @@ class JavaStdIOEvaluationTestCases(EvaluatorBaseTest): 'file_paths': self.file_paths, 'partial_grading': False, 'language': 'java' - }, - 'test_case_data': self.test_case_data, + }, 'test_case_data': self.test_case_data, } # When @@ -397,8 +452,7 @@ class JavaStdIOEvaluationTestCases(EvaluatorBaseTest): 'file_paths': self.file_paths, 'partial_grading': False, 'language': 'java' - }, - 'test_case_data': self.test_case_data, + }, 'test_case_data': self.test_case_data, } # When @@ -420,7 +474,7 @@ class JavaStdIOEvaluationTestCases(EvaluatorBaseTest): self.test_case_data = [{'expected_output': '11', 'test_case_type': 'stdiobasedtestcase', 'weight': 0.0 - }] + }] user_answer = dedent(""" class Test {public static void main(String[] args){ @@ -434,8 +488,7 @@ class JavaStdIOEvaluationTestCases(EvaluatorBaseTest): 'file_paths': self.file_paths, 'partial_grading': False, 'language': 'java' - }, - 'test_case_data': self.test_case_data, + }, 'test_case_data': self.test_case_data, } # When @@ -451,7 +504,7 @@ class JavaStdIOEvaluationTestCases(EvaluatorBaseTest): 'expected_input': 'Hello\nWorld', 'test_case_type': 'stdiobasedtestcase', 'weight': 0.0 - }] + }] user_answer = dedent(""" import java.util.Scanner; class Test @@ -467,8 +520,7 @@ class JavaStdIOEvaluationTestCases(EvaluatorBaseTest): 'file_paths': self.file_paths, 'partial_grading': False, 'language': 'java' - }, - 'test_case_data': self.test_case_data, + }, 'test_case_data': self.test_case_data, } # When @@ -485,7 +537,7 @@ class JavaStdIOEvaluationTestCases(EvaluatorBaseTest): 'expected_input': '', 'test_case_type': 'stdiobasedtestcase', 'weight': 0.0 - }] + }] user_answer = dedent(""" import java.io.BufferedReader; import java.io.FileReader; @@ -510,8 +562,7 @@ class JavaStdIOEvaluationTestCases(EvaluatorBaseTest): 'file_paths': self.file_paths, 'partial_grading': False, 'language': 'java' - }, - 'test_case_data': self.test_case_data, + }, 'test_case_data': self.test_case_data, } # When @@ -533,8 +584,8 @@ class JavaHookEvaluationTestCases(EvaluatorBaseTest): self.file_paths = None gd.SERVER_TIMEOUT = 9 self.timeout_msg = ("Code took more than {0} seconds to run. " - "You probably have an infinite loop in" - " your code.").format(gd.SERVER_TIMEOUT) + "You probably have an infinite loop in" + " your code.").format(gd.SERVER_TIMEOUT) def tearDown(self): gd.SERVER_TIMEOUT = 4 @@ -558,11 +609,11 @@ class JavaHookEvaluationTestCases(EvaluatorBaseTest): err = "Incorrect Answer" mark_fraction = 0.0 def _run_command(cmd): - proc = subprocess.Popen("{}".format(cmd), - shell=True, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE - ) + proc = subprocess.Popen( + "{}".format(cmd), shell=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE + ) stdout,stderr = proc.communicate() return stdout,stderr cmds = ["javac Test.java", "java Test"] @@ -571,20 +622,18 @@ class JavaHookEvaluationTestCases(EvaluatorBaseTest): if stdout.decode("utf-8") == "Hello, world!": success, err, mark_fraction = True, "", 1.0 return success, err, mark_fraction - """ - ) + """) test_case_data = [{"test_case_type": "hooktestcase", - "hook_code": hook_code,"weight": 1.0 - }] + "hook_code": hook_code, "weight": 1.0 + }] kwargs = { 'metadata': { 'user_answer': user_answer, 'file_paths': self.file_paths, 'partial_grading': False, 'language': 'java' - }, - 'test_case_data': test_case_data, + }, 'test_case_data': test_case_data, } # When @@ -624,20 +673,18 @@ class JavaHookEvaluationTestCases(EvaluatorBaseTest): if stdout.decode("utf-8") == "Hello, world!": success, err, mark_fraction = True, "", 1.0 return success, err, mark_fraction - """ - ) + """) test_case_data = [{"test_case_type": "hooktestcase", - "hook_code": hook_code,"weight": 1.0 - }] + "hook_code": hook_code, "weight": 1.0 + }] kwargs = { 'metadata': { 'user_answer': user_answer, 'file_paths': self.file_paths, 'partial_grading': False, 'language': 'java' - }, - 'test_case_data': test_case_data, + }, 'test_case_data': test_case_data, } # When @@ -647,10 +694,11 @@ class JavaHookEvaluationTestCases(EvaluatorBaseTest): # Then self.assertFalse(result.get('success')) self.assert_correct_output('Incorrect Answer', result.get('error')) - + def test_assert_with_hook(self): # Given - user_answer = "class Test {\n\tint square_num(int a) {\n\treturn a*a;\n\t}\n}" + user_answer = ("class Test {\n\tint square_num(int a)" + " {\n\treturn a*a;\n\t}\n}") assert_test_case = dedent(""" class main { @@ -658,11 +706,13 @@ class JavaHookEvaluationTestCases(EvaluatorBaseTest): { if(result.equals(expect)) { - System.out.println("Correct:Output expected "+expect+" and got "+result); + System.out.println("Correct:Output expected "+expect+ + " and got "+result); } else { - System.out.println("Incorrect:Output expected "+expect+" but got "+result); + System.out.println("Incorrect:Output expected "+expect+ + " but got "+result); System.exit(1); } } @@ -672,20 +722,23 @@ class JavaHookEvaluationTestCases(EvaluatorBaseTest): int result, input, output; input = 0; output = 0; result = t.square_num(input); - System.out.println("Input submitted to the function: "+input); + System.out.println("Input submitted to the function: "+ + input); check(output, result); input = 5; output = 25; result = t.square_num(input); - System.out.println("Input submitted to the function: "+input); + System.out.println("Input submitted to the function: "+ + input); check(output, result); input = 6; output = 36; result = t.square_num(input); - System.out.println("Input submitted to the function: "+input); + System.out.println("Input submitted to the function: "+ + input); check(output, result); } } """) - + hook_code = dedent("""\ def check_answer(user_answer): success = False @@ -694,9 +747,7 @@ class JavaHookEvaluationTestCases(EvaluatorBaseTest): if "return a*a" in user_answer: success, err, mark_fraction = True, "", 1.0 return success, err, mark_fraction - """ - ) - + """) test_case_data = [{"test_case_type": "standardtestcase", "test_case": assert_test_case, @@ -711,8 +762,7 @@ class JavaHookEvaluationTestCases(EvaluatorBaseTest): 'file_paths': self.file_paths, 'partial_grading': True, 'language': 'java' - }, - 'test_case_data': test_case_data, + }, 'test_case_data': test_case_data, } # When @@ -731,7 +781,7 @@ class JavaHookEvaluationTestCases(EvaluatorBaseTest): System.out.print("Hello, world!"); }} """) - + hook_code_1 = dedent("""\ def check_answer(user_answer): with open("Test.java", "w+") as f: @@ -741,11 +791,11 @@ class JavaHookEvaluationTestCases(EvaluatorBaseTest): err = "Incorrect Answer" mark_fraction = 0.0 def _run_command(cmd): - proc = subprocess.Popen("{}".format(cmd), - shell=True, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE - ) + proc = subprocess.Popen( + "{}".format(cmd), shell=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE + ) stdout,stderr = proc.communicate() return stdout,stderr cmds = ["javac Test.java", "java Test"] @@ -754,20 +804,18 @@ class JavaHookEvaluationTestCases(EvaluatorBaseTest): if stdout.decode("utf-8") == "Hello, world!": success, err, mark_fraction = True, "", 1.0 return success, err, mark_fraction - """ - ) + """) hook_code_2 = dedent("""\ def check_answer(user_answer): success = False err = "Incorrect Answer" mark_fraction = 0.0 - if 'System.out.print("Hello, world!");' in user_answer: + if ('System.out.print("Hello, world!");' in + user_answer): success, err, mark_fraction = True, "", 0.5 return success, err, mark_fraction - """ - ) - + """) test_case_data = [{"test_case_type": "hooktestcase", "hook_code": hook_code_1, 'weight': 1.0}, @@ -780,8 +828,7 @@ class JavaHookEvaluationTestCases(EvaluatorBaseTest): 'file_paths': self.file_paths, 'partial_grading': True, 'language': 'java' - }, - 'test_case_data': test_case_data, + }, 'test_case_data': test_case_data, } # When @@ -791,7 +838,7 @@ class JavaHookEvaluationTestCases(EvaluatorBaseTest): # Then self.assertTrue(result.get('success')) self.assertEqual(result.get("weight"), 1.5) - + def test_infinite_loop(self): # Given user_answer = dedent("""\ @@ -811,11 +858,11 @@ class JavaHookEvaluationTestCases(EvaluatorBaseTest): err = "Incorrect Answer" mark_fraction = 0.0 def _run_command(cmd): - proc = subprocess.Popen("{}".format(cmd), - shell=True, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE - ) + proc = subprocess.Popen( + "{}".format(cmd), shell=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE + ) stdout,stderr = proc.communicate() return stdout,stderr cmds = ["javac Test.java", "java Test"] @@ -824,13 +871,11 @@ class JavaHookEvaluationTestCases(EvaluatorBaseTest): if stdout.decode("utf-8") == "Hello, world!": success, err, mark_fraction = True, "", 1.0 return success, err, mark_fraction - """ - ) + """) - test_case_data = [{"test_case_type": "hooktestcase", - "hook_code": hook_code,"weight": 1.0 - }] + "hook_code": hook_code, "weight": 1.0 + }] kwargs = { 'metadata': { @@ -838,8 +883,7 @@ class JavaHookEvaluationTestCases(EvaluatorBaseTest): 'file_paths': self.file_paths, 'partial_grading': False, 'language': 'java' - }, - 'test_case_data': test_case_data, + }, 'test_case_data': test_case_data, } # When diff --git a/yaksh/evaluator_tests/test_python_evaluation.py b/yaksh/evaluator_tests/test_python_evaluation.py index 71d7732..1933d17 100644 --- a/yaksh/evaluator_tests/test_python_evaluation.py +++ b/yaksh/evaluator_tests/test_python_evaluation.py @@ -1,7 +1,6 @@ from __future__ import unicode_literals import unittest import os -import sys import tempfile import shutil from textwrap import dedent @@ -26,13 +25,13 @@ class PythonAssertionEvaluationTestCases(EvaluatorBaseTest): self.in_dir = tmp_in_dir_path self.test_case_data = [{"test_case_type": "standardtestcase", "test_case": 'assert(add(1,2)==3)', - 'weight': 0.0}, + 'weight': 0.0}, {"test_case_type": "standardtestcase", "test_case": 'assert(add(-1,2)==1)', - 'weight': 0.0}, + 'weight': 0.0}, {"test_case_type": "standardtestcase", "test_case": 'assert(add(-1,-2)==-3)', - 'weight': 0.0}, + 'weight': 0.0}, ] self.timeout_msg = ("Code took more than {0} seconds to run. " "You probably have an infinite loop in" @@ -46,14 +45,12 @@ class PythonAssertionEvaluationTestCases(EvaluatorBaseTest): def test_correct_answer(self): # Given user_answer = "def add(a,b):\n\treturn a + b" - kwargs = { - 'metadata': { - 'user_answer': user_answer, - 'file_paths': self.file_paths, - 'partial_grading': False, - 'language': 'python' - }, - 'test_case_data': self.test_case_data, + kwargs = {'metadata': { + 'user_answer': user_answer, + 'file_paths': self.file_paths, + 'partial_grading': False, + 'language': 'python'}, + 'test_case_data': self.test_case_data, } # When @@ -66,14 +63,12 @@ class PythonAssertionEvaluationTestCases(EvaluatorBaseTest): def test_incorrect_answer(self): # Given user_answer = "def add(a,b):\n\treturn a - b" - kwargs = { - 'metadata': { - 'user_answer': user_answer, - 'file_paths': self.file_paths, - 'partial_grading': False, - 'language': 'python' - }, - 'test_case_data': self.test_case_data, + kwargs = {'metadata': { + 'user_answer': user_answer, + 'file_paths': self.file_paths, + 'partial_grading': False, + 'language': 'python'}, + 'test_case_data': self.test_case_data, } # When @@ -85,13 +80,13 @@ class PythonAssertionEvaluationTestCases(EvaluatorBaseTest): given_test_case_list = [tc["test_case"] for tc in self.test_case_data] for error in result.get("error"): self.assertEqual(error['exception'], 'AssertionError') - self.assertEqual(error['message'], - "Expected answer from the test case did not match the output" - ) + self.assertEqual( + error['message'], + "Expected answer from the test case did not match the output" + ) error_testcase_list = [tc['test_case'] for tc in result.get('error')] self.assertEqual(error_testcase_list, given_test_case_list) - def test_partial_incorrect_answer(self): # Given user_answer = "def add(a,b):\n\treturn abs(a) + abs(b)" @@ -100,19 +95,17 @@ class PythonAssertionEvaluationTestCases(EvaluatorBaseTest): 'weight': 1.0}, {"test_case_type": "standardtestcase", "test_case": 'assert(add(-1,-2)==-3)', - 'weight': 1.0}, + 'weight': 1.0}, {"test_case_type": "standardtestcase", "test_case": 'assert(add(1,2)==3)', 'weight': 2.0} ] - kwargs = { - 'metadata': { - 'user_answer': user_answer, - 'file_paths': self.file_paths, - 'partial_grading': True, - 'language': 'python' - }, - 'test_case_data': test_case_data, + kwargs = {'metadata': { + 'user_answer': user_answer, + 'file_paths': self.file_paths, + 'partial_grading': True, + 'language': 'python'}, + 'test_case_data': test_case_data, } # When @@ -126,22 +119,22 @@ class PythonAssertionEvaluationTestCases(EvaluatorBaseTest): given_test_case_list.remove('assert(add(1,2)==3)') for error in result.get("error"): self.assertEqual(error['exception'], 'AssertionError') - self.assertEqual(error['message'], - "Expected answer from the test case did not match the output" - ) + self.assertEqual( + error['message'], + "Expected answer from the test case did not match the output" + ) error_testcase_list = [tc['test_case'] for tc in result.get('error')] self.assertEqual(error_testcase_list, given_test_case_list) + def test_infinite_loop(self): # Given user_answer = "def add(a, b):\n\twhile True:\n\t\tpass" - kwargs = { - 'metadata': { - 'user_answer': user_answer, - 'file_paths': self.file_paths, - 'partial_grading': False, - 'language': 'python' - }, - 'test_case_data': self.test_case_data, + kwargs = {'metadata': { + 'user_answer': user_answer, + 'file_paths': self.file_paths, + 'partial_grading': False, + 'language': 'python'}, + 'test_case_data': self.test_case_data, } # When @@ -168,14 +161,12 @@ class PythonAssertionEvaluationTestCases(EvaluatorBaseTest): "SyntaxError", "invalid syntax" ] - kwargs = { - 'metadata': { - 'user_answer': user_answer, - 'file_paths': self.file_paths, - 'partial_grading': False, - 'language': 'python' - }, - 'test_case_data': self.test_case_data, + kwargs = {'metadata': { + 'user_answer': user_answer, + 'file_paths': self.file_paths, + 'partial_grading': False, + 'language': 'python'}, + 'test_case_data': self.test_case_data, } # When @@ -201,14 +192,12 @@ class PythonAssertionEvaluationTestCases(EvaluatorBaseTest): "IndentationError", "indented block" ] - kwargs = { - 'metadata': { - 'user_answer': user_answer, - 'file_paths': self.file_paths, - 'partial_grading': False, - 'language': 'python' - }, - 'test_case_data': self.test_case_data, + kwargs = {'metadata': { + 'user_answer': user_answer, + 'file_paths': self.file_paths, + 'partial_grading': False, + 'language': 'python'}, + 'test_case_data': self.test_case_data, } # When @@ -220,9 +209,9 @@ class PythonAssertionEvaluationTestCases(EvaluatorBaseTest): self.assertFalse(result.get("success")) self.assertEqual(5, len(err)) for msg in indent_error_msg: - self.assert_correct_output(msg, - result.get("error")[0]['traceback'] - ) + self.assert_correct_output( + msg, result.get("error")[0]['traceback'] + ) def test_name_error(self): # Given @@ -234,15 +223,13 @@ class PythonAssertionEvaluationTestCases(EvaluatorBaseTest): "defined" ] - kwargs = { - 'metadata': { - 'user_answer': user_answer, - 'file_paths': self.file_paths, - 'partial_grading': False, - 'language': 'python' - }, - 'test_case_data': self.test_case_data, - } + kwargs = {'metadata': { + 'user_answer': user_answer, + 'file_paths': self.file_paths, + 'partial_grading': False, + 'language': 'python'}, + 'test_case_data': self.test_case_data, + } # When grader = Grader(self.in_dir) @@ -258,15 +245,13 @@ class PythonAssertionEvaluationTestCases(EvaluatorBaseTest): return add(3, 3) """) recursion_error_msg = "maximum recursion depth exceeded" - kwargs = { - 'metadata': { - 'user_answer': user_answer, - 'file_paths': self.file_paths, - 'partial_grading': False, - 'language': 'python' - }, - 'test_case_data': self.test_case_data, - } + kwargs = {'metadata': { + 'user_answer': user_answer, + 'file_paths': self.file_paths, + 'partial_grading': False, + 'language': 'python'}, + 'test_case_data': self.test_case_data, + } # When grader = Grader(self.in_dir) @@ -289,15 +274,13 @@ class PythonAssertionEvaluationTestCases(EvaluatorBaseTest): "argument" ] - kwargs = { - 'metadata': { - 'user_answer': user_answer, - 'file_paths': self.file_paths, - 'partial_grading': False, - 'language': 'python' - }, - 'test_case_data': self.test_case_data, - } + kwargs = {'metadata': { + 'user_answer': user_answer, + 'file_paths': self.file_paths, + 'partial_grading': False, + 'language': 'python'}, + 'test_case_data': self.test_case_data, + } # When grader = Grader(self.in_dir) @@ -323,25 +306,26 @@ class PythonAssertionEvaluationTestCases(EvaluatorBaseTest): "base" ] - kwargs = { - 'metadata': { - 'user_answer': user_answer, - 'file_paths': self.file_paths, - 'partial_grading': False, - 'language': 'python' - }, - 'test_case_data': self.test_case_data, - } + kwargs = {'metadata': { + 'user_answer': user_answer, + 'file_paths': self.file_paths, + 'partial_grading': False, + 'language': 'python'}, + 'test_case_data': self.test_case_data, + } # When grader = Grader(self.in_dir) result = grader.evaluate(kwargs) - err = result.get("error")[0]['traceback'] + errors = result.get("error") # Then self.assertFalse(result.get("success")) for msg in value_error_msg: - self.assert_correct_output(msg, err) + self.assert_correct_output(msg, errors[0]['traceback']) + for index, error in enumerate(errors): + self.assertEqual(error['test_case'], + self.test_case_data[index]['test_case']) def test_file_based_assert(self): # Given @@ -356,15 +340,13 @@ class PythonAssertionEvaluationTestCases(EvaluatorBaseTest): return f.read()[0] """) - kwargs = { - 'metadata': { - 'user_answer': user_answer, - 'file_paths': self.file_paths, - 'partial_grading': False, - 'language': 'python' - }, - 'test_case_data': self.test_case_data, - } + kwargs = {'metadata': { + 'user_answer': user_answer, + 'file_paths': self.file_paths, + 'partial_grading': False, + 'language': 'python'}, + 'test_case_data': self.test_case_data, + } # When grader = Grader(self.in_dir) @@ -390,25 +372,23 @@ class PythonAssertionEvaluationTestCases(EvaluatorBaseTest): ] kwargs = {'metadata': { - 'user_answer': user_answer, - 'file_paths': self.file_paths, - 'partial_grading': False, - 'language': 'python' - }, - 'test_case_data': test_case_data, - } + 'user_answer': user_answer, + 'file_paths': self.file_paths, + 'partial_grading': False, + 'language': 'python'}, + 'test_case_data': test_case_data, + } # When grader = Grader(self.in_dir) result = grader.evaluate(kwargs) - err = result.get("error")[0]['traceback'] + err = result.get("error")[0]['traceback'] # Then self.assertFalse(result.get("success")) for msg in syntax_error_msg: self.assert_correct_output(msg, err) - def test_multiple_testcase_error(self): """ Tests the user answer with an correct test case first and then with an incorrect test case """ @@ -418,7 +398,8 @@ class PythonAssertionEvaluationTestCases(EvaluatorBaseTest): "test_case": 'assert(palindrome("abba")==True)', "weight": 0.0}, {"test_case_type": "standardtestcase", - "test_case": 's="abbb"\nassert palindrome(S)==False', + "test_case": 's="abbb"\n' + 'assert palindrome(S)==False', "weight": 0.0} ] name_error_msg = ["Traceback", @@ -426,15 +407,13 @@ class PythonAssertionEvaluationTestCases(EvaluatorBaseTest): "NameError", "name 'S' is not defined" ] - kwargs = { - 'metadata': { - 'user_answer': user_answer, - 'file_paths': self.file_paths, - 'partial_grading': False, - 'language': 'python' - }, - 'test_case_data': test_case_data, - } + kwargs = {'metadata': { + 'user_answer': user_answer, + 'file_paths': self.file_paths, + 'partial_grading': False, + 'language': 'python'}, + 'test_case_data': test_case_data, + } # When grader = Grader(self.in_dir) @@ -454,18 +433,15 @@ class PythonAssertionEvaluationTestCases(EvaluatorBaseTest): return type(a) """) test_case_data = [{"test_case_type": "standardtestcase", - "test_case": 'assert(strchar("hello")==str)', - "weight": 0.0 - },] - kwargs = { - 'metadata': { - 'user_answer': user_answer, - 'file_paths': self.file_paths, - 'partial_grading': False, - 'language': 'python' - }, - 'test_case_data': test_case_data, - } + "test_case": 'assert(strchar("hello")==str)', + "weight": 0.0}] + kwargs = {'metadata': { + 'user_answer': user_answer, + 'file_paths': self.file_paths, + 'partial_grading': False, + 'language': 'python'}, + 'test_case_data': test_case_data, + } # When grader = Grader(self.in_dir) result = grader.evaluate(kwargs) @@ -473,6 +449,31 @@ class PythonAssertionEvaluationTestCases(EvaluatorBaseTest): # Then self.assertTrue(result.get("success")) + def test_incorrect_answer_with_nose_assert(self): + user_answer = dedent("""\ + def add(a, b): + return a - b + """) + test_case_data = [{"test_case_type": "standardtestcase", + "test_case": 'assert_equal(add(1, 2), 3)', + "weight": 0.0}] + kwargs = {'metadata': { + 'user_answer': user_answer, + 'file_paths': self.file_paths, + 'partial_grading': False, + 'language': 'python'}, + 'test_case_data': test_case_data, + } + # When + grader = Grader(self.in_dir) + result = grader.evaluate(kwargs) + + # Then + self.assertFalse(result.get("success")) + error = result.get("error")[0] + self.assertEqual(error['exception'], 'AssertionError') + self.assertEqual(error['message'], '-1 != 3') + class PythonStdIOEvaluationTestCases(EvaluatorBaseTest): def setUp(self): @@ -501,13 +502,12 @@ class PythonStdIOEvaluationTestCases(EvaluatorBaseTest): """ ) kwargs = {'metadata': { - 'user_answer': user_answer, - 'file_paths': self.file_paths, - 'partial_grading': False, - 'language': 'python' - }, - 'test_case_data': self.test_case_data - } + 'user_answer': user_answer, + 'file_paths': self.file_paths, + 'partial_grading': False, + 'language': 'python'}, + 'test_case_data': self.test_case_data + } # When grader = Grader(self.in_dir) @@ -534,13 +534,12 @@ class PythonStdIOEvaluationTestCases(EvaluatorBaseTest): ) kwargs = {'metadata': { - 'user_answer': user_answer, - 'file_paths': self.file_paths, - 'partial_grading': False, - 'language': 'python' - }, - 'test_case_data': self.test_case_data - } + 'user_answer': user_answer, + 'file_paths': self.file_paths, + 'partial_grading': False, + 'language': 'python'}, + 'test_case_data': self.test_case_data + } # When grader = Grader(self.in_dir) @@ -551,11 +550,13 @@ class PythonStdIOEvaluationTestCases(EvaluatorBaseTest): def test_correct_answer_string(self): # Given - self.test_case_data = [{"test_case_type": "stdiobasedtestcase", - "expected_input": ("the quick brown fox jumps over the lazy dog\nthe"), - "expected_output": "2", - "weight": 0.0 - }] + self.test_case_data = [{ + "test_case_type": "stdiobasedtestcase", + "expected_input": ("the quick brown fox jumps over " + "the lazy dog\nthe"), + "expected_output": "2", + "weight": 0.0 + }] user_answer = dedent(""" from six.moves import input a = str(input()) @@ -565,13 +566,12 @@ class PythonStdIOEvaluationTestCases(EvaluatorBaseTest): ) kwargs = {'metadata': { - 'user_answer': user_answer, - 'file_paths': self.file_paths, - 'partial_grading': False, - 'language': 'python' - }, - 'test_case_data': self.test_case_data - } + 'user_answer': user_answer, + 'file_paths': self.file_paths, + 'partial_grading': False, + 'language': 'python'}, + 'test_case_data': self.test_case_data + } # When grader = Grader(self.in_dir) @@ -594,13 +594,12 @@ class PythonStdIOEvaluationTestCases(EvaluatorBaseTest): """ ) kwargs = {'metadata': { - 'user_answer': user_answer, - 'file_paths': self.file_paths, - 'partial_grading': False, - 'language': 'python' - }, - 'test_case_data': self.test_case_data - } + 'user_answer': user_answer, + 'file_paths': self.file_paths, + 'partial_grading': False, + 'language': 'python'}, + 'test_case_data': self.test_case_data + } # When grader = Grader(self.in_dir) @@ -629,13 +628,12 @@ class PythonStdIOEvaluationTestCases(EvaluatorBaseTest): """ ) kwargs = {'metadata': { - 'user_answer': user_answer, - 'file_paths': self.file_paths, - 'partial_grading': False, - 'language': 'python' - }, - 'test_case_data': self.test_case_data - } + 'user_answer': user_answer, + 'file_paths': self.file_paths, + 'partial_grading': False, + 'language': 'python'}, + 'test_case_data': self.test_case_data + } # When grader = Grader(self.in_dir) @@ -646,24 +644,24 @@ class PythonStdIOEvaluationTestCases(EvaluatorBaseTest): def test_infinite_loop(self): # Given - self.test_case_data = [{"test_case_type": "stdiobasedtestcase", - "expected_input": "1\n2", - "expected_output": "3", - "weight": 0.0 - }] + self.test_case_data = [{ + "test_case_type": "stdiobasedtestcase", + "expected_input": "1\n2", + "expected_output": "3", + "weight": 0.0 + }] timeout_msg = ("Code took more than {0} seconds to run. " - "You probably have an infinite loop in" - " your code.").format(SERVER_TIMEOUT) + "You probably have an infinite loop in" + " your code.").format(SERVER_TIMEOUT) user_answer = "while True:\n\tpass" kwargs = {'metadata': { - 'user_answer': user_answer, - 'file_paths': self.file_paths, - 'partial_grading': False, - 'language': 'python' - }, - 'test_case_data': self.test_case_data - } + 'user_answer': user_answer, + 'file_paths': self.file_paths, + 'partial_grading': False, + 'language': 'python'}, + 'test_case_data': self.test_case_data + } # When grader = Grader(self.in_dir) @@ -675,7 +673,6 @@ class PythonStdIOEvaluationTestCases(EvaluatorBaseTest): ) self.assertFalse(result.get('success')) - def test_unicode_literal_bug(self): # Given user_answer = dedent("""\ @@ -687,21 +684,44 @@ class PythonStdIOEvaluationTestCases(EvaluatorBaseTest): "expected_output": "str", "weight": 0.0 }] - kwargs = { - 'metadata': { - 'user_answer': user_answer, - 'file_paths': self.file_paths, - 'partial_grading': False, - 'language': 'python' - }, - 'test_case_data': test_case_data, - } + kwargs = {'metadata': { + 'user_answer': user_answer, + 'file_paths': self.file_paths, + 'partial_grading': False, + 'language': 'python'}, + 'test_case_data': test_case_data, + } # When grader = Grader(self.in_dir) result = grader.evaluate(kwargs) # Then self.assertTrue(result.get("success")) + def test_get_error_lineno(self): + user_answer = dedent("""\ + print(1/0) + """) + test_case_data = [{"test_case_type": "stdiobasedtestcase", + "expected_input": "", + "expected_output": "1", + "weight": 0.0 + }] + kwargs = {'metadata': { + 'user_answer': user_answer, + 'file_paths': self.file_paths, + 'partial_grading': False, + 'language': 'python'}, + 'test_case_data': test_case_data, + } + # When + grader = Grader(self.in_dir) + result = grader.evaluate(kwargs) + # Then + self.assertFalse(result.get("success")) + error = result.get("error")[0] + self.assertEqual(error['line_no'], 1) + self.assertEqual(error['exception'], "ZeroDivisionError") + class PythonHookEvaluationTestCases(EvaluatorBaseTest): @@ -733,19 +753,17 @@ class PythonHookEvaluationTestCases(EvaluatorBaseTest): success, err, mark_fraction = True, "", 1.0 return success, err, mark_fraction """ - ) + ) test_case_data = [{"test_case_type": "hooktestcase", - "hook_code": hook_code,"weight": 1.0 - }] - kwargs = { - 'metadata': { - 'user_answer': user_answer, - 'file_paths': self.file_paths, - 'partial_grading': True, - 'language': 'python' - }, - 'test_case_data': test_case_data, + "hook_code": hook_code, "weight": 1.0 + }] + kwargs = {'metadata': { + 'user_answer': user_answer, + 'file_paths': self.file_paths, + 'partial_grading': True, + 'language': 'python'}, + 'test_case_data': test_case_data, } # When @@ -768,20 +786,18 @@ class PythonHookEvaluationTestCases(EvaluatorBaseTest): success, err, mark_fraction = True, "", 1.0 return success, err, mark_fraction """ - ) + ) test_case_data = [{"test_case_type": "hooktestcase", - "hook_code": hook_code,"weight": 1.0 - }] - - kwargs = { - 'metadata': { - 'user_answer': user_answer, - 'file_paths': self.file_paths, - 'partial_grading': False, - 'language': 'python' - }, - 'test_case_data': test_case_data, + "hook_code": hook_code, "weight": 1.0 + }] + + kwargs = {'metadata': { + 'user_answer': user_answer, + 'file_paths': self.file_paths, + 'partial_grading': False, + 'language': 'python'}, + 'test_case_data': test_case_data, } # When @@ -805,21 +821,19 @@ class PythonHookEvaluationTestCases(EvaluatorBaseTest): success, err, mark_fraction = True, "", 1.0 return success, err, mark_fraction """ - ) + ) test_case_data = [{"test_case_type": "standardtestcase", "test_case": assert_test_case, 'weight': 1.0}, {"test_case_type": "hooktestcase", "hook_code": hook_code, 'weight': 1.0}, ] - kwargs = { - 'metadata': { - 'user_answer': user_answer, - 'file_paths': self.file_paths, - 'partial_grading': True, - 'language': 'python' - }, - 'test_case_data': test_case_data, + kwargs = {'metadata': { + 'user_answer': user_answer, + 'file_paths': self.file_paths, + 'partial_grading': True, + 'language': 'python'}, + 'test_case_data': test_case_data, } # When @@ -842,7 +856,7 @@ class PythonHookEvaluationTestCases(EvaluatorBaseTest): success, err, mark_fraction = True, "", 0.5 return success, err, mark_fraction """ - ) + ) hook_code_2 = dedent("""\ def check_answer(user_answer): success = False @@ -853,22 +867,19 @@ class PythonHookEvaluationTestCases(EvaluatorBaseTest): success, err, mark_fraction = True, "", 1.0 return success, err, mark_fraction """ - ) - + ) test_case_data = [{"test_case_type": "hooktestcase", "hook_code": hook_code_1, 'weight': 1.0}, {"test_case_type": "hooktestcase", "hook_code": hook_code_2, 'weight': 1.0}, ] - kwargs = { - 'metadata': { - 'user_answer': user_answer, - 'file_paths': self.file_paths, - 'partial_grading': True, - 'language': 'python' - }, - 'test_case_data': test_case_data, + kwargs = {'metadata': { + 'user_answer': user_answer, + 'file_paths': self.file_paths, + 'partial_grading': True, + 'language': 'python'}, + 'test_case_data': test_case_data, } # When @@ -892,19 +903,18 @@ class PythonHookEvaluationTestCases(EvaluatorBaseTest): success, err, mark_fraction = True, "", 1.0 return success, err, mark_fraction """ - ) + ) + test_case_data = [{"test_case_type": "hooktestcase", - "hook_code": hook_code,"weight": 1.0 - }] - - kwargs = { - 'metadata': { - 'user_answer': user_answer, - 'file_paths': self.file_paths, - 'partial_grading': False, - 'language': 'python' - }, - 'test_case_data': test_case_data, + "hook_code": hook_code, "weight": 1.0 + }] + + kwargs = {'metadata': { + 'user_answer': user_answer, + 'file_paths': self.file_paths, + 'partial_grading': False, + 'language': 'python'}, + 'test_case_data': test_case_data, } # When @@ -931,19 +941,18 @@ class PythonHookEvaluationTestCases(EvaluatorBaseTest): success, err, mark_fraction = True, "", 1.0 return success, err, mark_fraction """ - ) + ) + test_case_data = [{"test_case_type": "hooktestcase", - "hook_code": hook_code,"weight": 1.0 - }] - kwargs = { - 'metadata': { - 'user_answer': user_answer, - 'file_paths': self.file_paths, - 'assign_files': [(self.tmp_file, False)], - 'partial_grading': False, - 'language': 'python' - }, - 'test_case_data': test_case_data, + "hook_code": hook_code, "weight": 1.0 + }] + kwargs = {'metadata': { + 'user_answer': user_answer, + 'file_paths': self.file_paths, + 'assign_files': [(self.tmp_file, False)], + 'partial_grading': False, + 'language': 'python'}, + 'test_case_data': test_case_data, } # When @@ -953,5 +962,6 @@ class PythonHookEvaluationTestCases(EvaluatorBaseTest): # Then self.assertTrue(result.get('success')) + if __name__ == '__main__': unittest.main() diff --git a/yaksh/evaluator_tests/test_python_stdio_evaluator.py b/yaksh/evaluator_tests/test_python_stdio_evaluator.py index 9b8d702..db2fd69 100644 --- a/yaksh/evaluator_tests/test_python_stdio_evaluator.py +++ b/yaksh/evaluator_tests/test_python_stdio_evaluator.py @@ -1,5 +1,6 @@ from yaksh.error_messages import compare_outputs + def test_compare_outputs(): exp = "5\n5\n" given = "5\n5\n" @@ -26,8 +27,8 @@ def test_compare_outputs(): success, msg = compare_outputs(given, exp) error_msg = msg.get('error_msg') assert not success - m = ("Incorrect Answer: We had expected 1 number of lines. " - + "We got 2 number of lines.") + m = ("Incorrect Answer: We had expected 1 number of lines. " + + "We got 2 number of lines.") assert m == error_msg exp = "5\n5\n" diff --git a/yaksh/evaluator_tests/test_scilab_evaluation.py b/yaksh/evaluator_tests/test_scilab_evaluation.py index f7a9925..d3f1dc8 100644 --- a/yaksh/evaluator_tests/test_scilab_evaluation.py +++ b/yaksh/evaluator_tests/test_scilab_evaluation.py @@ -6,12 +6,12 @@ import tempfile from psutil import Process from textwrap import dedent -#Local Import +# Local Import from yaksh import grader as gd from yaksh.grader import Grader -from yaksh.scilab_code_evaluator import ScilabCodeEvaluator from yaksh.evaluator_tests.test_python_evaluation import EvaluatorBaseTest + class ScilabEvaluationTestCases(EvaluatorBaseTest): def setUp(self): tmp_in_dir_path = tempfile.mkdtemp() @@ -54,7 +54,7 @@ class ScilabEvaluationTestCases(EvaluatorBaseTest): self.file_paths = None gd.SERVER_TIMEOUT = 9 self.timeout_msg = ("Code took more than {0} seconds to run. " - "You probably have an infinite loop" + "You probably have an infinite loop" " in your code.").format(gd.SERVER_TIMEOUT) def tearDown(self): @@ -63,15 +63,14 @@ class ScilabEvaluationTestCases(EvaluatorBaseTest): def test_correct_answer(self): user_answer = ("funcprot(0)\nfunction[c]=add(a,b)" - "\n\tc=a+b;\nendfunction") + "\n\tc=a+b;\nendfunction") kwargs = { 'metadata': { 'user_answer': user_answer, 'file_paths': self.file_paths, 'partial_grading': False, 'language': 'scilab' - }, - 'test_case_data': self.test_case_data, + }, 'test_case_data': self.test_case_data, } grader = Grader(self.in_dir) @@ -81,15 +80,14 @@ class ScilabEvaluationTestCases(EvaluatorBaseTest): def test_error(self): user_answer = ("funcprot(0)\nfunction[c]=add(a,b)" - "\n\tc=a+b;\ndis(\tendfunction") + "\n\tc=a+b;\ndis(\tendfunction") kwargs = { 'metadata': { 'user_answer': user_answer, 'file_paths': self.file_paths, 'partial_grading': False, 'language': 'scilab' - }, - 'test_case_data': self.test_case_data, + }, 'test_case_data': self.test_case_data, } grader = Grader(self.in_dir) @@ -98,18 +96,16 @@ class ScilabEvaluationTestCases(EvaluatorBaseTest): self.assertFalse(result.get("success")) self.assert_correct_output('error', result.get("error")) - def test_incorrect_answer(self): user_answer = ("funcprot(0)\nfunction[c]=add(a,b)" - "\n\tc=a-b;\nendfunction") + "\n\tc=a-b;\nendfunction") kwargs = { 'metadata': { 'user_answer': user_answer, 'file_paths': self.file_paths, 'partial_grading': False, 'language': 'scilab' - }, - 'test_case_data': self.test_case_data, + }, 'test_case_data': self.test_case_data, } grader = Grader(self.in_dir) @@ -122,15 +118,14 @@ class ScilabEvaluationTestCases(EvaluatorBaseTest): def test_infinite_loop(self): user_answer = ("funcprot(0)\nfunction[c]=add(a,b)" - "\n\tc=a;\nwhile(1==1)\nend\nendfunction") + "\n\tc=a;\nwhile(1==1)\nend\nendfunction") kwargs = { 'metadata': { 'user_answer': user_answer, 'file_paths': self.file_paths, 'partial_grading': False, 'language': 'scilab' - }, - 'test_case_data': self.test_case_data, + }, 'test_case_data': self.test_case_data, } grader = Grader(self.in_dir) diff --git a/yaksh/evaluator_tests/test_simple_question_types.py b/yaksh/evaluator_tests/test_simple_question_types.py index b86a9d8..f7a6cf6 100644 --- a/yaksh/evaluator_tests/test_simple_question_types.py +++ b/yaksh/evaluator_tests/test_simple_question_types.py @@ -1,61 +1,67 @@ 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 + AnswerPaper, Answer, Course, IntegerTestCase, FloatTestCase,\ + StringTestCase, McqTestCase, ArrangeTestCase def setUpModule(): - # create user profile + # Create user profile + # Create User 1 user = User.objects.create_user(username='demo_user_100', password='demo', email='demo@test.com') + Profile.objects.create(user=user, roll_number=1, institute='IIT', department='Aerospace', position='Student') + # Create User 2 + user2 = User.objects.create_user( + username='demo_user_101', password='demo', + email='demo@test.com') + + Profile.objects.create(user=user2, roll_number=2, + institute='IIT', department='Aerospace', + position='Student') + + # Create a course + Course.objects.create(name="Python Course 100", + enrollment="Enroll Request", creator=user) + + 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, pass_criteria=0, + description='demo quiz 100', + instructions="Demo Instructions", + creator=user + ) + QuestionPaper.objects.create(quiz=quiz, total_marks=1.0) - # create a course - course = Course.objects.create(name="Python Course 100", - enrollment="Enroll Request", creator=user) - - 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 100', - pass_criteria=0, - instructions="Demo Instructions" - ) - question_paper = QuestionPaper.objects.create(quiz=quiz, - total_marks=1.0) - - answerpaper = AnswerPaper.objects.create(user=user, 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="demo_user_100").delete() + User.objects.filter(username__in=["demo_user_100", "demo_user_101"])\ + .delete() + class IntegerQuestionTestCases(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) - #Creating User + # Creating User self.user = User.objects.get(username='demo_user_100') - #Creating Question + # Creating Question self.question1 = Question.objects.create(summary='int1', points=1, type='code', user=self.user) self.question1.language = 'python' @@ -64,22 +70,26 @@ class IntegerQuestionTestCases(unittest.TestCase): self.question1.description = 'sum of 12+13?' self.question1.save() - #Creating answerpaper - self.answerpaper = AnswerPaper.objects.get(question_paper\ - =self.question_paper) - self.answerpaper.attempt_number = 1 + # 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, course=self.course, + end_time=timezone.now()+timedelta(minutes=5), attempt_number=1 + ) self.answerpaper.questions.add(self.question1) self.answerpaper.save() - # For question + # For question self.integer_based_testcase = IntegerTestCase(question=self.question1, correct=25, - type = 'integertestcase', + type='integertestcase', ) self.integer_based_testcase.save() @classmethod def tearDownClass(self): - self.question1.delete() + self.question1.delete() + self.answerpaper.delete() def test_validate_regrade_integer_correct_answer(self): # Given @@ -100,12 +110,10 @@ class IntegerQuestionTestCases(unittest.TestCase): self.assertTrue(result['success']) # Regrade - # Given - self.answer.correct = True - self.answer.marks = 1 - - self.answer.answer = 200 - self.answer.save() + # Given + regrade_answer = Answer.objects.get(id=self.answer.id) + regrade_answer.answer = 200 + regrade_answer.save() # When details = self.answerpaper.regrade(self.question1.id) @@ -113,11 +121,11 @@ 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) - def test_validate_regrade_integer_incorrect_answer(self): # Given integer_answer = 26 @@ -137,12 +145,10 @@ class IntegerQuestionTestCases(unittest.TestCase): self.assertFalse(result['success']) # Regrade - # Given - self.answer.correct = True - self.answer.marks = 1 - - self.answer.answer = 25 - self.answer.save() + # Given + regrade_answer = Answer.objects.get(id=self.answer.id) + regrade_answer.answer = 25 + regrade_answer.save() # When details = self.answerpaper.regrade(self.question1.id) @@ -150,6 +156,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) @@ -158,13 +165,15 @@ class IntegerQuestionTestCases(unittest.TestCase): class StringQuestionTestCases(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) - #Creating User + # Creating User self.user = User.objects.get(username='demo_user_100') - #Creating Question + # Creating Question self.question1 = Question.objects.create(summary='str1', points=1, type='code', user=self.user) self.question1.language = 'python' @@ -181,37 +190,41 @@ class StringQuestionTestCases(unittest.TestCase): self.question2.description = 'Write Hello, EARTH!' self.question2.save() - #Creating answerpaper - self.answerpaper = AnswerPaper.objects.get(question_paper\ - =self.question_paper) - self.answerpaper.attempt_number = 1 + # 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, course=self.course, + end_time=timezone.now()+timedelta(minutes=5), attempt_number=1 + ) self.answerpaper.questions.add(*[self.question1, self.question2]) self.answerpaper.save() - # For question + # For question self.lower_string_testcase = StringTestCase(question=self.question1, correct="Hello, EARTH!", string_check="lower", - type = 'stringtestcase', + type='stringtestcase', ) self.lower_string_testcase.save() self.exact_string_testcase = StringTestCase(question=self.question2, correct="Hello, EARTH!", string_check="exact", - type = 'stringtestcase', + type='stringtestcase', ) self.exact_string_testcase.save() @classmethod def tearDownClass(self): - self.question1.delete() - self.question2.delete() + self.question1.delete() + self.question2.delete() + self.answerpaper.delete() def test_validate_regrade_case_insensitive_string_correct_answer(self): # Given string_answer = "hello, earth!" - answer = Answer(question=self.question1,answer=string_answer) + answer = Answer(question=self.question1, answer=string_answer) answer.save() self.answerpaper.answers.add(answer) @@ -224,19 +237,18 @@ class StringQuestionTestCases(unittest.TestCase): self.assertTrue(result['success']) # Regrade - # Given - answer.correct = True - answer.marks = 1 - - answer.answer = "hello, mars!" - answer.save() + # Given + regrade_answer = Answer.objects.get(id=answer.id) + regrade_answer.answer = "hello, mars!" + regrade_answer.save() # When details = self.answerpaper.regrade(self.question1.id) # Then - answer = self.answerpaper.answers.filter(question=self.question1)\ - .last() + 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) @@ -244,7 +256,7 @@ class StringQuestionTestCases(unittest.TestCase): def test_validate_regrade_case_insensitive_string_incorrect_answer(self): # Given string_answer = "hello, mars!" - answer = Answer(question=self.question1,answer=string_answer) + answer = Answer(question=self.question1, answer=string_answer) answer.save() self.answerpaper.answers.add(answer) @@ -258,19 +270,18 @@ class StringQuestionTestCases(unittest.TestCase): self.assertFalse(result['success']) # Regrade - # Given - answer.correct = True - answer.marks = 1 - - answer.answer = "hello, earth!" - answer.save() + # Given + regrade_answer = Answer.objects.get(id=answer.id) + regrade_answer.answer = "hello, earth!" + regrade_answer.save() # When details = self.answerpaper.regrade(self.question1.id) # Then - answer = self.answerpaper.answers.filter(question=self.question1)\ - .last() + 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) @@ -278,7 +289,7 @@ class StringQuestionTestCases(unittest.TestCase): def test_validate_regrade_case_sensitive_string_correct_answer(self): # Given string_answer = "Hello, EARTH!" - answer = Answer(question=self.question2,answer=string_answer) + answer = Answer(question=self.question2, answer=string_answer) answer.save() self.answerpaper.answers.add(answer) @@ -291,19 +302,18 @@ class StringQuestionTestCases(unittest.TestCase): self.assertTrue(result['success']) # Regrade - # Given - answer.correct = True - answer.marks = 1 - - answer.answer = "hello, earth!" - answer.save() + # Given + regrade_answer = Answer.objects.get(id=answer.id) + regrade_answer.answer = "hello, earth!" + regrade_answer.save() # When details = self.answerpaper.regrade(self.question2.id) # Then - answer = self.answerpaper.answers.filter(question=self.question2)\ - .last() + 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) @@ -311,7 +321,7 @@ class StringQuestionTestCases(unittest.TestCase): def test_case_sensitive_string_incorrect_answer(self): # Given string_answer = "hello, earth!" - answer = Answer(question=self.question2,answer=string_answer) + answer = Answer(question=self.question2, answer=string_answer) answer.save() self.answerpaper.answers.add(answer) @@ -325,19 +335,18 @@ class StringQuestionTestCases(unittest.TestCase): self.assertFalse(result['success']) # Regrade - # Given - answer.correct = True - answer.marks = 1 - - answer.answer = "Hello, EARTH!" - answer.save() + # Given + regrade_answer = Answer.objects.get(id=answer.id) + regrade_answer.answer = "Hello, EARTH!" + regrade_answer.save() # When details = self.answerpaper.regrade(self.question2.id) # Then - answer = self.answerpaper.answers.filter(question=self.question2)\ - .last() + 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) @@ -346,14 +355,16 @@ class StringQuestionTestCases(unittest.TestCase): class FloatQuestionTestCases(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) - #Creating User + # Creating User self.user = User.objects.get(username='demo_user_100') - #Creating Question + # Creating Question self.question1 = Question.objects.create(summary='flt1', points=1, type='code', user=self.user) self.question1.language = 'python' @@ -361,23 +372,28 @@ class FloatQuestionTestCases(unittest.TestCase): self.question1.test_case_type = 'floattestcase' self.question1.save() - #Creating answerpaper - self.answerpaper = AnswerPaper.objects.get(question_paper\ - =self.question_paper) - self.answerpaper.attempt_number = 1 + # 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, course=self.course, + end_time=timezone.now()+timedelta(minutes=5), attempt_number=1, + ) + self.answerpaper.questions.add(self.question1) self.answerpaper.save() - # For question + # For question self.float_based_testcase = FloatTestCase(question=self.question1, correct=100, error_margin=0.1, - type = 'floattestcase', + type='floattestcase', ) self.float_based_testcase.save() @classmethod def tearDownClass(self): - self.question1.delete() + self.question1.delete() + self.answerpaper.delete() def test_validate_regrade_float_correct_answer(self): # Given @@ -397,13 +413,11 @@ class FloatQuestionTestCases(unittest.TestCase): # Then self.assertTrue(result['success']) - # Regrade - # Given - self.answer.correct = True - self.answer.marks = 1 - - self.answer.answer = 0.0 - self.answer.save() + # Regrade with wrong answer + # Given + 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) @@ -411,6 +425,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) @@ -434,12 +449,270 @@ class FloatQuestionTestCases(unittest.TestCase): self.assertFalse(result['success']) # Regrade - # Given - self.answer.correct = True - self.answer.marks = 1 + # Given + 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() + + 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', course=self.course, + start_time=timezone.now(), question_paper=self.question_paper, + end_time=timezone.now()+timedelta(minutes=5), attempt_number=1 + ) + 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) - self.answer.answer = 99.9 + 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) @@ -447,6 +720,112 @@ 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) + + +class MCQShuffleTestCases(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() + + 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) + answerpaper3.delete() diff --git a/yaksh/file_utils.py b/yaksh/file_utils.py index b178eeb..6c3fd5d 100644 --- a/yaksh/file_utils.py +++ b/yaksh/file_utils.py @@ -4,6 +4,7 @@ import zipfile import tempfile import csv + def copy_files(file_paths): """ Copy Files to current directory, takes tuple with file paths and extract status""" @@ -65,4 +66,3 @@ def is_csv(document): except (csv.Error, UnicodeDecodeError): return False, None return True, dialect - diff --git a/yaksh/forms.py b/yaksh/forms.py index 258a1ee..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 = ( @@ -85,10 +86,12 @@ class UserRegisterForm(forms.Form): department = forms.CharField( max_length=64, help_text='Department you work/study at') position = forms.CharField( - max_length=64, help_text='Student/Faculty/Researcher/Industry/etc.') + max_length=64, + help_text='Student/Faculty/Researcher/Industry/Fellowship/etc.') timezone = forms.ChoiceField( choices=[(tz, tz) for tz in pytz.common_timezones], - initial=pytz.utc) + help_text='Course timings are shown based on the selected timezone', + initial=pytz.country_timezones['IN'][0]) def clean_username(self): u_name = self.cleaned_data["username"] @@ -279,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): @@ -308,7 +311,7 @@ class UploadFileForm(forms.Form): class QuestionPaperForm(forms.ModelForm): class Meta: model = QuestionPaper - fields = ['shuffle_questions'] + fields = ['shuffle_questions', 'shuffle_testcases'] class LessonForm(forms.ModelForm): diff --git a/yaksh/grader.py b/yaksh/grader.py index 38cce8d..320e7e7 100644 --- a/yaksh/grader.py +++ b/yaksh/grader.py @@ -1,22 +1,12 @@ #!/usr/bin/env python from __future__ import unicode_literals import sys -import pwd import os -import stat import contextlib -from os.path import isdir, dirname, abspath, join, isfile, exists +from os.path import dirname, abspath import signal import traceback -from multiprocessing import Process, Queue -import subprocess -import re -try: - from SimpleXMLRPCServer import SimpleXMLRPCServer -except ImportError: - # The above import will not work on Python-3.x. - from xmlrpc.server import SimpleXMLRPCServer # Local imports from .settings import SERVER_TIMEOUT @@ -26,11 +16,13 @@ from .error_messages import prettify_exceptions MY_DIR = abspath(dirname(__file__)) registry = None + # Raised when the code times-out. # c.f. http://pguides.net/python/timeout-a-function class TimeoutException(Exception): pass + @contextlib.contextmanager def change_dir(path): cur_dir = abspath(dirname(MY_DIR)) @@ -75,7 +67,6 @@ class Grader(object): self.timeout_msg = msg self.in_dir = in_dir if in_dir else MY_DIR - def evaluate(self, kwargs): """Evaluates given code with the test cases based on given arguments in test_case_data. @@ -122,7 +113,6 @@ class Grader(object): test_case_instances.append(test_case_instance) return test_case_instances - def safe_evaluate(self, test_case_instances): """ Handles code evaluation along with compilation, signal handling @@ -131,7 +121,9 @@ class Grader(object): # Add a new signal handler for the execution of this code. prev_handler = create_signal_handler() success = False - test_case_success_status = [False] * len(test_case_instances) + test_case_success_status = [False] + if len(test_case_instances) != 0: + test_case_success_status = [False] * len(test_case_instances) error = [] weight = 0.0 @@ -155,20 +147,24 @@ class Grader(object): test_case_instance.teardown() except TimeoutException: - error.append(prettify_exceptions("TimeoutException", - self.timeout_msg - ) - ) - except Exception: + error.append( + prettify_exceptions("TimeoutException", self.timeout_msg) + ) + except Exception as e: exc_type, exc_value, exc_tb = sys.exc_info() tb_list = traceback.format_exception(exc_type, exc_value, exc_tb) + try: + line_no = e.lineno + except AttributeError: + line_no = traceback.extract_tb(exc_tb)[-1][1] if len(tb_list) > 2: del tb_list[1:3] - error.append(prettify_exceptions(exc_type.__name__, - str(exc_value), - "".join(tb_list), - ) - ) + error.append( + prettify_exceptions( + exc_type.__name__, str(exc_value), "".join(tb_list), + line_no=line_no + ) + ) finally: # Set back any original signal handler. set_original_signal_handler(prev_handler) diff --git a/yaksh/hook_evaluator.py b/yaksh/hook_evaluator.py index 41ef6e4..33c1549 100644 --- a/yaksh/hook_evaluator.py +++ b/yaksh/hook_evaluator.py @@ -40,18 +40,19 @@ class HookEvaluator(BaseEvaluator): -------- Returns a tuple (success, error, test_case_weight) - success - Boolean, indicating if code was executed successfully, correctly + success - Boolean, indicating if code was executed successfully, + correctly mark_fraction - Float, indicating fraction of the weight to a test case error - String, error message if success is false - returns (True, "Correct answer", 1.0) : If the student script passes all - test cases/have same output, when compared to the instructor script + returns (True, "Correct answer", 1.0) : If the student script passes + all test cases/have same output, when compared to the instructor script returns (False, error_msg, 0.0): If the student script fails a single test/have dissimilar output, when compared to the instructor script. - Returns (False, error_msg, 0.0): If mandatory arguments are not files or if - the required permissions are not given to the file(s). + Returns (False, error_msg, 0.0): If mandatory arguments are not files + or if the required permissions are not given to the file(s). """ if self.file_paths: self.files = copy_files(self.file_paths) @@ -84,8 +85,8 @@ class HookEvaluator(BaseEvaluator): del tb_list[1:3] err = prettify_exceptions(exc_type.__name__, str(exc_value), - "Error in Hook Code:\n" - + "".join(tb_list) + "Error in Hook Code:\n" + "".join( + tb_list) ) return success, err, mark_fraction diff --git a/yaksh/java_code_evaluator.py b/yaksh/java_code_evaluator.py index e6dc628..5f2288d 100644 --- a/yaksh/java_code_evaluator.py +++ b/yaksh/java_code_evaluator.py @@ -1,11 +1,8 @@ #!/usr/bin/env python from __future__ import unicode_literals -import traceback -import pwd import os -from os.path import join, isfile +from os.path import isfile import subprocess -import importlib # Local imports from .base_evaluator import BaseEvaluator @@ -62,8 +59,8 @@ class JavaCodeEvaluator(BaseEvaluator): # create student code and moderator code file self.submit_code_path = self.create_submit_code_file('Test.java') self.test_code_path = self.create_submit_code_file('main.java') - self.write_to_submit_code_file(self.submit_code_path, - self.user_answer + self.write_to_submit_code_file( + self.submit_code_path, self.user_answer ) self.write_to_submit_code_file(self.test_code_path, self.test_case) clean_ref_code_path = self.test_code_path @@ -78,11 +75,11 @@ class JavaCodeEvaluator(BaseEvaluator): user_code_directory = os.getcwd() + '/' ref_file_name = (clean_ref_code_path.split('/')[-1]).split('.')[0] - self.user_output_path = self.set_file_paths(user_code_directory, - 'Test' + self.user_output_path = self.set_file_paths( + user_code_directory, 'Test' ) - self.ref_output_path = self.set_file_paths(user_code_directory, - ref_file_name + self.ref_output_path = self.set_file_paths( + user_code_directory, ref_file_name ) compile_command, self.compile_main = self.get_commands( clean_ref_code_path, @@ -93,13 +90,15 @@ class JavaCodeEvaluator(BaseEvaluator): ref_file_name ) - self.compiled_user_answer = self._run_command(compile_command, + self.compiled_user_answer = self._run_command( + compile_command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE ) - self.compiled_test_code = self._run_command(self.compile_main, + self.compiled_test_code = self._run_command( + self.compile_main, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE @@ -141,9 +140,9 @@ class JavaCodeEvaluator(BaseEvaluator): if main_err == '': ret = self._run_command(self.run_command_args, shell=True, - stdin=None, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE) + stdin=None, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE) proc, stdout, stderr = ret if proc.returncode == 0: success, err = True, None @@ -151,7 +150,7 @@ class JavaCodeEvaluator(BaseEvaluator): else: err = stdout + "\n" + stderr else: - err = "Error:" + err = "Test case Error:" try: error_lines = main_err.splitlines() for e in error_lines: @@ -159,7 +158,7 @@ class JavaCodeEvaluator(BaseEvaluator): err = err + "\n" + e.split(":", 1)[1] else: err = err + "\n" + e - except: + except Exception: err = err + "\n" + main_err else: err = "Compilation Error:" @@ -170,7 +169,7 @@ class JavaCodeEvaluator(BaseEvaluator): err = err + "\n" + e.split(":", 1)[1] else: err = err + "\n" + e - except: + except Exception: err = err + "\n" + stdnt_stderr return success, err, mark_fraction diff --git a/yaksh/java_stdio_evaluator.py b/yaksh/java_stdio_evaluator.py index 4e9238f..0504177 100644 --- a/yaksh/java_stdio_evaluator.py +++ b/yaksh/java_stdio_evaluator.py @@ -4,7 +4,7 @@ import subprocess import os from os.path import isfile -#Local imports +# Local imports from .stdio_evaluator import StdIOEvaluator from .file_utils import copy_files, delete_files @@ -22,7 +22,7 @@ class JavaStdIOEvaluator(StdIOEvaluator): # Set test case data values self.expected_input = test_case_data.get('expected_input') self.expected_output = test_case_data.get('expected_output') - self.weight = test_case_data.get('weight') + self.weight = test_case_data.get('weight') def teardown(self): os.remove(self.submit_code_path) @@ -84,7 +84,7 @@ class JavaStdIOEvaluator(StdIOEvaluator): err = err + "\n" + e.split(":", 1)[1] else: err = err + "\n" + e - except: + except Exception: err = err + "\n" + stdnt_stderr mark_fraction = 1.0 if self.partial_grading and success else 0.0 return success, err, mark_fraction diff --git a/yaksh/language_registry.py b/yaksh/language_registry.py index 994e9ed..ec5dae9 100644 --- a/yaksh/language_registry.py +++ b/yaksh/language_registry.py @@ -1,26 +1,29 @@ from __future__ import unicode_literals import importlib -import json -import six +import six # Local imports from .settings import code_evaluators registry = None - + + def get_registry(): global registry if registry is None: registry = _LanguageRegistry() return registry + def create_evaluator_instance(metadata, test_case): """Create instance of relevant EvaluateCode class based on language""" registry = get_registry() - cls = registry.get_class(metadata.get('language'), test_case.get('test_case_type')) + cls = registry.get_class(metadata.get('language'), + test_case.get('test_case_type')) instance = cls(metadata, test_case) return instance + class _LanguageRegistry(object): def __init__(self): self._register = {} @@ -33,7 +36,6 @@ class _LanguageRegistry(object): if not self._register.get(language): self._register[language] = code_evaluators.get(language) test_case_register = self._register[language] - cls = test_case_register.get(test_case_type) module_name, class_name = cls.rsplit(".", 1) # load the module, will raise ImportError if module cannot be loaded @@ -45,4 +47,3 @@ class _LanguageRegistry(object): def register(self, language, class_names): """ Register a new code evaluator class for language""" self._register[language] = class_names - diff --git a/yaksh/management/commands/add_group.py b/yaksh/management/commands/add_group.py deleted file mode 100644 index 624ff3c..0000000 --- a/yaksh/management/commands/add_group.py +++ /dev/null @@ -1,32 +0,0 @@ -''' - This command adds moderator group with permissions to add, change and delete - the objects in the exam app. - We can modify this command to add more groups by providing arguments. - Arguments like group-name, app-name can be passed. -''' - -# django imports -from django.core.management.base import BaseCommand, CommandError -from django.contrib.auth.models import Group, Permission -from django.contrib.contenttypes.models import ContentType -from django.db.utils import IntegrityError - -class Command(BaseCommand): - help = 'Adds the moderator group' - - def handle(self, *args, **options): - app_label = 'yaksh' - group = Group(name='moderator') - try: - group.save() - except IntegrityError: - raise CommandError("The group already exits") - else: - # Get the models for the given app - content_types = ContentType.objects.filter(app_label=app_label) - # Get list of permissions for the models - permission_list = Permission.objects.filter(content_type__in=content_types) - group.permissions.add(*permission_list) - group.save() - - self.stdout.write('Moderator group added successfully') diff --git a/yaksh/management/commands/create_moderator.py b/yaksh/management/commands/create_moderator.py new file mode 100644 index 0000000..86489d5 --- /dev/null +++ b/yaksh/management/commands/create_moderator.py @@ -0,0 +1,53 @@ +''' + This command creates a moderator group and adds users to the moderator group + with permissions to add, change and delete + the objects in the exam app. +''' + +# django imports +from django.core.management.base import BaseCommand, CommandError +from django.contrib.auth.models import User, Group, Permission +from django.contrib.contenttypes.models import ContentType + + +class Command(BaseCommand): + help = 'Adds users to the moderator group' + + def add_arguments(self, parser): + # Positional arguments + parser.add_argument('usernames', nargs='*', type=str) + + def handle(self, *args, **options): + app_label = 'yaksh' + + try: + group = Group.objects.get(name='moderator') + except Group.DoesNotExist: + group = Group(name='moderator') + group.save() + # Get the models for the given app + content_types = ContentType.objects.filter(app_label=app_label) + # Get list of permissions for the models + permission_list = Permission.objects.filter( + content_type__in=content_types) + group.permissions.add(*permission_list) + group.save() + self.stdout.write('Moderator group added successfully') + + if options['usernames']: + for uname in options['usernames']: + try: + user = User.objects.get(username=uname) + except User.DoesNotExist: + raise CommandError('User "{0}" does not exist'.format( + uname) + ) + if user in group.user_set.all(): + self.stdout.write('User "{0}" is ' + 'already a Moderator'.format(uname) + ) + else: + group.user_set.add(user) + self.stdout.write('Successfully added User "{0}"' + ' to Moderator group'.format(uname) + ) diff --git a/yaksh/management/commands/dump_user_data.py b/yaksh/management/commands/dump_user_data.py deleted file mode 100644 index 7deee03..0000000 --- a/yaksh/management/commands/dump_user_data.py +++ /dev/null @@ -1,98 +0,0 @@ -import sys - -# Django imports. -from django.core.management.base import BaseCommand -from django.template import Template, Context - -# Local imports. -from yaksh.views import get_user_data -from yaksh.models import User - -data_template = Template('''\ -=============================================================================== -Data for {{ data.user.get_full_name.title }} ({{ data.user.username }}) - -Name: {{ data.user.get_full_name.title }} -Username: {{ data.user.username }} -{% if data.profile %}\ -Roll number: {{ data.profile.roll_number }} -Position: {{ data.profile.position }} -Department: {{ data.profile.department }} -Institute: {{ data.profile.institute }} -{% endif %}\ -Email: {{ data.user.email }} -Date joined: {{ data.user.date_joined }} -Last login: {{ data.user.last_login }} -{% for paper in data.papers %} -Paper: {{ paper.quiz.description }} ---------------------------------------- -Marks obtained: {{ paper.get_total_marks }} -Questions correctly answered: {{ paper.get_answered_str }} -Total attempts at questions: {{ paper.answers.count }} -Start time: {{ paper.start_time }} -User IP address: {{ paper.user_ip }} -{% if paper.answers.count %} -Answers -------- -{% for question, answers in paper.get_question_answers.items %} -Question: {{ question.id }}. {{ question.summary }} (Points: {{ question.points }}) -{% if question.type == "mcq" %}\ -############################################################################### -Choices: {% for option in question.options.strip.splitlines %} {{option}}, {% endfor %} -Student answer: {{ answers.0|safe }} -{% else %}{# non-mcq questions #}\ -{% for answer in answers %}\ -############################################################################### -{{ answer.answer.strip|safe }} -# Autocheck: {{ answer.error|safe }} -{% endfor %}{# for answer in answers #}\ -{% endif %}\ -{% with answers|last as answer %}\ -Marks: {{answer.marks}} -{% endwith %}\ -{% endfor %}{# for question, answers ... #}\ - -Teacher comments ------------------ -{{ paper.comments|default:"None" }} -{% endif %}{# if paper.answers.count #}\ -{% endfor %}{# for paper in data.papers #} -''') - - -def dump_user_data(unames, stdout): - '''Dump user data given usernames (a sequence) if none is given dump all - their data. The data is dumped to stdout. - ''' - if not unames: - try: - users = User.objects.all() - except User.DoesNotExist: - pass - else: - users = [] - for uname in unames: - try: - user = User.objects.get(username__exact = uname) - except User.DoesNotExist: - stdout.write('User %s does not exist'%uname) - else: - users.append(user) - - for user in users: - data = get_user_data(user.username) - context = Context({'data': data}) - result = data_template.render(context) - stdout.write(result.encode('ascii', 'xmlcharrefreplace')) - -class Command(BaseCommand): - args = '<username1> ... <usernamen>' - help = '''Dumps all user data to stdout, optional usernames can be - specified. If none is specified all user data is dumped. - ''' - - def handle(self, *args, **options): - """Handle the command.""" - # Dump data. - dump_user_data(args, self.stdout) - diff --git a/yaksh/management/commands/load_exam.py b/yaksh/management/commands/load_exam.py deleted file mode 100644 index b354fbd..0000000 --- a/yaksh/management/commands/load_exam.py +++ /dev/null @@ -1,57 +0,0 @@ -# System library imports. -from os.path import basename - -# Django imports. -from django.core.management.base import BaseCommand - -# Local imports. -from yaksh.models import Question, Quiz - -def clear_exam(): - """Deactivate all questions from the database.""" - for question in Question.objects.all(): - question.active = False - question.save() - - # Deactivate old quizzes. - for quiz in Quiz.objects.all(): - quiz.active = False - quiz.save() - -def load_exam(filename): - """Load questions and quiz from the given Python file. The Python file - should declare a list of name "questions" which define all the questions - in pure Python. It can optionally load a Quiz from an optional 'quiz' - object. - """ - # Simply exec the given file and we are done. - exec(open(filename).read()) - - if 'questions' not in locals(): - msg = 'No variable named "questions" with the Questions in file.' - raise NameError(msg) - - for question in questions: - question[0].save() - for tag in question[1]: - question[0].tags.add(tag) - - if 'quiz' in locals(): - quiz.save() - -class Command(BaseCommand): - args = '<q_file1.py q_file2.py>' - help = '''loads the questions from given Python files which declare the - questions in a list called "questions".''' - - def handle(self, *args, **options): - """Handle the command.""" - # Delete existing stuff. - clear_exam() - - # Load from files. - for fname in args: - self.stdout.write('Importing from {0} ... '.format(basename(fname))) - load_exam(fname) - self.stdout.write('Done\n') - diff --git a/yaksh/management/commands/load_questions_xml.py b/yaksh/management/commands/load_questions_xml.py deleted file mode 100644 index 02714ea..0000000 --- a/yaksh/management/commands/load_questions_xml.py +++ /dev/null @@ -1,73 +0,0 @@ -# System library imports. -from os.path import basename -from xml.dom.minidom import parse -from htmlentitydefs import name2codepoint -import re - -# Django imports. -from django.core.management.base import BaseCommand - -# Local imports. -from yaksh.models import Question - -def decode_html(html_str): - """Un-escape or decode HTML strings to more usable Python strings. - From here: http://wiki.python.org/moin/EscapingHtml - """ - return re.sub('&(%s);' % '|'.join(name2codepoint), - lambda m: unichr(name2codepoint[m.group(1)]), html_str) - -def clear_questions(): - """Deactivate all questions from the database.""" - for question in Question.objects.all(): - question.active = False - question.save() - -def load_questions_xml(filename): - """Load questions from the given XML file.""" - q_bank = parse(filename).getElementsByTagName("question") - - for question in q_bank: - - summary_node = question.getElementsByTagName("summary")[0] - summary = (summary_node.childNodes[0].data).strip() - - desc_node = question.getElementsByTagName("description")[0] - description = (desc_node.childNodes[0].data).strip() - - type_node = question.getElementsByTagName("type")[0] - type = (type_node.childNodes[0].data).strip() - - points_node = question.getElementsByTagName("points")[0] - points = float((points_node.childNodes[0].data).strip()) \ - if points_node else 1.0 - - test_node = question.getElementsByTagName("test")[0] - test = decode_html((test_node.childNodes[0].data).strip()) - - opt_node = question.getElementsByTagName("options")[0] - opt = decode_html((opt_node.childNodes[0].data).strip()) - - new_question = Question(summary=summary, - description=description, - points=points, - options=opt, - type=type, - test=test) - new_question.save() - -class Command(BaseCommand): - args = '<q_file1.xml q_file2.xml>' - help = 'loads the questions from given XML files' - - def handle(self, *args, **options): - """Handle the command.""" - # Delete existing stuff. - clear_questions() - - # Load from files. - for fname in args: - self.stdout.write('Importing from {0} ... '.format(basename(fname))) - load_questions_xml(fname) - self.stdout.write('Done\n') - diff --git a/yaksh/management/commands/results2csv.py b/yaksh/management/commands/results2csv.py deleted file mode 100644 index 2644354..0000000 --- a/yaksh/management/commands/results2csv.py +++ /dev/null @@ -1,69 +0,0 @@ -# System library imports. -import sys -from os.path import basename - -# Django imports. -from django.core.management.base import BaseCommand -from django.template import Template, Context - -# Local imports. -from yaksh.models import Quiz, QuestionPaper - -result_template = Template('''\ -"name","username","rollno","email","answered","total","attempts","position",\ -"department","institute" -{% for paper in papers %}\ -"{{ paper.user.get_full_name.title }}",\ -"{{ paper.user.username }}",\ -"{{ paper.profile.roll_number }}",\ -"{{ paper.user.email }}",\ -"{{ paper.get_answered_str }}",\ -{{ paper.get_total_marks }},\ -{{ paper.answers.count }},\ -"{{ paper.profile.position }}",\ -"{{ paper.profile.department }}",\ -"{{ paper.profile.institute }}" -{% endfor %}\ -''') - -def results2csv(filename, stdout): - """Write exam data to a CSV file. It prompts the user to choose the - appropriate quiz. - """ - qs = Quiz.objects.all() - - if len(qs) > 1: - print "Select quiz to save:" - for q in qs: - stdout.write('%d. %s\n'%(q.id, q.description)) - quiz_id = int(raw_input("Please select quiz: ")) - try: - quiz = Quiz.objects.get(id=quiz_id) - except Quiz.DoesNotExist: - stdout.write("Sorry, quiz %d does not exist!\n"%quiz_id) - sys.exit(1) - else: - quiz = qs[0] - - papers = QuestionPaper.objects.filter(quiz=quiz, - user__profile__isnull=False) - stdout.write("Saving results of %s to %s ... "%(quiz.description, - basename(filename))) - # Render the data and write it out. - f = open(filename, 'w') - context = Context({'papers': papers}) - f.write(result_template.render(context)) - f.close() - - stdout.write('Done\n') - -class Command(BaseCommand): - args = '<results.csv>' - help = '''Writes out the results of a quiz to a CSV file. Prompt user - to select appropriate quiz if there are multiple. - ''' - - def handle(self, *args, **options): - """Handle the command.""" - # Save to file. - results2csv(args[0], self.stdout) diff --git a/yaksh/middleware/one_session_per_user.py b/yaksh/middleware/one_session_per_user.py index 92e888d..1ed1786 100644 --- a/yaksh/middleware/one_session_per_user.py +++ b/yaksh/middleware/one_session_per_user.py @@ -7,21 +7,27 @@ from yaksh.models import ConcurrentUser class OneSessionPerUserMiddleware(object): """ Middleware to handle multiple logins with same credentials - - Creates a Database entry to record the current user and active session key - - Checks if the current user has already been logged in. If True, the new session - key is stored with respect to the user and the old session key is deleted, + - Creates a Database entry to record the current user and active + session key + - Checks if the current user has already been logged in. If True, the + new session key is stored with respect to the user and the old + session key is deleted, effectively terminating the older session for the same user. - - The concurrentuser attribute of the User model refers to the ConcurrentUser - model object and not the concurrent_user field due to behaviour described - in the Documentation - Link: https://docs.djangoproject.com/en/1.5/topics/auth/customizing/#extending-the-existing-user-model) + - The concurrentuser attribute of the User model refers to the + ConcurrentUser + model object and not the concurrent_user field due to behaviour + described in the Documentation + Link: https://docs.djangoproject.com/en/1.5/topics/auth/customizing/ + #extending-the-existing-user-model """ def process_request(self, request): + """ + # Documentation: + # https://docs.djangoproject.com/en/1.5/topics/auth/customizing/ + #extending-the-existing-user-model + """ if isinstance(request.user, User): current_key = request.session.session_key - # - # Documentation: - # https://docs.djangoproject.com/en/1.5/topics/auth/customizing/#extending-the-existing-user-model if hasattr(request.user, 'concurrentuser'): active_key = request.user.concurrentuser.session_key if active_key != current_key: @@ -32,4 +38,4 @@ class OneSessionPerUserMiddleware(object): ConcurrentUser.objects.create( concurrent_user=request.user, session_key=current_key, - )
\ No newline at end of file + ) diff --git a/yaksh/middleware/user_time_zone.py b/yaksh/middleware/user_time_zone.py index ff9ec5c..206c08a 100644 --- a/yaksh/middleware/user_time_zone.py +++ b/yaksh/middleware/user_time_zone.py @@ -4,8 +4,10 @@ from django.utils import timezone class TimezoneMiddleware(object): - """ Middleware to get user's timezone and activate timezone - if user timezone is not available default value 'Asia/Kolkata' is activated """ + """ Middleware to get user's timezone and activate timezone + if user timezone is not available default value 'Asia/Kolkata' + is activated + """ def process_request(self, request): user = request.user user_tz = 'Asia/Kolkata' 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/migrations/0012_release_0_8_1.py b/yaksh/migrations/0012_release_0_8_1.py new file mode 100644 index 0000000..a6df02e --- /dev/null +++ b/yaksh/migrations/0012_release_0_8_1.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10 on 2018-06-18 06:13 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('yaksh', '0011_release_0_8_0'), + ] + + operations = [ + migrations.AddField( + model_name='coursestatus', + name='percent_completed', + field=models.IntegerField(default=0), + ), + migrations.AlterField( + model_name='quiz', + name='time_between_attempts', + field=models.FloatField(default=0.0, verbose_name='Time Between Quiz Attempts in hours'), + ), + ] diff --git a/yaksh/models.py b/yaksh/models.py index f065190..5d17dba 100644 --- a/yaksh/models.py +++ b/yaksh/models.py @@ -19,8 +19,6 @@ except ImportError: from io import BytesIO as string_io import pytz import os -import sys -import traceback import stat from os.path import join, exists import shutil @@ -35,7 +33,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 +52,8 @@ question_types = ( ("integer", "Answer in Integer"), ("string", "Answer in String"), ("float", "Answer in Float"), + ("arrange", "Arrange in Correct Order"), + ) enrollment_methods = ( @@ -69,6 +69,7 @@ test_case_types = ( ("integertestcase", "Integer Testcase"), ("stringtestcase", "String Testcase"), ("floattestcase", "Float Testcase"), + ("arrangetestcase", "Arrange Testcase"), ) string_check_type = ( @@ -89,10 +90,10 @@ FIXTURES_DIR_PATH = os.path.join(settings.BASE_DIR, 'yaksh', 'fixtures') def get_assignment_dir(instance, filename): upload_dir = instance.question_paper.quiz.description.replace(" ", "_") - return os.sep.join(( - upload_dir, instance.user.username, str(instance.assignmentQuestion.id), - filename - )) + return os.sep.join((upload_dir, instance.user.username, + str(instance.assignmentQuestion.id), + filename + )) def get_model_class(model): @@ -107,13 +108,14 @@ def get_upload_dir(instance, filename): 'question_%s' % (instance.question.id), filename )) + def dict_to_yaml(dictionary): - for k,v in dictionary.items(): + for k, v in dictionary.items(): if isinstance(v, list): - for nested_v in v: + for nested_v in v: if isinstance(nested_v, dict): dict_to_yaml(nested_v) - elif v and isinstance(v,str): + elif v and isinstance(v, str): dictionary[k] = PreservedScalarString(v) return ruamel.yaml.round_trip_dump(dictionary, explicit_start=True, default_flow_style=False, @@ -163,6 +165,23 @@ class Lesson(models.Model): def get_files(self): return LessonFile.objects.filter(lesson=self) + def _create_lesson_copy(self, user): + lesson_files = self.get_files() + new_lesson = self + new_lesson.id = None + new_lesson.name = "Copy of {0}".format(self.name) + new_lesson.creator = user + new_lesson.save() + for _file in lesson_files: + file_name = os.path.basename(_file.file.name) + if os.path.exists(_file.file.path): + lesson_file = open(_file.file.path, "rb") + django_file = File(lesson_file) + lesson_file_obj = LessonFile() + lesson_file_obj.lesson = new_lesson + lesson_file_obj.file.save(file_name, django_file, save=True) + return new_lesson + ############################################################################# class LessonFile(models.Model): @@ -184,12 +203,10 @@ class QuizManager(models.Manager): def create_trial_quiz(self, user): """Creates a trial quiz for testing questions""" - trial_quiz = self.create(duration=1000, - description="trial_questions", - is_trial=True, - time_between_attempts=0, - creator=user - ) + trial_quiz = self.create( + duration=1000, description="trial_questions", + is_trial=True, time_between_attempts=0, creator=user + ) return trial_quiz def create_trial_from_quiz(self, original_quiz_id, user, godmode, @@ -297,7 +314,7 @@ class Quiz(models.Model): attempts_allowed = models.IntegerField(default=1, choices=attempts) time_between_attempts = models.FloatField( - "Time Between Quiz Attempts in hours" + "Time Between Quiz Attempts in hours", default=0.0 ) is_trial = models.BooleanField(default=False) @@ -311,7 +328,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) @@ -366,6 +384,31 @@ class Quiz(models.Model): course=course, passed=False ).values_list("user", flat=True).distinct().count() + def get_answerpaper_status(self, user, course): + try: + qp = self.questionpaper_set.get().id + except QuestionPaper.DoesNotExist: + qp = None + ans_ppr = AnswerPaper.objects.filter( + user=user, course=course, question_paper=qp + ).order_by("-attempt_number") + if ans_ppr.exists(): + status = ans_ppr.first().status + else: + status = "not attempted" + return status + + def _create_quiz_copy(self, user): + question_papers = self.questionpaper_set.all() + new_quiz = self + new_quiz.id = None + new_quiz.description = "Copy of {0}".format(self.description) + new_quiz.creator = user + new_quiz.save() + for qp in question_papers: + qp._create_duplicate_questionpaper(new_quiz) + return new_quiz + def __str__(self): desc = self.description or 'Quiz' return '%s: on %s for %d minutes' % (desc, self.start_date_time, @@ -393,6 +436,8 @@ class LearningUnit(models.Model): if course_status.exists(): if self in course_status.first().completed_units.all(): state = "completed" + elif self.type == "quiz": + state = self.quiz.get_answerpaper_status(user, course) elif course_status.first().current_unit == self: state = "inprogress" return state @@ -416,6 +461,17 @@ class LearningUnit(models.Model): success = False return success + def _create_unit_copy(self, user): + if self.type == "quiz": + new_quiz = self.quiz._create_quiz_copy(user) + new_unit = LearningUnit.objects.create( + order=self.order, type="quiz", quiz=new_quiz) + else: + new_lesson = self.lesson._create_lesson_copy(user) + new_unit = LearningUnit.objects.create( + order=self.order, type="lesson", lesson=new_lesson) + return new_unit + ############################################################################### class LearningModule(models.Model): @@ -508,9 +564,21 @@ class LearningModule(models.Model): status_list = [unit.get_completion_status(user, course) for unit in units] count = status_list.count("completed") - percent = round((count / len(units)) * 100) + percent = round((count / units.count()) * 100) return percent + def _create_module_copy(self, user, module_name): + learning_units = self.learning_unit.order_by("order") + new_module = self + new_module.id = None + new_module.name = module_name + new_module.creator = user + new_module.save() + for unit in learning_units: + new_unit = unit._create_unit_copy(user) + new_module.learning_unit.add(new_unit) + return new_module + def __str__(self): return self.name @@ -551,6 +619,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): @@ -562,14 +632,13 @@ class Course(models.Model): return new_course def create_duplicate_course(self, user): - learning_modules = self.learning_module.all() - - new_course_name = "Copy Of {0}".format(self.name) - new_course = self._create_duplicate_instance(user, new_course_name) - - new_course.learning_module.add(*learning_modules) - - return new_course + learning_modules = self.learning_module.order_by("order") + copy_course_name = "Copy Of {0}".format(self.name) + new_course = self._create_duplicate_instance(user, copy_course_name) + for module in learning_modules: + copy_module_name = "Copy of {0}".format(module.name) + new_module = module._create_module_copy(user, copy_module_name) + new_course.learning_module.add(new_module) def request(self, *users): self.requests.add(*users) @@ -711,17 +780,45 @@ class Course(models.Model): next_index = 0 return modules.get(id=module_ids[next_index]) - def percent_completed(self, user): - modules = self.get_learning_modules() + def percent_completed(self, user, modules): if not modules: percent = 0.0 else: status_list = [module.get_module_complete_percent(self, user) for module in modules] count = sum(status_list) - percent = round((count / len(modules))) + percent = round((count / modules.count())) 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 get_current_unit(self, user): + course_status = CourseStatus.objects.filter(course=self, user=user) + if course_status.exists(): + return course_status.first().current_unit + + def days_before_start(self): + """ Get the days remaining for the start of the course """ + if timezone.now() < self.start_enroll_time: + remaining_days = (self.start_enroll_time - timezone.now()).days + 1 + else: + remaining_days = 0 + return remaining_days + + def get_completion_percent(self, user): + course_status = CourseStatus.objects.filter(course=self, user=user) + if course_status.exists(): + percentage = course_status.first().percent_completed + else: + percentage = 0 + return percentage + def __str__(self): return self.name @@ -735,7 +832,49 @@ 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) + percent_completed = models.IntegerField(default=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 + + def set_current_unit(self, unit): + self.current_unit = unit + self.save() ############################################################################### @@ -813,8 +952,9 @@ class Question(models.Model): # Check assignment upload based question grade_assignment_upload = models.BooleanField(default=False) - min_time = models.IntegerField("time in minutes", default=0) + min_time = models.IntegerField("time in minutes", default=0) + # Solution for the question. solution = models.TextField(blank=True) def consolidate_answer_data(self, user_answer, user=None): @@ -842,7 +982,7 @@ class Question(models.Model): ) if assignment_files: metadata['assign_files'] = [(file.assignmentFile.path, False) - for file in assignment_files] + for file in assignment_files] question_data['metadata'] = metadata return json.dumps(question_data) @@ -876,8 +1016,7 @@ class Question(models.Model): for question in questions: question['user'] = user file_names = question.pop('files') \ - if 'files' in question \ - else None + if 'files' in question else None tags = question.pop('tags') if 'tags' in question else None test_cases = question.pop('testcase') que, result = Question.objects.get_or_create(**question) @@ -896,8 +1035,8 @@ class Question(models.Model): new_test_case.type = test_case_type new_test_case.save() - except: - msg = "File not correct." + except Exception: + msg = "Unable to parse test case data" except Exception as exc_msg: msg = "Error Parsing Yaml: {0}".format(exc_msg) return msg @@ -928,6 +1067,17 @@ class Question(models.Model): return test_case + def get_ordered_test_cases(self, answerpaper): + try: + order = TestCaseOrder.objects.get(answer_paper=answerpaper, + question=self + ).order.split(",") + return [self.get_test_case(id=int(tc_id)) + for tc_id in order + ] + except TestCaseOrder.DoesNotExist: + return self.get_test_cases() + def get_maximum_test_case_weight(self, **kwargs): max_weight = 0.0 for test_case in self.get_test_cases(): @@ -956,13 +1106,11 @@ class Question(models.Model): file_upload.extract = extract file_upload.file.save(file_name, django_file, save=True) - def _add_yaml_to_zip(self, zip_file, q_dict,path_to_file=None): - + def _add_yaml_to_zip(self, zip_file, q_dict, path_to_file=None): tmp_file_path = tempfile.mkdtemp() yaml_path = os.path.join(tmp_file_path, "questions_dump.yaml") for elem in q_dict: relevant_dict = CommentedMap() - irrelevant_dict = CommentedMap() relevant_dict['summary'] = elem.pop('summary') relevant_dict['type'] = elem.pop('type') relevant_dict['language'] = elem.pop('language') @@ -970,8 +1118,8 @@ class Question(models.Model): relevant_dict['points'] = elem.pop('points') relevant_dict['testcase'] = elem.pop('testcase') relevant_dict.update(CommentedMap(sorted(elem.items(), - key=lambda x:x[0] - )) + key=lambda x: x[0] + )) ) yaml_block = dict_to_yaml(relevant_dict) @@ -987,7 +1135,7 @@ class Question(models.Model): if os.path.exists(yaml_file): with open(yaml_file, 'r') as q_file: questions_list = q_file.read() - msg = self.load_questions(questions_list, user, + msg = self.load_questions(questions_list, user, file_path, files ) else: @@ -1134,6 +1282,11 @@ class QuestionPaper(models.Model): # Sequence or Order of fixed questions fixed_question_order = models.CharField(max_length=255, blank=True) + # Shuffle testcase order. + shuffle_testcases = models.BooleanField("Shuffle testcase for each user", + default=True + ) + objects = QuestionPaperManager() def get_question_bank(self): @@ -1144,8 +1297,8 @@ class QuestionPaper(models.Model): return questions def _create_duplicate_questionpaper(self, quiz): - new_questionpaper = QuestionPaper.objects.create(quiz=quiz, - shuffle_questions=self.shuffle_questions, + new_questionpaper = QuestionPaper.objects.create( + quiz=quiz, shuffle_questions=self.shuffle_questions, total_marks=self.total_marks, fixed_question_order=self.fixed_question_order ) @@ -1197,7 +1350,20 @@ class QuestionPaper(models.Model): ans_paper.save() questions = self._get_questions_for_answerpaper() ans_paper.questions.add(*questions) - question_ids = [str(que.id) for que in questions] + question_ids = [] + for question in questions: + question_ids.append(str(question.id)) + 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] + ) + TestCaseOrder.objects.create( + answer_paper=ans_paper, question=question, + order=testcases_ids) + ans_paper.questions_order = ",".join(question_ids) ans_paper.save() ans_paper.questions_unanswered.add(*questions) @@ -1215,12 +1381,20 @@ class QuestionPaper(models.Model): user=user, questionpaper=self, course_id=course_id ) if last_attempt: - time_lag = (timezone.now() - last_attempt.start_time).total_seconds() / 3600 - return time_lag >= self.quiz.time_between_attempts + time_lag = (timezone.now() - last_attempt.start_time) + time_lag = time_lag.total_seconds()/3600 + can_attempt = time_lag >= self.quiz.time_between_attempts + msg = "You cannot start the next attempt for this quiz before"\ + "{0} hour(s)".format(self.quiz.time_between_attempts) \ + if not can_attempt else None + return can_attempt, msg else: - return True + return True, None else: - return False + msg = "You cannot attempt {0} quiz more than {1} time(s)".format( + self.quiz.description, self.quiz.attempts_allowed + ) + return False, msg def create_demo_quiz_ppr(self, demo_quiz, user): question_paper = QuestionPaper.objects.create(quiz=demo_quiz, @@ -1516,11 +1690,12 @@ class AnswerPaper(models.Model): ) def get_per_question_score(self, question_id): - if question_id not in self.get_questions().values_list('id', flat=True): + questions = self.get_questions().values_list('id', flat=True) + if question_id not in questions: return 'NA' answer = self.get_latest_answer(question_id) if answer: - return answer.marks + return answer.marks else: return 0 @@ -1536,7 +1711,8 @@ class AnswerPaper(models.Model): def get_current_question(self, questions): if self.questions_order: available_question_ids = questions.values_list('id', flat=True) - ordered_question_ids = [int(q) for q in self.questions_order.split(',')] + ordered_question_ids = [int(q) + for q in self.questions_order.split(',')] for qid in ordered_question_ids: if qid in available_question_ids: return questions.get(id=qid) @@ -1551,7 +1727,7 @@ class AnswerPaper(models.Model): Adds the completed question to the list of answered questions and returns the next question. """ - if question_id not in self.questions_answered.all(): + if question_id not in self.questions_answered.all(): self.questions_answered.add(question_id) self.questions_unanswered.remove(question_id) @@ -1739,11 +1915,11 @@ class AnswerPaper(models.Model): for tc in question.get_test_cases(): if tc.string_check == "lower": if tc.correct.lower().splitlines()\ - == user_answer.lower().splitlines(): + == user_answer.lower().splitlines(): tc_status.append(True) else: if tc.correct.splitlines()\ - == user_answer.splitlines(): + == user_answer.splitlines(): tc_status.append(True) if any(tc_status): result['success'] = True @@ -1760,6 +1936,14 @@ 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) @@ -1781,13 +1965,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) \ @@ -1829,7 +2021,7 @@ class AnswerPaper(models.Model): .format(u.first_name, u.last_name, q.description) -################################################################################ +############################################################################## class AssignmentUploadManager(models.Manager): def get_assignments(self, qp, que_id=None, user_id=None): @@ -1851,7 +2043,7 @@ class AssignmentUploadManager(models.Manager): return assignment_files, file_name -################################################################################ +############################################################################## class AssignmentUpload(models.Model): user = models.ForeignKey(User) assignmentQuestion = models.ForeignKey(Question) @@ -1860,7 +2052,7 @@ class AssignmentUpload(models.Model): objects = AssignmentUploadManager() -############################################################################### +############################################################################## class TestCase(models.Model): question = models.ForeignKey(Question, blank=True, null=True) type = models.CharField(max_length=24, choices=test_case_types, null=True) @@ -1943,6 +2135,7 @@ class HookTestCase(TestCase): def __str__(self): return u'Hook Testcase | Correct: {0}'.format(self.hook_code) + class IntegerTestCase(TestCase): correct = models.IntegerField(default=None) @@ -1955,11 +2148,11 @@ class IntegerTestCase(TestCase): class StringTestCase(TestCase): correct = models.TextField(default=None) - string_check = models.CharField(max_length=200,choices=string_check_type) + string_check = models.CharField(max_length=200, choices=string_check_type) def get_field_value(self): return {"test_case_type": "stringtestcase", "correct": self.correct, - "string_check":self.string_check} + "string_check": self.string_check} def __str__(self): return u'String Testcase | Correct: {0}'.format(self.correct) @@ -1972,9 +2165,39 @@ class FloatTestCase(TestCase): def get_field_value(self): return {"test_case_type": "floattestcase", "correct": self.correct, - "error_margin":self.error_margin} + "error_margin": self.error_margin} def __str__(self): return u'Testcase | Correct: {0} | Error Margin: +or- {1}'.format( self.correct, self.error_margin ) + + +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 + for each user. + """ + + # Answerpaper of the user. + answer_paper = models.ForeignKey(AnswerPaper, related_name="answer_paper") + + # Question in an answerpaper. + question = models.ForeignKey(Question) + + # Order of the test case for a question. + order = models.TextField() + + +############################################################################## diff --git a/yaksh/python_assertion_evaluator.py b/yaksh/python_assertion_evaluator.py index 440f422..4b016a1 100644 --- a/yaksh/python_assertion_evaluator.py +++ b/yaksh/python_assertion_evaluator.py @@ -1,10 +1,6 @@ #!/usr/bin/env python import sys import traceback -import os -import re -from os.path import join -import importlib # Local imports from .file_utils import copy_files, delete_files @@ -53,22 +49,24 @@ class PythonAssertionEvaluator(BaseEvaluator): -------- Returns a tuple (success, error, test_case_weight) - success - Boolean, indicating if code was executed successfully, correctly + success - Boolean, indicating if code was executed successfully, + correctly weight - Float, indicating total weight of all successful test cases error - String, error message if success is false - returns (True, "Correct answer", 1.0) : If the student script passes all - test cases/have same output, when compared to the instructor script + returns (True, "Correct answer", 1.0) : If the student script passes + all test cases/have same output, when compared to the instructor script returns (False, error_msg, 0.0): If the student script fails a single test/have dissimilar output, when compared to the instructor script. - Returns (False, error_msg, 0.0): If mandatory arguments are not files or if - the required permissions are not given to the file(s). + Returns (False, error_msg, 0.0): If mandatory arguments are not files + or if the required permissions are not given to the file(s). """ success = False mark_fraction = 0.0 try: + exec("from nose.tools import *", self.exec_scope) _tests = compile(self.test_case, '<string>', mode='exec') exec(_tests, self.exec_scope) except TimeoutException: @@ -76,12 +74,14 @@ class PythonAssertionEvaluator(BaseEvaluator): except Exception: exc_type, exc_value, exc_tb = sys.exc_info() tb_list = traceback.format_exception(exc_type, exc_value, exc_tb) + line_no = traceback.extract_tb(exc_tb)[-1][1] if len(tb_list) > 2: del tb_list[1:3] err = prettify_exceptions(exc_type.__name__, str(exc_value), "".join(tb_list), - self.test_case + self.test_case, + line_no ) else: success = True diff --git a/yaksh/python_stdio_evaluator.py b/yaksh/python_stdio_evaluator.py index b08103a..64a2809 100644 --- a/yaksh/python_stdio_evaluator.py +++ b/yaksh/python_stdio_evaluator.py @@ -21,6 +21,7 @@ def redirect_stdout(): finally: sys.stdout = old_target # restore to the previous value + class PythonStdIOEvaluator(BaseEvaluator): """Tests the Python code obtained from Code Server""" def __init__(self, metadata, test_case_data): diff --git a/yaksh/scilab_code_evaluator.py b/yaksh/scilab_code_evaluator.py index f5c81b5..9f26234 100644 --- a/yaksh/scilab_code_evaluator.py +++ b/yaksh/scilab_code_evaluator.py @@ -1,11 +1,8 @@ #!/usr/bin/env python from __future__ import unicode_literals -import traceback import os -from os.path import join, isfile import subprocess import re -import importlib # Local imports from .base_evaluator import BaseEvaluator @@ -41,8 +38,7 @@ class ScilabCodeEvaluator(BaseEvaluator): self.test_code_path = self.create_submit_code_file('main.sci') if self.file_paths: self.files = copy_files(self.file_paths) - ref_code_path = self.test_case - clean_ref_path, clean_test_case_path = self.test_code_path, None + clean_ref_path = self.test_code_path self.user_answer, terminate_commands = \ self._remove_scilab_exit(self.user_answer.lstrip()) @@ -61,12 +57,10 @@ class ScilabCodeEvaluator(BaseEvaluator): clean_ref_path ) cmd += ' | scilab-cli -nb' - ret = self._run_command(cmd, - shell=True, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE - ) - proc, stdout, stderr = ret + ret = self._run_command(cmd, shell=True, stdout=subprocess.PIPE, + stderr=subprocess.PIPE + ) + proc, stdout, stderr = ret # Get only the error. stderr = self._get_error(stdout) @@ -117,4 +111,3 @@ class ScilabCodeEvaluator(BaseEvaluator): if l.strip(): strip_out = strip_out+"\n"+l.strip() return strip_out - diff --git a/yaksh/send_emails.py b/yaksh/send_emails.py index ae49f23..061cb0e 100644 --- a/yaksh/send_emails.py +++ b/yaksh/send_emails.py @@ -6,7 +6,6 @@ except ImportError: from string import digits, punctuation import hashlib from textwrap import dedent -import smtplib import os # Django imports @@ -61,18 +60,20 @@ def send_user_mail(user_mail, key): return success, msg + def send_bulk_mail(subject, email_body, recipients, attachments): try: text_msg = "" msg = EmailMultiAlternatives(subject, text_msg, settings.SENDER_EMAIL, - recipients - ) + [settings.SENDER_EMAIL], bcc=recipients + ) msg.attach_alternative(email_body, "text/html") if attachments: for file in attachments: - path = default_storage.save('attachments/'+file.name, - ContentFile(file.read()) - ) + path = default_storage.save( + os.path.join('attachments', file.name), + ContentFile(file.read()) + ) msg.attach_file(os.sep.join((settings.MEDIA_ROOT, path)), mimetype="text/html" ) diff --git a/yaksh/settings.py b/yaksh/settings.py index d895d19..9e9597d 100644 --- a/yaksh/settings.py +++ b/yaksh/settings.py @@ -12,8 +12,8 @@ N_CODE_SERVERS = config('N_CODE_SERVERS', default=5, cast=int) # service is running. It should be > 1024 and less < 65535 though. SERVER_POOL_PORT = config('SERVER_POOL_PORT', default=55555, cast=int) +# Server host name SERVER_HOST_NAME = config('SERVER_HOST_NAME', default='http://localhost') -#'localhost' # Timeout for the code to run in seconds. This is an integer! SERVER_TIMEOUT = config('SERVER_TIMEOUT', default=4, cast=int) @@ -25,27 +25,34 @@ SERVER_TIMEOUT = config('SERVER_TIMEOUT', default=4, cast=int) URL_ROOT = '' code_evaluators = { - "python": {"standardtestcase": "yaksh.python_assertion_evaluator.PythonAssertionEvaluator", - "stdiobasedtestcase": "yaksh.python_stdio_evaluator.PythonStdIOEvaluator", - "hooktestcase": "yaksh.hook_evaluator.HookEvaluator" - }, + "python": { + "standardtestcase": + "yaksh.python_assertion_evaluator.PythonAssertionEvaluator", + "stdiobasedtestcase": + "yaksh.python_stdio_evaluator.PythonStdIOEvaluator", + "hooktestcase": "yaksh.hook_evaluator.HookEvaluator" + }, "c": {"standardtestcase": "yaksh.cpp_code_evaluator.CppCodeEvaluator", "stdiobasedtestcase": "yaksh.cpp_stdio_evaluator.CppStdIOEvaluator", "hooktestcase": "yaksh.hook_evaluator.HookEvaluator" }, "cpp": {"standardtestcase": "yaksh.cpp_code_evaluator.CppCodeEvaluator", - "stdiobasedtestcase": "yaksh.cpp_stdio_evaluator.CppStdIOEvaluator", + "stdiobasedtestcase": + "yaksh.cpp_stdio_evaluator.CppStdIOEvaluator", "hooktestcase": "yaksh.hook_evaluator.HookEvaluator" }, "java": {"standardtestcase": "yaksh.java_code_evaluator.JavaCodeEvaluator", - "stdiobasedtestcase": "yaksh.java_stdio_evaluator.JavaStdIOEvaluator", + "stdiobasedtestcase": + "yaksh.java_stdio_evaluator.JavaStdIOEvaluator", "hooktestcase": "yaksh.hook_evaluator.HookEvaluator" }, "bash": {"standardtestcase": "yaksh.bash_code_evaluator.BashCodeEvaluator", - "stdiobasedtestcase": "yaksh.bash_stdio_evaluator.BashStdIOEvaluator", + "stdiobasedtestcase": + "yaksh.bash_stdio_evaluator.BashStdIOEvaluator", "hooktestcase": "yaksh.hook_evaluator.HookEvaluator" }, - "scilab": {"standardtestcase": "yaksh.scilab_code_evaluator.ScilabCodeEvaluator", - "hooktestcase": "yaksh.hook_evaluator.HookEvaluator" - }, + "scilab": { + "standardtestcase": "yaksh.scilab_code_evaluator.ScilabCodeEvaluator", + "hooktestcase": "yaksh.hook_evaluator.HookEvaluator" + }, } diff --git a/yaksh/static/yaksh/js/add_question.js b/yaksh/static/yaksh/js/add_question.js index 346991a..0f02aab 100644 --- a/yaksh/static/yaksh/js/add_question.js +++ b/yaksh/static/yaksh/js/add_question.js @@ -126,8 +126,9 @@ function textareaformat() document.getElementById('my').innerHTML = document.getElementById('id_description').value ; document.getElementById('rend_solution').innerHTML = document.getElementById('id_solution').value ; + var question_type = document.getElementById('id_type').value if (document.getElementById('id_grade_assignment_upload').checked || - document.getElementById('id_type').value == 'upload'){ + question_type == 'upload'){ $("#id_grade_assignment_upload").prop("disabled", false); } else{ diff --git a/yaksh/static/yaksh/js/course.js b/yaksh/static/yaksh/js/course.js index 1c64a3e..bd197a8 100644 --- a/yaksh/static/yaksh/js/course.js +++ b/yaksh/static/yaksh/js/course.js @@ -42,7 +42,7 @@ $(function() { max_height: 200,
height: 200
});
- });
+});
$("#send_mail").click(function(){
var subject = $("#subject").val();
@@ -66,79 +66,38 @@ $("#send_mail").click(function(){ return status;
});
-// Download course status as csv
-function exportTableToCSV($table, filename) {
- var $headers = $table.find('tr:has(th)')
- ,$rows = $table.find('tr:has(td)')
-
- // Temporary delimiter characters unlikely to be typed by keyboard
- // This is to avoid accidentally splitting the actual contents
- ,tmpColDelim = String.fromCharCode(11) // vertical tab character
- ,tmpRowDelim = String.fromCharCode(0) // null character
-
- // actual delimiter characters for CSV format
- ,colDelim = '","'
- ,rowDelim = '"\r\n"';
- // Grab text from table into CSV formatted string
- var csv = '"';
- csv += formatRows($headers.map(grabRow));
- csv += rowDelim;
- csv += formatRows($rows.map(grabRow)) + '"';
-
- // Data URI
- var csvData = 'data:application/csv;charset=utf-8,' + encodeURIComponent(csv);
+// Table sorter for course details
+$("table").tablesorter({});
- // For IE (tested 10+)
- if (window.navigator.msSaveOrOpenBlob) {
- var blob = new Blob([decodeURIComponent(encodeURI(csv))], {
- type: "text/csv;charset=utf-8;"
+// Get user course completion status
+$('.user_data').click(function() {
+ var data = $(this).data('item-id');
+ course_id = data.split("+")[0];
+ student_id = data.split("+")[1];
+ var status_div = $("#show_status_"+course_id+"_"+student_id);
+ if(!status_div.is(":visible")){
+ var get_url = window.location.protocol + "//" + window.location.host +
+ "/exam/manage/get_user_status/" + course_id + "/" + student_id;
+ $.ajax({
+ url: get_url,
+ timeout: 8000,
+ type: "GET",
+ dataType: "json",
+ contentType: 'application/json; charset=utf-8',
+ success: function(data) {
+ status_div.toggle();
+ status_div.html(data.user_data);
+ },
+ error: function(jqXHR, textStatus) {
+ alert("Unable to get user data. Please Try again later.");
+ }
});
- navigator.msSaveBlob(blob, filename);
} else {
- $(this)
- .attr({
- 'download': filename,'href': csvData
- });
- }
-
- function formatRows(rows){
- return rows.get().join(tmpRowDelim)
- .split(tmpRowDelim).join(rowDelim)
- .split(tmpColDelim).join(colDelim);
- }
- // Grab and format a row from the table
- function grabRow(i,row){
- var $row = $(row);
- var $cols = $row.find('td');
- if(!$cols.length) $cols = $row.find('th');
-
- return $cols.map(grabCol)
- .get().join(tmpColDelim);
+ status_div.toggle();
}
- // Grab and format a column from the table
- function grabCol(j,col){
- var $col = $(col),
- $text = $col.text();
-
- return $text.replace('"', '""').replace("View Unit Status", '').replace("View Units", ""); // escape double quotes
-
- }
-}
-
-
-$("#export").click(function (event) {
- var outputFile = $("#course_name").val().replace(" ", "_") + '.csv';
-
- exportTableToCSV.apply(this, [$('#course_table'), outputFile]);
});
-// Table sorter for course details
-$("table").tablesorter({});
-
-});
+$('[data-toggle="tooltip"]').tooltip();
-function view_status(unit){
- title_list = $(unit).attr("title").split("/");
- $(unit).attr("title", title_list.join("\n"));
-}
+}); // end document ready
diff --git a/yaksh/static/yaksh/js/jquery-sortable.js b/yaksh/static/yaksh/js/jquery-sortable.js new file mode 100644 index 0000000..376880c --- /dev/null +++ b/yaksh/static/yaksh/js/jquery-sortable.js @@ -0,0 +1,693 @@ +/* =================================================== + * jquery-sortable.js v0.9.13 + * http://johnny.github.com/jquery-sortable/ + * =================================================== + * Copyright (c) 2012 Jonas von Andrian + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * The name of the author may not be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * ========================================================== */ + + +!function ( $, window, pluginName, undefined){ + var containerDefaults = { + // If true, items can be dragged from this container + drag: true, + // If true, items can be droped onto this container + drop: true, + // Exclude items from being draggable, if the + // selector matches the item + exclude: "", + // If true, search for nested containers within an item.If you nest containers, + // either the original selector with which you call the plugin must only match the top containers, + // or you need to specify a group (see the bootstrap nav example) + nested: true, + // If true, the items are assumed to be arranged vertically + vertical: true + }, // end container defaults + groupDefaults = { + // This is executed after the placeholder has been moved. + // $closestItemOrContainer contains the closest item, the placeholder + // has been put at or the closest empty Container, the placeholder has + // been appended to. + afterMove: function ($placeholder, container, $closestItemOrContainer) { + }, + // The exact css path between the container and its items, e.g. "> tbody" + containerPath: "", + // The css selector of the containers + containerSelector: "ol, ul", + // Distance the mouse has to travel to start dragging + distance: 0, + // Time in milliseconds after mousedown until dragging should start. + // This option can be used to prevent unwanted drags when clicking on an element. + delay: 0, + // The css selector of the drag handle + handle: "", + // The exact css path between the item and its subcontainers. + // It should only match the immediate items of a container. + // No item of a subcontainer should be matched. E.g. for ol>div>li the itemPath is "> div" + itemPath: "", + // The css selector of the items + itemSelector: "li", + // The class given to "body" while an item is being dragged + bodyClass: "dragging", + // The class giving to an item while being dragged + draggedClass: "dragged", + // Check if the dragged item may be inside the container. + // Use with care, since the search for a valid container entails a depth first search + // and may be quite expensive. + isValidTarget: function ($item, container) { + return true + }, + // Executed before onDrop if placeholder is detached. + // This happens if pullPlaceholder is set to false and the drop occurs outside a container. + onCancel: function ($item, container, _super, event) { + }, + // Executed at the beginning of a mouse move event. + // The Placeholder has not been moved yet. + onDrag: function ($item, position, _super, event) { + $item.css(position) + }, + // Called after the drag has been started, + // that is the mouse button is being held down and + // the mouse is moving. + // The container is the closest initialized container. + // Therefore it might not be the container, that actually contains the item. + onDragStart: function ($item, container, _super, event) { + $item.css({ + height: $item.outerHeight(), + width: $item.outerWidth() + }) + $item.addClass(container.group.options.draggedClass) + $("body").addClass(container.group.options.bodyClass) + }, + // Called when the mouse button is being released + onDrop: function ($item, container, _super, event) { + $item.removeClass(container.group.options.draggedClass).removeAttr("style") + $("body").removeClass(container.group.options.bodyClass) + }, + // Called on mousedown. If falsy value is returned, the dragging will not start. + // Ignore if element clicked is input, select or textarea + onMousedown: function ($item, _super, event) { + if (!event.target.nodeName.match(/^(input|select|textarea)$/i)) { + event.preventDefault() + return true + } + }, + // The class of the placeholder (must match placeholder option markup) + placeholderClass: "placeholder", + // Template for the placeholder. Can be any valid jQuery input + // e.g. a string, a DOM element. + // The placeholder must have the class "placeholder" + placeholder: '<li class="placeholder"></li>', + // If true, the position of the placeholder is calculated on every mousemove. + // If false, it is only calculated when the mouse is above a container. + pullPlaceholder: true, + // Specifies serialization of the container group. + // The pair $parent/$children is either container/items or item/subcontainers. + serialize: function ($parent, $children, parentIsContainer) { + var result = $.extend({}, $parent.data()) + + if(parentIsContainer) + return [$children] + else if ($children[0]){ + result.children = $children + } + + delete result.subContainers + delete result.sortable + + return result + }, + // Set tolerance while dragging. Positive values decrease sensitivity, + // negative values increase it. + tolerance: 0 + }, // end group defaults + containerGroups = {}, + groupCounter = 0, + emptyBox = { + left: 0, + top: 0, + bottom: 0, + right:0 + }, + eventNames = { + start: "touchstart.sortable mousedown.sortable", + drop: "touchend.sortable touchcancel.sortable mouseup.sortable", + drag: "touchmove.sortable mousemove.sortable", + scroll: "scroll.sortable" + }, + subContainerKey = "subContainers" + + /* + * a is Array [left, right, top, bottom] + * b is array [left, top] + */ + function d(a,b) { + var x = Math.max(0, a[0] - b[0], b[0] - a[1]), + y = Math.max(0, a[2] - b[1], b[1] - a[3]) + return x+y; + } + + function setDimensions(array, dimensions, tolerance, useOffset) { + var i = array.length, + offsetMethod = useOffset ? "offset" : "position" + tolerance = tolerance || 0 + + while(i--){ + var el = array[i].el ? array[i].el : $(array[i]), + // use fitting method + pos = el[offsetMethod]() + pos.left += parseInt(el.css('margin-left'), 10) + pos.top += parseInt(el.css('margin-top'),10) + dimensions[i] = [ + pos.left - tolerance, + pos.left + el.outerWidth() + tolerance, + pos.top - tolerance, + pos.top + el.outerHeight() + tolerance + ] + } + } + + function getRelativePosition(pointer, element) { + var offset = element.offset() + return { + left: pointer.left - offset.left, + top: pointer.top - offset.top + } + } + + function sortByDistanceDesc(dimensions, pointer, lastPointer) { + pointer = [pointer.left, pointer.top] + lastPointer = lastPointer && [lastPointer.left, lastPointer.top] + + var dim, + i = dimensions.length, + distances = [] + + while(i--){ + dim = dimensions[i] + distances[i] = [i,d(dim,pointer), lastPointer && d(dim, lastPointer)] + } + distances = distances.sort(function (a,b) { + return b[1] - a[1] || b[2] - a[2] || b[0] - a[0] + }) + + // last entry is the closest + return distances + } + + function ContainerGroup(options) { + this.options = $.extend({}, groupDefaults, options) + this.containers = [] + + if(!this.options.rootGroup){ + this.scrollProxy = $.proxy(this.scroll, this) + this.dragProxy = $.proxy(this.drag, this) + this.dropProxy = $.proxy(this.drop, this) + this.placeholder = $(this.options.placeholder) + + if(!options.isValidTarget) + this.options.isValidTarget = undefined + } + } + + ContainerGroup.get = function (options) { + if(!containerGroups[options.group]) { + if(options.group === undefined) + options.group = groupCounter ++ + + containerGroups[options.group] = new ContainerGroup(options) + } + + return containerGroups[options.group] + } + + ContainerGroup.prototype = { + dragInit: function (e, itemContainer) { + this.$document = $(itemContainer.el[0].ownerDocument) + + // get item to drag + var closestItem = $(e.target).closest(this.options.itemSelector); + // using the length of this item, prevents the plugin from being started if there is no handle being clicked on. + // this may also be helpful in instantiating multidrag. + if (closestItem.length) { + this.item = closestItem; + this.itemContainer = itemContainer; + if (this.item.is(this.options.exclude) || !this.options.onMousedown(this.item, groupDefaults.onMousedown, e)) { + return; + } + this.setPointer(e); + this.toggleListeners('on'); + this.setupDelayTimer(); + this.dragInitDone = true; + } + }, + drag: function (e) { + if(!this.dragging){ + if(!this.distanceMet(e) || !this.delayMet) + return + + this.options.onDragStart(this.item, this.itemContainer, groupDefaults.onDragStart, e) + this.item.before(this.placeholder) + this.dragging = true + } + + this.setPointer(e) + // place item under the cursor + this.options.onDrag(this.item, + getRelativePosition(this.pointer, this.item.offsetParent()), + groupDefaults.onDrag, + e) + + var p = this.getPointer(e), + box = this.sameResultBox, + t = this.options.tolerance + + if(!box || box.top - t > p.top || box.bottom + t < p.top || box.left - t > p.left || box.right + t < p.left) + if(!this.searchValidTarget()){ + this.placeholder.detach() + this.lastAppendedItem = undefined + } + }, + drop: function (e) { + this.toggleListeners('off') + + this.dragInitDone = false + + if(this.dragging){ + // processing Drop, check if placeholder is detached + if(this.placeholder.closest("html")[0]){ + this.placeholder.before(this.item).detach() + } else { + this.options.onCancel(this.item, this.itemContainer, groupDefaults.onCancel, e) + } + this.options.onDrop(this.item, this.getContainer(this.item), groupDefaults.onDrop, e) + + // cleanup + this.clearDimensions() + this.clearOffsetParent() + this.lastAppendedItem = this.sameResultBox = undefined + this.dragging = false + } + }, + searchValidTarget: function (pointer, lastPointer) { + if(!pointer){ + pointer = this.relativePointer || this.pointer + lastPointer = this.lastRelativePointer || this.lastPointer + } + + var distances = sortByDistanceDesc(this.getContainerDimensions(), + pointer, + lastPointer), + i = distances.length + + while(i--){ + var index = distances[i][0], + distance = distances[i][1] + + if(!distance || this.options.pullPlaceholder){ + var container = this.containers[index] + if(!container.disabled){ + if(!this.$getOffsetParent()){ + var offsetParent = container.getItemOffsetParent() + pointer = getRelativePosition(pointer, offsetParent) + lastPointer = getRelativePosition(lastPointer, offsetParent) + } + if(container.searchValidTarget(pointer, lastPointer)) + return true + } + } + } + if(this.sameResultBox) + this.sameResultBox = undefined + }, + movePlaceholder: function (container, item, method, sameResultBox) { + var lastAppendedItem = this.lastAppendedItem + if(!sameResultBox && lastAppendedItem && lastAppendedItem[0] === item[0]) + return; + + item[method](this.placeholder) + this.lastAppendedItem = item + this.sameResultBox = sameResultBox + this.options.afterMove(this.placeholder, container, item) + }, + getContainerDimensions: function () { + if(!this.containerDimensions) + setDimensions(this.containers, this.containerDimensions = [], this.options.tolerance, !this.$getOffsetParent()) + return this.containerDimensions + }, + getContainer: function (element) { + return element.closest(this.options.containerSelector).data(pluginName) + }, + $getOffsetParent: function () { + if(this.offsetParent === undefined){ + var i = this.containers.length - 1, + offsetParent = this.containers[i].getItemOffsetParent() + + if(!this.options.rootGroup){ + while(i--){ + if(offsetParent[0] != this.containers[i].getItemOffsetParent()[0]){ + // If every container has the same offset parent, + // use position() which is relative to this parent, + // otherwise use offset() + // compare #setDimensions + offsetParent = false + break; + } + } + } + + this.offsetParent = offsetParent + } + return this.offsetParent + }, + setPointer: function (e) { + var pointer = this.getPointer(e) + + if(this.$getOffsetParent()){ + var relativePointer = getRelativePosition(pointer, this.$getOffsetParent()) + this.lastRelativePointer = this.relativePointer + this.relativePointer = relativePointer + } + + this.lastPointer = this.pointer + this.pointer = pointer + }, + distanceMet: function (e) { + var currentPointer = this.getPointer(e) + return (Math.max( + Math.abs(this.pointer.left - currentPointer.left), + Math.abs(this.pointer.top - currentPointer.top) + ) >= this.options.distance) + }, + getPointer: function(e) { + var o = e.originalEvent || e.originalEvent.touches && e.originalEvent.touches[0] + return { + left: e.pageX || o.pageX, + top: e.pageY || o.pageY + } + }, + setupDelayTimer: function () { + var that = this + this.delayMet = !this.options.delay + + // init delay timer if needed + if (!this.delayMet) { + clearTimeout(this._mouseDelayTimer); + this._mouseDelayTimer = setTimeout(function() { + that.delayMet = true + }, this.options.delay) + } + }, + scroll: function (e) { + this.clearDimensions() + this.clearOffsetParent() // TODO is this needed? + }, + toggleListeners: function (method) { + var that = this, + events = ['drag','drop','scroll'] + + $.each(events,function (i,event) { + that.$document[method](eventNames[event], that[event + 'Proxy']) + }) + }, + clearOffsetParent: function () { + this.offsetParent = undefined + }, + // Recursively clear container and item dimensions + clearDimensions: function () { + this.traverse(function(object){ + object._clearDimensions() + }) + }, + traverse: function(callback) { + callback(this) + var i = this.containers.length + while(i--){ + this.containers[i].traverse(callback) + } + }, + _clearDimensions: function(){ + this.containerDimensions = undefined + }, + _destroy: function () { + containerGroups[this.options.group] = undefined + } + } + + function Container(element, options) { + this.el = element + this.options = $.extend( {}, containerDefaults, options) + + this.group = ContainerGroup.get(this.options) + this.rootGroup = this.options.rootGroup || this.group + this.handle = this.rootGroup.options.handle || this.rootGroup.options.itemSelector + + var itemPath = this.rootGroup.options.itemPath + this.target = itemPath ? this.el.find(itemPath) : this.el + + this.target.on(eventNames.start, this.handle, $.proxy(this.dragInit, this)) + + if(this.options.drop) + this.group.containers.push(this) + } + + Container.prototype = { + dragInit: function (e) { + var rootGroup = this.rootGroup + + if( !this.disabled && + !rootGroup.dragInitDone && + this.options.drag && + this.isValidDrag(e)) { + rootGroup.dragInit(e, this) + } + }, + isValidDrag: function(e) { + return e.which == 1 || + e.type == "touchstart" && e.originalEvent.touches.length == 1 + }, + searchValidTarget: function (pointer, lastPointer) { + var distances = sortByDistanceDesc(this.getItemDimensions(), + pointer, + lastPointer), + i = distances.length, + rootGroup = this.rootGroup, + validTarget = !rootGroup.options.isValidTarget || + rootGroup.options.isValidTarget(rootGroup.item, this) + + if(!i && validTarget){ + rootGroup.movePlaceholder(this, this.target, "append") + return true + } else + while(i--){ + var index = distances[i][0], + distance = distances[i][1] + if(!distance && this.hasChildGroup(index)){ + var found = this.getContainerGroup(index).searchValidTarget(pointer, lastPointer) + if(found) + return true + } + else if(validTarget){ + this.movePlaceholder(index, pointer) + return true + } + } + }, + movePlaceholder: function (index, pointer) { + var item = $(this.items[index]), + dim = this.itemDimensions[index], + method = "after", + width = item.outerWidth(), + height = item.outerHeight(), + offset = item.offset(), + sameResultBox = { + left: offset.left, + right: offset.left + width, + top: offset.top, + bottom: offset.top + height + } + if(this.options.vertical){ + var yCenter = (dim[2] + dim[3]) / 2, + inUpperHalf = pointer.top <= yCenter + if(inUpperHalf){ + method = "before" + sameResultBox.bottom -= height / 2 + } else + sameResultBox.top += height / 2 + } else { + var xCenter = (dim[0] + dim[1]) / 2, + inLeftHalf = pointer.left <= xCenter + if(inLeftHalf){ + method = "before" + sameResultBox.right -= width / 2 + } else + sameResultBox.left += width / 2 + } + if(this.hasChildGroup(index)) + sameResultBox = emptyBox + this.rootGroup.movePlaceholder(this, item, method, sameResultBox) + }, + getItemDimensions: function () { + if(!this.itemDimensions){ + this.items = this.$getChildren(this.el, "item").filter( + ":not(." + this.group.options.placeholderClass + ", ." + this.group.options.draggedClass + ")" + ).get() + setDimensions(this.items, this.itemDimensions = [], this.options.tolerance) + } + return this.itemDimensions + }, + getItemOffsetParent: function () { + var offsetParent, + el = this.el + // Since el might be empty we have to check el itself and + // can not do something like el.children().first().offsetParent() + if(el.css("position") === "relative" || el.css("position") === "absolute" || el.css("position") === "fixed") + offsetParent = el + else + offsetParent = el.offsetParent() + return offsetParent + }, + hasChildGroup: function (index) { + return this.options.nested && this.getContainerGroup(index) + }, + getContainerGroup: function (index) { + var childGroup = $.data(this.items[index], subContainerKey) + if( childGroup === undefined){ + var childContainers = this.$getChildren(this.items[index], "container") + childGroup = false + + if(childContainers[0]){ + var options = $.extend({}, this.options, { + rootGroup: this.rootGroup, + group: groupCounter ++ + }) + childGroup = childContainers[pluginName](options).data(pluginName).group + } + $.data(this.items[index], subContainerKey, childGroup) + } + return childGroup + }, + $getChildren: function (parent, type) { + var options = this.rootGroup.options, + path = options[type + "Path"], + selector = options[type + "Selector"] + + parent = $(parent) + if(path) + parent = parent.find(path) + + return parent.children(selector) + }, + _serialize: function (parent, isContainer) { + var that = this, + childType = isContainer ? "item" : "container", + + children = this.$getChildren(parent, childType).not(this.options.exclude).map(function () { + return that._serialize($(this), !isContainer) + }).get() + + return this.rootGroup.options.serialize(parent, children, isContainer) + }, + traverse: function(callback) { + $.each(this.items || [], function(item){ + var group = $.data(this, subContainerKey) + if(group) + group.traverse(callback) + }); + + callback(this) + }, + _clearDimensions: function () { + this.itemDimensions = undefined + }, + _destroy: function() { + var that = this; + + this.target.off(eventNames.start, this.handle); + this.el.removeData(pluginName) + + if(this.options.drop) + this.group.containers = $.grep(this.group.containers, function(val){ + return val != that + }) + + $.each(this.items || [], function(){ + $.removeData(this, subContainerKey) + }) + } + } + + var API = { + enable: function() { + this.traverse(function(object){ + object.disabled = false + }) + }, + disable: function (){ + this.traverse(function(object){ + object.disabled = true + }) + }, + serialize: function () { + return this._serialize(this.el, true) + }, + refresh: function() { + this.traverse(function(object){ + object._clearDimensions() + }) + }, + destroy: function () { + this.traverse(function(object){ + object._destroy(); + }) + } + } + + $.extend(Container.prototype, API) + + /** + * jQuery API + * + * Parameters are + * either options on init + * or a method name followed by arguments to pass to the method + */ + $.fn[pluginName] = function(methodOrOptions) { + var args = Array.prototype.slice.call(arguments, 1) + + return this.map(function(){ + var $t = $(this), + object = $t.data(pluginName) + + if(object && API[methodOrOptions]) + return API[methodOrOptions].apply(object, args) || this + else if(!object && (methodOrOptions === undefined || + typeof methodOrOptions === "object")) + $t.data(pluginName, new Container($t, methodOrOptions)) + + return this + }); + }; + +}(jQuery, window, 'sortable'); diff --git a/yaksh/static/yaksh/js/requesthandler.js b/yaksh/static/yaksh/js/requesthandler.js index ec2391a..952de3a 100644 --- a/yaksh/static/yaksh/js/requesthandler.js +++ b/yaksh/static/yaksh/js/requesthandler.js @@ -75,6 +75,24 @@ function response_handler(method_type, content_type, data, uid){ var error_output = document.getElementById("error_panel"); error_output.innerHTML = res.error; focus_on_error(error_output); + if(global_editor.editor){ + err_lineno = $("#err_lineno").val(); + if(marker){ + marker.clear(); + } + if(err_lineno){ + var lineno = parseInt(err_lineno) - 1; + var editor = global_editor.editor; + var line_length = editor.getLine(lineno).length; + marker = editor.markText({line: lineno, ch: 0}, {line: lineno, ch: line_length}, + {className: "activeline", clearOnEnter:true}); + } + else{ + if(marker){ + marker.clear(); + } + } + } } } else { reset_values(); @@ -125,6 +143,8 @@ function ajax_check_code(url, method_type, data_type, data, uid) var global_editor = {}; var csrftoken = jQuery("[name=csrfmiddlewaretoken]").val(); +var err_lineno; +var marker; $(document).ready(function(){ if(is_exercise == "True" && can_skip == "False"){ setTimeout(function() {show_solution();}, delay_time*1000); @@ -148,6 +168,7 @@ $(document).ready(function(){ mode: mode_dict[lang], gutter: true, lineNumbers: true, + styleSelectedText: true, onChange: function (instance, changes) { render(); } @@ -177,6 +198,13 @@ if (question_type == 'upload' || question_type == 'code') { global_editor.editor.setValue(init_val); global_editor.editor.clearHistory(); } - - }); +function user_arranged_options(){ + var temp_array = [] + var add_array = document.getElementById("arrange_order"); + var ans_array = order_array.children().get() + var answer_is = $.each(ans_array, function( index, value ) { + temp_array.push(value.id); + }); + add_array.value = temp_array +} diff --git a/yaksh/stdio_evaluator.py b/yaksh/stdio_evaluator.py index 55adb5c..ce4a70e 100644 --- a/yaksh/stdio_evaluator.py +++ b/yaksh/stdio_evaluator.py @@ -9,7 +9,8 @@ from .error_messages import compare_outputs class StdIOEvaluator(BaseEvaluator): - def evaluate_stdio(self, user_answer, proc, expected_input, expected_output): + def evaluate_stdio(self, user_answer, proc, + expected_input, expected_output): success = False try: if expected_input: @@ -21,7 +22,6 @@ class StdIOEvaluator(BaseEvaluator): else: user_output_bytes, output_err_bytes = proc.communicate() user_output = user_output_bytes.decode('utf-8') - output_err = output_err_bytes.decode('utf-8') except TimeoutException: os.killpg(os.getpgid(proc.pid), signal.SIGTERM) raise 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/complete.html b/yaksh/templates/yaksh/complete.html index 3d6cadc..a3627d0 100644 --- a/yaksh/templates/yaksh/complete.html +++ b/yaksh/templates/yaksh/complete.html @@ -3,6 +3,13 @@ {% block pagetitle %}<img src="{{ URL_ROOT }}/static/yaksh/images/yaksh_text.png" width="80" alt="YAKSH"></img>{% endblock %} {% block content %} +{% if module_id and not paper.question_paper.quiz.is_trial %} +<center> + <div class="alert alert-info"> + Note:- Please Click on the Next button to submit the quiz. Please do not close the browser without clicking Next. + </div> +</center> +{% endif %} {% csrf_token %} {% if paper.questions_answered.all or paper.questions_unanswered.all %} <center><table class="table table-bordered" > @@ -33,10 +40,7 @@ width="80" alt="YAKSH"></img>{% endblock %} <center><h3>{{message}}</h3></center> <center> <br> - {% if not module_id %} - <br><center><h4>You may now close the browser.</h4></center><br> - {% endif %} - {% if module_id and not user == "moderator" %} + {% if module_id and not paper.question_paper.quiz.is_trial %} {% if first_unit %} <a href="{{URL_ROOT}}/exam/next_unit/{{course_id}}/{{module_id}}/{{learning_unit.id}}/1" class="btn btn-info" id="Next"> Next <span class="glyphicon glyphicon-chevron-right"> diff --git a/yaksh/templates/yaksh/course_detail.html b/yaksh/templates/yaksh/course_detail.html index a5d10a7..2bf725c 100644 --- a/yaksh/templates/yaksh/course_detail.html +++ b/yaksh/templates/yaksh/course_detail.html @@ -2,7 +2,7 @@ {% load custom_filters %} {% block title %} Course Details {% endblock title %} -<div class="col-md-9 col-md-offset-2 main"> +<div class="col-md-9 col-md-offset-6 main"> {% block pagetitle %} Course Details for {{ course.name|title }} {% endblock %} </div> @@ -14,6 +14,12 @@ {% endblock %} {% block css %} <link rel="stylesheet" href="{{ URL_ROOT }}/static/yaksh/css/jquery-ui/jquery-ui.css"> +<style> + .user_data + .tooltip.top > .tooltip-inner { + padding: 12px; + font-size: 10px; + } +</style> {% endblock %} {% block content %} <br/> @@ -128,62 +134,51 @@ {% elif state == "course_status" %} <div class="course_data"> <input type="hidden" id="course_name" value="{{course.name}}"> - <a href="#" class="btn btn-info" id="export">Export to CSV</a> <center><h2>Course Status</h2></center> <table class="tablesorter table table-bordered" id="course_table" data-sortlist="[0,0]"> <thead> <tr> <th>Sr No.</th> - <th>Students</th> - <th>Total</th> - <th colspan="{{modules|length}}">Modules</th> - </tr> - <tr> - <th scope="row"></th> - <th></th> - <th></th> - {% if modules %} - {% for module in modules %} - <th> - {{module.name}} - <br> - ({{module.get_learning_units|length}} Units) - <br> - <a data-target="tooltip" title="{% for unit in module.get_learning_units %}{% if unit.type == 'quiz' %}{{unit.quiz.description}}{% else %}{{unit.lesson.name}}{% endif %} / {% endfor %}" id="unit_status{{module.id}}" onmouseover="view_status('#unit_status{{module.id}}')"> - View Units</a> - </th> - {% endfor %} - {% else %} - <th></th> - {% endif %} + <th>Email</th> + <th>Current Unit</th> + <th>Course Completion Percentage</th> + <th>Grade</th> </tr> </thead> <tbody> - {% for student in students %} + {% for student, grade, percent, unit in student_details %} <tr> <td width="5%"> {{forloop.counter}}. </td> + <td width="50%"> + <a class="user_data" data-item-id="{{course.id}}+{{student.id}}" data-toggle="tooltip" title="Click to view Overall Course progress" data-placement="top"> + {% if student.email %} + {{ student.email }} + {% else %} + {{ student.get_full_name|title}} + {% endif %} + </a> + <div id="show_status_{{course.id}}_{{student.id}}" style="display: None;"> + </div> + </td> <td> - {{ student.get_full_name|title }} + {% if unit %} + {% if unit.type == 'quiz' %} + {{unit.quiz.description}} + {% else %} + {{unit.lesson.name}} + {% endif %} + {% else %} + NA + {% endif%} </td> <td> - {% course_completion_percent course student as c_percent %} - {{c_percent}} % + {{percent}}% + </td> + <td> + {{grade}} </td> - {% if modules %} - {% for module in modules %} - <td> - {% module_completion_percent course module student as m_percent %} - {{m_percent}} % - <br> - <a data-target="tooltip" title="{% for unit in module.get_learning_units %}{% if unit.type == 'quiz' %}{{unit.quiz.description}}{% else %}{{unit.lesson.name}}{% endif %} - {% get_unit_status course module unit student as status %}{{status|title}} / {% endfor %}" id="unit_status{{module.id}}{{student.id}}" onmouseover="view_status('#unit_status{{module.id}}{{student.id}}')"> - View Unit Status</a> - </td> - {% endfor %} - {% else %} - <td>-------</td> - {% endif %} </tr> {% endfor %} </tbody> diff --git a/yaksh/templates/yaksh/course_modules.html b/yaksh/templates/yaksh/course_modules.html index fad1be0..5baa781 100644 --- a/yaksh/templates/yaksh/course_modules.html +++ b/yaksh/templates/yaksh/course_modules.html @@ -17,103 +17,118 @@ <center>{{ msg }}</center> </div> {% endif %} -{% if learning_modules %} - {% for module in learning_modules %} - <div class="row well"> +<b>Grade: {% if grade %} {{ grade }} {% else %} Will be available once the course is complete {% endif %}</b> +{% if modules %} + <br><br> + <strong>Overall Course Progress</strong> + <div class="progress"> + {% if course_percentage <= 50 %} + <div class="progress-bar progress-bar-danger" role="progressbar" aria-valuenow="40" + aria-valuemin="0" aria-valuemax="100" style="width:{{course_percentage}}%"> + {% elif course_percentage <= 75 %} + <div class="progress-bar progress-bar-warning" role="progressbar" aria-valuenow="40" + aria-valuemin="0" aria-valuemax="100" style="width:{{course_percentage}}%"> + {% else %} + <div class="progress-bar progress-bar-success" role="progressbar" aria-valuenow="40" + aria-valuemin="0" aria-valuemax="100" style="width:{{course_percentage}}%"> + {% endif %} + <b style="color: black;">{{course_percentage}}% Completed</b> + </div> + </div> + <div class="panel panel-default"> + <div class="panel panel-body"> <table class="table"> - <tr> - <td> - <a href="{{URL_ROOT}}/exam/quizzes/view_module/{{module.id}}/{{course.id}}"> - {{module.name|title}}</a> - </td> - <td> - <span class="glyphicon glyphicon-chevron-down" id="learning_units{{module.id}}{{course.id}}_down"> - </span> - <span class="glyphicon glyphicon-chevron-up" id="learning_units{{module.id}}{{course.id}}_up" style="display: none;"></span> - <a data-toggle="collapse" data-target="#learning_units{{module.id}}{{course.id}}" onclick="view_unit('learning_units{{module.id}}{{course.id}}');"> - View Lessons/Quizzes/Exercises</a> - </td> - <td> - {% get_module_status user module course as module_status %} - Status: - {% if module_status == "completed" %} - <span class="label label-success"> - {{module_status|title}} - </span> - {% elif module_status == "inprogress" %} - <span class="label label-info"> - {{module_status|title}} - </span> - {% else %} - <span class="label label-warning"> - {{module_status|title}} - </span> - {% endif %} - </td> - </tr> + {% for module, percent in modules %} + <tr> + <td width="25%"> + <a href="{{URL_ROOT}}/exam/quizzes/view_module/{{module.id}}/{{course.id}}"> + {{module.name|title}}</a> + </td> + <td> + <span class="glyphicon glyphicon-chevron-down" id="learning_units{{module.id}}{{course.id}}_down"> + </span> + <span class="glyphicon glyphicon-chevron-up" id="learning_units{{module.id}}{{course.id}}_up" style="display: none;"> + </span> + <a data-toggle="collapse" data-target="#learning_units{{module.id}}{{course.id}}" onclick="view_unit('learning_units{{module.id}}{{course.id}}');"> + View Lessons/Quizzes/Exercises</a> + <div id="learning_units{{module.id}}{{course.id}}" class="collapse"> + <table class="table table-bordered"> + <tr> + <th>Lesson/Quiz/Exercise</th> + <th>Status</th> + <th>Type</th> + <th>View AnswerPaper</th> + </tr> + {% for unit in module.get_learning_units %} + <tr> + <td> + {% if unit.type == "quiz" %} + {{unit.quiz.description}} + {% else %} + {{unit.lesson.name}} + {% endif %} + </td> + <td> + {% get_unit_status course module unit user as status %} + {% if status == "completed" %} + <span class="label label-success">{{status|title}} + </span> + {% elif status == "inprogress" %} + <span class="label label-info">{{status|title}} + </span> + {% else %} + <span class="label label-warning">{{status|title}} + </span> + {% endif %} + </td> + <td> + {% if unit.type == "quiz" %} + {% if unit.quiz.is_exercise %} + Exercise + {% else %} + Quiz + {% endif %} + {% else %} + Lesson + {% endif %} + </td> + <td> + {% if unit.type == "quiz" %} + {% if unit.quiz.view_answerpaper %} + <a href="{{ URL_ROOT }}/exam/view_answerpaper/{{ unit.quiz.questionpaper_set.get.id }}/{{course.id}}"> + <i class="fa fa-eye" aria-hidden="true"></i> Can View </a> + {% else %} + <a> + <i class="fa fa-eye-slash" aria-hidden="true"> + </i> Cannot view now </a> + {% endif %} + {% else %} + ------ + {% endif %} + </td> + </tr> + {% endfor %} + </table> + </div> + </td> + <td> + <div class="progress"> + {% if percent <= 50 %} + <div class="progress-bar progress-bar-danger" role="progressbar" aria-valuenow="40" aria-valuemin="0" aria-valuemax="100" style="width:{{percent}}%"> + {% elif percent <= 75 %} + <div class="progress-bar progress-bar-warning" role="progressbar" aria-valuenow="40" aria-valuemin="0" aria-valuemax="100" style="width:{{percent}}%"> + {% else %} + <div class="progress-bar progress-bar-success" role="progressbar" aria-valuenow="40" aria-valuemin="0" aria-valuemax="100" style="width:{{percent}}%"> + {% endif %} + <b style="color: black;">{{percent}}% Completed</b> + </div> + </div> + </td> + </tr> + {% endfor %} </table> </div> - <div id="learning_units{{module.id}}{{course.id}}" class="collapse"> - <table class="table"> - <tr> - <th>Lesson/Quiz/Exercise</th> - <th>Status</th> - <th>Type</th> - <th>View AnswerPaper</th> - </tr> - {% for unit in module.get_learning_units %} - <tr> - <ul class="inputs-list"> - <td> - {% if unit.type == "quiz" %} - {{unit.quiz.description}} - {% else %} - {{unit.lesson.name}} - {% endif %} - </td> - <td> - {% get_unit_status course module unit user as status %} - {% if status == "completed" %} - <span class="label label-success">{{status|title}} - </span> - {% elif status == "inprogress" %} - <span class="label label-info">{{status|title}} - </span> - {% else %} - <span class="label label-warning">{{status|title}} - </span> - {% endif %} - </td> - <td> - {% if unit.type == "quiz" %} - {% if unit.quiz.is_exercise %} - Exercise - {% else %} - Quiz - {% endif %} - {% else %} - Lesson - {% endif %} - </td> - <td> - {% if unit.type == "quiz" %} - {% if unit.quiz.view_answerpaper %} - <a href="{{ URL_ROOT }}/exam/view_answerpaper/{{ unit.quiz.questionpaper_set.get.id }}/{{course.id}}"><i class="fa fa-eye" aria-hidden="true"></i> Can View </a> - {% else %} - <a> - <i class="fa fa-eye-slash" aria-hidden="true"> - </i> Cannot view now </a> - {% endif %} - {% else %} - ------ - {% endif %} - </td> - </ul> - </tr> - {% endfor %} - </table> - </div> - {% endfor %} + </div> {% else %} <h3> No lectures found </h3> {% endif %} diff --git a/yaksh/templates/yaksh/courses.html b/yaksh/templates/yaksh/courses.html index bc96bf5..ba09c6d 100644 --- a/yaksh/templates/yaksh/courses.html +++ b/yaksh/templates/yaksh/courses.html @@ -4,6 +4,7 @@ {% block script %} <script> $(document).ready(function(){ + $('[data-toggle="tooltip"]').tooltip(); $("#created_courses").toggle(); $("#link_created_courses").click(function() { if ($("#allotted_courses").is(":visible")){ @@ -24,6 +25,14 @@ }); </script> {% endblock %} +{% block css %} +<style> + .test + .tooltip.top > .tooltip-inner { + padding: 15px; + font-size: 12px; + } +</style> +{% endblock %} {% block content %} <div class="row"> <div class="col-sm-3 col-md-2 sidebar"> @@ -47,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> @@ -99,7 +112,8 @@ <br><br> <ul> <li> - <a href="{{URL_ROOT}}/exam/manage/courses/designcourse/{{course.id}}/">Design Course + <a href="{{URL_ROOT}}/exam/manage/courses/designcourse/{{course.id}}/" data-toggle="tooltip" title="Add/Remove/Change course modules" data-placement="top"> + Design Course </a> </li> <br> @@ -123,7 +137,7 @@ </li> <br> <li> - <a href="{{URL_ROOT}}/exam/manage/duplicate_course/{{ course.id }}/"> + <a class="test" href="{{URL_ROOT}}/exam/manage/duplicate_course/{{ course.id }}/" data-toggle="tooltip" title="Creates Copy of selected Course as well as its Modules, Lessons/Quizzes" data-placement="top"> Clone Course</a> </li> </ul> @@ -259,7 +273,7 @@ </li> <br> <li> - <a href="{{URL_ROOT}}/exam/manage/duplicate_course/{{ course.id }}/"> + <a class="test" href="{{URL_ROOT}}/exam/manage/duplicate_course/{{ course.id }}/" data-toggle="tooltip" title="Creates Copy of selected Course as well as its Modules, Lessons/Quizzes" data-placement="top"> Clone Course</a> </li> </ul> @@ -358,6 +372,9 @@ {% if quiz.questionpaper_set.get %} <a href="{{URL_ROOT}}/exam/manage/designquestionpaper/{{ quiz.id }}/{{quiz.questionpaper_set.get.id}}/"> Question Paper for {{ quiz.description }}</a> + <a href="{{URL_ROOT}}/exam/manage/preview_questionpaper/{{quiz.questionpaper_set.get.id}}" class="btn btn-primary active btn-xs" target="_blank"> + View + </a> <br> {% else %} <p>No Question Paper diff --git a/yaksh/templates/yaksh/design_course_session.html b/yaksh/templates/yaksh/design_course_session.html index ee530e0..6542e3c 100644 --- a/yaksh/templates/yaksh/design_course_session.html +++ b/yaksh/templates/yaksh/design_course_session.html @@ -23,7 +23,7 @@ <div class="row"> <div class="col-md-8 col-md-offset-2 available-list"> <div id="fixed-available-wrapper"> - <p><u><b>Available Lessons and quizzes: (Add Lessons and Quizzes)</b></u></p> + <p><u><b>Available Modules:</b></u></p> <div id="fixed-available"> <table id="course-details" class="table table-bordered"> <tr> @@ -64,7 +64,7 @@ </div> <div class="col-md-8 col-md-offset-2"> <div id="fixed-added-wrapper"> - <p><u><b>Choosen Lessons and quizzes:</b></u></p> + <p><u><b>Choosen Modules:</b></u></p> <div id="fixed-added"> <table id="course-details" class="table table-bordered"> <tr> diff --git a/yaksh/templates/yaksh/design_questionpaper.html b/yaksh/templates/yaksh/design_questionpaper.html index c56a955..7e6d5c0 100644 --- a/yaksh/templates/yaksh/design_questionpaper.html +++ b/yaksh/templates/yaksh/design_questionpaper.html @@ -220,10 +220,14 @@ select <div class="tab-pane" id="finish"> <center> - <h5>Almost finished creating your question paper</h5> + <h5><u>Almost finished creating your question paper</u></h5> <label style="float: none;"> {{ qpaper_form.shuffle_questions }} - <span>Auto shuffle.</span> + <span>Shuffle questions' order for each student</span> + </label> <br><br> + <label style="float: none;"> + {{ qpaper_form.shuffle_testcases }} + <span>Shuffle MCQ/MCC options for each student</span> </label> <br><br> <input class ="btn primary large" type="submit" name="save" id="save" value="Save question paper"> <br> diff --git a/yaksh/templates/yaksh/error_template.html b/yaksh/templates/yaksh/error_template.html index 61657ae..301020e 100644 --- a/yaksh/templates/yaksh/error_template.html +++ b/yaksh/templates/yaksh/error_template.html @@ -3,7 +3,6 @@ {% endblock %} {% load custom_filters %} - {% if error_message %} {% for error in error_message %} @@ -35,6 +34,7 @@ </tr> <tr> {% if error.traceback %} + <input type="hidden" id="err_lineno" value="{{error.line_no}}"> <td><b>Full Traceback: </b></td> <td><pre>{{error.traceback}}</pre></td> {% endif %} diff --git a/yaksh/templates/yaksh/grade_user.html b/yaksh/templates/yaksh/grade_user.html index 93f00e0..8430e91 100644 --- a/yaksh/templates/yaksh/grade_user.html +++ b/yaksh/templates/yaksh/grade_user.html @@ -167,7 +167,7 @@ Status : <b style="color: red;"> Failed </b><br/> {% endif %} {% endfor %} - {% elif question.type == "integer" or "string" or "float" %} + {% elif question.type == "integer" or question.type == "string" or question.type == "float" %} <h5> <u>Correct Answer:</u></h5> {% for testcase in question.get_test_cases %} <strong>{{ testcase.correct|safe }}</strong> @@ -175,6 +175,14 @@ Status : <b style="color: red;"> Failed </b><br/> <strong>{{ testcase.error_margin|safe }}</strong> {% endif %} {% endfor %} + {% elif question.type == "arrange" %} + <h5> <u>Correct Order:</u></h5> + <div class="list-group" > + {% for testcase in question.get_test_cases %} + <li class="list-group-item"><strong>{{ testcase.options|safe }}</strong></li> + {% endfor %} + </div> + {% else %} <h5> <u>Test cases: </u></h5> {% for testcase in question.get_test_cases %} @@ -307,6 +315,15 @@ Status : <b style="color: red;"> Failed </b><br/> {% endif %} {% endfor %} </div> + + {% elif question.type == "arrange"%} + <div class="well well-sm"> + {% get_answer_for_arrange_options ans.answer.answer question as tc_list %} + {% for testcases in tc_list %} + <li>{{ testcases.options.strip|safe }}</li> + {% endfor %} + </div> + {% else %} <div class="well well-sm"> {{ ans.answer.answer.strip|safe }} diff --git a/yaksh/templates/yaksh/preview_questionpaper.html b/yaksh/templates/yaksh/preview_questionpaper.html new file mode 100644 index 0000000..123218f --- /dev/null +++ b/yaksh/templates/yaksh/preview_questionpaper.html @@ -0,0 +1,42 @@ +{% extends "base.html" %} + +{% block pagetitle %} Quiz: {{ paper.quiz.description }} {% endblock pagetitle %} + +{% block content %} +<div class="well"> + <div class="col-md-12"> + <div class="col-md-6">Maximum Mark(s): {{ paper.total_marks }}</div> + <div class="col-md-6"><span class="pull-right">Total Time: {{ paper.quiz.duration }} minutes</span></div> + </div> +</div> +<div class="panel panel-default"> + <div class="panel-heading">Instructions</div> + <div class="panel-body" id="instructions"> + {{ paper.quiz.instructions|safe }} + </div> +</div> +{% for question in questions %} + <div class="panel panel-info"> + <div class="panel-heading"> + <strong> {{forloop.counter}}. {{ question.summary }} + <span class="marks pull-right"> Mark(s): {{ question.points }} </span> + </strong> + </div> + <div class="panel-body"> + <h5><u>Question:</u></h5> <strong>{{ question.description|safe }}</strong> + <br/><b>Answer:</b><br/> + {% if question.type == "code" %} + <div class="well">{{ question.snippet }}<br/></div> + {% endif %} + {% if question.type == "mcq" or question.type == "mcc" %} + <h5> <u>Choices:</u></h5> + {% for testcase in question.get_test_cases %} + <br/><strong> + {{ forloop.counter }}. {{ testcase.options|safe }}</strong> + {% endfor %} + {% endif %} + + </div> + </div> +{% endfor %} +{% endblock %} diff --git a/yaksh/templates/yaksh/question.html b/yaksh/templates/yaksh/question.html index 9d6ce48..ebfe066 100644 --- a/yaksh/templates/yaksh/question.html +++ b/yaksh/templates/yaksh/question.html @@ -11,6 +11,10 @@ .CodeMirror{ border-style: groove; } + .activeline { + background: #FBC2C4 !important; + color: #8a1f11 !important; + } </style> {% endblock %} @@ -21,6 +25,7 @@ <script src="{{ URL_ROOT }}/static/yaksh/js/codemirror/mode/clike/clike.js"></script> <script src="{{ URL_ROOT }}/static/yaksh/js/codemirror/mode/shell/shell.js"></script> <script src="{{ URL_ROOT }}/static/yaksh/js/mathjax/MathJax.js?config=TeX-MML-AM_CHTML"></script> +<script src="{{ URL_ROOT }}/static/yaksh/js/jquery-sortable.js"></script> <script> init_val = '{{ last_attempt|escape_quotes|safe }}'; lang = "{{ question.language }}" @@ -175,10 +180,12 @@ question_type = "{{ question.type }}" {% else %} <h5>(CASE SENSITIVE)</h5> {% endif %} - {% elif question.type == "float" %} (FILL IN THE BLANKS WITH FLOAT ANSWER) + {% elif question.type == "arrange" %} + (ARRANGE THE OPTIONS IN CORRECT ORDER) {% endif %} + </u> <font class=pull-right>(Marks : {{ question.points }}) </font> </h4> @@ -218,7 +225,7 @@ question_type = "{{ question.type }}" {% if question.type == "integer" %} Enter Integer:<br/> - <input autofocus name="answer" type="number" id="integer" value={{ last_attempt|safe }} /> + <input autofocus name="answer" type="number" id="integer" value="{{ last_attempt|safe }}" /> <br/><br/> {% endif %} @@ -230,7 +237,7 @@ question_type = "{{ question.type }}" {% if question.type == "float" %} Enter Decimal Value :<br/> - <input autofocus name="answer" type="number" step="any" id="float" value={{ last_attempt|safe }} /> + <input autofocus name="answer" type="number" step="any" id="float" value="{{ last_attempt|safe }}" /> <br/><br/> {% endif %} @@ -251,6 +258,27 @@ question_type = "{{ question.type }}" <input type=file id="assignment" name="assignment" multiple=""> <hr> {% endif %} + + {% if question.type == "arrange" %} + {% if last_attempt %} + {% get_answer_for_arrange_options last_attempt question as test_cases %} + {% endif %} + <input name="answer" type="hidden" id='arrange_order'/> + <div class="list-group"> + <ol class="arrange"> + {% for test_case in test_cases %} + <li class="list-group-item" id={{test_case.id}}>{{test_case.options| safe }}</li> + {% endfor %} + </ol> + </div> + + <script type="text/javascript"> + var arrange = $("ol.arrange"); + var order_array = $(arrange).sortable(['serialize']); + </script> + {% endif %} + + {% if question.type == "code" %} <div class="row"> <div class="col-md-9"> @@ -269,6 +297,9 @@ question_type = "{{ question.type }}" <br><button class="btn btn-primary" type="submit" name="check" id="check">Submit Answer</button> {% elif question.type == "upload" %} <br><button class="btn btn-primary" type="submit" name="check" id="check" onClick="return validate();">Upload</button> + {% elif question.type == "arrange" %} + <br><button class="btn btn-primary" type="submit" name="check" id="check" onClick="return user_arranged_options();">Submit Answer</button> + {% else %} {% if question in paper.get_questions_unanswered or quiz.is_exercise %} diff --git a/yaksh/templates/yaksh/quizzes_user.html b/yaksh/templates/yaksh/quizzes_user.html index cf08752..49f8d2d 100644 --- a/yaksh/templates/yaksh/quizzes_user.html +++ b/yaksh/templates/yaksh/quizzes_user.html @@ -39,25 +39,44 @@ No Courses to display </b></h4> </div> <div class="col-md-4"> - {% if not course.active %} - <span class="label label-danger">Closed</span> - {% endif %} {% if user in course.requests.all %} <span class="label label-warning">Request Pending </span> {% elif user in course.rejected.all %}<span class="label label-danger">Request Rejected</span> {% elif user in course.students.all %}<span class="label label-info">Enrolled</span> {% else %} - {% if course.is_active_enrollment %} - {% if course.is_self_enroll %} - <a class="btn btn-success" href="{{ URL_ROOT }}/exam/self_enroll/{{ course.id }}">Enroll</a> + {% if course.active %} + {% if course.is_active_enrollment %} + {% if course.is_self_enroll %} + <a class="btn btn-success" href="{{ URL_ROOT }}/exam/self_enroll/{{ course.id }}">Enroll</a> + {% else %} + <a class="btn btn-success" href="{{ URL_ROOT }}/exam/enroll_request/{{ course.id }}">Enroll</a> + {% endif %} {% else %} - <a class="btn btn-success" href="{{ URL_ROOT }}/exam/enroll_request/{{ course.id }}">Enroll</a> + <span class="label label-danger" style="font-size: 15px"> + Enrollment Closed + </span> {% endif %} {% else %} - <span class="label label-danger">Enrollment Closed</span> + <span class="label label-danger" style="font-size: 15px"> + Course is not activated + </span> {% endif %} {% endif %} </div> + <div class="col-md-4"> + {% if course.days_before_start != 0 %} + <span class="label label-info" style="font-size: 15px"> + {{course.days_before_start}} day(s) to start + </span> + {% endif %} + </div> </div> + {% if course.is_active_enrollment %} + <div class="alert alert-info"> + Start Date : {{course.start_enroll_time}} + <br> + End Date : {{course.end_enroll_time}} + </div> + {% endif %} {% if course.instructions %} <div class="row"> diff --git a/yaksh/templates/yaksh/show_video.html b/yaksh/templates/yaksh/show_video.html index 17f9d86..eae3762 100644 --- a/yaksh/templates/yaksh/show_video.html +++ b/yaksh/templates/yaksh/show_video.html @@ -11,7 +11,11 @@ {% block main %} <div class="col-sm-3 col-md-2 sidebar"> - <center><h4>{{course.name}}</h4></center> + <center> + <a href="{{URL_ROOT}}/exam/course_modules/{{course.id}}"> + <h4>{{course.name}}</h4> + </a> + </center> <br> {% for module in all_modules %} {% if module.id == learning_module.id %} diff --git a/yaksh/templates/yaksh/user_data.html b/yaksh/templates/yaksh/user_data.html index 45867d2..9449fcc 100644 --- a/yaksh/templates/yaksh/user_data.html +++ b/yaksh/templates/yaksh/user_data.html @@ -74,13 +74,19 @@ User IP address: {{ paper.user_ip }} {% endif %} {% endfor %} - {% elif question.type == "integer" or question.type == "string" - or question.type == "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> @@ -100,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 %} @@ -108,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 %} @@ -116,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/user_status.html b/yaksh/templates/yaksh/user_status.html new file mode 100644 index 0000000..5f006c9 --- /dev/null +++ b/yaksh/templates/yaksh/user_status.html @@ -0,0 +1,44 @@ +{% if status %} + <strong>Student Name: {{student.get_full_name|title}}</strong> + <br> + <strong>Overall Course Progress:</strong> + <div class="progress"> + {% if course_percentage <= 50 %} + <div class="progress-bar progress-bar-danger" role="progressbar" aria-valuenow="40" + aria-valuemin="0" aria-valuemax="100" style="width:{{course_percentage}}%"> + {% elif course_percentage <= 75 %} + <div class="progress-bar progress-bar-warning" role="progressbar" aria-valuenow="40" + aria-valuemin="0" aria-valuemax="100" style="width:{{course_percentage}}%"> + {% else %} + <div class="progress-bar progress-bar-success" role="progressbar" aria-valuenow="40" + aria-valuemin="0" aria-valuemax="100" style="width:{{course_percentage}}%"> + {% endif %} + <b style="color: black;">{{course_percentage}}% Completed</b> + </div> + </div> + <br> + <strong>Per Module Progress:</strong> + <br> + <table class="table"> + {% for module, percent in modules %} + <tr> + <td width="30%">{{ module.name }}</td> + <td> + <div class="progress"> + {% if percent <= 50 %} + <div class="progress-bar progress-bar-danger" role="progressbar" aria-valuenow="40" aria-valuemin="0" aria-valuemax="100" style="width:{{percent}}%"> + {% elif percent <= 75 %} + <div class="progress-bar progress-bar-warning" role="progressbar" aria-valuenow="40" aria-valuemin="0" aria-valuemax="100" style="width:{{percent}}%"> + {% else %} + <div class="progress-bar progress-bar-success" role="progressbar" aria-valuenow="40" aria-valuemin="0" aria-valuemax="100" style="width:{{percent}}%"> + {% endif %} + <b style="color: black;">{{percent}}% Completed</b> + </div> + </div> + </td> + </tr> + {% endfor %} + </table> +{% else %} + {{ msg }} +{% endif %}
\ No newline at end of file diff --git a/yaksh/templates/yaksh/view_answerpaper.html b/yaksh/templates/yaksh/view_answerpaper.html index 410b578..7cbec91 100644 --- a/yaksh/templates/yaksh/view_answerpaper.html +++ b/yaksh/templates/yaksh/view_answerpaper.html @@ -34,10 +34,10 @@ Start time: {{ paper.start_time }} <br/> End time : {{ paper.end_time }} <br/> Percentage obtained: {{ paper.percent }}% <br/> - {% if paper.passed == 0 %} - Status : <b style="color: red;"> Failed </b><br/> - {% else %} + {% if paper.passed %} Status : <b style="color: green;"> Passed </b><br/> + {% else %} + Status : <b style="color: red;"> Failed </b><br/> {% endif %} </p> @@ -55,7 +55,8 @@ <h5><u>Question:</u></h5> <strong>{{ question.description|safe }}</strong> {% if question.type == "mcq" or question.type == "mcc" %} <h5> <u>Choices:</u></h5> - {% for testcase in question.get_test_cases %} + {% get_ordered_testcases question paper as testcases %} + {% for testcase in testcases %} {% if testcase.correct %} <br/> <strong>{{ forloop.counter }}. {{ testcase.options|safe }}</strong> @@ -66,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 %} @@ -107,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/templates/yaksh/view_profile.html b/yaksh/templates/yaksh/view_profile.html index 5f06135..ce95226 100644 --- a/yaksh/templates/yaksh/view_profile.html +++ b/yaksh/templates/yaksh/view_profile.html @@ -31,6 +31,10 @@ <th><label for="id_position"><h5>Position:</h5></label></th> <th><label for="id_position"><h5>{{ user.profile.position }}</h5></label></th> </tr> + <tr> + <th><label for="id_position"><h5>Timezone:</h5></label></th> + <th><label for="id_position"><h5>{{ user.profile.timezone }}</h5></label></th> + </tr> </table> <a class="btn btn-primary pull-right" href="{{ URL_ROOT }}/exam/editprofile/">Edit Profile</a> {% endblock %} diff --git a/yaksh/templatetags/custom_filters.py b/yaksh/templatetags/custom_filters.py index 3c2c6fd..ee0d51a 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 @@ -62,3 +63,22 @@ def module_completion_percent(course, module, user): @register.simple_tag def course_completion_percent(course, user): return course.percent_completed(user) + + +@register.simple_tag +def get_ordered_testcases(question, answerpaper): + 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..e8d1d61 --- /dev/null +++ b/yaksh/templatetags/test_custom_filters.py @@ -0,0 +1,147 @@ +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, + AnswerPaper, Course, 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.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 cd4279b..eaf5bbc 100644 --- a/yaksh/test_models.py +++ b/yaksh/test_models.py @@ -3,18 +3,16 @@ from yaksh.models import User, Profile, Question, Quiz, QuestionPaper,\ QuestionSet, AnswerPaper, Answer, Course, StandardTestCase,\ StdIOBasedTestCase, FileUpload, McqTestCase, AssignmentUpload,\ LearningModule, LearningUnit, Lesson, LessonFile, CourseStatus -from yaksh.code_server import(ServerPool, - get_result as get_result_from_code_server - ) +from yaksh.code_server import ( + ServerPool, get_result as get_result_from_code_server + ) import json import ruamel.yaml as yaml from datetime import datetime, timedelta from django.utils import timezone import pytz -from django.contrib.auth.models import Group from django.db import IntegrityError from django.core.files import File -from django.forms.models import model_to_dict from textwrap import dedent import zipfile import os @@ -23,6 +21,7 @@ import tempfile from threading import Thread from yaksh import settings + def setUpModule(): # create user profile user = User.objects.create_user(username='creator', @@ -39,31 +38,29 @@ def setUpModule(): Profile.objects.create(user=student, roll_number=3, institute='IIT', department='Chemical', position='Student') - user4 = User.objects.create_user(username='demo_user4', - password='demo', - email='demo4@test.com' + user4 = User.objects.create_user( + username='demo_user4', password='demo', email='demo4@test.com' ) Profile.objects.create(user=user4, roll_number=4, institute='IIT', department='Chemical', position='Student') - # create a course course = Course.objects.create(name="Python Course", enrollment="Enroll Request", creator=user) # create 20 questions for i in range(1, 21): - Question.objects.create(summary='Q%d' % (i), points=1, type='code', user=user) + Question.objects.create(summary='Q%d' % (i), points=1, + type='code', user=user) # 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 1', pass_criteria=0, - instructions="Demo Instructions") + 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 1', pass_criteria=0, + instructions="Demo Instructions") Quiz.objects.create(start_date_time=datetime(2014, 10, 9, 10, 8, 15, 0, tzinfo=pytz.utc), @@ -78,13 +75,12 @@ def setUpModule(): f.write('2'.encode('ascii')) # Learing module - learning_module_one = LearningModule.objects.create(name='LM1', - description='module one', - creator=user) - learning_module_two = LearningModule.objects.create(name='LM2', - description='module two', - creator=user, - order=1) + learning_module_one = LearningModule.objects.create( + name='LM1', description='module one', creator=user + ) + learning_module_two = LearningModule.objects.create( + name='LM2', description='module two', creator=user, order=1 + ) lesson = Lesson.objects.create(name='L1', description='Video Lesson', creator=user) learning_unit_lesson = LearningUnit.objects.create(order=1, lesson=lesson, @@ -113,6 +109,7 @@ def tearDownModule(): Lesson.objects.all().delete() LearningUnit.objects.all().delete() LearningModule.objects.all().delete() + AnswerPaper.objects.all().delete() ############################################################################### @@ -209,10 +206,11 @@ class LearningModuleTestCases(unittest.TestCase): # Given module_status = 'not attempted' # When + self.learning_module.learning_unit.remove(self.learning_unit_two) status = self.learning_module.get_status(self.student, self.course) # Then self.assertEqual(status, module_status) - + self.learning_module.learning_unit.add(self.learning_unit_two) # Module in progress # Given @@ -286,30 +284,23 @@ class ProfileTestCases(unittest.TestCase): self.assertEqual(self.profile.department, 'Chemical') self.assertEqual(self.profile.position, 'Student') + ############################################################################### class QuestionTestCases(unittest.TestCase): def setUp(self): # Single question details self.user1 = User.objects.get(username="creator") self.user2 = User.objects.get(username="demo_user2") - self.question1 = Question.objects.create(summary='Demo Python 1', - language='Python', - type='Code', - active=True, - description='Write a function', - points=1.0, - snippet='def myfunc()', - user=self.user1 + self.question1 = Question.objects.create( + summary='Demo Python 1', language='Python', type='Code', + active=True, description='Write a function', points=1.0, + snippet='def myfunc()', user=self.user1 ) - self.question2 = Question.objects.create(summary='Yaml Json', - language='python', - type='code', - active=True, - description='factorial of a no', - points=2.0, - snippet='def fact()', - user=self.user2 + self.question2 = Question.objects.create( + summary='Yaml Json', language='python', type='code', + active=True, description='factorial of a no', points=2.0, + snippet='def fact()', user=self.user2 ) # create a temp directory and add files for loading questions test @@ -324,26 +315,28 @@ class QuestionTestCases(unittest.TestCase): file2 = os.path.join(self.dump_tmp_path, "test.txt") upload_file = open(file2, "r") django_file = File(upload_file) - file = FileUpload.objects.create(file=django_file, - question=self.question2 - ) + FileUpload.objects.create(file=django_file, + question=self.question2 + ) self.question1.tags.add('python', 'function') - self.assertion_testcase = StandardTestCase(question=self.question1, + self.assertion_testcase = StandardTestCase( + question=self.question1, test_case='assert myfunc(12, 13) == 15', type='standardtestcase' ) - self.upload_test_case = StandardTestCase(question=self.question2, + self.upload_test_case = StandardTestCase( + question=self.question2, test_case='assert fact(3) == 6', type='standardtestcase' ) self.upload_test_case.save() self.user_answer = "demo_answer" self.test_case_upload_data = [{"test_case": "assert fact(3)==6", - "test_case_type": "standardtestcase", - "test_case_args": "", - "weight": 1.0 - }] + "test_case_type": "standardtestcase", + "test_case_args": "", + "weight": 1.0 + }] questions_data = [{"snippet": "def fact()", "active": True, "points": 1.0, "description": "factorial of a no", @@ -353,20 +346,33 @@ class QuestionTestCases(unittest.TestCase): "summary": "Yaml Demo", "tags": ['yaml_demo'] }] - questions_data_with_missing_fields = [{"active": True, - "points": 1.0, - "description":\ - "factorial of a no", - "language": "Python", - "type": "Code", - "testcase":\ - self.test_case_upload_data, - "summary": "Yaml Demo 2" - }] + questions_data_with_missing_fields = [{ + "active": True, "points": 1.0, "description": "factorial of a no", + "language": "Python", "type": "Code", + "testcase": self.test_case_upload_data, + "summary": "Yaml Demo 2" + }] self.yaml_questions_data = yaml.safe_dump_all(questions_data) self.yaml_questions_data_with_missing_fields = yaml.safe_dump_all( questions_data_with_missing_fields ) + self.bad_yaml_question_data = '''[{ + "active": True, "points": 1.0, "description" "factorial of a no", + "language": "Python", "type": "Code", + "testcase": self.test_case_upload_data, + "summary": "bad yaml" + }]''' + + self.test_case_without_type = [{"test_case": "assert fact(3)==6", + "test_case_args": "", + "weight": 1.0 + }] + self.yaml_question_data_without_test_case_type = yaml.safe_dump_all([{ + "active": True, "points": 1.0, "description": "factorial of a no", + "language": "Python", "type": "Code", + "testcase": self.test_case_without_type, + "summary": "bad yaml" + }]) def tearDown(self): shutil.rmtree(self.load_tmp_path) @@ -417,8 +423,9 @@ class QuestionTestCases(unittest.TestCase): self.assertTrue(self.question2.active) self.assertEqual(self.question2.snippet, q['snippet']) self.assertEqual(os.path.basename(que_file.file.path), - q['files'][0][0]) - self.assertEqual([case.get_field_value() for case in test_case], + q['files'][0][0]) + self.assertEqual([case.get_field_value() + for case in test_case], q['testcase'] ) for file in zip_file.namelist(): @@ -427,7 +434,7 @@ class QuestionTestCases(unittest.TestCase): def test_load_questions_with_all_fields(self): """ Test load questions into database from Yaml """ question = Question() - result = question.load_questions(self.yaml_questions_data, self.user1) + question.load_questions(self.yaml_questions_data, self.user1) question_data = Question.objects.get(summary="Yaml Demo") file = FileUpload.objects.get(question=question_data) test_case = question_data.get_test_cases() @@ -437,7 +444,7 @@ class QuestionTestCases(unittest.TestCase): self.assertEqual(question_data.description, 'factorial of a no') self.assertEqual(question_data.points, 1.0) self.assertTrue(question_data.active) - tags = question_data.tags.all().values_list("name",flat=True) + tags = question_data.tags.all().values_list("name", flat=True) self.assertListEqual(list(tags), ['yaml_demo']) self.assertEqual(question_data.snippet, 'def fact()') self.assertEqual(os.path.basename(file.file.path), "test.txt") @@ -449,27 +456,45 @@ class QuestionTestCases(unittest.TestCase): """ Test load questions into database from Yaml with missing fields like files, snippet and tags. """ question = Question() - result = question.load_questions( - self.yaml_questions_data_with_missing_fields, - self.user1 - ) + question.load_questions( + self.yaml_questions_data_with_missing_fields, + self.user1 + ) question_data = Question.objects.get(summary="Yaml Demo 2") file = FileUpload.objects.filter(question=question_data) test_case = question_data.get_test_cases() - self.assertEqual(question_data.summary,'Yaml Demo 2') - self.assertEqual(question_data.language,'Python') + self.assertEqual(question_data.summary, 'Yaml Demo 2') + self.assertEqual(question_data.language, 'Python') self.assertEqual(question_data.type, 'Code') - self.assertEqual(question_data.description,'factorial of a no') + self.assertEqual(question_data.description, 'factorial of a no') self.assertEqual(question_data.points, 1.0) self.assertTrue(question_data.active) - self.assertEqual(question_data.snippet,'') - self.assertListEqual(list(file),[]) + self.assertEqual(question_data.snippet, '') + self.assertListEqual(list(file), []) self.assertEqual([case.get_field_value() for case in test_case], self.test_case_upload_data ) - tags = question_data.tags.all().values_list("name",flat=True) + tags = question_data.tags.all().values_list("name", flat=True) self.assertListEqual(list(tags), []) + def test_load_questions_with_bad_yaml(self): + """ + Test if yaml file is parsed correctly + """ + question = Question() + msg = question.load_questions( + self.bad_yaml_question_data, + self.user1 + ) + self.assertIn("Error Parsing Yaml", msg) + + msg = question.load_questions( + self.yaml_question_data_without_test_case_type, + self.user1 + ) + self.assertEqual(msg, "Unable to parse test case data") + + ############################################################################### class QuizTestCases(unittest.TestCase): def setUp(self): @@ -481,7 +506,8 @@ class QuizTestCases(unittest.TestCase): self.quiz1 = Quiz.objects.get(description='demo quiz 1') self.quiz2 = Quiz.objects.get(description='demo quiz 2') self.quiz3 = Quiz.objects.create( - start_date_time=datetime(2015, 10, 9, 10, 8, 15, 0, tzinfo=pytz.utc), + 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, @@ -490,7 +516,8 @@ class QuizTestCases(unittest.TestCase): ) self.question_paper3 = QuestionPaper.objects.create(quiz=self.quiz3) self.quiz4 = Quiz.objects.create( - start_date_time=datetime(2015, 10, 9, 10, 8, 15, 0, tzinfo=pytz.utc), + 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, @@ -625,23 +652,39 @@ class QuestionPaperTestCases(unittest.TestCase): @classmethod def setUpClass(self): self.course = Course.objects.get(name="Python Course") + self.user = User.objects.get(username='creator') # All active questions - self.questions = Question.objects.filter(active=True) + self.questions = Question.objects.filter(active=True, user=self.user) self.quiz = Quiz.objects.get(description="demo quiz 1") + self.quiz_with_time_between_attempts = Quiz.objects.create( + description="demo quiz with time between attempts", + start_date_time=datetime(2015, 10, 9, 10, 8, 15, 0, + tzinfo=pytz.utc), + end_date_time=datetime(2199, 10, 9, 10, 8, 15, 0, tzinfo=pytz.utc), + duration=30, active=True, + attempts_allowed=3, time_between_attempts=1.0, + pass_criteria=0, + instructions="Demo Instructions" + ) # create question paper with only fixed questions self.question_paper_fixed_questions = QuestionPaper.objects.create( quiz=self.quiz) self.question_paper_fixed_questions.fixed_questions.add( - self.questions.get(summary='Q11'), self.questions.get(summary='Q10')) + self.questions.get(summary='Q11'), + self.questions.get(summary='Q10') + ) # create question paper with only random questions self.question_paper_random_questions = QuestionPaper.objects.create( quiz=self.quiz) - self.question_set_random = QuestionSet.objects.create(marks=2, - num_questions=2) - self.question_set_random.questions.add(self.questions.get(summary='Q13'), - self.questions.get(summary='Q5'), self.questions.get(summary='Q7')) + self.question_set_random = QuestionSet.objects.create( + marks=2, num_questions=2 + ) + self.question_set_random.questions.add( + self.questions.get(summary='Q13'), + self.questions.get(summary='Q5'), self.questions.get(summary='Q7') + ) self.question_paper_random_questions.random_questions.add( self.question_set_random) @@ -650,43 +693,47 @@ class QuestionPaperTestCases(unittest.TestCase): quiz=self.quiz) # create question paper - self.question_paper = QuestionPaper.objects.create(quiz=self.quiz, - total_marks=0.0, - shuffle_questions=True + self.question_paper = QuestionPaper.objects.create( + quiz=self.quiz, total_marks=0.0, shuffle_questions=True ) + self.question_paper_with_time_between_attempts = \ + QuestionPaper.objects.create( + quiz=self.quiz_with_time_between_attempts, + total_marks=0.0, + shuffle_questions=True + ) + self.question_paper.fixed_question_order = "{0}, {1}".format( self.questions[3].id, self.questions[5].id ) # add fixed set of questions to the question paper - self.question_paper.fixed_questions.add(self.questions[3], - self.questions[5] - ) + self.question_paper.fixed_questions.add( + self.questions[3], self.questions[5] + ) # create two QuestionSet for random questions # QuestionSet 1 - self.question_set_1 = QuestionSet.objects.create(marks=2, - num_questions=2 + self.question_set_1 = QuestionSet.objects.create( + marks=2, num_questions=2 ) # add pool of questions for random sampling - self.question_set_1.questions.add(self.questions[6], - self.questions[7], - self.questions[8], - self.questions[9] + self.question_set_1.questions.add( + self.questions[6], self.questions[7], + self.questions[8], self.questions[9] ) # add question set 1 to random questions in Question Paper self.question_paper.random_questions.add(self.question_set_1) # QuestionSet 2 - self.question_set_2 = QuestionSet.objects.create(marks=3, - num_questions=3 + self.question_set_2 = QuestionSet.objects.create( + marks=3, num_questions=3 ) # add pool of questions - self.question_set_2.questions.add(self.questions[11], - self.questions[12], - self.questions[13], - self.questions[14] + self.question_set_2.questions.add( + self.questions[11], self.questions[12], + self.questions[13], self.questions[14] ) # add question set 2 self.question_paper.random_questions.add(self.question_set_2) @@ -706,6 +753,9 @@ class QuestionPaperTestCases(unittest.TestCase): self.trial_course = Course.objects.create_trial_course(self.user) self.trial_quiz = Quiz.objects.create_trial_quiz(self.user) + @classmethod + def tearDownClass(self): + self.quiz.questionpaper_set.all().delete() def test_get_question_bank(self): # Given @@ -717,10 +767,11 @@ class QuestionPaperTestCases(unittest.TestCase): self.assertSequenceEqual(questions, question_bank) # Given - summaries = ['Q13','Q5','Q7'] + summaries = ['Q13', 'Q5', 'Q7'] questions = list(Question.objects.filter(summary__in=summaries)) # When - question_bank = self.question_paper_random_questions.get_question_bank() + question_bank = \ + self.question_paper_random_questions.get_question_bank() # Then self.assertSequenceEqual(questions, question_bank) @@ -735,8 +786,8 @@ class QuestionPaperTestCases(unittest.TestCase): """ Test question paper""" self.assertEqual(self.question_paper.quiz.description, 'demo quiz 1') self.assertSequenceEqual(self.question_paper.fixed_questions.all(), - [self.questions[3], self.questions[5]] - ) + [self.questions[3], self.questions[5]] + ) self.assertTrue(self.question_paper.shuffle_questions) def test_update_total_marks(self): @@ -749,8 +800,8 @@ class QuestionPaperTestCases(unittest.TestCase): """ Test get_random_questions() method of Question Paper""" random_questions_set_1 = self.question_set_1.get_random_questions() random_questions_set_2 = self.question_set_2.get_random_questions() - total_random_questions = len(random_questions_set_1 + \ - random_questions_set_2) + total_random_questions = len(random_questions_set_1 + + random_questions_set_2) self.assertEqual(total_random_questions, 5) # To check whether random questions are from random_question_set @@ -782,30 +833,74 @@ class QuestionPaperTestCases(unittest.TestCase): answerpaper.passed = True answerpaper.save() # test can_attempt_now(self): - self.assertFalse(self.question_paper.can_attempt_now(self.user, - self.course.id)) + result = (False, + u'You cannot attempt demo quiz 1 quiz more than 1 time(s)') + self.assertEquals( + self.question_paper.can_attempt_now(self.user, self.course.id), + result + ) # trying to create an answerpaper with same parameters passed. - answerpaper2 = self.question_paper.make_answerpaper(self.user, self.ip, - attempt_num, - self.course.id) + answerpaper2 = self.question_paper.make_answerpaper( + self.user, self.ip, attempt_num, self.course.id + ) # check if make_answerpaper returned an object instead of creating one. self.assertEqual(answerpaper, answerpaper2) + def test_time_between_attempt(self): + """ Test make_answerpaper() method of Question Paper""" + attempt_num = 1 + + self.first_start_time = timezone.now() + self.first_end_time = self.first_start_time + timedelta(minutes=20) + self.second_start_time = self.first_start_time + timedelta(minutes=30) + self.second_end_time = self.second_start_time + timedelta(minutes=20) + + # create answerpaper + self.first_answerpaper = AnswerPaper( + user=self.user, + question_paper=self.question_paper_with_time_between_attempts, + start_time=self.first_start_time, + end_time=self.first_end_time, + user_ip=self.ip, + course=self.course, + attempt_number=attempt_num + ) + self.first_answerpaper.passed = True + self.first_answerpaper.save() + self.second_answerpaper = AnswerPaper( + user=self.user, + question_paper=self.question_paper_with_time_between_attempts, + start_time=self.second_start_time, + end_time=self.second_end_time, + user_ip=self.ip, + course=self.course, + attempt_number=attempt_num + 1 + ) + self.second_answerpaper.passed = True + self.second_answerpaper.save() + msg = u'You cannot start the next attempt ' +\ + 'for this quiz before1.0 hour(s)' + result = (False, msg) + self.assertEquals( + self.question_paper_with_time_between_attempts.can_attempt_now( + self.user, self.course.id), result + ) def test_create_trial_paper_to_test_quiz(self): qu_list = [str(self.questions_list[0]), str(self.questions_list[1])] - trial_paper = QuestionPaper.objects.create_trial_paper_to_test_quiz\ - (self.trial_quiz, - self.quiz.id - ) + trial_paper = \ + QuestionPaper.objects.create_trial_paper_to_test_quiz( + self.trial_quiz, self.quiz.id + ) trial_paper.random_questions.add(self.question_set_1) trial_paper.random_questions.add(self.question_set_2) trial_paper.fixed_question_order = ",".join(qu_list) self.assertEqual(trial_paper.quiz, self.trial_quiz) - self.assertSequenceEqual(trial_paper.get_ordered_questions(), - self.question_paper.get_ordered_questions() - ) + self.assertSequenceEqual( + trial_paper.get_ordered_questions(), + self.question_paper.get_ordered_questions() + ) trial_paper_ran = [q_set.id for q_set in trial_paper.random_questions.all()] qp_ran = [q_set.id for q_set in @@ -815,10 +910,10 @@ class QuestionPaperTestCases(unittest.TestCase): def test_create_trial_paper_to_test_questions(self): qu_list = [str(self.questions_list[0]), str(self.questions_list[1])] - trial_paper = QuestionPaper.objects.\ - create_trial_paper_to_test_questions( - self.trial_quiz, qu_list - ) + trial_paper = \ + QuestionPaper.objects.create_trial_paper_to_test_questions( + self.trial_quiz, qu_list + ) self.assertEqual(trial_paper.quiz, self.trial_quiz) fixed_q = self.question_paper.fixed_questions.values_list( 'id', flat=True) @@ -848,7 +943,7 @@ class AnswerPaperTestCases(unittest.TestCase): ) self.qtn_paper_with_single_question.save() - all_questions = Question.objects.all() + all_questions = Question.objects.filter(user=self.user).order_by("id") self.questions = all_questions[0:3] self.start_time = timezone.now() self.end_time = self.start_time + timedelta(minutes=20) @@ -858,7 +953,8 @@ class AnswerPaperTestCases(unittest.TestCase): self.question4 = all_questions[3] # create answerpaper - self.answerpaper = AnswerPaper(user=self.user, + self.answerpaper = AnswerPaper( + user=self.user, question_paper=self.question_paper, start_time=self.start_time, end_time=self.end_time, @@ -874,15 +970,20 @@ class AnswerPaperTestCases(unittest.TestCase): self.answerpaper.attempt_number = already_attempted + 1 self.answerpaper.save() self.answerpaper.questions.add(*self.questions) + self.answerpaper.questions_order = ",".join( + [str(q.id) for q in self.questions] + ) self.answerpaper.questions_unanswered.add(*self.questions) self.answerpaper.save() # answers for the Answer Paper - self.answer_right = Answer(question=self.question1, + self.answer_right = Answer( + question=self.question1, answer="Demo answer", correct=True, marks=1, error=json.dumps([]) ) - self.answer_wrong = Answer(question=self.question2, + self.answer_wrong = Answer( + question=self.question2, answer="My answer", correct=False, marks=0, @@ -900,7 +1001,8 @@ class AnswerPaperTestCases(unittest.TestCase): self.answerpaper.answers.add(self.answer1) # create an answerpaper with only one question - self.answerpaper_single_question = AnswerPaper(user=self.user, + self.answerpaper_single_question = AnswerPaper( + user=self.user, question_paper=self.question_paper, start_time=self.start_time, end_time=self.end_time, @@ -915,10 +1017,13 @@ class AnswerPaperTestCases(unittest.TestCase): self.answerpaper_single_question.attempt_number = already_attempted + 1 self.answerpaper_single_question.save() self.answerpaper_single_question.questions.add(self.question4) - self.answerpaper_single_question.questions_unanswered.add(self.question4) + self.answerpaper_single_question.questions_unanswered.add( + self.question4 + ) self.answerpaper_single_question.save() # answers for the Answer Paper - self.single_answer = Answer(question=self.question4, + self.single_answer = Answer( + question=self.question4, answer="Demo answer", correct=True, marks=1, error=json.dumps([]) @@ -928,38 +1033,38 @@ class AnswerPaperTestCases(unittest.TestCase): self.question1.language = 'python' self.question1.test_case_type = 'standardtestcase' - self.question1.summary = "Question1" + self.question1.summary = "Q1" self.question1.save() self.question2.language = 'python' self.question2.type = 'mcq' self.question2.test_case_type = 'mcqtestcase' - self.question2.summary = "Question2" + self.question2.summary = "Q2" self.question2.save() self.question3.language = 'python' self.question3.type = 'mcc' self.question3.test_case_type = 'mcqtestcase' - self.question3.summary = "Question3" + self.question3.summary = "Q3" self.question3.save() self.assertion_testcase = StandardTestCase( question=self.question1, test_case='assert add(1, 3) == 4', - type = 'standardtestcase' + type='standardtestcase' ) self.assertion_testcase.save() self.mcq_based_testcase = McqTestCase( - options = 'a', + options='a', question=self.question2, - correct = True, - type = 'mcqtestcase' + correct=True, + type='mcqtestcase' ) self.mcq_based_testcase.save() self.mcc_based_testcase = McqTestCase( question=self.question3, - options = 'a', - correct = True, - type = 'mcqtestcase' + options='a', + correct=True, + type='mcqtestcase' ) self.mcc_based_testcase.save() @@ -991,9 +1096,10 @@ class AnswerPaperTestCases(unittest.TestCase): self.server_pool = server_pool self.server_thread = t = Thread(target=server_pool.run) t.start() - + @classmethod def tearDownClass(self): + self.quiz.questionpaper_set.all().delete() self.server_pool.stop() self.server_thread.join() settings.code_evaluators['python']['standardtestcase'] = \ @@ -1004,7 +1110,9 @@ class AnswerPaperTestCases(unittest.TestCase): question_id = self.question4.id expected_score = 1 # When - score = self.answerpaper_single_question.get_per_question_score(question_id) + score = self.answerpaper_single_question.get_per_question_score( + question_id + ) # Then self.assertEqual(score, expected_score) @@ -1031,10 +1139,10 @@ class AnswerPaperTestCases(unittest.TestCase): # Before questions are answered self.assertEqual(self.answerpaper_single_question.questions_left(), 1) - current_question = self.answerpaper_single_question.add_completed_question( - self.question4.id - ) - + current_question = \ + self.answerpaper_single_question.add_completed_question( + self.question4.id + ) # Then self.assertEqual( @@ -1056,9 +1164,10 @@ class AnswerPaperTestCases(unittest.TestCase): self.assertEqual(next_question.summary, "Q4") # When - current_question = self.answerpaper_single_question.get_current_question( - self.answerpaper_single_question.questions.all() - ) + current_question = \ + self.answerpaper_single_question.get_current_question( + self.answerpaper_single_question.questions.all() + ) # Then self.assertEqual(self.answerpaper_single_question.questions_left(), 0) @@ -1097,7 +1206,8 @@ class AnswerPaperTestCases(unittest.TestCase): details = self.answerpaper.regrade(self.question3.id) # Then - self.answer = self.answerpaper.answers.filter(question=self.question3).last() + self.answer = self.answerpaper.answers.filter( + question=self.question3).last() self.assertTrue(details[0]) self.assertEqual(self.answer.marks, 0) self.assertFalse(self.answer.correct) @@ -1105,7 +1215,6 @@ class AnswerPaperTestCases(unittest.TestCase): def test_validate_and_regrade_code_correct_answer(self): # Given # Start code server - user_answer = dedent("""\ def add(a,b): return a+b @@ -1118,9 +1227,9 @@ class AnswerPaperTestCases(unittest.TestCase): user = self.answerpaper.user # When - json_data = self.question1.consolidate_answer_data(user_answer, - user - ) + json_data = self.question1.consolidate_answer_data( + user_answer, user + ) get_result = self.answerpaper.validate_answer(user_answer, self.question1, json_data, @@ -1128,7 +1237,7 @@ class AnswerPaperTestCases(unittest.TestCase): self.SERVER_POOL_PORT ) url = 'http://localhost:%s' % self.SERVER_POOL_PORT - check_result = get_result_from_code_server(url,get_result['uid'], + check_result = get_result_from_code_server(url, get_result['uid'], block=True ) result = json.loads(check_result.get('result')) @@ -1164,7 +1273,8 @@ class AnswerPaperTestCases(unittest.TestCase): def test_validate_and_regrade_mcq_correct_answer(self): # Given mcq_answer = str(self.mcq_based_testcase.id) - self.answer = Answer(question=self.question2, + self.answer = Answer( + question=self.question2, answer=mcq_answer, ) self.answer.save() @@ -1192,7 +1302,8 @@ class AnswerPaperTestCases(unittest.TestCase): details = self.answerpaper.regrade(self.question2.id) # Then - self.answer = self.answerpaper.answers.filter(question=self.question2).last() + self.answer = self.answerpaper.answers.filter( + question=self.question2).last() self.assertTrue(details[0]) self.assertEqual(self.answer.marks, 0) self.assertFalse(self.answer.correct) @@ -1200,7 +1311,8 @@ class AnswerPaperTestCases(unittest.TestCase): def test_mcq_incorrect_answer(self): # Given mcq_answer = 'b' - self.answer = Answer(question=self.question2, + self.answer = Answer( + question=self.question2, answer=mcq_answer, ) self.answer.save() @@ -1237,9 +1349,9 @@ class AnswerPaperTestCases(unittest.TestCase): """ Test Answer Paper""" self.assertEqual(self.answerpaper.user.username, 'creator') self.assertEqual(self.answerpaper.user_ip, self.ip) - questions = self.answerpaper.get_questions() + questions = [q.id for q in self.answerpaper.get_questions()] num_questions = len(questions) - self.assertSequenceEqual(list(questions), list(self.questions)) + self.assertEqual(set(questions), set([q.id for q in self.questions])) self.assertEqual(num_questions, 3) self.assertEqual(self.answerpaper.question_paper, self.question_paper) self.assertEqual(self.answerpaper.start_time, self.start_time) @@ -1250,7 +1362,7 @@ class AnswerPaperTestCases(unittest.TestCase): self.assertEqual(self.answerpaper.questions_left(), 3) # Test current_question() method of Answer Paper current_question = self.answerpaper.current_question() - self.assertEqual(current_question.summary, "Question1") + self.assertEqual(current_question.summary, "Q1") # Test completed_question() method of Answer Paper question = self.answerpaper.add_completed_question(self.question1.id) @@ -1259,14 +1371,14 @@ class AnswerPaperTestCases(unittest.TestCase): # Test next_question() method of Answer Paper current_question = self.answerpaper.current_question() - self.assertEqual(current_question.summary, "Question2") + self.assertEqual(current_question.summary, "Q2") # When next_question_id = self.answerpaper.next_question(current_question.id) # Then self.assertTrue(next_question_id is not None) - self.assertEqual(next_question_id.summary, "Question3") + self.assertEqual(next_question_id.summary, "Q3") # Given, here question is already answered current_question_id = self.question1.id @@ -1276,7 +1388,7 @@ class AnswerPaperTestCases(unittest.TestCase): # Then self.assertTrue(next_question_id is not None) - self.assertEqual(next_question_id.summary, "Question2") + self.assertEqual(next_question_id.summary, "Q2") # Given, wrong question id current_question_id = 12 @@ -1286,7 +1398,7 @@ class AnswerPaperTestCases(unittest.TestCase): # Then self.assertTrue(next_question_id is not None) - self.assertEqual(next_question_id.summary, "Question1") + self.assertEqual(next_question_id.summary, "Q1") # Given, last question in the list current_question_id = self.question3.id @@ -1297,7 +1409,7 @@ class AnswerPaperTestCases(unittest.TestCase): # Then self.assertTrue(next_question_id is not None) - self.assertEqual(next_question_id.summary, "Question1") + self.assertEqual(next_question_id.summary, "Q1") # Test get_questions_answered() method # When @@ -1312,8 +1424,9 @@ class AnswerPaperTestCases(unittest.TestCase): # Then self.assertEqual(questions_unanswered.count(), 2) - self.assertSequenceEqual(questions_unanswered, - [self.questions[1], self.questions[2]]) + self.assertEqual(set([q.id for q in questions_unanswered]), + set([self.questions[1].id, self.questions[2].id]) + ) # Test completed_question and next_question # When all questions are answered @@ -1325,7 +1438,7 @@ class AnswerPaperTestCases(unittest.TestCase): # Then self.assertEqual(self.answerpaper.questions_left(), 1) self.assertIsNotNone(current_question) - self.assertEqual(current_question.summary, "Question3") + self.assertEqual(current_question.summary, "Q3") # When current_question = self.answerpaper.add_completed_question( @@ -1335,7 +1448,9 @@ class AnswerPaperTestCases(unittest.TestCase): # Then self.assertEqual(self.answerpaper.questions_left(), 0) self.assertIsNotNone(current_question) - self.assertTrue(current_question == self.answerpaper.questions.all()[0]) + self.assertTrue( + current_question == self.answerpaper.get_all_ordered_questions()[0] + ) # When next_question_id = self.answerpaper.next_question(current_question_id) @@ -1359,16 +1474,24 @@ class AnswerPaperTestCases(unittest.TestCase): def test_set_end_time(self): current_time = timezone.now() self.answerpaper.set_end_time(current_time) - self.assertEqual(self.answerpaper.end_time,current_time) + self.assertEqual(self.answerpaper.end_time, current_time) 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])) @@ -1405,7 +1528,7 @@ class AnswerPaperTestCases(unittest.TestCase): def test_duplicate_attempt_answerpaper(self): with self.assertRaises(IntegrityError): - new_answerpaper = AnswerPaper.objects.create( + AnswerPaper.objects.create( user=self.answerpaper.user, question_paper=self.answerpaper.question_paper, attempt_number=self.answerpaper.attempt_number, @@ -1426,7 +1549,9 @@ class CourseTestCases(unittest.TestCase): self.student2 = User.objects.get(username="demo_user3") self.quiz1 = Quiz.objects.get(description='demo quiz 1') self.quiz2 = Quiz.objects.get(description='demo quiz 2') - self.questions = Question.objects.filter(active=True) + self.questions = Question.objects.filter(active=True, + user=self.creator + ) self.modules = LearningModule.objects.filter(creator=self.creator) # create courses with disabled enrollment @@ -1435,22 +1560,22 @@ class CourseTestCases(unittest.TestCase): enrollment="Enroll Request", creator=self.creator, start_enroll_time=datetime(2015, 10, 9, 10, 8, 15, 0, - tzinfo=pytz.utc - ), + tzinfo=pytz.utc + ), end_enroll_time=datetime(2015, 11, 9, 10, 8, 15, 0, - tzinfo=pytz.utc - ), + tzinfo=pytz.utc + ), ) self.open_course = Course.objects.create( name="Open Course With Enrollment Disabled", enrollment="Open Course", creator=self.creator, start_enroll_time=datetime(2015, 10, 9, 10, 8, 15, 0, - tzinfo=pytz.utc - ), + tzinfo=pytz.utc + ), end_enroll_time=datetime(2015, 11, 9, 10, 8, 15, 0, - tzinfo=pytz.utc - ), + tzinfo=pytz.utc + ), ) # create a course that will be cloned @@ -1459,20 +1584,20 @@ class CourseTestCases(unittest.TestCase): enrollment="Open Course", creator=self.creator, start_enroll_time=datetime(2015, 10, 9, 10, 8, 15, 0, - tzinfo=pytz.utc - ), + tzinfo=pytz.utc + ), end_enroll_time=datetime(2015, 11, 9, 10, 8, 15, 0, - tzinfo=pytz.utc - ), + tzinfo=pytz.utc + ), ) self.template_quiz = Quiz.objects.create( start_date_time=datetime(2014, 10, 9, 10, 8, 15, 0, - tzinfo=pytz.utc - ), + tzinfo=pytz.utc + ), end_date_time=datetime(2015, 10, 9, 10, 8, 15, 0, - tzinfo=pytz.utc - ), + tzinfo=pytz.utc + ), duration=30, active=False, attempts_allowed=-1, @@ -1488,13 +1613,13 @@ class CourseTestCases(unittest.TestCase): shuffle_questions=True ) - self.template_question_paper.fixed_questions.add(self.questions[1], - self.questions[2], - self.questions[3] + self.template_question_paper.fixed_questions.add( + self.questions[1], self.questions[2], self.questions[3] ) self.template_quiz2 = Quiz.objects.create( - start_date_time=datetime(2015, 10, 9, 10, 8, 15, 0, tzinfo=pytz.utc), + 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, @@ -1510,9 +1635,8 @@ class CourseTestCases(unittest.TestCase): shuffle_questions=True ) - self.template_question_paper2.fixed_questions.add(self.questions[1], - self.questions[2], - self.questions[3] + self.template_question_paper2.fixed_questions.add( + self.questions[1], self.questions[2], self.questions[3] ) def test_get_learning_modules(self): @@ -1608,7 +1732,7 @@ class CourseTestCases(unittest.TestCase): """ Test to add teachers to a course""" self.course.add_teachers(self.student1, self.student2) self.assertSequenceEqual(self.course.get_teachers(), - [self.student1, self.student2]) + [self.student1, self.student2]) def test_remove_teachers(self): """ Test to remove teachers from a course""" @@ -1648,11 +1772,12 @@ class CourseTestCases(unittest.TestCase): # for course with no modules self.no_module_course = Course.objects.create( name="test_course", creator=self.creator, enrollment="open") - percent = self.course.percent_completed(self.student1) + modules = self.course.get_learning_modules() + percent = self.course.percent_completed(self.student1, modules) self.assertEqual(percent, 0) - + self.quiz1.questionpaper_set.all().delete() # for course with module but zero percent completed - percent = self.course.percent_completed(self.student1) + percent = self.course.percent_completed(self.student1, modules) self.assertEqual(percent, 0) # Add completed unit to course status and check percent @@ -1662,31 +1787,41 @@ class CourseTestCases(unittest.TestCase): course_status = CourseStatus.objects.create( course=self.course, user=self.student1) course_status.completed_units.add(self.completed_unit) - updated_percent = self.course.percent_completed(self.student1) + updated_percent = self.course.percent_completed(self.student1, modules) self.assertEqual(updated_percent, 25) + def test_course_time_remaining_to_start(self): + # check if course has 0 days left to start + self.assertEqual(self.course.days_before_start(), 0) + + # check if course has some days left to start + course_time = self.course.start_enroll_time + self.course.start_enroll_time = datetime( + 2199, 12, 31, 10, 8, 15, 0, + tzinfo=pytz.utc + ) + self.course.save() + updated_course = Course.objects.get(id=self.course.id) + time_diff = updated_course.start_enroll_time - timezone.now() + actual_days = time_diff.days + 1 + self.assertEqual(updated_course.days_before_start(), actual_days) + self.course.start_enroll_time = course_time + self.course.save() + ############################################################################### class TestCaseTestCases(unittest.TestCase): def setUp(self): self.user = User.objects.get(username="creator") - self.question1 = Question(summary='Demo question 1', - language='Python', - type='Code', - active=True, - description='Write a function', - points=1.0, - user=self.user, - snippet='def myfunc()' + self.question1 = Question( + summary='Demo question 1', language='Python', + type='Code', active=True, description='Write a function', + points=1.0, user=self.user, snippet='def myfunc()' ) - self.question2 = Question(summary='Demo question 2', - language='Python', - type='Code', - active=True, - description='Write to standard output', - points=1.0, - user=self.user, - snippet='def myfunc()' + self.question2 = Question( + summary='Demo question 2', language='Python', + type='Code', active=True, description='Write to standard output', + points=1.0, user=self.user, snippet='def myfunc()' ) self.question1.save() self.question2.save() @@ -1703,30 +1838,31 @@ class TestCaseTestCases(unittest.TestCase): ) self.assertion_testcase.save() self.stdout_based_testcase.save() - answer_data = {'metadata': { 'user_answer': 'demo_answer', - 'language': 'python', - 'partial_grading': False - }, - 'test_case_data': [{'test_case': 'assert myfunc(12, 13) == 15', + answer_data = {'metadata': {'user_answer': 'demo_answer', + 'language': 'python', + 'partial_grading': False + }, + 'test_case_data': [ + {'test_case': 'assert myfunc(12, 13) == 15', 'test_case_type': 'standardtestcase', 'test_case_args': "", 'weight': 1.0 }] - } + } self.answer_data_json = json.dumps(answer_data) def test_assertion_testcase(self): """ Test question """ self.assertEqual(self.assertion_testcase.question, self.question1) self.assertEqual(self.assertion_testcase.test_case, - 'assert myfunc(12, 13) == 15') + 'assert myfunc(12, 13) == 15') def test_stdout_based_testcase(self): """ Test question """ self.assertEqual(self.stdout_based_testcase.question, self.question2) self.assertEqual(self.stdout_based_testcase.expected_output, - 'Hello World' - ) + 'Hello World' + ) def test_consolidate_answer_data(self): """ Test consolidate answer data model method """ @@ -1735,8 +1871,10 @@ class TestCaseTestCases(unittest.TestCase): ) actual_data = json.loads(result) exp_data = json.loads(self.answer_data_json) - self.assertEqual(actual_data['metadata']['user_answer'], exp_data['metadata']['user_answer']) - self.assertEqual(actual_data['test_case_data'], exp_data['test_case_data']) + self.assertEqual(actual_data['metadata']['user_answer'], + exp_data['metadata']['user_answer']) + self.assertEqual(actual_data['test_case_data'], + exp_data['test_case_data']) class AssignmentUploadTestCases(unittest.TestCase): @@ -1751,30 +1889,26 @@ class AssignmentUploadTestCases(unittest.TestCase): self.user2.save() self.quiz = Quiz.objects.get(description="demo quiz 1") - self.questionpaper = QuestionPaper.objects.create(quiz=self.quiz, - total_marks=0.0, - shuffle_questions=True + self.questionpaper = QuestionPaper.objects.create( + quiz=self.quiz, total_marks=0.0, shuffle_questions=True ) - self.question = Question.objects.create(summary='Assignment', - language='Python', - type='upload', - active=True, - description='Upload a file', - points=1.0, - snippet='', + self.question = Question.objects.create( + summary='Assignment', language='Python', type='upload', + active=True, description='Upload a file', points=1.0, snippet='', user=self.user1 ) - self.questionpaper.fixed_question_order = "{0}".format(self.question.id) + self.questionpaper.fixed_question_order = "{0}".format( + self.question.id) self.questionpaper.fixed_questions.add(self.question) file_path1 = os.path.join(tempfile.gettempdir(), "upload1.txt") file_path2 = os.path.join(tempfile.gettempdir(), "upload2.txt") - self.assignment1 = AssignmentUpload.objects.create(user=self.user1, - assignmentQuestion=self.question, assignmentFile=file_path1, - question_paper=self.questionpaper + self.assignment1 = AssignmentUpload.objects.create( + user=self.user1, assignmentQuestion=self.question, + assignmentFile=file_path1, question_paper=self.questionpaper ) - self.assignment2 = AssignmentUpload.objects.create(user=self.user2, - assignmentQuestion=self.question, assignmentFile=file_path2, - question_paper=self.questionpaper + self.assignment2 = AssignmentUpload.objects.create( + user=self.user2, assignmentQuestion=self.question, + assignmentFile=file_path2, question_paper=self.questionpaper ) def test_get_assignments_for_user_files(self): @@ -1801,3 +1935,102 @@ class AssignmentUploadTestCases(unittest.TestCase): actual_file_name = self.quiz.description.replace(" ", "_") file_name = file_name.replace(" ", "_") self.assertIn(actual_file_name, file_name) + + +class CourseStatusTestCases(unittest.TestCase): + def setUp(self): + user = User.objects.get(username='creator') + self.course = Course.objects.create(name="Demo Course", creator=user, + enrollment="Enroll Request") + self.module = LearningModule.objects.create(name='M1', creator=user, + description='module one') + self.quiz1 = Quiz.objects.create(time_between_attempts=0, weightage=50, + description='qz1') + self.quiz2 = Quiz.objects.create( + time_between_attempts=0, weightage=100, description='qz2' + ) + question = Question.objects.first() + self.qpaper1 = QuestionPaper.objects.create(quiz=self.quiz1) + self.qpaper2 = QuestionPaper.objects.create(quiz=self.quiz2) + self.qpaper1.fixed_questions.add(question) + self.qpaper2.fixed_questions.add(question) + self.qpaper1.update_total_marks() + self.qpaper2.update_total_marks() + self.qpaper1.save() + self.qpaper2.save() + self.unit_1_quiz = LearningUnit.objects.create(order=1, type='quiz', + quiz=self.quiz1) + self.unit_2_quiz = LearningUnit.objects.create(order=2, type='quiz', + quiz=self.quiz2) + self.module.learning_unit.add(self.unit_1_quiz) + self.module.learning_unit.add(self.unit_2_quiz) + self.module.save() + self.course.learning_module.add(self.module) + student = User.objects.get(username='course_user') + self.course.students.add(student) + self.course.save() + + attempt = 1 + ip = '127.0.0.1' + self.answerpaper1 = self.qpaper1.make_answerpaper(student, ip, attempt, + self.course.id) + self.answerpaper2 = self.qpaper2.make_answerpaper(student, ip, attempt, + self.course.id) + + self.course_status = CourseStatus.objects.create(course=self.course, + user=student) + + def tearDown(self): + self.course_status.delete() + self.answerpaper1.delete() + self.answerpaper2.delete() + self.qpaper1.delete() + self.qpaper2.delete() + self.quiz1.delete() + self.quiz2.delete() + self.unit_1_quiz.delete() + self.unit_2_quiz.delete() + self.module.delete() + self.course.delete() + + def test_course_is_complete(self): + # When + self.course_status.completed_units.add(self.unit_1_quiz) + # Then + self.assertFalse(self.course_status.is_course_complete()) + + # When + self.course_status.completed_units.add(self.unit_2_quiz) + # Then + self.assertTrue(self.course_status.is_course_complete()) + + # Given + self.answerpaper1.marks_obtained = 1 + self.answerpaper1.save() + self.answerpaper2.marks_obtained = 0 + self.answerpaper2.save() + # When + self.course_status.calculate_percentage() + # Then + self.assertEqual(round(self.course_status.percentage, 2), 33.33) + # When + self.course_status.set_grade() + # Then + self.assertEqual(self.course_status.get_grade(), 'F') + + # Given + self.answerpaper1.marks_obtained = 0 + self.answerpaper1.save() + self.answerpaper2.marks_obtained = 1 + self.answerpaper2.save() + # When + self.course_status.calculate_percentage() + # Then + self.assertEqual(round(self.course_status.percentage, 2), 66.67) + # When + self.course_status.set_grade() + # Then + self.assertEqual(self.course_status.get_grade(), 'B') + + # Test get course grade after completion + self.assertEqual(self.course.get_grade(self.answerpaper1.user), 'B') diff --git a/yaksh/test_views.py b/yaksh/test_views.py index 8fa4747..5bd55eb 100644 --- a/yaksh/test_views.py +++ b/yaksh/test_views.py @@ -8,9 +8,7 @@ except ImportError: from io import BytesIO as string_io import zipfile import shutil -from textwrap import dedent from markdown import Markdown - from django.contrib.auth.models import Group from django.contrib.auth import authenticate from django.core.urlresolvers import reverse @@ -20,12 +18,15 @@ from django.utils import timezone from django.core import mail from django.conf import settings from django.core.files.uploadedfile import SimpleUploadedFile +from django.core.files import File + -from yaksh.models import User, Profile, Question, Quiz, QuestionPaper,\ - QuestionSet, AnswerPaper, Answer, Course, StandardTestCase,\ - AssignmentUpload, FileUpload, McqTestCase, IntegerTestCase, StringTestCase,\ - FloatTestCase, FIXTURES_DIR_PATH, LearningModule, LearningUnit, Lesson,\ - LessonFile +from yaksh.models import ( + User, Profile, Question, Quiz, QuestionPaper, AnswerPaper, Answer, Course, + AssignmentUpload, McqTestCase, IntegerTestCase, StringTestCase, + FloatTestCase, FIXTURES_DIR_PATH, LearningModule, LearningUnit, Lesson, + LessonFile, CourseStatus +) from yaksh.decorators import user_has_profile @@ -37,21 +38,23 @@ class TestUserRegistration(TestCase): self.registered_user.delete() def test_register_user_post(self): - response = self.client.post(reverse('yaksh:register'), - data={'username': 'register_user', - 'email':'register_user@mail.com', 'password': 'reg_user', - 'confirm_password': 'reg_user', 'first_name': 'user1_f_name', - 'last_name': 'user1_l_name', 'roll_number': '1', - 'institute': 'demo_institute', 'department': 'demo_dept', - 'position': 'student', 'timezone': pytz.utc.zone - } + self.client.post( + reverse('yaksh:register'), + data={'username': 'register_user', + 'email': 'register_user@mail.com', 'password': 'reg_user', + 'confirm_password': 'reg_user', 'first_name': 'user1_f_name', + 'last_name': 'user1_l_name', 'roll_number': '1', + 'institute': 'demo_institute', 'department': 'demo_dept', + 'position': 'student', 'timezone': pytz.utc.zone + } ) self.registered_user = User.objects.get(username='register_user') self.assertEqual(self.registered_user.email, 'register_user@mail.com') self.assertEqual(self.registered_user.first_name, 'user1_f_name') self.assertEqual(self.registered_user.last_name, 'user1_l_name') self.assertEqual(self.registered_user.profile.roll_number, '1') - self.assertEqual(self.registered_user.profile.institute, 'demo_institute') + self.assertEqual(self.registered_user.profile.institute, + 'demo_institute') self.assertEqual(self.registered_user.profile.department, 'demo_dept') self.assertEqual(self.registered_user.profile.position, 'student') self.assertEqual(self.registered_user.profile.timezone, 'UTC') @@ -93,7 +96,6 @@ class TestProfile(TestCase): self.user1.delete() self.user2.delete() - def test_user_has_profile_for_user_without_profile(self): """ If no profile exists for user passed as argument return False @@ -108,7 +110,6 @@ class TestProfile(TestCase): has_profile_status = user_has_profile(self.user2) self.assertTrue(has_profile_status) - def test_view_profile_denies_anonymous(self): """ If not logged in redirect to login page @@ -149,15 +150,16 @@ class TestProfile(TestCase): username=self.user2.username, password=self.user2_plaintext_pass ) - post_response = self.client.post(reverse('yaksh:new_activation'), - data={'email':self.user2.email} + post_response = self.client.post( + reverse('yaksh:new_activation'), + data={'email': self.user2.email} ) subject = mail.outbox[0].subject.replace(" ", "_") activation_key = mail.outbox[0].body.split("\n")[2].split("/")[-1] - get_response = self.client.get(reverse('yaksh:activate', - kwargs={'key': activation_key}), - follow=True - ) + get_response = self.client.get( + reverse('yaksh:activate', kwargs={'key': activation_key}), + follow=True + ) updated_profile_user = User.objects.get(id=self.user2.id) updated_profile = Profile.objects.get(user=updated_profile_user) self.assertEqual(post_response.status_code, 200) @@ -166,6 +168,14 @@ class TestProfile(TestCase): self.assertEqual(updated_profile.is_email_verified, True) self.assertTemplateUsed(get_response, 'yaksh/activation_status.html') + post_response = self.client.post( + reverse('yaksh:new_activation'), + data={'email': 'user@mail.com'} + ) + self.assertEqual(post_response.status_code, 200) + self.assertFalse(post_response.context['success']) + self.assertTemplateUsed(get_response, 'yaksh/activation_status.html') + def test_edit_profile_post(self): """ POST request to edit_profile view should update the user's profile @@ -174,7 +184,8 @@ class TestProfile(TestCase): username=self.user2.username, password=self.user2_plaintext_pass ) - response = self.client.post(reverse('yaksh:edit_profile'), + response = self.client.post( + reverse('yaksh:edit_profile'), data={ 'user': self.user2, 'first_name': 'new_first_name', @@ -206,7 +217,8 @@ class TestProfile(TestCase): username=self.user1.username, password=self.user1_plaintext_pass ) - response = self.client.post(reverse('yaksh:edit_profile'), + response = self.client.post( + reverse('yaksh:edit_profile'), data={ 'user': self.user1, 'first_name': 'new_first_name', @@ -274,10 +286,11 @@ class TestProfile(TestCase): username=self.user2.username, password=self.user2_plaintext_pass ) - response = self.client.post(reverse('yaksh:update_email'), + response = self.client.post( + reverse('yaksh:update_email'), data={ 'username': self.user2.username, - 'email':"demo_user2@mail.com" + 'email': "demo_user2@mail.com" } ) updated_user = User.objects.get(id=self.user2.id) @@ -338,12 +351,15 @@ class TestStudentDashboard(TestCase): timezone='UTC' ) - self.course = Course.objects.create(name="Python Course", - enrollment="Enroll Request", creator=self.user) + self.course = Course.objects.create( + name="Python Course", + enrollment="Enroll Request", creator=self.user + ) - self.hidden_course = Course.objects.create(name="Hidden Course", - enrollment="Enroll Request", creator=self.user, code="hide", - hidden=True) + self.hidden_course = Course.objects.create( + name="Hidden Course", enrollment="Enroll Request", + creator=self.user, code="hide", hidden=True + ) def tearDown(self): self.client.logout() @@ -387,7 +403,7 @@ class TestStudentDashboard(TestCase): def test_student_dashboard_all_courses_get(self): """ - Check student dashboard for all non hidden courses + Check student dashboard for all non hidden courses """ self.client.login( username=self.student.username, @@ -403,7 +419,7 @@ class TestStudentDashboard(TestCase): def test_student_dashboard_enrolled_courses_get(self): """ - Check student dashboard for all courses in which student is + Check student dashboard for all courses in which student is enrolled """ self.client.login( @@ -412,7 +428,7 @@ class TestStudentDashboard(TestCase): ) self.course.students.add(self.student) response = self.client.get(reverse('yaksh:quizlist_user', - kwargs={'enrolled': "enrolled"}), + kwargs={'enrolled': "enrolled"}), follow=True ) self.assertEqual(response.status_code, 200) @@ -430,8 +446,8 @@ class TestStudentDashboard(TestCase): password=self.student_plaintext_pass ) response = self.client.post(reverse('yaksh:quizlist_user'), - data={'course_code': 'hide'} - ) + data={'course_code': 'hide'} + ) self.assertEqual(response.status_code, 200) self.assertTemplateUsed(response, "yaksh/quizzes_user.html") self.assertEqual(response.context['title'], 'Search') @@ -485,8 +501,10 @@ class TestMonitor(TestCase): # Add to moderator group self.mod_group.user_set.add(self.user) - self.course = Course.objects.create(name="Python Course", - enrollment="Open Enrollment", creator=self.user) + self.course = Course.objects.create( + name="Python Course", + enrollment="Open Enrollment", creator=self.user + ) self.quiz = Quiz.objects.create( start_date_time=datetime(2014, 10, 9, 10, 8, 15, 0, tzone), @@ -509,13 +527,16 @@ class TestMonitor(TestCase): points=1.0, language="python", type="code", user=self.user ) - self.question_paper = QuestionPaper.objects.create(quiz=self.quiz, + self.question_paper = QuestionPaper.objects.create( + quiz=self.quiz, total_marks=1.0, fixed_question_order=str(self.question) ) self.question_paper.fixed_questions.add(self.question) user_answer = "def add(a, b)\n\treturn a+b" - self.new_answer = Answer(question=self.question, answer=user_answer, - correct=True, error=json.dumps([])) + self.new_answer = Answer( + question=self.question, answer=user_answer, + correct=True, error=json.dumps([]) + ) self.new_answer.save() self.answerpaper = AnswerPaper.objects.create( user=self.student, question_paper=self.question_paper, @@ -578,17 +599,19 @@ class TestMonitor(TestCase): username=self.user.username, password=self.user_plaintext_pass ) - response = self.client.get(reverse('yaksh:monitor', - kwargs={'quiz_id': self.quiz.id, - 'course_id': self.course.id}), - follow=True - ) + response = self.client.get( + reverse('yaksh:monitor', + kwargs={'quiz_id': self.quiz.id, + 'course_id': self.course.id}), + follow=True + ) self.assertEqual(response.status_code, 200) self.assertTemplateUsed(response, "yaksh/monitor.html") self.assertEqual(response.context['msg'], "Quiz Results") self.assertEqual(response.context['papers'][0], self.answerpaper) - self.assertEqual(response.context['latest_attempts'][0], self.answerpaper) + self.assertEqual(response.context['latest_attempts'][0], + self.answerpaper) def test_get_quiz_user_data(self): """ @@ -598,19 +621,23 @@ class TestMonitor(TestCase): username=self.user.username, password=self.user_plaintext_pass ) - response = self.client.get(reverse('yaksh:user_data', - kwargs={'user_id':self.student.id, - 'questionpaper_id': self.question_paper.id, - 'course_id': self.course.id}), - follow=True - ) + response = self.client.get( + reverse('yaksh:user_data', + kwargs={'user_id': self.student.id, + 'questionpaper_id': self.question_paper.id, + 'course_id': self.course.id}), + follow=True + ) self.assertEqual(response.status_code, 200) self.assertTemplateUsed(response, 'yaksh/user_data.html') - self.assertEqual(response.context['data']['papers'][0], self.answerpaper) - self.assertEqual(response.context['data']['profile'], self.student.profile) + self.assertEqual(response.context['data']['papers'][0], + self.answerpaper) + self.assertEqual(response.context['data']['profile'], + self.student.profile) self.assertEqual(response.context['data']['user'], self.student) self.assertEqual(response.context['data']['questionpaperid'], - str(self.question_paper.id)) + str(self.question_paper.id)) + class TestGradeUser(TestCase): def setUp(self): @@ -659,7 +686,8 @@ class TestGradeUser(TestCase): # Add to moderator group self.mod_group.user_set.add(self.user) - self.course = Course.objects.create(name="Python Course", + self.course = Course.objects.create( + name="Python Course", enrollment="Open Enrollment", creator=self.user) self.quiz = Quiz.objects.create( @@ -683,13 +711,16 @@ class TestGradeUser(TestCase): points=1.0, language="python", type="code", user=self.user ) - self.question_paper = QuestionPaper.objects.create(quiz=self.quiz, + self.question_paper = QuestionPaper.objects.create( + quiz=self.quiz, total_marks=1.0, fixed_question_order=str(self.question.id) ) self.question_paper.fixed_questions.add(self.question) user_answer = "def add(a, b)\n\treturn a+b" - self.new_answer = Answer(question=self.question, answer=user_answer, - correct=True, error=json.dumps([]), marks=0.5) + self.new_answer = Answer( + question=self.question, answer=user_answer, + correct=True, error=json.dumps([]), marks=0.5 + ) self.new_answer.save() self.answerpaper = AnswerPaper.objects.create( user=self.student, question_paper=self.question_paper, @@ -752,15 +783,16 @@ class TestGradeUser(TestCase): username=self.user.username, password=self.user_plaintext_pass ) - response = self.client.get(reverse('yaksh:grade_user', - kwargs={"quiz_id": self.quiz.id, - 'course_id': self.course.id}), - follow=True - ) + response = self.client.get( + reverse('yaksh:grade_user', + kwargs={"quiz_id": self.quiz.id, + 'course_id': self.course.id}), + follow=True + ) self.assertEqual(response.status_code, 200) self.assertTemplateUsed(response, "yaksh/grade_user.html") self.assertEqual(response.context['users'][0]['user__first_name'], - self.student.first_name) + self.student.first_name) self.assertEqual(response.context['quiz'], self.quiz) self.assertFalse(response.context['has_quiz_assignments']) @@ -772,12 +804,13 @@ class TestGradeUser(TestCase): username=self.user.username, password=self.user_plaintext_pass ) - response = self.client.get(reverse('yaksh:grade_user', - kwargs={"quiz_id": self.quiz.id, - "course_id": self.course.id, - "user_id": self.student.id}), - follow=True - ) + response = self.client.get( + reverse('yaksh:grade_user', + kwargs={"quiz_id": self.quiz.id, + "course_id": self.course.id, + "user_id": self.student.id}), + follow=True + ) self.assertEqual(response.status_code, 200) self.assertTemplateUsed(response, "yaksh/grade_user.html") self.assertFalse(response.context['has_user_assignments']) @@ -793,25 +826,28 @@ class TestGradeUser(TestCase): username=self.user.username, password=self.user_plaintext_pass ) - self.client.get(reverse('yaksh:grade_user', + self.client.get(reverse('yaksh:grade_user', kwargs={"quiz_id": self.quiz.id, "course_id": self.course.id, "user_id": self.student.id}), - follow=True - ) + follow=True + ) question_marks = "q{0}_marks".format(self.question.id) - response = self.client.post(reverse('yaksh:grade_user', - kwargs={"quiz_id": self.quiz.id, - "user_id": self.student.id, - "course_id": self.course.id, - "attempt_number": self.answerpaper.attempt_number}), - data={question_marks: 1.0} - ) + response = self.client.post( + reverse( + 'yaksh:grade_user', + kwargs={"quiz_id": self.quiz.id, + "user_id": self.student.id, + "course_id": self.course.id, + "attempt_number": self.answerpaper.attempt_number} + ), + data={question_marks: 1.0} + ) - updated_ans_paper = AnswerPaper.objects.get(user=self.student, - question_paper=self.question_paper, - attempt_number=self.answerpaper.attempt_number - ) + updated_ans_paper = AnswerPaper.objects.get( + user=self.student, question_paper=self.question_paper, + attempt_number=self.answerpaper.attempt_number + ) updated_ans = Answer.objects.get(question=self.question) self.assertEqual(response.status_code, 200) self.assertTemplateUsed(response, "yaksh/grade_user.html") @@ -866,8 +902,10 @@ class TestDownloadAssignment(TestCase): email='demo_student2@test.com' ) - self.course = Course.objects.create(name="Python Course", - enrollment="Enroll Request", creator=self.user) + self.course = Course.objects.create( + name="Python Course", + enrollment="Enroll Request", creator=self.user + ) self.quiz = Quiz.objects.create( start_date_time=datetime(2014, 10, 9, 10, 8, 15, 0, tzone), @@ -889,7 +927,8 @@ class TestDownloadAssignment(TestCase): points=1.0, language="python", type="upload", user=self.user ) - self.question_paper = QuestionPaper.objects.create(quiz=self.quiz, + self.question_paper = QuestionPaper.objects.create( + quiz=self.quiz, total_marks=1.0, fixed_question_order=str(self.question.id) ) self.question_paper.fixed_questions.add(self.question) @@ -897,16 +936,14 @@ class TestDownloadAssignment(TestCase): # create assignment file assignment_file1 = SimpleUploadedFile("file1.txt", b"Test") assignment_file2 = SimpleUploadedFile("file2.txt", b"Test") - assignment_file3 = SimpleUploadedFile("file3.txt", b"Test") - self.assignment1 = AssignmentUpload.objects.create(user=self.student1, - assignmentQuestion=self.question, - assignmentFile=assignment_file1, - question_paper=self.question_paper + SimpleUploadedFile("file3.txt", b"Test") + self.assignment1 = AssignmentUpload.objects.create( + user=self.student1, assignmentQuestion=self.question, + assignmentFile=assignment_file1, question_paper=self.question_paper ) - self.assignment2 = AssignmentUpload.objects.create(user=self.student2, - assignmentQuestion=self.question, - assignmentFile=assignment_file2, - question_paper=self.question_paper + self.assignment2 = AssignmentUpload.objects.create( + user=self.student2, assignmentQuestion=self.question, + assignmentFile=assignment_file2, question_paper=self.question_paper ) def tearDown(self): @@ -934,7 +971,7 @@ class TestDownloadAssignment(TestCase): password=self.student1_plaintext_pass ) response = self.client.get(reverse('yaksh:download_quiz_assignment', - kwargs={'quiz_id': self.quiz.id}), + kwargs={'quiz_id': self.quiz.id}), follow=True ) self.assertEqual(response.status_code, 404) @@ -955,7 +992,7 @@ class TestDownloadAssignment(TestCase): file_name = file_name.replace(" ", "_") self.assertEqual(response.status_code, 200) self.assertEqual(response.get('Content-Disposition'), - "attachment; filename={0}".format(file_name)) + "attachment; filename={0}".format(file_name)) zip_file = string_io(response.content) zipped_file = zipfile.ZipFile(zip_file, 'r') self.assertIsNone(zipped_file.testzip()) @@ -972,18 +1009,19 @@ class TestDownloadAssignment(TestCase): username=self.user.username, password=self.user_plaintext_pass ) - response = self.client.get(reverse('yaksh:download_user_assignment', - kwargs={'quiz_id': self.quiz.id, - 'question_id': self.question.id, - 'user_id': self.student2.id - }), - follow=True - ) + response = self.client.get( + reverse('yaksh:download_user_assignment', + kwargs={'quiz_id': self.quiz.id, + 'question_id': self.question.id, + 'user_id': self.student2.id + }), + follow=True + ) file_name = "{0}.zip".format(self.student2.get_full_name()) file_name = file_name.replace(" ", "_") self.assertEqual(response.status_code, 200) self.assertEqual(response.get('Content-Disposition'), - "attachment; filename={0}".format(file_name)) + "attachment; filename={0}".format(file_name)) zip_file = string_io(response.content) zipped_file = zipfile.ZipFile(zip_file, 'r') self.assertIsNone(zipped_file.testzip()) @@ -1030,8 +1068,10 @@ class TestAddQuiz(TestCase): # Add to moderator group self.mod_group.user_set.add(self.user) - self.course = Course.objects.create(name="Python Course", - enrollment="Enroll Request", creator=self.user) + self.course = Course.objects.create( + name="Python Course", + enrollment="Enroll Request", creator=self.user + ) self.quiz = Quiz.objects.create( start_date_time=datetime(2014, 10, 9, 10, 8, 15, 0, tzone), @@ -1075,7 +1115,6 @@ class TestAddQuiz(TestCase): username=self.student.username, password=self.student_plaintext_pass ) - course_id = self.course.id response = self.client.get(reverse('yaksh:add_quiz'), follow=True ) @@ -1104,8 +1143,9 @@ class TestAddQuiz(TestCase): password=self.user_plaintext_pass ) tzone = pytz.timezone('UTC') - response = self.client.post(reverse('yaksh:edit_quiz', - kwargs={'quiz_id': self.quiz.id}), + response = self.client.post( + reverse('yaksh:edit_quiz', + kwargs={'quiz_id': self.quiz.id}), data={ 'start_date_time': '2016-01-10 09:00:15', 'end_date_time': '2016-01-15 09:00:15', @@ -1121,10 +1161,12 @@ class TestAddQuiz(TestCase): ) updated_quiz = Quiz.objects.get(id=self.quiz.id) - self.assertEqual(updated_quiz.start_date_time, + self.assertEqual( + updated_quiz.start_date_time, datetime(2016, 1, 10, 9, 0, 15, 0, tzone) ) - self.assertEqual(updated_quiz.end_date_time, + self.assertEqual( + updated_quiz.end_date_time, datetime(2016, 1, 15, 9, 0, 15, 0, tzone) ) self.assertEqual(updated_quiz.duration, 30) @@ -1147,7 +1189,8 @@ class TestAddQuiz(TestCase): ) tzone = pytz.timezone('UTC') - response = self.client.post(reverse('yaksh:add_quiz'), + response = self.client.post( + reverse('yaksh:add_quiz'), data={ 'start_date_time': '2016-01-10 09:00:15', 'end_date_time': '2016-01-15 09:00:15', @@ -1163,10 +1206,12 @@ class TestAddQuiz(TestCase): ) quiz_list = Quiz.objects.all().order_by('-id') new_quiz = quiz_list[0] - self.assertEqual(new_quiz.start_date_time, + self.assertEqual( + new_quiz.start_date_time, datetime(2016, 1, 10, 9, 0, 15, 0, tzone) ) - self.assertEqual(new_quiz.end_date_time, + self.assertEqual( + new_quiz.end_date_time, datetime(2016, 1, 15, 9, 0, 15, 0, tzone) ) self.assertEqual(new_quiz.duration, 50) @@ -1197,7 +1242,6 @@ class TestAddQuiz(TestCase): username=self.student.username, password=self.student_plaintext_pass ) - course_id = self.course.id response = self.client.get(reverse('yaksh:add_exercise'), follow=True ) @@ -1225,9 +1269,9 @@ class TestAddQuiz(TestCase): username=self.user.username, password=self.user_plaintext_pass ) - tzone = pytz.timezone('UTC') - response = self.client.post(reverse('yaksh:edit_exercise', - kwargs={'quiz_id': self.exercise.id}), + response = self.client.post( + reverse('yaksh:edit_exercise', + kwargs={'quiz_id': self.exercise.id}), data={ 'description': 'updated demo exercise', 'active': True @@ -1253,9 +1297,8 @@ class TestAddQuiz(TestCase): username=self.user.username, password=self.user_plaintext_pass ) - - tzone = pytz.timezone('UTC') - response = self.client.post(reverse('yaksh:add_exercise'), + response = self.client.post( + reverse('yaksh:add_exercise'), data={ 'description': "Demo Exercise", 'active': True @@ -1327,8 +1370,10 @@ class TestAddTeacher(TestCase): # Add to moderator group self.mod_group.user_set.add(self.user) - self.course = Course.objects.create(name="Python Course", - enrollment="Enroll Request", creator=self.user) + self.course = Course.objects.create( + name="Python Course", + enrollment="Enroll Request", creator=self.user + ) self.pre_req_quiz = Quiz.objects.create( start_date_time=datetime(2014, 2, 1, 5, 8, 15, 0, tzone), @@ -1360,13 +1405,16 @@ class TestAddTeacher(TestCase): """ If not logged in redirect to login page """ - response = self.client.get(reverse('yaksh:add_teacher', - kwargs={'course_id': self.course.id} - ), + response = self.client.get( + reverse('yaksh:add_teacher', + kwargs={'course_id': self.course.id} + ), follow=True ) - redirect_destination = ('/exam/login/?next=/exam' - '/manage/addteacher/{0}/'.format(self.course.id)) + redirect_destination = ( + '/exam/login/?next=/exam/manage/addteacher/{0}/'.format( + self.course.id) + ) self.assertRedirects(response, redirect_destination) def test_add_teacher_denies_non_moderator(self): @@ -1378,9 +1426,10 @@ class TestAddTeacher(TestCase): password=self.student_plaintext_pass ) - response = self.client.get(reverse('yaksh:add_teacher', - kwargs={'course_id': self.course.id} - ), + response = self.client.get( + reverse('yaksh:add_teacher', + kwargs={'course_id': self.course.id} + ), follow=True ) self.assertEqual(response.status_code, 404) @@ -1393,9 +1442,10 @@ class TestAddTeacher(TestCase): username=self.user.username, password=self.user_plaintext_pass ) - response = self.client.get(reverse('yaksh:add_teacher', - kwargs={'course_id': self.course.id} - ) + response = self.client.get( + reverse('yaksh:add_teacher', + kwargs={'course_id': self.course.id} + ) ) self.assertEqual(response.status_code, 200) self.assertTemplateUsed(response, 'yaksh/addteacher.html') @@ -1421,7 +1471,7 @@ class TestAddTeacher(TestCase): email='demo{}@test.com'.format(i) ) - teacher_profile = Profile.objects.create( + Profile.objects.create( user=teacher, roll_number='T{}'.format(i), institute='IIT', @@ -1431,9 +1481,10 @@ class TestAddTeacher(TestCase): ) teacher_id_list.append(teacher.id) - response = self.client.post(reverse('yaksh:add_teacher', - kwargs={'course_id': self.course.id} - ), + response = self.client.post( + reverse('yaksh:add_teacher', + kwargs={'course_id': self.course.id} + ), data={'check': teacher_id_list} ) @@ -1485,7 +1536,8 @@ class TestRemoveTeacher(TestCase): # Add to moderator group self.mod_group.user_set.add(self.user) - self.course = Course.objects.create(name="Python Course", + self.course = Course.objects.create( + name="Python Course", enrollment="Enroll Request", creator=self.user) self.pre_req_quiz = Quiz.objects.create( @@ -1505,6 +1557,7 @@ class TestRemoveTeacher(TestCase): description='demo quiz', pass_criteria=40, creator=self.user ) + def tearDown(self): self.client.logout() self.user.delete() @@ -1517,13 +1570,16 @@ class TestRemoveTeacher(TestCase): """ If not logged in redirect to login page """ - response = self.client.get(reverse('yaksh:remove_teacher', - kwargs={'course_id': self.course.id} - ), + response = self.client.get( + reverse('yaksh:remove_teacher', + kwargs={'course_id': self.course.id} + ), follow=True ) - redirect_destination = ('/exam/login/?next=/exam' - '/manage/remove_teachers/{0}/'.format(self.course.id)) + redirect_destination = ( + '/exam/login/?next=/exam/manage/remove_teachers/{0}/'.format( + self.course.id) + ) self.assertRedirects(response, redirect_destination) def test_remove_teacher_denies_non_moderator(self): @@ -1535,9 +1591,10 @@ class TestRemoveTeacher(TestCase): password=self.student_plaintext_pass ) - response = self.client.get(reverse('yaksh:remove_teacher', - kwargs={'course_id': self.course.id} - ), + response = self.client.get( + reverse('yaksh:remove_teacher', + kwargs={'course_id': self.course.id} + ), follow=True ) self.assertEqual(response.status_code, 404) @@ -1561,7 +1618,7 @@ class TestRemoveTeacher(TestCase): email='remove_teacher{}@test.com'.format(i) ) - teacher_profile = Profile.objects.create( + Profile.objects.create( user=teacher, roll_number='RT{}'.format(i), institute='IIT', @@ -1572,16 +1629,17 @@ class TestRemoveTeacher(TestCase): teacher_id_list.append(teacher.id) self.course.teachers.add(teacher) - response = self.client.post(reverse('yaksh:remove_teacher', - kwargs={'course_id': self.course.id} - ), + response = self.client.post( + reverse('yaksh:remove_teacher', + kwargs={'course_id': self.course.id} + ), data={'remove': teacher_id_list} ) self.assertEqual(response.status_code, 302) redirect_destination = '/exam/manage/courses' - self.assertRedirects(response, redirect_destination, - status_code=302, + self.assertRedirects( + response, redirect_destination, status_code=302, target_status_code=301 ) for t_id in teacher_id_list: @@ -1668,13 +1726,38 @@ class TestCourses(TestCase): order=0, name="test module", description="module", check_prerequisite=False, creator=self.teacher) - self.user1_course = Course.objects.create(name="Python Course", + self.user1_course = Course.objects.create( + name="Python Course", enrollment="Enroll Request", creator=self.user1) + # Create Learning Module for Python Course + self.learning_module1 = LearningModule.objects.create( + order=0, name="demo module", description="module", + check_prerequisite=False, creator=self.user1) + + self.quiz = Quiz.objects.create( + time_between_attempts=0, description='demo quiz', + creator=self.user1) + self.question_paper = QuestionPaper.objects.create( + quiz=self.quiz, total_marks=1.0) + self.lesson = Lesson.objects.create( + name="demo lesson", description="test description", + creator=self.user1) + + self.lesson_unit = LearningUnit.objects.create( + order=1, type="lesson", lesson=self.lesson) + self.quiz_unit = LearningUnit.objects.create( + order=2, type="quiz", quiz=self.quiz) + + # Add units to module + self.learning_module1.learning_unit.add(self.lesson_unit) + self.learning_module1.learning_unit.add(self.quiz_unit) + # Add teacher to user1 course self.user1_course.teachers.add(self.teacher) - self.user2_course = Course.objects.create(name="Java Course", + self.user2_course = Course.objects.create( + name="Java Course", enrollment="Enroll Request", creator=self.user2) self.user2_course.learning_module.add(self.learning_module) @@ -1683,20 +1766,14 @@ class TestCourses(TestCase): self.user1.delete() self.user2.delete() self.student.delete() - self.user1_course.delete() - self.user2_course.delete() self.teacher.delete() - self.learning_module.delete() def test_courses_denies_anonymous(self): """ If not logged in redirect to login page """ - response = self.client.get(reverse('yaksh:courses'), - follow=True - ) - redirect_destination = ('/exam/login/?next=/exam' - '/manage/courses/') + response = self.client.get(reverse('yaksh:courses'), follow=True) + redirect_destination = ('/exam/login/?next=/exam/manage/courses/') self.assertRedirects(response, redirect_destination) def test_courses_denies_non_moderator(self): @@ -1708,9 +1785,7 @@ class TestCourses(TestCase): password=self.student_plaintext_pass ) - response = self.client.get(reverse('yaksh:courses'), - follow=True - ) + response = self.client.get(reverse('yaksh:courses'), follow=True) self.assertEqual(response.status_code, 404) def test_courses_get(self): @@ -1721,9 +1796,7 @@ class TestCourses(TestCase): username=self.user1.username, password=self.user1_plaintext_pass ) - response = self.client.get(reverse('yaksh:courses'), - follow=True - ) + response = self.client.get(reverse('yaksh:courses'), follow=True) self.assertEqual(response.status_code, 200) self.assertTemplateUsed(response, 'yaksh/courses.html') @@ -1833,11 +1906,12 @@ class TestCourses(TestCase): self.assertEqual(response.status_code, 200) self.assertTemplateUsed(response, "yaksh/course_modules.html") self.assertEqual(response.context['course'], self.user1_course) - self.assertEqual(response.context['learning_modules'][0], - self.learning_module) + module, percent = response.context['modules'][0] + self.assertEqual(module, self.learning_module) + self.assertEqual(percent, 0.0) def test_duplicate_course(self): - """ Test To clone/duplicate course """ + """ Test To clone/duplicate course and link modules""" # Student Login self.client.login( @@ -1869,27 +1943,64 @@ class TestCourses(TestCase): self.assertTemplateUsed(response, "yaksh/complete.html") self.assertIn(err_msg, response.context['message']) - # Moderator/Course creator login + # Test clone/duplicate courses and create copies of modules and units + + # Teacher Login + # Given + # Add files to a lesson + lesson_file = SimpleUploadedFile("file1.txt", b"Test") + django_file = File(lesson_file) + lesson_file_obj = LessonFile() + lesson_file_obj.lesson = self.lesson + lesson_file_obj.file.save(lesson_file.name, django_file, save=True) + + # Add module to Python Course + self.user1_course.learning_module.add(self.learning_module1) self.client.login( - username=self.user2.username, - password=self.user2_plaintext_pass + username=self.teacher.username, + password=self.teacher_plaintext_pass ) - - # Allows creator to duplicate the course response = self.client.get( reverse('yaksh:duplicate_course', - kwargs={"course_id": self.user2_course.id}), + kwargs={"course_id": self.user1_course.id}), follow=True ) - self.assertEqual(response.status_code, 200) + # When courses = Course.objects.filter( - creator=self.user2).order_by("id") - self.assertEqual(courses.count(), 2) - self.assertEqual(courses.last().creator, self.user2) - self.assertEqual(courses.last().name, "Copy Of Java Course") - self.assertEqual(courses.last().get_learning_modules()[0].id, - self.user2_course.get_learning_modules()[0].id) + creator=self.teacher).order_by("id") + module = courses.last().get_learning_modules()[0] + units = module.get_learning_units() + cloned_lesson = units[0].lesson + cloned_quiz = units[1].quiz + expected_lesson_files = cloned_lesson.get_files() + actual_lesson_files = self.lesson.get_files() + cloned_qp = cloned_quiz.questionpaper_set.get() + self.all_files = LessonFile.objects.filter( + lesson_id__in=[self.lesson.id, cloned_lesson.id]) + + # Then + self.assertEqual(response.status_code, 200) + self.assertEqual(courses.last().creator, self.teacher) + self.assertEqual(courses.last().name, "Copy Of Python Course") + self.assertEqual(module.name, "Copy of demo module") + self.assertEqual(module.creator, self.teacher) + self.assertEqual(module.order, 0) + self.assertEqual(len(units), 2) + self.assertEqual(cloned_lesson.name, "Copy of demo lesson") + self.assertEqual(cloned_lesson.creator, self.teacher) + self.assertEqual(cloned_quiz.description, "Copy of demo quiz") + self.assertEqual(cloned_quiz.creator, self.teacher) + self.assertEqual(cloned_qp.__str__(), + "Question Paper for Copy of demo quiz") + self.assertEqual(os.path.basename(expected_lesson_files[0].file.name), + os.path.basename(actual_lesson_files[0].file.name)) + + for lesson_file in self.all_files: + file_path = lesson_file.file.path + if os.path.exists(file_path): + os.remove(file_path) + shutil.rmtree(os.path.dirname(file_path)) class TestAddCourse(TestCase): @@ -1951,7 +2062,8 @@ class TestAddCourse(TestCase): self.mod_group.user_set.add(self.user) self.mod_group.user_set.add(self.teacher) - self.course = Course.objects.create(name="Python Course", + self.course = Course.objects.create( + name="Python Course", enrollment="Enroll Request", creator=self.user) self.course.teachers.add(self.teacher) @@ -1989,8 +2101,7 @@ class TestAddCourse(TestCase): response = self.client.get(reverse('yaksh:add_course'), follow=True ) - redirect_destination = ('/exam/login/?next=/' - 'exam/manage/add_course/') + redirect_destination = ('/exam/login/?next=/exam/manage/add_course/') self.assertRedirects(response, redirect_destination) def test_add_course_denies_non_moderator(self): @@ -2001,10 +2112,7 @@ class TestAddCourse(TestCase): username=self.student.username, password=self.student_plaintext_pass ) - course_id = self.course.id - response = self.client.get(reverse('yaksh:add_course'), - follow=True - ) + response = self.client.get(reverse('yaksh:add_course'), follow=True) self.assertEqual(response.status_code, 404) def test_add_course_get(self): @@ -2029,13 +2137,14 @@ class TestAddCourse(TestCase): password=self.user_plaintext_pass ) - response = self.client.post(reverse('yaksh:add_course'), + response = self.client.post( + reverse('yaksh:add_course'), data={'name': 'new_demo_course_1', - 'active': True, - 'enrollment': 'open', - 'start_enroll_time': '2016-01-10 09:00:15', - 'end_enroll_time': '2016-01-15 09:00:15', - } + 'active': True, + 'enrollment': 'open', + 'start_enroll_time': '2016-01-10 09:00:15', + 'end_enroll_time': '2016-01-15 09:00:15', + } ) new_course = Course.objects.latest('created_on') self.assertEqual(new_course.name, 'new_demo_course_1') @@ -2054,14 +2163,15 @@ class TestAddCourse(TestCase): password=self.teacher_plaintext_pass ) - response = self.client.post(reverse('yaksh:edit_course', - kwargs={"course_id": self.course.id}), + response = self.client.post( + reverse('yaksh:edit_course', + kwargs={"course_id": self.course.id}), data={'name': 'Teacher_course', - 'active': True, - 'enrollment': 'open', - 'start_enroll_time': '2016-01-10 09:00:15', - 'end_enroll_time': '2016-01-15 09:00:15', - } + 'active': True, + 'enrollment': 'open', + 'start_enroll_time': '2016-01-10 09:00:15', + 'end_enroll_time': '2016-01-15 09:00:15', + } ) updated_course = Course.objects.get(id=self.course.id) self.assertEqual(updated_course.name, 'Teacher_course') @@ -2138,13 +2248,21 @@ class TestCourseDetail(TestCase): self.mod_group.user_set.add(self.user1) self.mod_group.user_set.add(self.user2) - self.user1_course = Course.objects.create(name="Python Course", - enrollment="Enroll Request", creator=self.user1) + self.user1_course = Course.objects.create( + name="Python Course", + enrollment="Enroll Request", creator=self.user1 + ) self.learning_module = LearningModule.objects.create( name="test module", description="test description module", html_data="test html description module", creator=self.user1, order=1) self.user1_course.learning_module.add(self.learning_module) + self.lesson = Lesson.objects.create( + name="test lesson", description="test description", + creator=self.user1) + self.learning_unit1 = LearningUnit.objects.create( + order=1, type="lesson", lesson=self.lesson) + self.learning_module.learning_unit.add(self.learning_unit1) def tearDown(self): self.client.logout() @@ -2164,9 +2282,10 @@ class TestCourseDetail(TestCase): upload_file = SimpleUploadedFile(csv_file_path, csv_file.read()) # When - response = self.client.post(reverse('yaksh:upload_users', - kwargs={'course_id': self.user1_course.id}), - data={'csv_file': upload_file}) + response = self.client.post( + reverse('yaksh:upload_users', + kwargs={'course_id': self.user1_course.id}), + data={'csv_file': upload_file}) csv_file.close() # Then @@ -2188,9 +2307,10 @@ class TestCourseDetail(TestCase): upload_file = SimpleUploadedFile(csv_file_path, csv_file.read()) # When - response = self.client.post(reverse('yaksh:upload_users', - kwargs={'course_id': self.user1_course.id}), - data={'csv_file': upload_file}) + response = self.client.post( + reverse('yaksh:upload_users', + kwargs={'course_id': self.user1_course.id}), + data={'csv_file': upload_file}) csv_file.close() # Then @@ -2215,9 +2335,10 @@ class TestCourseDetail(TestCase): message = "The file uploaded is not a CSV file." # When - response = self.client.post(reverse('yaksh:upload_users', - kwargs={'course_id': self.user1_course.id}), - data={'csv_file': upload_file}) + response = self.client.post( + reverse('yaksh:upload_users', + kwargs={'course_id': self.user1_course.id}), + data={'csv_file': upload_file}) csv_file.close() # Then @@ -2233,15 +2354,17 @@ class TestCourseDetail(TestCase): username=self.user1.username, password=self.user1_plaintext_pass ) - csv_file_path = os.path.join(FIXTURES_DIR_PATH, "users_some_headers_missing.csv") + csv_file_path = os.path.join(FIXTURES_DIR_PATH, + "users_some_headers_missing.csv") csv_file = open(csv_file_path, 'rb') upload_file = SimpleUploadedFile(csv_file_path, csv_file.read()) message = "The CSV file does not contain the required headers" # When - response = self.client.post(reverse('yaksh:upload_users', - kwargs={'course_id': self.user1_course.id}), - data={'csv_file': upload_file}) + response = self.client.post( + reverse('yaksh:upload_users', + kwargs={'course_id': self.user1_course.id}), + data={'csv_file': upload_file}) csv_file.close() # Then @@ -2257,21 +2380,24 @@ class TestCourseDetail(TestCase): username=self.user1.username, password=self.user1_plaintext_pass ) - csv_file_path = os.path.join(FIXTURES_DIR_PATH, "users_with_no_values.csv") + csv_file_path = os.path.join(FIXTURES_DIR_PATH, + "users_with_no_values.csv") csv_file = open(csv_file_path, 'rb') upload_file = SimpleUploadedFile(csv_file_path, csv_file.read()) # When - response = self.client.post(reverse('yaksh:upload_users', - kwargs={'course_id': self.user1_course.id}), - data={'csv_file': upload_file}) + response = self.client.post( + reverse('yaksh:upload_users', + kwargs={'course_id': self.user1_course.id}), + data={'csv_file': upload_file}) csv_file.close() # Then self.assertEqual(response.status_code, 200) self.assertIn('upload_details', response.context) self.assertNotIn('message', response.context) - self.assertIn("No rows in the CSV file", response.context['upload_details']) + self.assertIn("No rows in the CSV file", + response.context['upload_details']) self.assertTemplateUsed(response, 'yaksh/course_detail.html') def test_upload_users_csv_with_missing_values(self): @@ -2292,14 +2418,17 @@ class TestCourseDetail(TestCase): username=self.user1.username, password=self.user1_plaintext_pass ) - csv_file_path = os.path.join(FIXTURES_DIR_PATH, "users_some_values_missing.csv") + csv_file_path = os.path.join(FIXTURES_DIR_PATH, + "users_some_values_missing.csv") csv_file = open(csv_file_path, 'rb') upload_file = SimpleUploadedFile(csv_file_path, csv_file.read()) # When - response = self.client.post(reverse('yaksh:upload_users', - kwargs={'course_id': self.user1_course.id}), - data={'csv_file': upload_file}) + response = self.client.post( + reverse('yaksh:upload_users', + kwargs={'course_id': self.user1_course.id}), + data={'csv_file': upload_file} + ) csv_file.close() # Then @@ -2314,13 +2443,16 @@ class TestCourseDetail(TestCase): """ If not logged in redirect to login page """ - response = self.client.get(reverse('yaksh:course_detail', - kwargs={'course_id': self.user1_course.id} - ), + response = self.client.get( + reverse('yaksh:course_detail', + kwargs={'course_id': self.user1_course.id} + ), follow=True ) - redirect_destination = ('/exam/login/?next=/exam/' - 'manage/course_detail/{0}/'.format(self.user1_course.id)) + redirect_destination = ( + '/exam/login/?next=/exam/manage/course_detail/{0}/'.format( + self.user1_course.id) + ) self.assertRedirects(response, redirect_destination) def test_course_detail_denies_non_moderator(self): @@ -2332,9 +2464,10 @@ class TestCourseDetail(TestCase): password=self.student_plaintext_pass ) - response = self.client.get(reverse('yaksh:course_detail', - kwargs={'course_id': self.user1_course.id} - ), + response = self.client.get( + reverse('yaksh:course_detail', + kwargs={'course_id': self.user1_course.id} + ), follow=True ) self.assertEqual(response.status_code, 404) @@ -2347,9 +2480,10 @@ class TestCourseDetail(TestCase): username=self.user2.username, password=self.user2_plaintext_pass ) - response = self.client.get(reverse('yaksh:course_detail', - kwargs={'course_id': self.user1_course.id} - ), + response = self.client.get( + reverse('yaksh:course_detail', + kwargs={'course_id': self.user1_course.id} + ), follow=True ) self.assertEqual(response.status_code, 404) @@ -2362,9 +2496,10 @@ class TestCourseDetail(TestCase): username=self.user1.username, password=self.user1_plaintext_pass ) - response = self.client.get(reverse('yaksh:course_detail', - kwargs={'course_id': self.user1_course.id} - ), + response = self.client.get( + reverse('yaksh:course_detail', + kwargs={'course_id': self.user1_course.id} + ), follow=True ) self.assertEqual(self.user1_course, response.context['course']) @@ -2379,7 +2514,8 @@ class TestCourseDetail(TestCase): username=self.user1.username, password=self.user1_plaintext_pass ) - response = self.client.get(reverse('yaksh:enroll_user', + response = self.client.get( + reverse('yaksh:enroll_user', kwargs={'course_id': self.user1_course.id, 'user_id': self.student.id}) ) @@ -2395,10 +2531,11 @@ class TestCourseDetail(TestCase): username=self.user1.username, password=self.user1_plaintext_pass ) - response = self.client.post(reverse('yaksh:enroll_users', + response = self.client.post( + reverse('yaksh:enroll_users', kwargs={'course_id': self.user1_course.id}), - data={'check': self.student1.id} - ) + data={'check': self.student1.id} + ) enrolled_student = self.user1_course.students.all() self.assertEqual(response.status_code, 200) self.assertSequenceEqual([self.student1], enrolled_student) @@ -2411,7 +2548,8 @@ class TestCourseDetail(TestCase): username=self.user1.username, password=self.user1_plaintext_pass ) - response = self.client.get(reverse('yaksh:reject_user', + response = self.client.get( + reverse('yaksh:reject_user', kwargs={'course_id': self.user1_course.id, 'user_id': self.student.id}) ) @@ -2427,10 +2565,11 @@ class TestCourseDetail(TestCase): username=self.user1.username, password=self.user1_plaintext_pass ) - response = self.client.post(reverse('yaksh:reject_users', + response = self.client.post( + reverse('yaksh:reject_users', kwargs={'course_id': self.user1_course.id}), - data={'check': self.student1.id} - ) + data={'check': self.student1.id} + ) enrolled_student = self.user1_course.rejected.all() self.assertEqual(response.status_code, 200) self.assertSequenceEqual([self.student1], enrolled_student) @@ -2440,9 +2579,10 @@ class TestCourseDetail(TestCase): username=self.user1.username, password=self.user1_plaintext_pass ) - response = self.client.post(reverse('yaksh:toggle_course_status', + response = self.client.post( + reverse('yaksh:toggle_course_status', kwargs={'course_id': self.user1_course.id}) - ) + ) self.assertEqual(response.status_code, 302) course = Course.objects.get(name="Python Course") self.assertFalse(course.active) @@ -2496,7 +2636,7 @@ class TestCourseDetail(TestCase): attachment_file = mail.outbox[0].attachments[0][0] subject = mail.outbox[0].subject body = mail.outbox[0].alternatives[0][0] - recipients = mail.outbox[0].recipients() + recipients = mail.outbox[0].bcc self.assertEqual(attachment_file, "file.txt") self.assertEqual(subject, "test_bulk_mail") self.assertEqual(body, "Test_Mail") @@ -2548,12 +2688,82 @@ class TestCourseDetail(TestCase): username=self.user1.username, password=self.user1_plaintext_pass ) + self.user1_course.students.add(self.student) + + # Check student details when course is not started response = self.client.get(reverse('yaksh:course_status', kwargs={'course_id': self.user1_course.id})) self.assertEqual(response.status_code, 200) self.assertEqual(response.context['state'], "course_status") self.assertEqual(response.context['course'], self.user1_course) - self.assertEqual(response.context['modules'][0], self.learning_module) + student_details = response.context['student_details'][0] + student, grade, percent, current_unit = student_details + self.assertEqual(student.username, "demo_student") + self.assertEqual(grade, "NA") + self.assertEqual(percent, 0.0) + self.assertIsNone(current_unit) + + # Check student details when student starts the course + self.course_status = CourseStatus.objects.create( + course=self.user1_course, user=self.student) + response = self.client.get(reverse('yaksh:course_status', + kwargs={'course_id': self.user1_course.id})) + self.assertEqual(response.status_code, 200) + self.assertEqual(response.context['state'], "course_status") + self.assertEqual(response.context['course'], self.user1_course) + student_details = response.context['student_details'][0] + student, grade, percent, current_unit = student_details + self.assertEqual(student.username, "demo_student") + self.assertIsNone(grade) + self.assertEqual(percent, 0) + self.assertIsNone(current_unit) + + self.user1_course.students.remove(self.student) + + def test_course_status_per_user(self): + """ Test course status for a particular student""" + self.client.login( + username=self.student.username, + password=self.student_plaintext_pass + ) + + # Denies student to view course status + response = self.client.get(reverse('yaksh:get_user_data', + kwargs={'course_id': self.user1_course.id, + 'student_id': self.student.id})) + err_msg = response.json()['user_data'].replace("\n", "").strip() + self.assertEqual(response.status_code, 200) + self.assertEqual(err_msg, "You are not a moderator") + + # Other Moderator Login + self.client.login( + username=self.user2.username, + password=self.user2_plaintext_pass + ) + response = self.client.get(reverse('yaksh:get_user_data', + kwargs={'course_id': self.user1_course.id, + 'student_id': self.student.id})) + err_msg = response.json()['user_data'].strip() + actual_err = ('You are neither course creator ' + 'nor course teacher for {0}'.format( + self.user1_course.name) + ) + self.assertEqual(response.status_code, 200) + self.assertEqual(err_msg, actual_err) + + # Actual course creator login + self.client.login( + username=self.user1.username, + password=self.user1_plaintext_pass + ) + response = self.client.get(reverse('yaksh:get_user_data', + kwargs={'course_id': self.user1_course.id, + 'student_id': self.student.id})) + self.assertEqual(response.status_code, 200) + data = response.json()['user_data'] + self.assertIn("Student_First_Name Student_Last_Name", data) + self.assertIn("Overall Course Progress", data) + self.assertIn("Per Module Progress", data) class TestEnrollRequest(TestCase): @@ -2613,8 +2823,10 @@ class TestEnrollRequest(TestCase): self.mod_group.user_set.add(self.user1) self.mod_group.user_set.add(self.user2) - self.course = Course.objects.create(name="Python Course", - enrollment="Enroll Request", creator=self.user1) + self.course = Course.objects.create( + name="Python Course", + enrollment="Enroll Request", creator=self.user1 + ) def tearDown(self): self.client.logout() @@ -2627,13 +2839,16 @@ class TestEnrollRequest(TestCase): """ If not logged in redirect to login page """ - response = self.client.get(reverse('yaksh:enroll_request', - kwargs={'course_id': self.course.id} - ), + response = self.client.get( + reverse('yaksh:enroll_request', + kwargs={'course_id': self.course.id} + ), follow=True ) - redirect_destination = ('/exam/login/?next=/exam' - '/enroll_request/{}/'.format(self.course.id)) + redirect_destination = ( + '/exam/login/?next=/exam/enroll_request/{}/'.format( + self.course.id) + ) self.assertRedirects(response, redirect_destination) def test_enroll_request_get_for_student(self): @@ -2642,9 +2857,10 @@ class TestEnrollRequest(TestCase): password=self.student_plaintext_pass ) - response = self.client.get(reverse('yaksh:enroll_request', - kwargs={'course_id': self.course.id} - ), + response = self.client.get( + reverse('yaksh:enroll_request', + kwargs={'course_id': self.course.id} + ), follow=True ) self.assertRedirects(response, '/exam/quizzes/') @@ -2655,9 +2871,10 @@ class TestEnrollRequest(TestCase): password=self.user2_plaintext_pass ) - response = self.client.get(reverse('yaksh:enroll_request', - kwargs={'course_id': self.course.id} - ), + response = self.client.get( + reverse('yaksh:enroll_request', + kwargs={'course_id': self.course.id} + ), follow=True ) self.assertRedirects(response, '/exam/manage/courses/') @@ -2679,26 +2896,31 @@ class TestViewAnswerPaper(TestCase): self.user1 = User.objects.get(username="demo_user1") - self.course = Course.objects.create(name="Python Course", - enrollment="Enroll Request", - creator=self.user1) + self.course = Course.objects.create( + name="Python Course", enrollment="Enroll Request", + creator=self.user1 + ) - self.question = Question.objects.create(summary='Dummy', points=1, - type='code', user=self.user1) + self.question = Question.objects.create( + summary='Dummy', points=1, + type='code', user=self.user1 + ) self.quiz = Quiz.objects.create(time_between_attempts=0, description='demo quiz') self.user3 = User.objects.get(username="demo_user3") - self.question_paper = QuestionPaper.objects.create(quiz=self.quiz, - total_marks=1.0) + self.question_paper = QuestionPaper.objects.create( + quiz=self.quiz, total_marks=1.0 + ) self.question_paper.fixed_questions.add(self.question) self.question_paper.save() - self.ans_paper = AnswerPaper.objects.create(user=self.user3, - attempt_number=1, question_paper=self.question_paper, - start_time=timezone.now(), user_ip='101.0.0.1', - end_time=timezone.now()+timezone.timedelta(minutes=20), - course=self.course) + self.ans_paper = AnswerPaper.objects.create( + user=self.user3, attempt_number=1, + question_paper=self.question_paper, start_time=timezone.now(), + user_ip='101.0.0.1', course=self.course, + end_time=timezone.now()+timezone.timedelta(minutes=20) + ) def tearDown(self): User.objects.all().delete() @@ -2710,15 +2932,17 @@ class TestViewAnswerPaper(TestCase): def test_anonymous_user(self): # Given, user not logged in - redirect_destination = ('/exam/login/?next=/exam' - '/view_answerpaper/{0}/{1}'.format( - self.question_paper.id, self.course.id)) + redirect_destination = ( + '/exam/login/?next=/exam/view_answerpaper/{0}/{1}'.format( + self.question_paper.id, self.course.id) + ) # When - response = self.client.get(reverse('yaksh:view_answerpaper', - kwargs={'questionpaper_id': self.question_paper.id, - 'course_id': self.course.id} - ), + response = self.client.get( + reverse('yaksh:view_answerpaper', + kwargs={'questionpaper_id': self.question_paper.id, + 'course_id': self.course.id} + ), follow=True ) @@ -2738,10 +2962,11 @@ class TestViewAnswerPaper(TestCase): ) # When - response = self.client.get(reverse('yaksh:view_answerpaper', - kwargs={'questionpaper_id': self.question_paper.id, - 'course_id': self.course.id} - ), + response = self.client.get( + reverse('yaksh:view_answerpaper', + kwargs={'questionpaper_id': self.question_paper.id, + 'course_id': self.course.id} + ), follow=True ) @@ -2753,7 +2978,6 @@ class TestViewAnswerPaper(TestCase): user3 = User.objects.get(username="demo_user3") self.course.students.add(user3) self.course.save() - answerpaper = AnswerPaper.objects.get(pk=self.ans_paper.id) self.quiz.view_answerpaper = True self.quiz.save() self.client.login( @@ -2762,10 +2986,11 @@ class TestViewAnswerPaper(TestCase): ) # When - response = self.client.get(reverse('yaksh:view_answerpaper', - kwargs={'questionpaper_id': self.question_paper.id, - 'course_id': self.course.id} - ), + response = self.client.get( + reverse('yaksh:view_answerpaper', + kwargs={'questionpaper_id': self.question_paper.id, + 'course_id': self.course.id} + ), follow=True ) @@ -2775,19 +3000,18 @@ class TestViewAnswerPaper(TestCase): self.assertTrue('quiz' in response.context) self.assertTemplateUsed(response, 'yaksh/view_answerpaper.html') - # When, wrong question paper id - response = self.client.get(reverse('yaksh:view_answerpaper', - kwargs={'questionpaper_id': 190, - 'course_id': self.course.id} - ), + response = self.client.get( + reverse('yaksh:view_answerpaper', + kwargs={'questionpaper_id': 190, + 'course_id': self.course.id} + ), follow=True ) # Then self.assertEqual(response.status_code, 404) - def test_view_when_not_enrolled(self): # Given, user tries to view when not enrolled in the course user2 = User.objects.get(username="demo_user2") @@ -2801,10 +3025,11 @@ class TestViewAnswerPaper(TestCase): self.quiz.save() # When - response = self.client.get(reverse('yaksh:view_answerpaper', - kwargs={'questionpaper_id': self.question_paper.id, - 'course_id': self.course.id} - ), + response = self.client.get( + reverse('yaksh:view_answerpaper', + kwargs={'questionpaper_id': self.question_paper.id, + 'course_id': self.course.id} + ), follow=True ) @@ -2867,8 +3092,10 @@ class TestSelfEnroll(TestCase): self.mod_group.user_set.add(self.user1) self.mod_group.user_set.add(self.user2) - self.course = Course.objects.create(name="Python Course", - enrollment="Enroll Request", creator=self.user1) + self.course = Course.objects.create( + name="Python Course", + enrollment="Enroll Request", creator=self.user1 + ) def tearDown(self): self.client.logout() @@ -2878,13 +3105,15 @@ class TestSelfEnroll(TestCase): self.course.delete() def test_self_enroll_denies_anonymous(self): - response = self.client.get(reverse('yaksh:self_enroll', - kwargs={'course_id': self.course.id} - ), + response = self.client.get( + reverse('yaksh:self_enroll', + kwargs={'course_id': self.course.id} + ), follow=True ) - redirect_destination = ('/exam/login/?next=/exam' - '/self_enroll/{}/'.format(self.course.id)) + redirect_destination = ( + '/exam/login/?next=/exam/self_enroll/{}/'.format(self.course.id) + ) self.assertRedirects(response, redirect_destination) def test_enroll_request_get_for_student(self): @@ -2893,9 +3122,10 @@ class TestSelfEnroll(TestCase): password=self.student_plaintext_pass ) - response = self.client.get(reverse('yaksh:self_enroll', - kwargs={'course_id': self.course.id} - ), + response = self.client.get( + reverse('yaksh:self_enroll', + kwargs={'course_id': self.course.id} + ), follow=True ) self.assertRedirects(response, '/exam/quizzes/') @@ -2906,9 +3136,10 @@ class TestSelfEnroll(TestCase): password=self.user2_plaintext_pass ) - response = self.client.get(reverse('yaksh:self_enroll', - kwargs={'course_id': self.course.id} - ), + response = self.client.get( + reverse('yaksh:self_enroll', + kwargs={'course_id': self.course.id} + ), follow=True ) self.assertRedirects(response, '/exam/manage/') @@ -2971,25 +3202,30 @@ class TestGrader(TestCase): self.mod_group.user_set.add(self.user1) self.mod_group.user_set.add(self.user2) - self.course = Course.objects.create(name="Python Course", - enrollment="Enroll Request", creator=self.user1) + self.course = Course.objects.create( + name="Python Course", + enrollment="Enroll Request", creator=self.user1 + ) - self.question = Question.objects.create(summary='Dummy', points=1, - type='code', user=self.user1) + self.question = Question.objects.create( + summary='Dummy', points=1, type='code', user=self.user1 + ) self.quiz = Quiz.objects.create(time_between_attempts=0, description='demo quiz') - self.question_paper = QuestionPaper.objects.create(quiz=self.quiz, - total_marks=1.0) + self.question_paper = QuestionPaper.objects.create( + quiz=self.quiz, total_marks=1.0 + ) self.question_paper.fixed_questions.add(self.question) self.question_paper.save() - self.answerpaper = AnswerPaper.objects.create(user=self.user2, - attempt_number=1, question_paper=self.question_paper, - start_time=timezone.now(), user_ip='101.0.0.1', - end_time=timezone.now()+timezone.timedelta(minutes=20), - course=self.course) + self.answerpaper = AnswerPaper.objects.create( + user=self.user2, attempt_number=1, + question_paper=self.question_paper, start_time=timezone.now(), + user_ip='101.0.0.1', course=self.course, + end_time=timezone.now()+timezone.timedelta(minutes=20), + ) def tearDown(self): User.objects.all().delete() @@ -3009,7 +3245,6 @@ class TestGrader(TestCase): # Then self.assertRedirects(response, redirect_destination) - def test_grader_denies_students(self): # Given self.client.login( @@ -3023,25 +3258,26 @@ class TestGrader(TestCase): # Then self.assertEqual(response.status_code, 404) - def test_regrade_denies_anonymous(self): # Given - redirect_destination = dedent('''\ - /exam/login/?next=/exam/manage/regrade/answerpaper/{}/{}/{}/'''.format( + url = "/exam/login/?next=/exam/manage/regrade/answerpaper" + redirect_destination = ( + url + "/{}/{}/{}/".format( self.course.id, self.question.id, self.answerpaper.id) ) # When - response = self.client.get(reverse('yaksh:regrade', - kwargs={'course_id': self.course.id, - 'question_id': self.question.id, - 'answerpaper_id': self.answerpaper.id}), - follow=True) + response = self.client.get( + reverse('yaksh:regrade', + kwargs={'course_id': self.course.id, + 'question_id': self.question.id, + 'answerpaper_id': self.answerpaper.id}), + follow=True + ) # Then self.assertRedirects(response, redirect_destination) - def test_regrade_denies_students(self): # Given self.client.login( @@ -3050,16 +3286,17 @@ class TestGrader(TestCase): ) # When - response = self.client.get(reverse('yaksh:regrade', - kwargs={'course_id': self.course.id, - 'question_id': self.question.id, - 'answerpaper_id': self.answerpaper.id}), - follow=True) + response = self.client.get( + reverse('yaksh:regrade', + kwargs={'course_id': self.course.id, + 'question_id': self.question.id, + 'answerpaper_id': self.answerpaper.id}), + follow=True + ) # Then self.assertEqual(response.status_code, 404) - def test_grader_by_moderator(self): # Given self.client.login( @@ -3068,15 +3305,13 @@ class TestGrader(TestCase): ) # When - response = self.client.get(reverse('yaksh:grader'), - follow=True) + response = self.client.get(reverse('yaksh:grader'), follow=True) # Then self.assertEqual(response.status_code, 200) self.assertTrue('courses' in response.context) self.assertTemplateUsed(response, 'yaksh/regrade.html') - def test_regrade_by_moderator(self): # Given self.client.login( @@ -3085,10 +3320,11 @@ class TestGrader(TestCase): ) # When - response = self.client.get(reverse('yaksh:regrade', - kwargs={'course_id': self.course.id, - 'question_id': self.question.id, - 'answerpaper_id': self.answerpaper.id}), + response = self.client.get( + reverse('yaksh:regrade', + kwargs={'course_id': self.course.id, + 'question_id': self.question.id, + 'answerpaper_id': self.answerpaper.id}), follow=True) # Then @@ -3097,6 +3333,32 @@ class TestGrader(TestCase): self.assertTrue('details' in response.context) self.assertTemplateUsed(response, 'yaksh/regrade.html') + # When + response = self.client.get( + reverse('yaksh:regrade', + kwargs={'course_id': self.course.id, + 'answerpaper_id': self.answerpaper.id}), + follow=True) + + # Then + self.assertEqual(response.status_code, 200) + self.assertTrue('courses' in response.context) + self.assertTrue('details' in response.context) + self.assertTemplateUsed(response, 'yaksh/regrade.html') + + # When + response = self.client.get( + reverse('yaksh:regrade', + kwargs={'course_id': self.course.id, + 'question_id': self.question.id, + 'questionpaper_id': self.question_paper.id}), + follow=True) + + # Then + self.assertEqual(response.status_code, 200) + self.assertTrue('courses' in response.context) + self.assertTrue('details' in response.context) + self.assertTemplateUsed(response, 'yaksh/regrade.html') def test_regrade_denies_moderator_not_in_course(self): # Given @@ -3105,15 +3367,19 @@ class TestGrader(TestCase): password=self.user2_plaintext_pass ) + self.mod_group.user_set.remove(self.user2) # When - response = self.client.get(reverse('yaksh:regrade', - kwargs={'course_id': self.course.id, - 'question_id': self.question.id, - 'answerpaper_id': self.answerpaper.id}), + response = self.client.get( + reverse('yaksh:regrade', + kwargs={'course_id': self.course.id, + 'question_id': self.question.id, + 'answerpaper_id': self.answerpaper.id}), follow=True) # Then self.assertEqual(response.status_code, 404) + self.mod_group.user_set.add(self.user2) + class TestPasswordReset(TestCase): def setUp(self): @@ -3144,7 +3410,8 @@ class TestPasswordReset(TestCase): POST request to password_reset view should return a valid response """ # When - response = self.client.post(reverse('password_reset'), + response = self.client.post( + reverse('password_reset'), data={ 'email': self.user1.email, } @@ -3166,7 +3433,8 @@ class TestPasswordReset(TestCase): ) # When - response = self.client.post(reverse('password_change'), + response = self.client.post( + reverse('password_change'), data={ 'old_password': self.user1_plaintext_pass, 'new_password1': 'new_demo1_pass', @@ -3175,7 +3443,8 @@ class TestPasswordReset(TestCase): ) # Then - self.assertIsNotNone(authenticate(username='demo_user1', password='new_demo1_pass')) + self.assertIsNotNone(authenticate(username='demo_user1', + password='new_demo1_pass')) self.assertEqual(response.status_code, 302) self.assertRedirects(response, '/exam/reset/password_change/done/') @@ -3236,8 +3505,10 @@ class TestModeratorDashboard(TestCase): ) self.mod_group.user_set.add(self.user) - self.course = Course.objects.create(name="Python Course", - enrollment="Enroll Request", creator=self.user) + self.course = Course.objects.create( + name="Python Course", + enrollment="Enroll Request", creator=self.user + ) self.quiz = Quiz.objects.create( start_date_time=datetime(2014, 10, 9, 10, 8, 15, 0, tzone), @@ -3253,15 +3524,18 @@ class TestModeratorDashboard(TestCase): points=1.0, language="python", type="code", user=self.user ) - self.question_paper = QuestionPaper.objects.create(quiz=self.quiz, + self.question_paper = QuestionPaper.objects.create( + quiz=self.quiz, total_marks=1.0, fixed_question_order=str(self.question.id) ) self.question_paper.fixed_questions.add(self.question) # student answerpaper user_answer = "def add(a, b)\n\treturn a+b" - self.new_answer = Answer(question=self.question, answer=user_answer, - correct=True, error=json.dumps([]), marks=0.5) + self.new_answer = Answer( + question=self.question, answer=user_answer, + correct=True, error=json.dumps([]), marks=0.5 + ) self.new_answer.save() self.answerpaper = AnswerPaper.objects.create( user=self.student, question_paper=self.question_paper, @@ -3276,8 +3550,10 @@ class TestModeratorDashboard(TestCase): self.answerpaper.questions.add(self.question) # moderator trial answerpaper - self.trial_course = Course.objects.create(name="Trial Course", - enrollment="Enroll Request", creator=self.user, is_trial=True) + self.trial_course = Course.objects.create( + name="Trial Course", + enrollment="Enroll Request", creator=self.user, is_trial=True + ) self.trial_quiz = Quiz.objects.create( start_date_time=datetime(2014, 10, 9, 10, 8, 15, 0, tzone), end_date_time=datetime(2015, 10, 9, 10, 8, 15, 0, tzone), @@ -3293,8 +3569,10 @@ class TestModeratorDashboard(TestCase): ) self.trial_question_paper.fixed_questions.add(self.question) - self.new_answer1 = Answer(question=self.question, answer=user_answer, - correct=True, error=json.dumps([]), marks=0.5) + self.new_answer1 = Answer( + question=self.question, answer=user_answer, + correct=True, error=json.dumps([]), marks=0.5 + ) self.new_answer1.save() self.trial_answerpaper = AnswerPaper.objects.create( user=self.user, question_paper=self.trial_question_paper, @@ -3362,12 +3640,11 @@ class TestModeratorDashboard(TestCase): password=self.user_plaintext_pass ) - response = self.client.get(reverse('yaksh:manage'), - follow=True - ) + response = self.client.get(reverse('yaksh:manage'), follow=True) self.assertEqual(response.status_code, 200) self.assertTemplateUsed(response, "yaksh/moderator_dashboard.html") - self.assertEqual(response.context['trial_paper'][0], self.trial_answerpaper) + self.assertEqual(response.context['trial_paper'][0], + self.trial_answerpaper) self.assertEqual(response.context['courses'][0], self.course) def test_moderator_dashboard_delete_trial_papers(self): @@ -3378,16 +3655,17 @@ class TestModeratorDashboard(TestCase): username=self.user.username, password=self.user_plaintext_pass ) - self.course.is_trial=True + self.course.is_trial = True self.course.save() - response = self.client.post(reverse('yaksh:manage'), - data={'delete_paper': [self.trial_answerpaper.id]} - ) + response = self.client.post( + reverse('yaksh:manage'), + data={'delete_paper': [self.trial_answerpaper.id]} + ) self.assertEqual(response.status_code, 200) self.assertTemplateUsed(response, "yaksh/moderator_dashboard.html") updated_answerpaper = AnswerPaper.objects.filter(user=self.user) - updated_quiz = Quiz.objects.filter( + updated_quiz = Quiz.objects.filter( description=self.trial_question_paper.quiz.description ) updated_course = Course.objects.filter( @@ -3396,6 +3674,7 @@ class TestModeratorDashboard(TestCase): self.assertSequenceEqual(updated_quiz, []) self.assertSequenceEqual(updated_course, []) + class TestUserLogin(TestCase): def setUp(self): self.client = Client() @@ -3428,10 +3707,11 @@ class TestUserLogin(TestCase): """ Check if user is successfully logged in """ - response = self.client.post(reverse('yaksh:login'), - data={'username': self.user1.username, - 'password': self.user1_plaintext_pass} - ) + response = self.client.post( + reverse('yaksh:login'), + data={'username': self.user1.username, + 'password': self.user1_plaintext_pass} + ) self.assertEqual(response.status_code, 302) self.assertRedirects(response, '/exam/quizzes/') @@ -3451,10 +3731,11 @@ class TestUserLogin(TestCase): Check email verified decorator to check for user login """ settings.IS_DEVELOPMENT = False - response = self.client.post(reverse('yaksh:login'), - data={'username': self.user1.username, - 'password': self.user1_plaintext_pass} - ) + response = self.client.post( + reverse('yaksh:login'), + data={'username': self.user1.username, + 'password': self.user1_plaintext_pass} + ) self.assertEqual(response.status_code, 200) self.assertTemplateUsed(response, "yaksh/activation_status.html") @@ -3502,8 +3783,10 @@ class TestDownloadcsv(TestCase): timezone='UTC' ) self.mod_group.user_set.add(self.user) - self.course = Course.objects.create(name="Python Course", - enrollment="Enroll Request", creator=self.user) + self.course = Course.objects.create( + name="Python Course", + enrollment="Enroll Request", creator=self.user + ) self.course.students.add(self.student) self.quiz = Quiz.objects.create( @@ -3520,15 +3803,18 @@ class TestDownloadcsv(TestCase): points=1.0, language="python", type="code", user=self.user ) - self.question_paper = QuestionPaper.objects.create(quiz=self.quiz, + self.question_paper = QuestionPaper.objects.create( + quiz=self.quiz, total_marks=1.0, fixed_question_order=str(self.question.id) ) self.question_paper.fixed_questions.add(self.question) # student answerpaper user_answer = "def add(a, b)\n\treturn a+b" - self.new_answer = Answer(question=self.question, answer=user_answer, - correct=True, error=json.dumps([]), marks=0.5) + self.new_answer = Answer( + question=self.question, answer=user_answer, + correct=True, error=json.dumps([]), marks=0.5 + ) self.new_answer.save() self.answerpaper = AnswerPaper.objects.create( user=self.student, question_paper=self.question_paper, @@ -3617,14 +3903,15 @@ class TestDownloadcsv(TestCase): username=self.user.username, password=self.user_plaintext_pass ) - response = self.client.get(reverse('yaksh:download_course_csv', - kwargs={'course_id': self.course.id}), - follow=True - ) + response = self.client.get( + reverse('yaksh:download_course_csv', + kwargs={'course_id': self.course.id}), + follow=True + ) file_name = "{0}.csv".format(self.course.name.lower()) self.assertEqual(response.status_code, 200) self.assertEqual(response.get('Content-Disposition'), - 'attachment; filename="{0}"'.format(file_name)) + 'attachment; filename="{0}"'.format(file_name)) def test_download_quiz_csv(self): """ @@ -3634,23 +3921,24 @@ class TestDownloadcsv(TestCase): username=self.user.username, password=self.user_plaintext_pass ) - response = self.client.get(reverse('yaksh:download_quiz_csv', - kwargs={"course_id": self.course.id, - "quiz_id": self.quiz.id}), - - follow=True - ) - file_name = "{0}-{1}-attempt{2}.csv".format(self.course.name.replace('.', ''), - self.quiz.description.replace('.', ''), 1) + response = self.client.get( + reverse('yaksh:download_quiz_csv', + kwargs={"course_id": self.course.id, + "quiz_id": self.quiz.id}), + follow=True + ) + file_name = "{0}-{1}-attempt{2}.csv".format( + self.course.name.replace('.', ''), + self.quiz.description.replace('.', ''), 1 + ) self.assertEqual(response.status_code, 200) self.assertEqual(response.get('Content-Disposition'), - 'attachment; filename="{0}"'.format(file_name)) + 'attachment; filename="{0}"'.format(file_name)) class TestShowQuestions(TestCase): def setUp(self): self.client = Client() - tzone = pytz.timezone("utc") self.mod_group = Group.objects.create(name='moderator') # student self.student_plaintext_pass = 'student' @@ -3722,9 +4010,9 @@ class TestShowQuestions(TestCase): username=self.user.username, password=self.user_plaintext_pass ) - response = self.client.get(reverse('yaksh:show_questions'), - follow=True - ) + response = self.client.get( + reverse('yaksh:show_questions'), follow=True + ) self.assertEqual(response.status_code, 200) self.assertTemplateUsed(response, 'yaksh/showquestions.html') self.assertEqual(response.context['questions'][0], self.question) @@ -3737,14 +4025,15 @@ class TestShowQuestions(TestCase): username=self.user.username, password=self.user_plaintext_pass ) - response = self.client.post(reverse('yaksh:show_questions'), - data={'question': [self.question.id], - 'download': 'download'} - ) + response = self.client.post( + reverse('yaksh:show_questions'), + data={'question': [self.question.id], + 'download': 'download'} + ) file_name = "{0}_questions.zip".format(self.user) self.assertEqual(response.status_code, 200) self.assertEqual(response.get('Content-Disposition'), - "attachment; filename={0}".format(file_name)) + "attachment; filename={0}".format(file_name)) zip_file = string_io(response.content) zipped_file = zipfile.ZipFile(zip_file, 'r') self.assertIsNone(zipped_file.testzip()) @@ -3752,15 +4041,15 @@ class TestShowQuestions(TestCase): zip_file.close() zipped_file.close() - response = self.client.post(reverse('yaksh:show_questions'), - data={'question': [], - 'download': 'download'} - ) + response = self.client.post( + reverse('yaksh:show_questions'), + data={'question': [], + 'download': 'download'} + ) self.assertEqual(response.status_code, 200) self.assertTemplateUsed(response, 'yaksh/showquestions.html') self.assertIn("download", response.context['msg']) - def test_upload_questions(self): """ Check for uploading questions zip file @@ -3771,30 +4060,32 @@ class TestShowQuestions(TestCase): ) ques_file = os.path.join(FIXTURES_DIR_PATH, "demo_questions.zip") f = open(ques_file, 'rb') - questions_file = SimpleUploadedFile(ques_file, f.read(), + questions_file = SimpleUploadedFile(ques_file, f.read(), content_type="application/zip") - response = self.client.post(reverse('yaksh:show_questions'), - data={'file': questions_file, - 'upload': 'upload'} - ) + response = self.client.post( + reverse('yaksh:show_questions'), + data={'file': questions_file, + 'upload': 'upload'} + ) summaries = ['Roots of quadratic equation', 'Print Output', 'Adding decimals', 'For Loop over String', 'Hello World in File', 'Extract columns from files', 'Check Palindrome', 'Add 3 numbers', 'Reverse a string' ] - uploaded_ques = Question.objects.filter(active=True, - summary__in=summaries, - user=self.user).count() + uploaded_ques = Question.objects.filter( + active=True, summary__in=summaries, + user=self.user).count() self.assertEqual(response.status_code, 200) self.assertTemplateUsed(response, 'yaksh/showquestions.html') self.assertEqual(uploaded_ques, 9) f.close() dummy_file = SimpleUploadedFile("test.txt", b"test") - response = self.client.post(reverse('yaksh:show_questions'), - data={'file': dummy_file, - 'upload': 'upload'} - ) + response = self.client.post( + reverse('yaksh:show_questions'), + data={'file': dummy_file, + 'upload': 'upload'} + ) self.assertEqual(response.status_code, 200) self.assertTemplateUsed(response, 'yaksh/showquestions.html') self.assertIn("ZIP file", response.context['message']) @@ -3807,10 +4098,11 @@ class TestShowQuestions(TestCase): username=self.user.username, password=self.user_plaintext_pass ) - response = self.client.post(reverse('yaksh:show_questions'), - data={'question': [self.question.id], - 'test': 'test'} - ) + response = self.client.post( + reverse('yaksh:show_questions'), + data={'question': [self.question.id], + 'test': 'test'} + ) trial_que_paper = QuestionPaper.objects.get( quiz__description="trial_questions" ) @@ -3824,18 +4116,19 @@ class TestShowQuestions(TestCase): def test_ajax_questions_filter(self): """ - Check for filter questions based type, marks and + Check for filter questions based type, marks and language of a question """ self.client.login( username=self.user.username, password=self.user_plaintext_pass ) - response = self.client.post(reverse('yaksh:questions_filter'), - data={'question_type': 'mcq', - 'marks': '1.0', 'language': 'python' - } - ) + response = self.client.post( + reverse('yaksh:questions_filter'), + data={'question_type': 'mcq', + 'marks': '1.0', 'language': 'python' + } + ) self.assertEqual(response.status_code, 200) self.assertTemplateUsed(response, 'yaksh/ajax_question_filter.html') self.assertEqual(response.context['questions'][0], self.question1) @@ -3861,6 +4154,37 @@ class TestShowQuestions(TestCase): self.assertEqual(response.get('Content-Disposition'), 'attachment; filename="questions_dump.yaml"') + def test_delete_questions(self): + """ Test to check if questions are set to not active when deleted """ + self.client.login( + username=self.user.username, + password=self.user_plaintext_pass + ) + response = self.client.post( + reverse('yaksh:show_questions'), + data={'question': [self.question.id], + 'delete': 'delete'} + ) + self.assertEqual(response.status_code, 200) + self.assertTemplateUsed(response, 'yaksh/showquestions.html') + updated_que = Question.objects.get(id=self.question.id) + self.assertFalse(updated_que.active) + + def test_search_tags(self): + """ Test to check if questions are obtained with tags """ + self.client.login( + username=self.user.username, + password=self.user_plaintext_pass + ) + self.question.tags.add('code') + response = self.client.post( + reverse('yaksh:show_questions'), + data={'question_tags': ['code']} + ) + self.assertEqual(response.status_code, 200) + self.assertTemplateUsed(response, 'yaksh/showquestions.html') + self.assertEqual(response.context['questions'][0], self.question) + class TestShowStatistics(TestCase): def setUp(self): @@ -3909,8 +4233,10 @@ class TestShowStatistics(TestCase): # Add to moderator group self.mod_group.user_set.add(self.user) - self.course = Course.objects.create(name="Python Course", - enrollment="Open Enrollment", creator=self.user) + self.course = Course.objects.create( + name="Python Course", + enrollment="Open Enrollment", creator=self.user + ) self.quiz = Quiz.objects.create( start_date_time=datetime(2014, 10, 9, 10, 8, 15, 0, tzone), @@ -3926,13 +4252,16 @@ class TestShowStatistics(TestCase): points=1.0, language="python", type="code", user=self.user ) - self.question_paper = QuestionPaper.objects.create(quiz=self.quiz, + self.question_paper = QuestionPaper.objects.create( + quiz=self.quiz, total_marks=1.0, fixed_question_order=str(self.question) ) self.question_paper.fixed_questions.add(self.question) user_answer = "def add(a, b)\n\treturn a+b" - self.new_answer = Answer(question=self.question, answer=user_answer, - correct=True, error=json.dumps([])) + self.new_answer = Answer( + question=self.question, answer=user_answer, + correct=True, error=json.dumps([]) + ) self.new_answer.save() self.answerpaper = AnswerPaper.objects.create( user=self.student, question_paper=self.question_paper, @@ -3965,11 +4294,12 @@ class TestShowStatistics(TestCase): username=self.student.username, password=self.student_plaintext_pass ) - response = self.client.get(reverse('yaksh:show_statistics', - kwargs={"questionpaper_id": self.question_paper.id, - "course_id": self.course.id}), - follow=True - ) + response = self.client.get( + reverse('yaksh:show_statistics', + kwargs={"questionpaper_id": self.question_paper.id, + "course_id": self.course.id}), + follow=True + ) self.assertEqual(response.status_code, 404) def test_show_statistics_for_student(self): @@ -3980,18 +4310,19 @@ class TestShowStatistics(TestCase): username=self.user.username, password=self.user_plaintext_pass ) - response = self.client.get(reverse('yaksh:show_statistics', - kwargs={'questionpaper_id': self.question_paper.id, - "course_id": self.course.id}), - follow=True - ) + response = self.client.get( + reverse('yaksh:show_statistics', + kwargs={'questionpaper_id': self.question_paper.id, + "course_id": self.course.id}), + follow=True + ) self.assertEqual(response.status_code, 200) self.assertTemplateUsed(response, 'yaksh/statistics_question.html') self.assertEqual(response.context['quiz'], self.quiz) self.assertEqual(response.context['attempts'][0], - self.answerpaper.attempt_number) + self.answerpaper.attempt_number) self.assertEqual(response.context['questionpaper_id'], - str(self.question_paper.id)) + str(self.question_paper.id)) def test_show_statistics_for_student_per_attempt(self): """ @@ -4001,16 +4332,18 @@ class TestShowStatistics(TestCase): username=self.user.username, password=self.user_plaintext_pass ) - response = self.client.get(reverse('yaksh:show_statistics', + response = self.client.get( + reverse('yaksh:show_statistics', kwargs={'questionpaper_id': self.question_paper.id, 'attempt_number': self.answerpaper.attempt_number, "course_id": self.course.id}), - follow=True - ) + follow=True + ) self.assertEqual(response.status_code, 200) self.assertTemplateUsed(response, 'yaksh/statistics_question.html') - self.assertSequenceEqual(response.context['question_stats'][self.question], - [1, 1]) + self.assertSequenceEqual( + response.context['question_stats'][self.question], [1, 1] + ) self.assertEqual(response.context['attempts'][0], 1) self.assertEqual(response.context['total'], 1) @@ -4040,6 +4373,24 @@ class TestQuestionPaper(TestCase): timezone='UTC' ) + self.user2_plaintext_pass = 'demo2' + self.user2 = User.objects.create_user( + username='demo_user2', + password=self.user_plaintext_pass, + first_name='first_name2', + last_name='last_name2', + email='demo2@test.com' + ) + + Profile.objects.create( + user=self.user2, + roll_number=11, + institute='IIT', + department='Chemical', + position='Student', + timezone='UTC' + ) + self.teacher_plaintext_pass = 'demo_teacher' self.teacher = User.objects.create_user( username='demo_teacher', @@ -4092,6 +4443,16 @@ class TestQuestionPaper(TestCase): self.learning_module.learning_unit.add(self.learning_unit.id) self.course.learning_module.add(self.learning_module) + # Questions for random set + self.random_que1 = Question.objects.create( + summary="Random 1", description="Test Random 1", + points=1.0, language="python", type="code", user=self.user + ) + self.random_que2 = Question.objects.create( + summary="Random 2", description="Test Random 2", + points=1.0, language="python", type="code", user=self.user + ) + # Mcq Question self.question_mcq = Question.objects.create( summary="Test_mcq_question", description="Test MCQ", @@ -4201,6 +4562,54 @@ class TestQuestionPaper(TestCase): self.learning_module.delete() self.learning_unit.delete() + def test_preview_questionpaper_correct(self): + self.client.login( + username=self.user.username, + password=self.user_plaintext_pass + ) + + # Should successfully preview question paper + response = self.client.get( + reverse('yaksh:preview_questionpaper', + kwargs={"questionpaper_id": self.question_paper.id} + ) + ) + self.assertEqual(response.status_code, 200) + self.assertTemplateUsed(response, 'yaksh/preview_questionpaper.html') + self.assertEqual( + response.context['questions'], + self.questions_list + ) + self.assertEqual(response.context['paper'], self.question_paper) + + def test_preview_questionpaper_without_moderator(self): + self.client.login( + username=self.user2.username, + password=self.user_plaintext_pass + ) + + # Should raise an HTTP 404 response + response = self.client.get( + reverse('yaksh:preview_questionpaper', + kwargs={"questionpaper_id": self.question_paper.id} + ) + ) + self.assertEqual(response.status_code, 404) + + def test_preview_qustionpaper_without_quiz_owner(self): + self.client.login( + username=self.teacher.username, + password=self.teacher_plaintext_pass + ) + + # Should raise an HTTP 404 response + response = self.client.get( + reverse('yaksh:preview_questionpaper', + kwargs={"questionpaper_id": self.question_paper.id} + ) + ) + self.assertEqual(response.status_code, 404) + def test_mcq_attempt_right_after_wrong(self): """ Case:- Check if answerpaper and answer marks are updated after attempting same mcq question with wrong answer and then right @@ -4525,8 +4934,28 @@ class TestQuestionPaper(TestCase): "course_id": self.course.id, "questionpaper_id": self.question_paper.id}), data={"question_tags": search_tag}) + self.assertEqual(response.context["questions"][0], self.tagged_que) + # Add random questions in question paper + response = self.client.post( + reverse('yaksh:designquestionpaper', + kwargs={"quiz_id": self.quiz.id, + "course_id": self.course.id, + "questionpaper_id": self.question_paper.id}), + data={'random_questions': [self.random_que1.id, + self.random_que2.id], + 'marks': ['1.0'], 'question_type': ['code'], + 'add-random': ['']} + ) + + self.assertEqual(response.status_code, 200) + self.assertTemplateUsed(response, 'yaksh/design_questionpaper.html') + random_set = response.context['random_sets'][0] + added_random_ques = random_set.questions.all() + self.assertIn(self.random_que1, added_random_ques) + self.assertIn(self.random_que2, added_random_ques) + class TestLearningModule(TestCase): def setUp(self): diff --git a/yaksh/tests/test_code_server.py b/yaksh/tests/test_code_server.py index 1309624..8237256 100644 --- a/yaksh/tests/test_code_server.py +++ b/yaksh/tests/test_code_server.py @@ -106,6 +106,39 @@ class TestCodeServer(unittest.TestCase): self.assertFalse(data['success']) self.assertTrue('AssertionError' in data['error'][0]['exception']) + def test_question_with_no_testcases(self): + # Given + testdata = { + 'metadata': { + 'user_answer': 'def f(): return 1', + 'language': 'python', + 'partial_grading': False + }, + 'test_case_data': [] + } + + # When + submit(self.url, '0', json.dumps(testdata), '') + result = get_result(self.url, '0', block=True) + + # Then + data = json.loads(result.get('result')) + self.assertFalse(data['success']) + + # With correct answer and test case + testdata["metadata"]["user_answer"] = 'def f(): return 2' + testdata["test_case_data"] = [{'test_case': 'assert f() == 2', + 'test_case_type': 'standardtestcase', + 'weight': 0.0 + }] + # When + submit(self.url, '0', json.dumps(testdata), '') + result = get_result(self.url, '0', block=True) + + # Then + data = json.loads(result.get('result')) + self.assertTrue(data['success']) + def test_multiple_simultaneous_hits(self): # Given results = Queue() diff --git a/yaksh/urls.py b/yaksh/urls.py index 08c2091..1e1def6 100644 --- a/yaksh/urls.py +++ b/yaksh/urls.py @@ -1,4 +1,4 @@ -from django.conf.urls import patterns, url +from django.conf.urls import url from yaksh import views urlpatterns = [ @@ -9,47 +9,54 @@ urlpatterns = [ url(r'^activate/(?P<key>.+)$', views.activate_user, name="activate"), url(r'^new_activation/$', views.new_activation, name='new_activation'), url(r'^quizzes/$', views.quizlist_user, name='quizlist_user'), - url(r'^quizzes/(?P<enrolled>\w+)/$', views.quizlist_user, name='quizlist_user'), + url(r'^quizzes/(?P<enrolled>\w+)/$', views.quizlist_user, + name='quizlist_user'), url(r'^results/$', views.results_user), - url(r'^start/(?P<questionpaper_id>\d+)/(?P<module_id>\d+)/(?P<course_id>\d+)/$', - views.start), - url(r'^start/(?P<attempt_num>\d+)/(?P<module_id>\d+)/(?P<questionpaper_id>\d+)/(?P<course_id>\d+)/$', - views.start), - url(r'^quit/(?P<attempt_num>\d+)/(?P<module_id>\d+)/(?P<questionpaper_id>\d+)/(?P<course_id>\d+)/$', - views.quit), + url(r'^start/(?P<questionpaper_id>\d+)/(?P<module_id>\d+)/' + '(?P<course_id>\d+)/$', views.start), + url(r'^start/(?P<attempt_num>\d+)/(?P<module_id>\d+)/' + '(?P<questionpaper_id>\d+)/(?P<course_id>\d+)/$', views.start), + url(r'^quit/(?P<attempt_num>\d+)/(?P<module_id>\d+)/' + '(?P<questionpaper_id>\d+)/(?P<course_id>\d+)/$', views.quit), url(r'^complete/$', views.complete), - url(r'^complete/(?P<attempt_num>\d+)/(?P<module_id>\d+)/(?P<questionpaper_id>\d+)/(?P<course_id>\d+)/$',\ - views.complete), + url(r'^complete/(?P<attempt_num>\d+)/(?P<module_id>\d+)/' + '(?P<questionpaper_id>\d+)/(?P<course_id>\d+)/$', views.complete), url(r'^register/$', views.user_register, name="register"), url(r'^(?P<q_id>\d+)/check/$', views.check, name="check"), url(r'^get_result/(?P<uid>\d+)/(?P<course_id>\d+)/(?P<module_id>\d+)/$', views.get_result), - url(r'^(?P<q_id>\d+)/check/(?P<attempt_num>\d+)/(?P<module_id>\d+)/(?P<questionpaper_id>\d+)/(?P<course_id>\d+)/$',\ - views.check, name="check"), - url(r'^(?P<q_id>\d+)/skip/(?P<attempt_num>\d+)/(?P<module_id>\d+)/(?P<questionpaper_id>\d+)/(?P<course_id>\d+)/$', + url(r'^(?P<q_id>\d+)/check/(?P<attempt_num>\d+)/(?P<module_id>\d+)/' + '(?P<questionpaper_id>\d+)/(?P<course_id>\d+)/$', + views.check, name="check"), + url(r'^(?P<q_id>\d+)/skip/(?P<attempt_num>\d+)/(?P<module_id>\d+)/' + '(?P<questionpaper_id>\d+)/(?P<course_id>\d+)/$', views.skip), - url(r'^(?P<q_id>\d+)/skip/(?P<next_q>\d+)/(?P<attempt_num>\d+)/(?P<module_id>\d+)/(?P<questionpaper_id>\d+)/(?P<course_id>\d+)/$', + url(r'^(?P<q_id>\d+)/skip/(?P<next_q>\d+)/(?P<attempt_num>\d+)/' + '(?P<module_id>\d+)/(?P<questionpaper_id>\d+)/(?P<course_id>\d+)/$', views.skip), - url(r'^enroll_request/(?P<course_id>\d+)/$', views.enroll_request, name='enroll_request'), - url(r'^self_enroll/(?P<course_id>\d+)/$', views.self_enroll, name='self_enroll'), + url(r'^enroll_request/(?P<course_id>\d+)/$', views.enroll_request, + name='enroll_request'), + url(r'^self_enroll/(?P<course_id>\d+)/$', views.self_enroll, + name='self_enroll'), url(r'^view_answerpaper/(?P<questionpaper_id>\d+)/(?P<course_id>\d+)$', views.view_answerpaper, name='view_answerpaper'), - url(r'^show_lesson/(?P<lesson_id>\d+)/(?P<module_id>\d+)/(?P<course_id>\d+)/$', - views.show_lesson, name='show_lesson'), + url(r'^show_lesson/(?P<lesson_id>\d+)/(?P<module_id>\d+)/' + '(?P<course_id>\d+)/$', views.show_lesson, name='show_lesson'), url(r'^quizzes/view_module/(?P<module_id>\d+)/(?P<course_id>\d+)/$', views.view_module, name='view_module'), - url(r'^next_unit/(?P<course_id>\d+)/(?P<module_id>\d+)/(?P<current_unit_id>\d+)/$', - views.get_next_unit, name='next_unit'), + url(r'^next_unit/(?P<course_id>\d+)/(?P<module_id>\d+)/' + '(?P<current_unit_id>\d+)/$', views.get_next_unit, name='next_unit'), url(r'^next_unit/(?P<course_id>\d+)/(?P<module_id>\d+)/$', views.get_next_unit, name='next_unit'), - url(r'^next_unit/(?P<course_id>\d+)/(?P<module_id>\d+)/(?P<current_unit_id>\d+)/(?P<first_unit>\d+)/$', + url(r'^next_unit/(?P<course_id>\d+)/(?P<module_id>\d+)/' + '(?P<current_unit_id>\d+)/(?P<first_unit>\d+)/$', views.get_next_unit, name='next_unit'), url(r'^course_modules/(?P<course_id>\d+)/$', views.course_modules, name='course_modules'), url(r'^manage/$', views.prof_manage, name='manage'), url(r'^manage/addquestion/$', views.add_question, name="add_question"), url(r'^manage/addquestion/(?P<question_id>\d+)/$', views.add_question, - name="add_question"), + name="add_question"), url(r'^manage/addquiz/$', views.add_quiz, name='add_quiz'), url(r'^manage/add_exercise/$', views.add_exercise, name='add_exercise'), url(r'^manage/add_exercise/(?P<quiz_id>\d+)/$', views.add_exercise, @@ -62,79 +69,97 @@ urlpatterns = [ views.add_quiz, name='edit_quiz'), url(r'^manage/gradeuser/$', views.grade_user, name="grade_user"), url(r'^manage/gradeuser/(?P<quiz_id>\d+)/(?P<course_id>\d+)/$', - views.grade_user, name="grade_user"), - url(r'^manage/gradeuser/(?P<quiz_id>\d+)/(?P<user_id>\d+)/(?P<course_id>\d+)/$', - views.grade_user, name="grade_user"), - url(r'^manage/gradeuser/(?P<quiz_id>\d+)/(?P<user_id>\d+)/(?P<attempt_number>\d+)/(?P<course_id>\d+)/$', - views.grade_user, name="grade_user"), - url(r'^manage/questions/$', views.show_all_questions, name="show_questions"), + views.grade_user, name="grade_user"), + url(r'^manage/gradeuser/(?P<quiz_id>\d+)/(?P<user_id>\d+)/' + '(?P<course_id>\d+)/$', + views.grade_user, name="grade_user"), + url(r'^manage/gradeuser/(?P<quiz_id>\d+)/(?P<user_id>\d+)/' + '(?P<attempt_number>\d+)/(?P<course_id>\d+)/$', + views.grade_user, name="grade_user"), + url(r'^manage/questions/$', views.show_all_questions, + name="show_questions"), url(r'^manage/monitor/$', views.monitor, name="monitor"), url(r'^manage/monitor/(?P<quiz_id>\d+)/(?P<course_id>\d+)/$', views.monitor, name="monitor"), - url(r'^manage/user_data/(?P<user_id>\d+)/(?P<questionpaper_id>\d+)/(?P<course_id>\d+)/$', + url(r'^manage/user_data/(?P<user_id>\d+)/(?P<questionpaper_id>\d+)/' + '(?P<course_id>\d+)/$', views.user_data, name="user_data"), url(r'^manage/user_data/(?P<user_id>\d+)/$', views.user_data), - url(r'^manage/quiz/designquestionpaper/(?P<quiz_id>\d+)/$', views.design_questionpaper, - name='design_questionpaper'), - url(r'^manage/designquestionpaper/(?P<quiz_id>\d+)/(?P<questionpaper_id>\d+)/$', + url(r'^manage/quiz/designquestionpaper/(?P<quiz_id>\d+)/$', + views.design_questionpaper, name='design_questionpaper'), + url(r'^manage/designquestionpaper/(?P<quiz_id>\d+)/' + '(?P<questionpaper_id>\d+)/$', views.design_questionpaper, name='designquestionpaper'), - url(r'^manage/designquestionpaper/(?P<quiz_id>\d+)/(?P<questionpaper_id>\d+)/(?P<course_id>\d+)/$', + url(r'^manage/designquestionpaper/(?P<quiz_id>\d+)/' + '(?P<questionpaper_id>\d+)/(?P<course_id>\d+)/$', views.design_questionpaper, name='designquestionpaper'), - url(r'^manage/statistics/question/(?P<questionpaper_id>\d+)/(?P<course_id>\d+)/$', + url(r'^manage/statistics/question/(?P<questionpaper_id>\d+)/' + '(?P<course_id>\d+)/$', views.show_statistics, name="show_statistics"), - url(r'^manage/statistics/question/(?P<questionpaper_id>\d+)/(?P<attempt_number>\d+)/(?P<course_id>\d+)/$', + url(r'^manage/statistics/question/(?P<questionpaper_id>\d+)/' + '(?P<attempt_number>\d+)/(?P<course_id>\d+)/$', views.show_statistics, name="show_statistics"), url(r'^manage/download_quiz_csv/(?P<course_id>\d+)/(?P<quiz_id>\d+)/$', views.download_quiz_csv, name="download_quiz_csv"), - url(r'^manage/duplicate_course/(?P<course_id>\d+)/$', views.duplicate_course, - name='duplicate_course'), + url(r'^manage/duplicate_course/(?P<course_id>\d+)/$', + views.duplicate_course, name='duplicate_course'), url(r'manage/courses/$', views.courses, name='courses'), url(r'manage/add_course/$', views.add_course, name='add_course'), - url(r'manage/edit_course/(?P<course_id>\d+)$', views.add_course, name='edit_course'), - url(r'manage/course_detail/(?P<course_id>\d+)/$', views.course_detail, name='course_detail'), + url(r'manage/edit_course/(?P<course_id>\d+)$', views.add_course, + name='edit_course'), + url(r'manage/course_detail/(?P<course_id>\d+)/$', views.course_detail, + name='course_detail'), url(r'manage/enroll/(?P<course_id>\d+)/(?P<user_id>\d+)/$', views.enroll, - name="enroll_user"), + name="enroll_user"), url(r'manage/enroll/rejected/(?P<course_id>\d+)/(?P<user_id>\d+)/$', views.enroll, {'was_rejected': True}), - url(r'manage/upload_users/(?P<course_id>\d+)/$', views.upload_users, name="upload_users"), - url(r'manage/send_mail/(?P<course_id>\d+)/$', views.send_mail, name="send_mail"), + url(r'manage/upload_users/(?P<course_id>\d+)/$', views.upload_users, + name="upload_users"), + url(r'manage/send_mail/(?P<course_id>\d+)/$', views.send_mail, + name="send_mail"), url(r'manage/reject/(?P<course_id>\d+)/(?P<user_id>\d+)/$', views.reject, - name="reject_user"), + name="reject_user"), url(r'manage/enrolled/reject/(?P<course_id>\d+)/(?P<user_id>\d+)/$', views.reject, {'was_enrolled': True}, name="reject_user"), - url(r'manage/toggle_status/(?P<course_id>\d+)/$', views.toggle_course_status, - name="toggle_course_status"), + url(r'manage/toggle_status/(?P<course_id>\d+)/$', + views.toggle_course_status, name="toggle_course_status"), url(r'^ajax/questions/filter/$', views.ajax_questions_filter, - name="questions_filter"), + name="questions_filter"), url(r'^editprofile/$', views.edit_profile, name='edit_profile'), url(r'^viewprofile/$', views.view_profile, name='view_profile'), - url(r'^manage/enroll/(?P<course_id>\d+)/$', views.enroll, name="enroll_users"), + url(r'^manage/enroll/(?P<course_id>\d+)/$', views.enroll, + name="enroll_users"), url(r'manage/enroll/rejected/(?P<course_id>\d+)/$', views.enroll, {'was_rejected': True}), url(r'manage/enrolled/reject/(?P<course_id>\d+)/$', views.reject, {'was_enrolled': True}, name="reject_users"), url(r'^manage/searchteacher/(?P<course_id>\d+)/$', views.search_teacher), - url(r'^manage/addteacher/(?P<course_id>\d+)/$', views.add_teacher, name='add_teacher'), - url(r'^manage/remove_teachers/(?P<course_id>\d+)/$', views.remove_teachers, name='remove_teacher'), + url(r'^manage/addteacher/(?P<course_id>\d+)/$', views.add_teacher, + name='add_teacher'), + url(r'^manage/remove_teachers/(?P<course_id>\d+)/$', views.remove_teachers, + name='remove_teacher'), url(r'^manage/download_questions/$', views.show_all_questions, - name="show_questions"), + name="show_questions"), url(r'^manage/upload_questions/$', views.show_all_questions, - name="show_questions"), + name="show_questions"), url(r'^manage/grader/$', views.grader, name='grader'), url(r'^manage/regrade/question/(?P<course_id>\d+)/(?P<question_id>\d+)/$', - views.regrade, name='regrade'), - url(r'^manage/regrade/questionpaper/(?P<course_id>\d+)/(?P<question_id>\d+)/(?P<questionpaper_id>\d+)/$', - views.regrade, name='regrade'), - url(r'^manage/regrade/answerpaper/(?P<course_id>\d+)/(?P<question_id>\d+)/(?P<answerpaper_id>\d+)/$', - views.regrade, name='regrade'), + views.regrade, name='regrade'), + url(r'^manage/regrade/questionpaper/(?P<course_id>\d+)/' + '(?P<question_id>\d+)/(?P<questionpaper_id>\d+)/$', + views.regrade, name='regrade'), + url(r'^manage/regrade/answerpaper/(?P<course_id>\d+)/' + '(?P<question_id>\d+)/(?P<answerpaper_id>\d+)/$', + views.regrade, name='regrade'), url(r'^manage/regrade/paper/(?P<course_id>\d+)/(?P<answerpaper_id>\d+)/$', - views.regrade, name='regrade'), - url(r'^manage/(?P<mode>godmode|usermode)/(?P<quiz_id>\d+)/(?P<course_id>\d+)/$', - views.test_quiz), + views.regrade, name='regrade'), + url(r'^manage/(?P<mode>godmode|usermode)/(?P<quiz_id>\d+)/' + '(?P<course_id>\d+)/$', views.test_quiz), url(r'^manage/create_demo_course/$', views.create_demo_course), url(r'^manage/courses/download_course_csv/(?P<course_id>\d+)/$', views.download_course_csv, name="download_course_csv"), - url(r'^manage/download/user_assignment/(?P<question_id>\d+)/(?P<user_id>\d+)/(?P<quiz_id>\d+)/$', + url(r'^manage/download/user_assignment/(?P<question_id>\d+)/' + '(?P<user_id>\d+)/(?P<quiz_id>\d+)/$', views.download_assignment_file, name="download_user_assignment"), url(r'^manage/download/quiz_assignments/(?P<quiz_id>\d+)/$', views.download_assignment_file, name="download_quiz_assignment"), @@ -150,8 +175,8 @@ urlpatterns = [ views.edit_lesson, name="edit_lesson"), url(r'^manage/courses/designmodule/(?P<module_id>\d+)/$', views.design_module, name="design_module"), - url(r'^manage/courses/designmodule/(?P<module_id>\d+)/(?P<course_id>\d+)/$', - views.design_module, name="design_module"), + url(r'^manage/courses/designmodule/(?P<module_id>\d+)/' + '(?P<course_id>\d+)/$', views.design_module, name="design_module"), url(r'^manage/courses/all_quizzes/$', views.show_all_quizzes, name="show_all_quizzes"), url(r'^manage/courses/all_lessons/$', @@ -172,4 +197,8 @@ urlpatterns = [ views.design_course, name="design_course"), url(r'^manage/course_status/(?P<course_id>\d+)/$', views.course_status, name="course_status"), + url(r'^manage/preview_questionpaper/(?P<questionpaper_id>\d+)/$', + views.preview_questionpaper, name="preview_questionpaper"), + url(r'^manage/get_user_status/(?P<course_id>\d+)/(?P<student_id>\d+)/$', + views.get_user_data, name="get_user_data") ] diff --git a/yaksh/urls_password_reset.py b/yaksh/urls_password_reset.py index c1e36c6..4a7ddf3 100644 --- a/yaksh/urls_password_reset.py +++ b/yaksh/urls_password_reset.py @@ -1,4 +1,4 @@ -from django.conf.urls import patterns, url +from django.conf.urls import url from django.contrib.auth.views import password_reset, password_reset_confirm,\ password_reset_done, password_reset_complete, password_change,\ password_change_done diff --git a/yaksh/views.py b/yaksh/views.py index 5772df1..9116026 100644 --- a/yaksh/views.py +++ b/yaksh/views.py @@ -1,29 +1,20 @@ -import random -import string import os -from datetime import datetime, timedelta -import collections import csv from django.http import HttpResponse, JsonResponse -from django.core.urlresolvers import reverse from django.contrib.auth import login, logout, authenticate -from django.shortcuts import render_to_response, get_object_or_404, redirect -from django.template import RequestContext, Context, Template +from django.shortcuts import render, get_object_or_404, redirect +from django.template import Context, Template from django.http import Http404 -from django.db.models import Sum, Max, Q, F +from django.db.models import Max, Q, F from django.views.decorators.csrf import csrf_exempt from django.contrib.auth.decorators import login_required from django.contrib.auth.models import Group from django.forms.models import inlineformset_factory from django.utils import timezone -from django.core.validators import URLValidator from django.core.exceptions import ( - MultipleObjectsReturned, ObjectDoesNotExist, ValidationError + MultipleObjectsReturned, ObjectDoesNotExist ) -from django.conf import settings -import pytz from taggit.models import Tag -from itertools import chain import json import six from textwrap import dedent @@ -37,24 +28,22 @@ import re # Local imports. from yaksh.code_server import get_result as get_result_from_code_server from yaksh.models import ( - Answer, AnswerPaper, AssignmentUpload, Course, FileUpload, FloatTestCase, - HookTestCase, IntegerTestCase, McqTestCase, Profile, - QuestionPaper, QuestionSet, Quiz, Question, StandardTestCase, - StdIOBasedTestCase, StringTestCase, TestCase, User, - get_model_class, FIXTURES_DIR_PATH, Lesson, LessonFile, - LearningUnit, LearningModule, + Answer, AnswerPaper, AssignmentUpload, Course, FileUpload, Profile, + QuestionPaper, QuestionSet, Quiz, Question, TestCase, User, + FIXTURES_DIR_PATH, Lesson, LessonFile, LearningUnit, LearningModule, CourseStatus ) from yaksh.forms import ( UserRegisterForm, UserLoginForm, QuizForm, QuestionForm, - RandomQuestionForm, QuestionFilterForm, CourseForm, ProfileForm, - UploadFileForm, get_object_form, FileForm, QuestionPaperForm, LessonForm, + QuestionFilterForm, CourseForm, ProfileForm, + UploadFileForm, FileForm, QuestionPaperForm, LessonForm, LessonFileForm, LearningModuleForm, ExerciseForm ) -from yaksh.settings import SERVER_POOL_PORT, SERVER_HOST_NAME +from yaksh.settings import SERVER_POOL_PORT, SERVER_HOST_NAME from .settings import URL_ROOT from .file_utils import extract_files, is_csv -from .send_emails import send_user_mail, generate_activation_key, send_bulk_mail +from .send_emails import (send_user_mail, + generate_activation_key, send_bulk_mail) from .decorators import email_verified, has_profile @@ -64,14 +53,14 @@ def my_redirect(url): return redirect(URL_ROOT + url) -def my_render_to_response(template, context=None, **kwargs): +def my_render_to_response(request, template, context=None, **kwargs): """Overridden render_to_response. """ if context is None: context = {'URL_ROOT': URL_ROOT} else: context['URL_ROOT'] = URL_ROOT - return render_to_response(template, context, **kwargs) + return render(request, template, context, **kwargs) def is_moderator(user): @@ -87,8 +76,9 @@ def add_to_group(users): if not is_moderator(user): user.groups.add(group) + CSV_FIELDS = ['name', 'username', 'roll_number', 'institute', 'department', - 'questions', 'marks_obtained', 'out_of', 'percentage', 'status'] + 'questions', 'marks_obtained', 'out_of', 'percentage', 'status'] def get_html_text(md_text): @@ -114,14 +104,12 @@ def user_register(request): Create a user and corresponding profile and store roll_number also.""" user = request.user - ci = RequestContext(request) if user.is_authenticated(): return my_redirect("/exam/quizzes/") context = {} if request.method == "POST": form = UserRegisterForm(request.POST) if form.is_valid(): - data = form.cleaned_data u_name, pwd, user_email, key = form.save() new_user = authenticate(username=u_name, password=pwd) login(request, new_user) @@ -129,16 +117,18 @@ def user_register(request): success, msg = send_user_mail(user_email, key) context = {'activation_msg': msg} return my_render_to_response( + request, 'yaksh/activation_status.html', context ) return index(request) else: - return my_render_to_response('yaksh/register.html', {'form': form}, - context_instance=ci) + return my_render_to_response( + request, 'yaksh/register.html', {'form': form} + ) else: form = UserRegisterForm() return my_render_to_response( - 'yaksh/register.html', {'form': form}, context_instance=ci + request, 'yaksh/register.html', {'form': form} ) @@ -146,7 +136,7 @@ def user_logout(request): """Show a page to inform user that the quiz has been compeleted.""" logout(request) context = {'message': "You have been logged out successfully"} - return my_render_to_response('yaksh/complete.html', context) + return my_render_to_response(request, 'yaksh/complete.html', context) @login_required @@ -155,7 +145,6 @@ def user_logout(request): def quizlist_user(request, enrolled=None, msg=None): """Show All Quizzes that is available to logged-in user.""" user = request.user - ci = RequestContext(request) if request.method == "POST": course_code = request.POST.get('course_code') @@ -177,9 +166,7 @@ def quizlist_user(request, enrolled=None, msg=None): context = {'user': user, 'courses': courses, 'title': title, 'msg': msg} - return my_render_to_response( - "yaksh/quizzes_user.html", context, context_instance=ci - ) + return my_render_to_response(request, "yaksh/quizzes_user.html", context) @login_required @@ -189,14 +176,13 @@ def results_user(request): user = request.user papers = AnswerPaper.objects.get_user_answerpapers(user) context = {'papers': papers} - return my_render_to_response("yaksh/results_user.html", context) + return my_render_to_response(request, "yaksh/results_user.html", context) @login_required @email_verified def add_question(request, question_id=None): user = request.user - ci = RequestContext(request) test_case_type = None if question_id is None: @@ -258,7 +244,7 @@ def add_question(request, question_id=None): 'uploaded_files': uploaded_files } return my_render_to_response( - "yaksh/add_question.html", context, context_instance=ci + request, "yaksh/add_question.html", context ) qform = QuestionForm(instance=question) @@ -283,7 +269,7 @@ def add_question(request, question_id=None): context = {'qform': qform, 'fileform': fileform, 'question': question, 'formsets': formsets, 'uploaded_files': uploaded_files} return my_render_to_response( - "yaksh/add_question.html", context, context_instance=ci + request, "yaksh/add_question.html", context ) @@ -293,7 +279,6 @@ def add_quiz(request, quiz_id=None, course_id=None): """To add a new quiz in the database. Create a new quiz and store it.""" user = request.user - ci = RequestContext(request) if not is_moderator(user): raise Http404('You are not allowed to view this course !') if quiz_id: @@ -324,16 +309,13 @@ def add_quiz(request, quiz_id=None, course_id=None): context["course_id"] = course_id context["quiz"] = quiz context["form"] = form - return my_render_to_response( - 'yaksh/add_quiz.html', context, context_instance=ci - ) + return my_render_to_response(request, 'yaksh/add_quiz.html', context) @login_required @email_verified def add_exercise(request, quiz_id=None, course_id=None): user = request.user - ci = RequestContext(request) if not is_moderator(user): raise Http404('You are not allowed to view this course !') if quiz_id: @@ -373,9 +355,7 @@ def add_exercise(request, quiz_id=None, course_id=None): context["exercise"] = quiz context["course_id"] = course_id context["form"] = form - return my_render_to_response( - 'yaksh/add_exercise.html', context, context_instance=ci - ) + return my_render_to_response(request, 'yaksh/add_exercise.html', context) @login_required @@ -385,13 +365,12 @@ def prof_manage(request, msg=None): """Take credentials of the user with professor/moderator rights/permissions and log in.""" user = request.user - ci = RequestContext(request) if not user.is_authenticated(): return my_redirect('/exam/login') if not is_moderator(user): return my_redirect('/exam/') - courses = Course.objects.filter(creator=user, is_trial=False) - + courses = Course.objects.filter(Q(creator=user) | Q(teachers=user), + is_trial=False) trial_paper = AnswerPaper.objects.filter( user=user, question_paper__quiz__is_trial=True, course__is_trial=True @@ -410,11 +389,12 @@ def prof_manage(request, msg=None): qpaper.quiz.delete() else: answerpaper.delete() + context = {'user': user, 'courses': courses, 'trial_paper': trial_paper, 'msg': msg } return my_render_to_response( - 'yaksh/moderator_dashboard.html', context, context_instance=ci + request, 'yaksh/moderator_dashboard.html', context ) @@ -422,7 +402,6 @@ def user_login(request): """Take the credentials of the user and log the user in.""" user = request.user - ci = RequestContext(request) context = {} if user.is_authenticated(): return index(request) @@ -442,8 +421,7 @@ def user_login(request): form = UserLoginForm() context = {"form": form} - return my_render_to_response('yaksh/login.html', context, - context_instance=ci) + return my_render_to_response(request, 'yaksh/login.html', context) @login_required @@ -453,7 +431,6 @@ def start(request, questionpaper_id=None, attempt_num=None, course_id=None, """Check the user cedentials and if any quiz is available, start the exam.""" user = request.user - ci = RequestContext(request) # check conditions try: quest_paper = QuestionPaper.objects.get(id=questionpaper_id) @@ -486,9 +463,6 @@ def start(request, questionpaper_id=None, attempt_num=None, course_id=None, learning_module.name) return course_modules(request, course_id, msg) - # update course status with current unit - _update_unit_status(course_id, user, learning_unit) - # is user enrolled in the course if not course.is_enrolled(user): msg = 'You are not enrolled in {0} course'.format(course.name) @@ -522,6 +496,9 @@ def start(request, questionpaper_id=None, attempt_num=None, course_id=None, return view_module(request, module_id=module_id, course_id=course_id, msg=msg) + # update course status with current unit + _update_unit_status(course_id, user, learning_unit) + # if any previous attempt last_attempt = AnswerPaper.objects.get_user_last_attempt( quest_paper, user, course_id) @@ -532,9 +509,8 @@ def start(request, questionpaper_id=None, attempt_num=None, course_id=None, previous_question=last_attempt.current_question() ) # allowed to start - if not quest_paper.can_attempt_now(user, course_id): - msg = "You cannot attempt {0} quiz more than {1} time(s)".format( - quest_paper.quiz.description, quest_paper.quiz.attempts_allowed) + if not quest_paper.can_attempt_now(user, course_id)[0]: + msg = quest_paper.can_attempt_now(user, course_id)[1] if is_moderator(user): return prof_manage(request, msg=msg) return view_module(request, module_id=module_id, course_id=course_id, @@ -553,8 +529,7 @@ def start(request, questionpaper_id=None, attempt_num=None, course_id=None, } if is_moderator(user): context["status"] = "moderator" - return my_render_to_response('yaksh/intro.html', context, - context_instance=ci) + return my_render_to_response(request, 'yaksh/intro.html', context) else: ip = request.META['REMOTE_ADDR'] if not hasattr(user, 'profile'): @@ -562,21 +537,23 @@ def start(request, questionpaper_id=None, attempt_num=None, course_id=None, raise Http404(msg) new_paper = quest_paper.make_answerpaper(user, ip, attempt_number, course_id) - if new_paper.status == 'inprogress': - return show_question(request, new_paper.current_question(), - new_paper, course_id=course_id, - module_id=module_id, previous_question=None - ) + if new_paper.status == 'inprogress': + return show_question( + request, new_paper.current_question(), + new_paper, course_id=course_id, + module_id=module_id, previous_question=None + ) else: msg = 'You have already finished the quiz!' raise Http404(msg) + @login_required @email_verified -def show_question(request, question, paper, error_message=None, notification=None, - course_id=None, module_id=None, previous_question=None): +def show_question(request, question, paper, error_message=None, + notification=None, course_id=None, module_id=None, + previous_question=None): """Show a question if possible.""" - user = request.user quiz = paper.question_paper.quiz quiz_type = 'Exam' can_skip = False @@ -586,7 +563,8 @@ def show_question(request, question, paper, error_message=None, notification=Non delay_time = paper.time_left_on_question(question) if previous_question and quiz.is_exercise: - if delay_time <= 0 or previous_question in paper.questions_answered.all(): + if (delay_time <= 0 or previous_question in + paper.questions_answered.all()): can_skip = True question = previous_question if not question: @@ -616,7 +594,10 @@ def show_question(request, question, paper, error_message=None, notification=Non if question.type == "code" else 'You have already attempted this question' ) - test_cases = question.get_test_cases() + if question.type in ['mcc', 'mcq', 'arrange']: + test_cases = question.get_ordered_test_cases(paper) + else: + test_cases = question.get_test_cases() files = FileUpload.objects.filter(question_id=question.id, hide=False) course = Course.objects.get(id=course_id) module = course.learning_module.get(id=module_id) @@ -642,16 +623,13 @@ def show_question(request, question, paper, error_message=None, notification=Non last_attempt = answers[0].answer if last_attempt: context['last_attempt'] = last_attempt.encode('unicode-escape') - ci = RequestContext(request) - return my_render_to_response('yaksh/question.html', context, - context_instance=ci) + return my_render_to_response(request, 'yaksh/question.html', context) @login_required @email_verified def skip(request, q_id, next_q=None, attempt_num=None, questionpaper_id=None, course_id=None, module_id=None): - user = request.user paper = get_object_or_404( AnswerPaper, user=request.user, attempt_number=attempt_num, question_paper=questionpaper_id, course_id=course_id @@ -723,6 +701,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 @@ -735,7 +716,7 @@ def check(request, q_id, attempt_num=None, questionpaper_id=None, previous_question=current_question ) for fname in assignment_filename: - fname._name = fname._name.replace(" ","_") + fname._name = fname._name.replace(" ", "_") assignment_files = AssignmentUpload.objects.filter( assignmentQuestion=current_question, assignmentFile__icontains=fname, user=user, @@ -774,34 +755,36 @@ def check(request, q_id, attempt_num=None, questionpaper_id=None, previous_question=current_question ) if current_question in paper.get_questions_answered()\ - and current_question.type not in ['code', 'upload']: + and current_question.type not in ['code', 'upload']: new_answer = paper.get_latest_answer(current_question.id) new_answer.answer = user_answer new_answer.correct = False else: new_answer = Answer( - question=current_question, answer=user_answer, - correct=False, error=json.dumps([]) - ) + question=current_question, answer=user_answer, + correct=False, error=json.dumps([]) + ) new_answer.save() uid = new_answer.id paper.answers.add(new_answer) # If we were not skipped, we were asked to check. For any non-mcq # questions, we obtain the results via XML-RPC with the code executed # safely in a separate process (the code_server.py) running as nobody. - json_data = current_question.consolidate_answer_data(user_answer, user) \ - if current_question.type == 'code' or \ + json_data = current_question.consolidate_answer_data( + user_answer, user) if current_question.type == 'code' or \ current_question.type == 'upload' else None result = paper.validate_answer( user_answer, current_question, json_data, uid ) if current_question.type in ['code', 'upload']: - if paper.time_left() <= 0 and not paper.question_paper.quiz.is_exercise: + if (paper.time_left() <= 0 and not + paper.question_paper.quiz.is_exercise): url = '{0}:{1}'.format(SERVER_HOST_NAME, SERVER_POOL_PORT) - result_details = get_result_from_code_server(url, uid, block=True) + result_details = get_result_from_code_server(url, uid, + block=True) result = json.loads(result_details.get('result')) - next_question, error_message, paper = _update_paper(request, uid, - result) + next_question, error_message, paper = _update_paper( + request, uid, result) return show_question(request, next_question, paper, error_message, course_id=course_id, module_id=module_id, @@ -809,7 +792,8 @@ def check(request, q_id, attempt_num=None, questionpaper_id=None, else: return JsonResponse(result) else: - next_question, error_message, paper = _update_paper(request, uid, result) + next_question, error_message, paper = _update_paper( + request, uid, result) return show_question(request, next_question, paper, error_message, course_id=course_id, module_id=module_id, previous_question=current_question) @@ -858,22 +842,25 @@ def _update_paper(request, uid, result): if result.get('success'): new_answer.marks = (current_question.points * result['weight'] / - current_question.get_maximum_test_case_weight()) \ + current_question.get_maximum_test_case_weight()) \ if current_question.partial_grading and \ - current_question.type == 'code' or current_question.type == 'upload' \ - else current_question.points + current_question.type == 'code' or \ + current_question.type == 'upload' else current_question.points new_answer.correct = result.get('success') error_message = None new_answer.error = json.dumps(result.get('error')) next_question = paper.add_completed_question(current_question.id) else: new_answer.marks = (current_question.points * result['weight'] / - current_question.get_maximum_test_case_weight()) \ + current_question.get_maximum_test_case_weight()) \ if current_question.partial_grading and \ - current_question.type == 'code' or current_question.type == 'upload' \ + current_question.type == 'code' or \ + current_question.type == 'upload' \ else 0 - error_message = result.get('error') if current_question.type == 'code' \ - or current_question.type == 'upload' else None + error_message = result.get('error') \ + if current_question.type == 'code' or \ + current_question.type == 'upload' \ + else None new_answer.error = json.dumps(result.get('error')) next_question = current_question if current_question.type == 'code' \ or current_question.type == 'upload' \ @@ -895,21 +882,20 @@ def quit(request, reason=None, attempt_num=None, questionpaper_id=None, course_id=course_id) context = {'paper': paper, 'message': reason, 'course_id': course_id, 'module_id': module_id} - return my_render_to_response('yaksh/quit.html', context, - context_instance=RequestContext(request)) + return my_render_to_response(request, 'yaksh/quit.html', context) @login_required @email_verified def complete(request, reason=None, attempt_num=None, questionpaper_id=None, course_id=None, module_id=None): - """Show a page to inform user that the quiz has been compeleted.""" + """Show a page to inform user that the quiz has been completed.""" user = request.user if questionpaper_id is None: message = reason or "An Unexpected Error occurred. Please contact your '\ 'instructor/administrator.'" context = {'message': message} - return my_render_to_response('yaksh/complete.html', context) + return my_render_to_response(request, 'yaksh/complete.html', context) else: q_paper = QuestionPaper.objects.get(id=questionpaper_id) paper = AnswerPaper.objects.get( @@ -929,14 +915,13 @@ def complete(request, reason=None, attempt_num=None, questionpaper_id=None, 'course_id': course_id, 'learning_unit': learning_unit} if is_moderator(user): context['user'] = "moderator" - return my_render_to_response('yaksh/complete.html', context) + return my_render_to_response(request, 'yaksh/complete.html', context) @login_required @email_verified def add_course(request, course_id=None): user = request.user - ci = RequestContext(request) if course_id: course = Course.objects.get(id=course_id) if not course.is_creator(user) and not course.is_teacher(user): @@ -955,12 +940,12 @@ def add_course(request, course_id=None): return my_redirect('/exam/manage/courses') else: return my_render_to_response( - 'yaksh/add_course.html', {'form': form}, context_instance=ci + request, 'yaksh/add_course.html', {'form': form} ) else: form = CourseForm(instance=course) return my_render_to_response( - 'yaksh/add_course.html', {'form': form}, context_instance=ci + request, 'yaksh/add_course.html', {'form': form} ) @@ -968,7 +953,6 @@ def add_course(request, course_id=None): @email_verified def enroll_request(request, course_id): user = request.user - ci = RequestContext(request) course = get_object_or_404(Course, pk=course_id) if not course.is_active_enrollment() and course.hidden: msg = ( @@ -988,7 +972,6 @@ def enroll_request(request, course_id): @email_verified def self_enroll(request, course_id): user = request.user - ci = RequestContext(request) course = get_object_or_404(Course, pk=course_id) if course.is_self_enroll(): was_rejected = False @@ -1003,7 +986,6 @@ def self_enroll(request, course_id): @email_verified def courses(request): user = request.user - ci = RequestContext(request) if not is_moderator(user): raise Http404('You are not allowed to view this page') courses = Course.objects.filter( @@ -1012,15 +994,13 @@ def courses(request): teachers=user, is_trial=False).order_by('-active', '-id') context = {'courses': courses, "allotted_courses": allotted_courses, "type": "courses"} - return my_render_to_response('yaksh/courses.html', context, - context_instance=ci) + return my_render_to_response(request, 'yaksh/courses.html', context) @login_required @email_verified def course_detail(request, course_id): user = request.user - ci = RequestContext(request) if not is_moderator(user): raise Http404('You are not allowed to view this page') @@ -1030,7 +1010,7 @@ def course_detail(request, course_id): raise Http404('This course does not belong to you') return my_render_to_response( - 'yaksh/course_detail.html', {'course': course}, context_instance=ci + request, 'yaksh/course_detail.html', {'course': course} ) @@ -1038,7 +1018,6 @@ def course_detail(request, course_id): @email_verified def enroll(request, course_id, user_id=None, was_rejected=False): user = request.user - ci = RequestContext(request) if not is_moderator(user): raise Http404('You are not allowed to view this page') @@ -1060,7 +1039,7 @@ def enroll(request, course_id, user_id=None, was_rejected=False): enroll_ids = [user_id] if not enroll_ids: return my_render_to_response( - 'yaksh/course_detail.html', {'course': course}, context_instance=ci + request, 'yaksh/course_detail.html', {'course': course} ) users = User.objects.filter(id__in=enroll_ids) course.enroll(was_rejected, *users) @@ -1071,7 +1050,6 @@ def enroll(request, course_id, user_id=None, was_rejected=False): @email_verified def send_mail(request, course_id, user_id=None): user = request.user - ci = RequestContext(request) if not is_moderator(user): raise Http404('You are not allowed to view this page') @@ -1095,16 +1073,13 @@ def send_mail(request, course_id, user_id=None): 'course': course, 'message': message, 'state': 'mail' } - return my_render_to_response( - 'yaksh/course_detail.html', context, context_instance=ci - ) + return my_render_to_response(request, 'yaksh/course_detail.html', context) @login_required @email_verified def reject(request, course_id, user_id=None, was_enrolled=False): user = request.user - ci = RequestContext(request) if not is_moderator(user): raise Http404('You are not allowed to view this page') @@ -1119,8 +1094,8 @@ def reject(request, course_id, user_id=None, was_enrolled=False): if not reject_ids: message = "Please select atleast one User" return my_render_to_response( - 'yaksh/course_detail.html', {'course': course, 'message': message}, - context_instance=ci + request, 'yaksh/course_detail.html', + {'course': course, 'message': message}, ) users = User.objects.filter(id__in=reject_ids) course.reject(was_enrolled, *users) @@ -1160,8 +1135,9 @@ def show_statistics(request, questionpaper_id, attempt_number=None, context = {'quiz': quiz, 'attempts': attempt_numbers, 'questionpaper_id': questionpaper_id, 'course_id': course_id} - return my_render_to_response('yaksh/statistics_question.html', context, - context_instance=RequestContext(request)) + return my_render_to_response( + request, 'yaksh/statistics_question.html', context + ) total_attempt = AnswerPaper.objects.get_count(questionpaper_id, attempt_number, course_id) @@ -1175,8 +1151,9 @@ def show_statistics(request, questionpaper_id, attempt_number=None, 'questionpaper_id': questionpaper_id, 'attempts': attempt_numbers, 'total': total_attempt, 'course_id': course_id} - return my_render_to_response('yaksh/statistics_question.html', context, - context_instance=RequestContext(request)) + return my_render_to_response( + request, 'yaksh/statistics_question.html', context + ) @login_required @@ -1185,7 +1162,6 @@ def monitor(request, quiz_id=None, course_id=None): """Monitor the progress of the papers taken so far.""" user = request.user - ci = RequestContext(request) if not user.is_authenticated() or not is_moderator(user): raise Http404('You are not allowed to view this page!') @@ -1198,9 +1174,7 @@ def monitor(request, quiz_id=None, course_id=None): "papers": [], "course_details": course_details, "msg": "Monitor" } - return my_render_to_response( - 'yaksh/monitor.html', context, context_instance=ci - ) + return my_render_to_response(request, 'yaksh/monitor.html', context) # quiz_id is not None. try: quiz = get_object_or_404(Quiz, id=quiz_id) @@ -1246,8 +1220,7 @@ def monitor(request, quiz_id=None, course_id=None): "attempt_numbers": attempt_numbers, "course": course } - return my_render_to_response('yaksh/monitor.html', context, - context_instance=ci) + return my_render_to_response(request, 'yaksh/monitor.html', context) @csrf_exempt @@ -1270,7 +1243,7 @@ def ajax_questions_filter(request): questions = Question.objects.filter(**filter_dict) return my_render_to_response( - 'yaksh/ajax_question_filter.html', {'questions': questions} + request, 'yaksh/ajax_question_filter.html', {'questions': questions} ) @@ -1340,14 +1313,12 @@ def design_questionpaper(request, quiz_id, questionpaper_id=None, qpaper_form = QuestionPaperForm(instance=question_paper) if request.method == 'POST': - filter_form = QuestionFilterForm(request.POST, user=user) qpaper_form = QuestionPaperForm(request.POST, instance=question_paper) question_type = request.POST.get('question_type', None) marks = request.POST.get('marks', None) state = request.POST.get('is_active', None) tags = request.POST.get('question_tags', None) - if 'add-fixed' in request.POST: question_ids = request.POST.get('checked_ques', None) if question_ids: @@ -1386,8 +1357,8 @@ def design_questionpaper(request, quiz_id, questionpaper_id=None, random_set = QuestionSet(marks=marks, num_questions=num_of_questions) random_set.save() - for question in Question.objects.filter(id__in=question_ids): - random_set.questions.add(question) + random_ques = Question.objects.filter(id__in=question_ids) + random_set.questions.add(*random_ques) question_paper.random_questions.add(random_set) if 'remove-random' in request.POST: @@ -1423,9 +1394,7 @@ def design_questionpaper(request, quiz_id, questionpaper_id=None, 'all_tags': all_tags } return my_render_to_response( - 'yaksh/design_questionpaper.html', - context, - context_instance=RequestContext(request) + request, 'yaksh/design_questionpaper.html', context ) @@ -1435,7 +1404,6 @@ def show_all_questions(request): """Show a list of all the questions currently in the database.""" user = request.user - ci = RequestContext(request) context = {} if not is_moderator(user): raise Http404("You are not allowed to view this page !") @@ -1489,8 +1457,8 @@ def show_all_questions(request): response.write(zip_file.read()) return response else: - context['msg'] = """Please select atleast one - question to download""" + context['msg'] = ("Please select atleast" + + "one question to download") if request.POST.get('test') == 'test': question_ids = request.POST.getlist("question") @@ -1509,8 +1477,7 @@ def show_all_questions(request): search_result = _get_questions_from_tags(question_tags, user) context['questions'] = search_result - return my_render_to_response('yaksh/showquestions.html', context, - context_instance=ci) + return my_render_to_response(request, 'yaksh/showquestions.html', context) @login_required @@ -1524,16 +1491,16 @@ def user_data(request, user_id, questionpaper_id=None, course_id=None): data = AnswerPaper.objects.get_user_data(user, questionpaper_id, course_id) context = {'data': data, 'course_id': course_id} - return my_render_to_response('yaksh/user_data.html', context, - context_instance=RequestContext(request)) + return my_render_to_response(request, 'yaksh/user_data.html', context) def _expand_questions(questions, field_list): i = field_list.index('questions') field_list.remove('questions') for question in questions: - field_list.insert(i, '{0}-{1}'.format(question.summary, question.points)) - return field_list + field_list.insert( + i, '{0}-{1}'.format(question.summary, question.points)) + return field_list @login_required @@ -1544,7 +1511,8 @@ def download_quiz_csv(request, course_id, quiz_id): raise Http404('You are not allowed to view this page!') course = get_object_or_404(Course, id=course_id) quiz = get_object_or_404(Quiz, id=quiz_id) - if not course.is_creator(current_user) and not course.is_teacher(current_user): + if not course.is_creator(current_user) and \ + not course.is_teacher(current_user): raise Http404('The quiz does not belong to your course') users = course.get_enrolled().order_by('first_name') if not users: @@ -1556,19 +1524,22 @@ def download_quiz_csv(request, course_id, quiz_id): question_paper.id, course.id).last() if request.method == 'POST': csv_fields = request.POST.getlist('csv_fields') - attempt_number = request.POST.get('attempt_number', last_attempt_number) + attempt_number = request.POST.get('attempt_number', + last_attempt_number) if not csv_fields: csv_fields = CSV_FIELDS if not attempt_number: attempt_number = last_attempt_number questions = question_paper.get_question_bank() - answerpapers = AnswerPaper.objects.filter(question_paper=question_paper, - attempt_number=attempt_number, course_id=course_id) + answerpapers = AnswerPaper.objects.filter( + question_paper=question_paper, + attempt_number=attempt_number, course_id=course_id) if not answerpapers: return monitor(request, quiz_id, course_id) response = HttpResponse(content_type='text/csv') - response['Content-Disposition'] = 'attachment; filename="{0}-{1}-attempt{2}.csv"'.format( + response['Content-Disposition'] = \ + 'attachment; filename="{0}-{1}-attempt{2}.csv"'.format( course.name.replace('.', ''), quiz.description.replace('.', ''), attempt_number) writer = csv.writer(response) @@ -1576,21 +1547,24 @@ def download_quiz_csv(request, course_id, quiz_id): csv_fields = _expand_questions(questions, csv_fields) writer.writerow(csv_fields) - csv_fields_values = {'name': 'user.get_full_name().title()', + csv_fields_values = { + 'name': 'user.get_full_name().title()', 'roll_number': 'user.profile.roll_number', 'institute': 'user.profile.institute', 'department': 'user.profile.department', 'username': 'user.username', 'marks_obtained': 'answerpaper.marks_obtained', 'out_of': 'question_paper.total_marks', - 'percentage': 'answerpaper.percent', 'status': 'answerpaper.status'} + 'percentage': 'answerpaper.percent', + 'status': 'answerpaper.status'} questions_scores = {} for question in questions: questions_scores['{0}-{1}'.format(question.summary, question.points)] \ - = 'answerpaper.get_per_question_score({0})'.format(question.id) + = 'answerpaper.get_per_question_score({0})'.format(question.id) csv_fields_values.update(questions_scores) - users = users.exclude(id=course.creator.id).exclude(id__in=course.teachers.all()) + users = users.exclude(id=course.creator.id).exclude( + id__in=course.teachers.all()) for user in users: row = [] answerpaper = None @@ -1614,7 +1588,6 @@ def grade_user(request, quiz_id=None, user_id=None, attempt_number=None, and update all their marks and also give comments for each paper. """ current_user = request.user - ci = RequestContext(request) if not current_user.is_authenticated() or not is_moderator(current_user): raise Http404('You are not allowed to view this page!') course_details = Course.objects.filter(Q(creator=current_user) | @@ -1674,7 +1647,8 @@ def grade_user(request, quiz_id=None, user_id=None, attempt_number=None, if request.method == "POST": papers = data['papers'] for paper in papers: - for question, answers in six.iteritems(paper.get_question_answers()): + for question, answers in six.iteritems( + paper.get_question_answers()): marks = float(request.POST.get('q%d_marks' % question.id, 0)) answer = answers[-1]['answer'] answer.set_marks(marks) @@ -1684,9 +1658,11 @@ def grade_user(request, quiz_id=None, user_id=None, attempt_number=None, 'comments_%d' % paper.question_paper.id, 'No comments') paper.save() - return my_render_to_response( - 'yaksh/grade_user.html', context, context_instance=ci - ) + course_status = CourseStatus.objects.filter(course=course, user=user) + if course_status.exists(): + course_status.first().set_grade() + + return my_render_to_response(request, 'yaksh/grade_user.html', context) @login_required @@ -1695,13 +1671,12 @@ def grade_user(request, quiz_id=None, user_id=None, attempt_number=None, def view_profile(request): """ view moderators and users profile """ user = request.user - ci = RequestContext(request) if is_moderator(user): template = 'manage.html' else: template = 'user.html' context = {'template': template, 'user': user} - return my_render_to_response('yaksh/view_profile.html', context) + return my_render_to_response(request, 'yaksh/view_profile.html', context) @login_required @@ -1710,7 +1685,6 @@ def edit_profile(request): """ edit profile details facility for moderator and students """ user = request.user - ci = RequestContext(request) if is_moderator(user): template = 'manage.html' else: @@ -1730,19 +1704,17 @@ def edit_profile(request): form_data.user.last_name = request.POST['last_name'] form_data.user.save() form_data.save() - return my_render_to_response( - 'yaksh/profile_updated.html', context_instance=ci - ) + return my_render_to_response(request, 'yaksh/profile_updated.html') else: context['form'] = form return my_render_to_response( - 'yaksh/editprofile.html', context, context_instance=ci + request, 'yaksh/editprofile.html', context ) else: form = ProfileForm(user=user, instance=profile) context['form'] = form return my_render_to_response( - 'yaksh/editprofile.html', context, context_instance=ci + request, 'yaksh/editprofile.html', context ) @@ -1751,7 +1723,6 @@ def edit_profile(request): def search_teacher(request, course_id): """ search teachers for the course """ user = request.user - ci = RequestContext(request) if not is_moderator(user): raise Http404('You are not allowed to view this page!') @@ -1778,9 +1749,7 @@ def search_teacher(request, course_id): ) context['success'] = True context['teachers'] = teachers - return my_render_to_response( - 'yaksh/addteacher.html', context, context_instance=ci - ) + return my_render_to_response(request, 'yaksh/addteacher.html', context) @login_required @@ -1789,14 +1758,13 @@ def add_teacher(request, course_id): """ add teachers to the course """ user = request.user - ci = RequestContext(request) if not is_moderator(user): raise Http404('You are not allowed to view this page!') context = {} course = get_object_or_404(Course, pk=course_id) - if user == course.creator or user in course.teachers.all(): + if course.is_creator(user) or course.is_teacher(user): context['course'] = course else: raise Http404('You are not allowed to view this page!') @@ -1808,9 +1776,7 @@ def add_teacher(request, course_id): course.add_teachers(*teachers) context['status'] = True context['teachers_added'] = teachers - return my_render_to_response( - 'yaksh/addteacher.html', context, context_instance=ci - ) + return my_render_to_response(request, 'yaksh/addteacher.html', context) @login_required @@ -1820,8 +1786,8 @@ def remove_teachers(request, course_id): user = request.user course = get_object_or_404(Course, pk=course_id) - if not is_moderator(user) and (user != course.creator and user - not in course.teachers.all()): + if not is_moderator(user) and (not course.is_creator(user) and + course.is_teacher(user)): raise Http404('You are not allowed to view this page!') if request.method == "POST": @@ -1838,9 +1804,10 @@ def test_mode(user, godmode=False, questions_list=None, quiz_id=None, if questions_list is not None: trial_course = Course.objects.create_trial_course(user) trial_quiz = Quiz.objects.create_trial_quiz(user) - trial_questionpaper = QuestionPaper.objects.create_trial_paper_to_test_questions( - trial_quiz, questions_list - ) + trial_questionpaper = QuestionPaper.objects. \ + create_trial_paper_to_test_questions( + trial_quiz, questions_list + ) trial_unit, created = LearningUnit.objects.get_or_create( order=1, type="quiz", quiz=trial_quiz, check_prerequisite=False) @@ -1853,9 +1820,10 @@ def test_mode(user, godmode=False, questions_list=None, quiz_id=None, trial_quiz, trial_course, module = Quiz.objects.create_trial_from_quiz( quiz_id, user, godmode, course_id ) - trial_questionpaper = QuestionPaper.objects.create_trial_paper_to_test_quiz( - trial_quiz, quiz_id - ) + trial_questionpaper = QuestionPaper.objects. \ + create_trial_paper_to_test_quiz( + trial_quiz, quiz_id + ) return trial_questionpaper, trial_course, module @@ -1890,7 +1858,9 @@ def view_answerpaper(request, questionpaper_id, course_id): ).exists() context = {'data': data, 'quiz': quiz, "has_user_assignment": has_user_assignment} - return my_render_to_response('yaksh/view_answerpaper.html', context) + return my_render_to_response( + request, 'yaksh/view_answerpaper.html', context + ) else: return my_redirect('/exam/quizzes/') @@ -1900,9 +1870,8 @@ def view_answerpaper(request, questionpaper_id, course_id): def create_demo_course(request): """ creates a demo course for user """ user = request.user - ci = RequestContext(request) if not is_moderator(user): - raise("You are not allowed to view this page") + raise Http404("You are not allowed to view this page") demo_course = Course() success = demo_course.create_demo(user) if success: @@ -1919,11 +1888,12 @@ def grader(request, extra_context=None): if not is_moderator(user): raise Http404('You are not allowed to view this page!') courses = Course.objects.filter(is_trial=False) - user_courses = list(courses.filter(creator=user)) + list(courses.filter(teachers=user)) + user_courses = list(courses.filter(creator=user)) + \ + list(courses.filter(teachers=user)) context = {'courses': user_courses} if extra_context: context.update(extra_context) - return my_render_to_response('yaksh/regrade.html', context) + return my_render_to_response(request, 'yaksh/regrade.html', context) @login_required @@ -1932,21 +1902,36 @@ def regrade(request, course_id, question_id=None, answerpaper_id=None, questionpaper_id=None): user = request.user course = get_object_or_404(Course, pk=course_id) - if not is_moderator(user) or (user != course.creator and user not in course.teachers.all()): + if not is_moderator(user) or (course.is_creator(user) and + course.is_teacher(user)): raise Http404('You are not allowed to view this page!') details = [] if answerpaper_id is not None and question_id is 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) + 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}) @@ -1973,8 +1958,8 @@ def download_course_csv(request, course_id): total_course_marks = 0.0 user_course_marks = 0.0 for quiz in quizzes: - quiz_best_marks = AnswerPaper.objects.get_user_best_of_attempts_marks\ - (quiz, student["id"], course_id) + quiz_best_marks = AnswerPaper.objects. \ + get_user_best_of_attempts_marks(quiz, student["id"], course_id) user_course_marks += quiz_best_marks total_course_marks += quiz.questionpaper_set.values_list( "total_marks", flat=True)[0] @@ -1985,7 +1970,7 @@ def download_course_csv(request, course_id): response['Content-Disposition'] = 'attachment; filename="{0}.csv"'.format( (course.name).lower().replace('.', '')) header = ['first_name', 'last_name', "roll_number", "email", "institute"]\ - + [quiz.description for quiz in quizzes] + ['total_scored', 'out_of'] + + [quiz.description for quiz in quizzes] + ['total_scored', 'out_of'] writer = csv.DictWriter(response, fieldnames=header, extrasaction='ignore') writer.writeheader() for student in students: @@ -1994,14 +1979,13 @@ def download_course_csv(request, course_id): def activate_user(request, key): - ci = RequestContext(request) profile = get_object_or_404(Profile, activation_key=key) context = {} context['success'] = False if profile.is_email_verified: context['activation_msg'] = "Your account is already verified" return my_render_to_response( - 'yaksh/activation_status.html', context, context_instance=ci + request, 'yaksh/activation_status.html', context ) if timezone.now() > profile.key_expiry_time: @@ -2015,12 +1999,11 @@ def activate_user(request, key): profile.save() context['msg'] = "Your account is activated" return my_render_to_response( - 'yaksh/activation_status.html', context, context_instance=ci + request, 'yaksh/activation_status.html', context ) def new_activation(request, email=None): - ci = RequestContext(request) context = {} if request.method == "POST": email = request.POST.get('email') @@ -2031,15 +2014,15 @@ def new_activation(request, email=None): context['email_err_msg'] = "Multiple entries found for this email"\ "Please change your email" return my_render_to_response( - 'yaksh/activation_status.html', context, context_instance=ci + request, 'yaksh/activation_status.html', context ) except ObjectDoesNotExist: context['success'] = False context['msg'] = "Your account is not verified. \ Please verify your account" - return render_to_response( - 'yaksh/activation_status.html', context, context_instance=ci - ) + return my_render_to_response( + request, 'yaksh/activation_status.html', context + ) if not user.profile.is_email_verified: user.profile.activation_key = generate_activation_key(user.username) @@ -2058,13 +2041,12 @@ def new_activation(request, email=None): context['activation_msg'] = "Your account is already verified" return my_render_to_response( - 'yaksh/activation_status.html', context, context_instance=ci + request, 'yaksh/activation_status.html', context ) def update_email(request): context = {} - ci = RequestContext(request) if request.method == "POST": email = request.POST.get('email') username = request.POST.get('username') @@ -2075,7 +2057,7 @@ def update_email(request): else: context['email_err_msg'] = "Please Update your email" return my_render_to_response( - 'yaksh/activation_status.html', context, context_instance=ci + request, 'yaksh/activation_status.html', context ) @@ -2114,7 +2096,6 @@ def download_assignment_file(request, quiz_id, question_id=None, user_id=None): @email_verified def upload_users(request, course_id): user = request.user - ci = RequestContext(request) course = get_object_or_404(Course, pk=course_id) context = {'course': course} @@ -2124,32 +2105,38 @@ def upload_users(request, course_id): if request.method == 'POST': if 'csv_file' not in request.FILES: context['message'] = "Please upload a CSV file." - return my_render_to_response('yaksh/course_detail.html', context, - context_instance=ci) + return my_render_to_response( + request, 'yaksh/course_detail.html', context + ) csv_file = request.FILES['csv_file'] is_csv_file, dialect = is_csv(csv_file) if not is_csv_file: context['message'] = "The file uploaded is not a CSV file." - return my_render_to_response('yaksh/course_detail.html', context, - context_instance=ci) + return my_render_to_response( + request, 'yaksh/course_detail.html', context + ) required_fields = ['firstname', 'lastname', 'email'] try: - reader = csv.DictReader(csv_file.read().decode('utf-8').splitlines(), - dialect=dialect) + reader = csv.DictReader( + csv_file.read().decode('utf-8').splitlines(), + dialect=dialect) except TypeError: context['message'] = "Bad CSV file" - return my_render_to_response('yaksh/course_detail.html', context, - context_instance=ci) - stripped_fieldnames = [field.strip().lower() for field in reader.fieldnames] + return my_render_to_response( + request, 'yaksh/course_detail.html', context + ) + stripped_fieldnames = [ + field.strip().lower() for field in reader.fieldnames] for field in required_fields: if field not in stripped_fieldnames: - context['message'] = "The CSV file does not contain the required headers" - return my_render_to_response('yaksh/course_detail.html', context, - context_instance=ci) + context['message'] = "The CSV file does not contain the"\ + " required headers" + return my_render_to_response( + request, 'yaksh/course_detail.html', context + ) reader.fieldnames = stripped_fieldnames context['upload_details'] = _read_user_csv(reader, course) - return my_render_to_response('yaksh/course_detail.html', context, - context_instance=ci) + return my_render_to_response(request, 'yaksh/course_detail.html', context) def _read_user_csv(reader, course): @@ -2181,9 +2168,11 @@ def _read_user_csv(reader, course): continue user_defaults = {'email': email, 'first_name': first_name, 'last_name': last_name} - user, created = _create_or_update_user(username, password, user_defaults) + user, created = _create_or_update_user(username, password, + user_defaults) profile_defaults = {'institute': institute, 'roll_number': roll_no, - 'department': department, 'is_email_verified': True} + 'department': department, + 'is_email_verified': True} _create_or_update_profile(user, profile_defaults) if created: state = "Added" @@ -2219,8 +2208,8 @@ def _get_csv_values(row, fields): username = row['username'].strip() if 'remove' in fields: remove = row['remove'] - return (username, email, first_name, last_name, password, roll_no, institute, - department, remove) + return (username, email, first_name, last_name, password, + roll_no, institute, department, remove) def _remove_from_course(user, course): @@ -2272,10 +2261,15 @@ def duplicate_course(request, course_id): raise Http404('You are not allowed to view this page!') if course.is_teacher(user) or course.is_creator(user): + # Create new entries of modules, lessons/quizzes + # from current course to copied course course.create_duplicate_course(user) else: - msg = 'You do not have permissions to clone this course, please contact your '\ - 'instructor/administrator.' + msg = dedent( + '''\ + You do not have permissions to clone {0} course, please contact + your instructor/administrator.'''.format(course.name) + ) return complete(request, msg, attempt_num=None, questionpaper_id=None) return my_redirect('/exam/manage/courses/') @@ -2302,7 +2296,6 @@ def download_yaml_template(request): @email_verified def edit_lesson(request, lesson_id=None, course_id=None): user = request.user - ci = RequestContext(request) if not is_moderator(user): raise Http404('You are not allowed to view this page!') if lesson_id: @@ -2355,9 +2348,7 @@ def edit_lesson(request, lesson_id=None, course_id=None): context['lesson_file_form'] = lesson_files_form context['lesson_files'] = lesson_files context['course_id'] = course_id - return my_render_to_response( - 'yaksh/add_lesson.html', context, context_instance=ci - ) + return my_render_to_response(request, 'yaksh/add_lesson.html', context) @login_required @@ -2398,14 +2389,13 @@ def show_lesson(request, lesson_id, module_id, course_id): 'course': course, 'state': "lesson", "all_modules": all_modules, 'learning_units': learning_units, "current_unit": learn_unit, 'learning_module': learn_module} - return my_render_to_response('yaksh/show_video.html', context) + return my_render_to_response(request, 'yaksh/show_video.html', context) @login_required @email_verified def design_module(request, module_id, course_id=None): user = request.user - ci = RequestContext(request) if not is_moderator(user): raise Http404('You are not allowed to view this page!') context = {} @@ -2471,15 +2461,13 @@ def design_module(request, module_id, course_id=None): context['status'] = 'design' context['module_id'] = module_id context['course_id'] = course_id - return my_render_to_response('yaksh/add_module.html', context, - context_instance=ci) + return my_render_to_response(request, 'yaksh/add_module.html', context) @login_required @email_verified def add_module(request, module_id=None, course_id=None): user = request.user - ci = RequestContext(request) if not is_moderator(user): raise Http404('You are not allowed to view this page!') redirect_url = "/exam/manage/courses/all_learning_module/" @@ -2512,8 +2500,7 @@ def add_module(request, module_id=None, course_id=None): context['module_form'] = module_form context['course_id'] = course_id context['status'] = "add" - return my_render_to_response("yaksh/add_module.html", - context, context_instance=ci) + return my_render_to_response(request, "yaksh/add_module.html", context) @login_required @@ -2524,7 +2511,7 @@ def show_all_quizzes(request): raise Http404('You are not allowed to view this page!') quizzes = Quiz.objects.filter(creator=user, is_trial=False) context = {"quizzes": quizzes, "type": "quiz"} - return my_render_to_response('yaksh/courses.html', context) + return my_render_to_response(request, 'yaksh/courses.html', context) @login_required @@ -2535,7 +2522,7 @@ def show_all_lessons(request): raise Http404('You are not allowed to view this page!') lessons = Lesson.objects.filter(creator=user) context = {"lessons": lessons, "type": "lesson"} - return my_render_to_response('yaksh/courses.html', context) + return my_render_to_response(request, 'yaksh/courses.html', context) @login_required @@ -2547,7 +2534,7 @@ def show_all_modules(request): learning_modules = LearningModule.objects.filter( creator=user, is_trial=False) context = {"learning_modules": learning_modules, "type": "learning_module"} - return my_render_to_response('yaksh/courses.html', context) + return my_render_to_response(request, 'yaksh/courses.html', context) @login_required @@ -2588,20 +2575,17 @@ def get_next_unit(request, course_id, module_id, current_unit_id=None, else: next_unit = learning_module.get_next_unit(current_learning_unit.id) - course_status = CourseStatus.objects.filter( + course_status, created = CourseStatus.objects.get_or_create( user=user, course_id=course_id, ) - if not course_status.exists(): - course_status = CourseStatus.objects.create( - user=user, course_id=course_id - ) - else: - course_status = course_status.first() # Add learning unit to completed units list if not first_unit: course_status.completed_units.add(current_learning_unit.id) + # Update course completion percentage + _update_course_percent(course, user) + # if last unit of current module go to next module is_last_unit = course.is_last_unit(learning_module, current_learning_unit.id) @@ -2622,7 +2606,6 @@ def get_next_unit(request, course_id, module_id, current_unit_id=None, @email_verified def design_course(request, course_id): user = request.user - ci = RequestContext(request) if not is_moderator(user): raise Http404('You are not allowed to view this page!') course = Course.objects.get(id=course_id) @@ -2676,8 +2659,9 @@ def design_course(request, course_id): context['added_learning_modules'] = added_learning_modules context['learning_modules'] = learning_modules context['course_id'] = course_id - return my_render_to_response('yaksh/design_course_session.html', context, - context_instance=ci) + return my_render_to_response( + request, 'yaksh/design_course_session.html', context + ) @login_required @@ -2712,7 +2696,7 @@ def view_module(request, module_id, course_id, msg=None): context['course'] = course context['state'] = "module" context['msg'] = msg - return my_render_to_response('yaksh/show_video.html', context) + return my_render_to_response(request, 'yaksh/show_video.html', context) @login_required @@ -2728,9 +2712,19 @@ def course_modules(request, course_id, msg=None): msg = "{0} is either expired or not active".format(course.name) return quizlist_user(request, msg=msg) learning_modules = course.get_learning_modules() - context = {"course": course, "learning_modules": learning_modules, - "user": user, "msg": msg} - return my_render_to_response('yaksh/course_modules.html', context) + context = {"course": course, "user": user, "msg": msg} + course_status = CourseStatus.objects.filter(course=course, user=user) + context['course_percentage'] = course.get_completion_percent(user) + context['modules'] = [ + (module, module.get_module_complete_percent(course, user)) + for module in learning_modules + ] + if course_status.exists(): + course_status = course_status.first() + if not course_status.grade: + course_status.set_grade() + context['grade'] = course_status.get_grade() + return my_render_to_response(request, 'yaksh/course_modules.html', context) @login_required @@ -2743,24 +2737,91 @@ def course_status(request, course_id): if not course.is_creator(user) and not course.is_teacher(user): raise Http404('This course does not belong to you') students = course.get_only_students() + stud_details = [(student, course.get_grade(student), + course.get_completion_percent(student), + course.get_current_unit(student)) for student in students] context = { - 'course': course, 'students': students, - 'state': 'course_status', 'modules': course.get_learning_modules() + 'course': course, 'student_details': stud_details, + 'state': 'course_status' } - return my_render_to_response('yaksh/course_detail.html', context) + return my_render_to_response(request, 'yaksh/course_detail.html', context) def _update_unit_status(course_id, user, unit): """ Update course status with current unit """ - course_status = CourseStatus.objects.filter( + course_status, created = CourseStatus.objects.get_or_create( user=user, course_id=course_id, ) - if not course_status.exists(): - course_status = CourseStatus.objects.create( - user=user, course_id=course_id - ) - else: - course_status = course_status.first() # make next available unit as current unit - course_status.current_unit = unit + course_status.set_current_unit(unit) + + +def _update_course_percent(course, user): + course_status, created = CourseStatus.objects.get_or_create( + user=user, course=course, + ) + # Update course completion percent + modules = course.get_learning_modules() + course_status.percent_completed = course.percent_completed(user, modules) course_status.save() + + +@login_required +@email_verified +def preview_questionpaper(request, questionpaper_id): + user = request.user + if not is_moderator(user): + raise Http404('You are not allowed to view this page!') + paper = QuestionPaper.objects.get(id=questionpaper_id) + if not paper.quiz.creator == user: + raise Http404('This questionpaper does not belong to you') + context = { + 'questions': paper._get_questions_for_answerpaper(), + 'paper': paper, + } + + return my_render_to_response( + request, 'yaksh/preview_questionpaper.html', context + ) + + +@login_required +@email_verified +def get_user_data(request, course_id, student_id): + user = request.user + data = {} + response_kwargs = {} + response_kwargs['content_type'] = 'application/json' + course = Course.objects.get(id=course_id) + if not is_moderator(user): + data['msg'] = 'You are not a moderator' + data['status'] = False + elif not course.is_creator(user) and not course.is_teacher(user): + msg = dedent( + """\ + You are neither course creator nor course teacher for {0} + """.format(course.name) + ) + data['msg'] = msg + data['status'] = False + else: + student = User.objects.get(id=student_id) + data['status'] = True + modules = course.get_learning_modules() + module_percent = [ + (module, module.get_module_complete_percent(course, student)) + for module in modules + ] + data['modules'] = module_percent + _update_course_percent(course, student) + data['course_percentage'] = course.get_completion_percent(student) + data['student'] = student + template_path = os.path.join( + os.path.dirname(__file__), "templates", "yaksh", "user_status.html" + ) + with open(template_path) as f: + template_data = f.read() + template = Template(template_data) + context = Context(data) + data = template.render(context) + return HttpResponse(json.dumps({"user_data": data}), **response_kwargs) |