diff options
-rw-r--r-- | .travis.yml | 1 | ||||
-rw-r--r-- | grades/__init__.py | 0 | ||||
-rw-r--r-- | grades/admin.py | 9 | ||||
-rw-r--r-- | grades/apps.py | 5 | ||||
-rw-r--r-- | grades/forms.py | 8 | ||||
-rw-r--r-- | grades/migrations/0001_initial.py | 43 | ||||
-rw-r--r-- | grades/migrations/__init__.py | 0 | ||||
-rw-r--r-- | grades/migrations/default_grading_system.py | 41 | ||||
-rw-r--r-- | grades/models.py | 46 | ||||
-rw-r--r-- | grades/templates/add_grades.html | 35 | ||||
-rw-r--r-- | grades/templates/grading_systems.html | 74 | ||||
-rw-r--r-- | grades/tests/__init__.py | 0 | ||||
-rw-r--r-- | grades/tests/test_models.py | 28 | ||||
-rw-r--r-- | grades/tests/test_views.py | 105 | ||||
-rw-r--r-- | grades/urls.py | 10 | ||||
-rw-r--r-- | grades/views.py | 45 | ||||
-rw-r--r-- | online_test/settings.py | 1 | ||||
-rw-r--r-- | online_test/urls.py | 1 | ||||
-rw-r--r-- | yaksh/forms.py | 2 | ||||
-rw-r--r-- | yaksh/models.py | 54 | ||||
-rw-r--r-- | yaksh/templates/manage.html | 2 | ||||
-rw-r--r-- | yaksh/templates/yaksh/course_detail.html | 6 | ||||
-rw-r--r-- | yaksh/templates/yaksh/course_modules.html | 1 | ||||
-rw-r--r-- | yaksh/templates/yaksh/courses.html | 4 | ||||
-rw-r--r-- | yaksh/templatetags/custom_filters.py | 7 | ||||
-rw-r--r-- | yaksh/test_models.py | 98 | ||||
-rw-r--r-- | yaksh/views.py | 23 |
27 files changed, 643 insertions, 6 deletions
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/grades/__init__.py b/grades/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/grades/__init__.py diff --git a/grades/admin.py b/grades/admin.py new file mode 100644 index 0000000..548791e --- /dev/null +++ b/grades/admin.py @@ -0,0 +1,9 @@ +from django.contrib import admin +from grades.models import GradingSystem, GradeRange + + +class GradingSystemAdmin(admin.ModelAdmin): + readonly_fields = ('creator',) + +admin.site.register(GradingSystem, GradingSystemAdmin) +admin.site.register(GradeRange) diff --git a/grades/apps.py b/grades/apps.py new file mode 100644 index 0000000..6d0985e --- /dev/null +++ b/grades/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class GradesConfig(AppConfig): + name = 'grades' diff --git a/grades/forms.py b/grades/forms.py new file mode 100644 index 0000000..130659d --- /dev/null +++ b/grades/forms.py @@ -0,0 +1,8 @@ +from grades.models import GradingSystem +from django import forms + + +class GradingSystemForm(forms.ModelForm): + class Meta: + model = GradingSystem + fields = ['name', 'description'] diff --git a/grades/migrations/0001_initial.py b/grades/migrations/0001_initial.py new file mode 100644 index 0000000..04a3006 --- /dev/null +++ b/grades/migrations/0001_initial.py @@ -0,0 +1,43 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.5 on 2018-02-12 11:12 +from __future__ import unicode_literals + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='GradeRange', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('lower_limit', models.FloatField()), + ('upper_limit', models.FloatField()), + ('grade', models.CharField(max_length=10)), + ('description', models.CharField(blank=True, max_length=127, null=True)), + ], + ), + migrations.CreateModel( + name='GradingSystem', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=255, unique=True)), + ('description', models.TextField(default='About the grading system!')), + ('creator', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + ), + migrations.AddField( + model_name='graderange', + name='system', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='grades.GradingSystem'), + ), + ] diff --git a/grades/migrations/__init__.py b/grades/migrations/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/grades/migrations/__init__.py diff --git a/grades/migrations/default_grading_system.py b/grades/migrations/default_grading_system.py new file mode 100644 index 0000000..85390d6 --- /dev/null +++ b/grades/migrations/default_grading_system.py @@ -0,0 +1,41 @@ +from django.db import migrations + + +def create_default_system(apps, schema_editor): + GradingSystem = apps.get_model('grades', 'GradingSystem') + GradeRange = apps.get_model('grades', 'GradeRange') + db = schema_editor.connection.alias + + default_system = GradingSystem.objects.using(db).create(name='default') + + graderanges_objects = [ + GradeRange(system=default_system, lower_limit=0, upper_limit=40, + grade='F', description='Fail'), + GradeRange(system=default_system, lower_limit=40, upper_limit=55, + grade='P', description='Pass'), + GradeRange(system=default_system, lower_limit=55, upper_limit=60, + grade='C', description='Average'), + GradeRange(system=default_system, lower_limit=60, upper_limit=75, + grade='B', description='Satisfactory'), + GradeRange(system=default_system, lower_limit=75, upper_limit=90, + grade='A', description='Good'), + GradeRange(system=default_system, lower_limit=90, upper_limit=101, + grade='A+', description='Excellent') + ] + GradeRange.objects.using(db).bulk_create(graderanges_objects) + + +def delete_default_system(apps, schema_editor): + GradingSystem = apps.get_model('grades', 'GradingSystem') + GradeRange = apps.get_model('grades', 'GradeRange') + db = schema_editor.connection.alias + + default_system = GradingSystem.objects.using(db).get(creator=None) + GradeRange.object.using(db).filter(system=default_system).delete() + default_system.delete() + + +class Migration(migrations.Migration): + dependencies = [('grades', '0001_initial'), ] + operations = [migrations.RunPython(create_default_system, + delete_default_system), ] diff --git a/grades/models.py b/grades/models.py new file mode 100644 index 0000000..fcea510 --- /dev/null +++ b/grades/models.py @@ -0,0 +1,46 @@ +from django.db import models +from django.contrib.auth.models import User + + +class GradingSystem(models.Model): + name = models.CharField(max_length=255, unique=True) + description = models.TextField(default='About the grading system!') + creator = models.ForeignKey(User, null=True, blank=True) + + def get_grade(self, marks): + ranges = self.graderange_set.all() + lower_limits = ranges.values_list('lower_limit', flat=True) + upper_limits = ranges.values_list('upper_limit', flat=True) + lower_limit = self._get_lower_limit(marks, lower_limits) + upper_limit = self._get_upper_limit(marks, upper_limits) + grade_range = ranges.filter(lower_limit=lower_limit, + upper_limit=upper_limit).first() + if grade_range: + return grade_range.grade + + def _get_upper_limit(self, marks, upper_limits): + greater_than = [upper_limit for upper_limit in upper_limits + if upper_limit > marks] + if greater_than: + return min(greater_than, key=lambda x: x-marks) + + def _get_lower_limit(self, marks, lower_limits): + less_than = [] + for lower_limit in lower_limits: + if lower_limit == marks: + return lower_limit + if lower_limit < marks: + less_than.append(lower_limit) + if less_than: + return max(less_than, key=lambda x: x-marks) + + def __str__(self): + return self.name.title() + + +class GradeRange(models.Model): + system = models.ForeignKey(GradingSystem) + lower_limit = models.FloatField() + upper_limit = models.FloatField() + grade = models.CharField(max_length=10) + description = models.CharField(max_length=127, null=True, blank=True) diff --git a/grades/templates/add_grades.html b/grades/templates/add_grades.html new file mode 100644 index 0000000..a3f52da --- /dev/null +++ b/grades/templates/add_grades.html @@ -0,0 +1,35 @@ +{% extends "manage.html" %} +{% block main %} +<html> +<a href="{% url 'grades:grading_systems'%}" class="btn btn-danger"> Back to Grading Systems </a> +<br><br> +<p><b>Note: For grade range lower limit is inclusive and upper limit is exclusive</b></p> +<br> +{% if not system_id %} + <form action="{% url 'grades:add_grade' %}" method="POST"> +{% else %} + <form action="{% url 'grades:edit_grade' system_id %}" method="POST"> +{% endif %} + {% csrf_token %} + <table class="table"> + {{ grade_form }} + </table> + {{ formset.management_form }} + <br> + <b><u>Grade Ranges</u></b> + <hr> + {% for form in formset %} + <div> + {{ form }} + </div> + <hr> + {% endfor %} + {% if not is_default %} + <input type="submit" id="add" name="add" value="Add" class="btn btn-info"> + <input type="submit" id="save" name="save" value="Save" class="btn btn-success"> + {% else %} + <p><b>Note: This is a default grading system. You cannot change this.</b></p> + {% endif %} +</form> +</html> +{% endblock %} diff --git a/grades/templates/grading_systems.html b/grades/templates/grading_systems.html new file mode 100644 index 0000000..3a71ebf --- /dev/null +++ b/grades/templates/grading_systems.html @@ -0,0 +1,74 @@ +{% extends "manage.html" %} +{% block main %} +<html> + <a href="{% url 'grades:add_grade' %}" class="btn btn-primary"> Add a Grading System </a> + <a href="{% url 'yaksh:courses' %}" class="btn btn-danger"> Back to Courses </a> + <br><br> + <b> Available Grading Systems: </b> + <table class="table"> + <tr> + <th>Grading System</th> + <th>Grading Ranges</th> + </tr> + <tr> + <td> + <a href="{% url 'grades:edit_grade' default_grading_system.id %}"> + {{ default_grading_system.name }}</a> (<b>Default Grading System</b>) + </td> + <td> + <table class="table"> + <tr> + <th>Lower Limit</th> + <th>Upper Limit</th> + <th>Grade</th> + <th>Description</th> + </tr> + {% for range in default_grading_system.graderange_set.all %} + <tr> + <td>{{range.lower_limit}}</td> + <td>{{range.upper_limit}}</td> + <td>{{range.grade}}</td> + {% if range.description %} + <td>{{range.description}}</td> + {% else %} + <td>------</td> + {% endif %} + </tr> + {% endfor %} + </table> + </td> + </tr> + {% if grading_systems %} + {% for system in grading_systems %} + <tr> + <td> + <a href="{% url 'grades:edit_grade' system.id %}">{{ system.name }}</a> + </td> + <td> + <table class="table"> + <tr> + <th>Lower Limit</th> + <th>Upper Limit</th> + <th>Grade</th> + <th>Description</th> + </tr> + {% for range in system.graderange_set.all %} + <tr> + <td>{{range.lower_limit}}</td> + <td>{{range.upper_limit}}</td> + <td>{{range.grade}}</td> + {% if range.description %} + <td>{{range.description}}</td> + {% else %} + <td>------</td> + {% endif %} + </tr> + {% endfor %} + </table> + </td> + </tr> + {% endfor %} + </table> + {% endif %} +</html> +{% endblock %} diff --git a/grades/tests/__init__.py b/grades/tests/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/grades/tests/__init__.py diff --git a/grades/tests/test_models.py b/grades/tests/test_models.py new file mode 100644 index 0000000..f8d5c5c --- /dev/null +++ b/grades/tests/test_models.py @@ -0,0 +1,28 @@ +from django.test import TestCase +from grades.models import GradingSystem, GradeRange + + +class GradingSystemTestCase(TestCase): + def setUp(self): + GradingSystem.objects.create(name='unusable') + + def test_get_grade(self): + # Given + grading_system = GradingSystem.objects.get(name='default') + expected_grades = {0: 'F', 31: 'F', 49: 'P', 55: 'C', 60: 'B', 80: 'A', + 95: 'A+', 100: 'A+', 100.5: 'A+', 101: None, + 109: None} + for marks in expected_grades.keys(): + # When + grade = grading_system.get_grade(marks) + # Then + self.assertEqual(expected_grades.get(marks), grade) + + def test_grade_system_unusable(self): + # Given + # System with out ranges + grading_system = GradingSystem.objects.get(name='unusable') + # When + grade = grading_system.get_grade(29) + # Then + self.assertIsNone(grade) diff --git a/grades/tests/test_views.py b/grades/tests/test_views.py new file mode 100644 index 0000000..c944f03 --- /dev/null +++ b/grades/tests/test_views.py @@ -0,0 +1,105 @@ +from django.test import TestCase, Client +from django.contrib.auth.models import User +from django.core.urlresolvers import reverse +from grades.models import GradingSystem + + +def setUpModule(): + user = User.objects.create_user(username='grades_user', + password='grades_user') + + +def tearDownModule(): + User.objects.all().delete() + + +class GradeViewTest(TestCase): + def setUp(self): + self.client = Client() + + def tearDown(self): + self.client.logout() + + def test_grade_view(self): + # Given + # URL redirection due to no login credentials + status_code = 302 + # When + response = self.client.get(reverse('grades:grading_systems')) + # Then + self.assertEqual(response.status_code, status_code) + + # Given + # successful login and grading systems views + self.client.login(username='grades_user', password='grades_user') + status_code = 200 + # When + response = self.client.get(reverse('grades:grading_systems')) + # Then + self.assertEqual(response.status_code, status_code) + self.assertTemplateUsed(response, 'grading_systems.html') + + +class AddGradingSystemTest(TestCase): + def setUp(self): + self.client = Client() + + def tearDown(self): + self.client.logout() + + def test_add_grades_view(self): + # Given + status_code = 302 + # When + response = self.client.get(reverse('grades:add_grade')) + # Then + self.assertEqual(response.status_code, status_code) + + # Given + status_code = 200 + self.client.login(username='grades_user', password='grades_user') + # When + response = self.client.get(reverse('grades:add_grade')) + # Then + self.assertEqual(response.status_code, status_code) + self.assertTemplateUsed(response, 'add_grades.html') + + def test_add_grades_post(self): + # Given + self.client.login(username='grades_user', password='grades_user') + data = {'name': ['new_sys'], 'description': ['About grading system!'], + 'graderange_set-MIN_NUM_FORMS': ['0'], + 'graderange_set-TOTAL_FORMS': ['0'], + 'graderange_set-MAX_NUM_FORMS': ['1000'], 'add': ['Add'], + 'graderange_set-INITIAL_FORMS': ['0']} + # When + response = self.client.post(reverse('grades:add_grade'), data) + # Then + grading_systems = GradingSystem.objects.filter(name='new_sys') + self.assertEqual(len(grading_systems), 1) + + # Given + grading_system = grading_systems.first() + # When + ranges = grading_system.graderange_set.all() + # Then + self.assertEqual(len(ranges), 0) + + # Given + data = {'graderange_set-0-upper_limit': ['40'], + 'graderange_set-0-description': ['Fail'], + 'graderange_set-0-lower_limit': ['0'], + 'graderange_set-0-system': [''], 'name': ['new_sys'], + 'graderange_set-MIN_NUM_FORMS': ['0'], + 'graderange_set-TOTAL_FORMS': ['1'], + 'graderange_set-MAX_NUM_FORMS': ['1000'], + 'graderange_set-0-id': [''], + 'description': ['About the grading system!'], + 'graderange_set-0-grade': ['F'], + 'graderange_set-INITIAL_FORMS': ['0'], 'save': ['Save']} + # When + response = self.client.post(reverse('grades:edit_grade', + kwargs={'system_id': 2}), data) + # Then + ranges = grading_system.graderange_set.all() + self.assertEqual(len(ranges), 1) diff --git a/grades/urls.py b/grades/urls.py new file mode 100644 index 0000000..49276ba9 --- /dev/null +++ b/grades/urls.py @@ -0,0 +1,10 @@ +from django.conf.urls import url, patterns +from grades import views + +urlpatterns = [ + url(r'^$', views.grading_systems, name="grading_systems_home"), + url(r'^grading_systems/$', views.grading_systems, name="grading_systems"), + url(r'^add_grade/$', views.add_grading_system, name="add_grade"), + url(r'^add_grade/(?P<system_id>\d+)/$', views.add_grading_system, + name="edit_grade"), +] diff --git a/grades/views.py b/grades/views.py new file mode 100644 index 0000000..10f9999 --- /dev/null +++ b/grades/views.py @@ -0,0 +1,45 @@ +from django.shortcuts import render +from django.contrib.auth.decorators import login_required +from django.forms import inlineformset_factory +from grades.forms import GradingSystemForm +from grades.models import GradingSystem, GradeRange + + +@login_required +def grading_systems(request): + user = request.user + default_grading_system = GradingSystem.objects.get(name='default') + grading_systems = GradingSystem.objects.filter(creator=user) + return render(request, 'grading_systems.html', {'default_grading_system': + default_grading_system, 'grading_systems': grading_systems}) + + +@login_required +def add_grading_system(request, system_id=None): + user = request.user + grading_system = None + if system_id is not None: + grading_system = GradingSystem.objects.get(id=system_id) + GradeRangeFormSet = inlineformset_factory(GradingSystem, GradeRange, + fields='__all__', extra=0) + grade_form = GradingSystemForm(instance=grading_system) + is_default = grading_system is not None and grading_system.name == 'default' + + if request.method == 'POST': + formset = GradeRangeFormSet(request.POST, instance=grading_system) + grade_form = GradingSystemForm(request.POST, instance=grading_system) + if grade_form.is_valid(): + system = grade_form.save(commit=False) + system.creator = user + system.save() + system_id = system.id + if formset.is_valid(): + formset.save() + if 'add' in request.POST: + GradeRangeFormSet = inlineformset_factory(GradingSystem, GradeRange, + fields='__all__', extra=1) + formset = GradeRangeFormSet(instance=grading_system) + + return render(request, 'add_grades.html', {'formset': formset, + 'grade_form': grade_form, "system_id": system_id, + 'is_default': is_default}) diff --git a/online_test/settings.py b/online_test/settings.py index c55a056..797d982 100644 --- a/online_test/settings.py +++ b/online_test/settings.py @@ -44,6 +44,7 @@ INSTALLED_APPS = ( 'yaksh', 'taggit', 'social.apps.django_app.default', + 'grades', ) MIDDLEWARE_CLASSES = ( diff --git a/online_test/urls.py b/online_test/urls.py index ce0de41..3e62fd6 100644 --- a/online_test/urls.py +++ b/online_test/urls.py @@ -13,5 +13,6 @@ urlpatterns = [ url(r'^exam/', include('yaksh.urls', namespace='yaksh', app_name='yaksh')), url(r'^exam/reset/', include('yaksh.urls_password_reset')), url(r'^', include('social.apps.django_app.urls', namespace='social')), + url(r'^grades/', include('grades.urls', namespace='grades', app_name='grades')), ] urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) diff --git a/yaksh/forms.py b/yaksh/forms.py index 9a44cfa..41c9176 100644 --- a/yaksh/forms.py +++ b/yaksh/forms.py @@ -282,7 +282,7 @@ class CourseForm(forms.ModelForm): class Meta: model = Course fields = ['name', 'enrollment', 'active', 'code', 'instructions', - 'start_enroll_time', 'end_enroll_time'] + 'start_enroll_time', 'end_enroll_time', 'grading_system'] class ProfileForm(forms.ModelForm): diff --git a/yaksh/models.py b/yaksh/models.py index 826c0e8..d011bb0 100644 --- a/yaksh/models.py +++ b/yaksh/models.py @@ -35,7 +35,7 @@ from yaksh.code_server import ( from yaksh.settings import SERVER_POOL_PORT, SERVER_HOST_NAME from django.conf import settings from django.forms.models import model_to_dict - +from grades.models import GradingSystem languages = ( ("python", "Python"), @@ -331,7 +331,8 @@ class Quiz(models.Model): allow_skip = models.BooleanField("Allow students to skip questions", default=True) - weightage = models.FloatField(default=1.0) + weightage = models.FloatField(help_text='Will be considered as percentage', + default=100) is_exercise = models.BooleanField(default=False) @@ -605,6 +606,8 @@ class Course(models.Model): null=True ) + grading_system = models.ForeignKey(GradingSystem, null=True, blank=True) + objects = CourseManager() def _create_duplicate_instance(self, creator, course_name=None): @@ -775,6 +778,14 @@ class Course(models.Model): percent = round((count / len(modules))) return percent + def get_grade(self, user): + course_status = CourseStatus.objects.filter(course=self, user=user) + if course_status.exists(): + grade = course_status.first().get_grade() + else: + grade = "NA" + return grade + def days_before_start(self): """ Get the days remaining for the start of the course """ if timezone.now() < self.start_enroll_time: @@ -796,7 +807,44 @@ class CourseStatus(models.Model): course = models.ForeignKey(Course) user = models.ForeignKey(User) grade = models.CharField(max_length=255, null=True, blank=True) - total_marks = models.FloatField(default=0.0) + percentage = models.FloatField(default=0.0) + + def get_grade(self): + return self.grade + + def set_grade(self): + if self.is_course_complete(): + self.calculate_percentage() + if self.course.grading_system is None: + grading_system = GradingSystem.objects.get(name='default') + else: + grading_system = self.course.grading_system + grade = grading_system.get_grade(self.percentage) + self.grade = grade + self.save() + + def calculate_percentage(self): + if self.is_course_complete(): + quizzes = self.course.get_quizzes() + total_weightage = 0 + sum = 0 + for quiz in quizzes: + total_weightage += quiz.weightage + marks = AnswerPaper.objects.get_user_best_of_attempts_marks( + quiz, self.user.id, self.course.id) + out_of = quiz.questionpaper_set.first().total_marks + sum += (marks/out_of)*quiz.weightage + self.percentage = (sum/total_weightage)*100 + self.save() + + def is_course_complete(self): + modules = self.course.get_learning_modules() + complete = False + for module in modules: + complete = module.get_status(self.user, self.course) == 'completed' + if not complete: + break + return complete ############################################################################### 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/course_detail.html b/yaksh/templates/yaksh/course_detail.html index a5d10a7..9fcae68 100644 --- a/yaksh/templates/yaksh/course_detail.html +++ b/yaksh/templates/yaksh/course_detail.html @@ -136,12 +136,14 @@ <th>Sr No.</th> <th>Students</th> <th>Total</th> + <th>Grade</th> <th colspan="{{modules|length}}">Modules</th> </tr> <tr> <th scope="row"></th> <th></th> <th></th> + <th></th> {% if modules %} {% for module in modules %} <th> @@ -171,6 +173,10 @@ {% course_completion_percent course student as c_percent %} {{c_percent}} % </td> + <td> + {% course_grade course student as grade %} + {{grade}} + </td> {% if modules %} {% for module in modules %} <td> diff --git a/yaksh/templates/yaksh/course_modules.html b/yaksh/templates/yaksh/course_modules.html index afbae75..6c93e97 100644 --- a/yaksh/templates/yaksh/course_modules.html +++ b/yaksh/templates/yaksh/course_modules.html @@ -17,6 +17,7 @@ <center>{{ msg }}</center> </div> {% endif %} +<b>Grade: {% if grade %} {{ grade }} {% else %} Will be available once the course is complete {% endif %}</b> {% if learning_modules %} <table class="table"> {% for module in learning_modules %} diff --git a/yaksh/templates/yaksh/courses.html b/yaksh/templates/yaksh/courses.html index dabf8eb..ba09c6d 100644 --- a/yaksh/templates/yaksh/courses.html +++ b/yaksh/templates/yaksh/courses.html @@ -56,6 +56,10 @@ <a href="{{URL_ROOT}}/exam/manage/courses/all_learning_module"> Add/View Modules</a> </li> + <li> + <a href="{% url 'grades:grading_systems'%}"> + Add/View Grading Systems </a> + </li> </ul> </div> </div> diff --git a/yaksh/templatetags/custom_filters.py b/yaksh/templatetags/custom_filters.py index 26646b1..0c5eb5a 100644 --- a/yaksh/templatetags/custom_filters.py +++ b/yaksh/templatetags/custom_filters.py @@ -66,8 +66,13 @@ def course_completion_percent(course, user): @register.simple_tag +def course_grade(course, user): + return course.get_grade(user) + + +@register.simple_tag def get_ordered_testcases(question, answerpaper): - return question.get_ordered_test_cases(answerpaper) + return question.get_ordered_test_cases(answerpaper) @register.simple_tag def get_answer_for_arrange_options(ans, question): diff --git a/yaksh/test_models.py b/yaksh/test_models.py index e7a6fe5..41730c3 100644 --- a/yaksh/test_models.py +++ b/yaksh/test_models.py @@ -1838,3 +1838,101 @@ class AssignmentUploadTestCases(unittest.TestCase): actual_file_name = self.quiz.description.replace(" ", "_") file_name = file_name.replace(" ", "_") self.assertIn(actual_file_name, file_name) + + +class CourseStatusTestCases(unittest.TestCase): + def setUp(self): + user = User.objects.get(username='creator') + self.course = Course.objects.create(name="Demo Course", creator=user, + enrollment="Enroll Request") + self.module = LearningModule.objects.create(name='M1', creator=user, + description='module one') + self.quiz1 = Quiz.objects.create(time_between_attempts=0, weightage=50, + description='qz1') + self.quiz2 = Quiz.objects.create(time_between_attempts=0, weightage=100, + description='qz2') + question = Question.objects.first() + self.qpaper1 = QuestionPaper.objects.create(quiz=self.quiz1) + self.qpaper2 = QuestionPaper.objects.create(quiz=self.quiz2) + self.qpaper1.fixed_questions.add(question) + self.qpaper2.fixed_questions.add(question) + self.qpaper1.update_total_marks() + self.qpaper2.update_total_marks() + self.qpaper1.save() + self.qpaper2.save() + self.unit_1_quiz = LearningUnit.objects.create(order=1, type='quiz', + quiz=self.quiz1) + self.unit_2_quiz = LearningUnit.objects.create(order=2, type='quiz', + quiz=self.quiz2) + self.module.learning_unit.add(self.unit_1_quiz) + self.module.learning_unit.add(self.unit_2_quiz) + self.module.save() + self.course.learning_module.add(self.module) + student = User.objects.get(username='course_user') + self.course.students.add(student) + self.course.save() + + attempt = 1 + ip = '127.0.0.1' + self.answerpaper1 = self.qpaper1.make_answerpaper(student, ip, attempt, + self.course.id) + self.answerpaper2 = self.qpaper2.make_answerpaper(student, ip, attempt, + self.course.id) + + self.course_status = CourseStatus.objects.create(course=self.course, + user=student) + + def tearDown(self): + self.course_status.delete() + self.answerpaper1.delete() + self.answerpaper2.delete() + self.qpaper1.delete() + self.qpaper2.delete() + self.quiz1.delete() + self.quiz2.delete() + self.unit_1_quiz.delete() + self.unit_2_quiz.delete() + self.module.delete() + self.course.delete() + + def test_course_is_complete(self): + # When + self.course_status.completed_units.add(self.unit_1_quiz) + # Then + self.assertFalse(self.course_status.is_course_complete()) + + # When + self.course_status.completed_units.add(self.unit_2_quiz) + # Then + self.assertTrue(self.course_status.is_course_complete()) + + # Given + self.answerpaper1.marks_obtained = 1 + self.answerpaper1.save() + self.answerpaper2.marks_obtained = 0 + self.answerpaper2.save() + # When + self.course_status.calculate_percentage() + # Then + self.assertEqual(round(self.course_status.percentage, 2), 33.33) + # When + self.course_status.set_grade() + # Then + self.assertEqual(self.course_status.get_grade(), 'F') + + # Given + self.answerpaper1.marks_obtained = 0 + self.answerpaper1.save() + self.answerpaper2.marks_obtained = 1 + self.answerpaper2.save() + # When + self.course_status.calculate_percentage() + # Then + self.assertEqual(round(self.course_status.percentage, 2), 66.67) + # When + self.course_status.set_grade() + # Then + self.assertEqual(self.course_status.get_grade(), 'B') + + # Test get course grade after completion + self.assertEqual(self.course.get_grade(self.answerpaper1.user), 'B') diff --git a/yaksh/views.py b/yaksh/views.py index 083fd67..1cb77fc 100644 --- a/yaksh/views.py +++ b/yaksh/views.py @@ -1672,6 +1672,10 @@ def grade_user(request, quiz_id=None, user_id=None, attempt_number=None, 'comments_%d' % paper.question_paper.id, 'No comments') paper.save() + course_status = CourseStatus.objects.filter(course=course, user=user) + if course_status.exists(): + course_status.first().set_grade() + return my_render_to_response( 'yaksh/grade_user.html', context, context_instance=ci ) @@ -1927,14 +1931,27 @@ def regrade(request, course_id, question_id=None, answerpaper_id=None, answerpaper = get_object_or_404(AnswerPaper, pk=answerpaper_id) for question in answerpaper.questions.all(): details.append(answerpaper.regrade(question.id)) + course_status = CourseStatus.objects.filter(user=answerpaper.user, + course=answerpaper.course) + if course_status.exists(): + course_status.first().set_grade() if questionpaper_id is not None and question_id is not None: answerpapers = AnswerPaper.objects.filter(questions=question_id, question_paper_id=questionpaper_id, course_id=course_id) for answerpaper in answerpapers: details.append(answerpaper.regrade(question_id)) + course_status = CourseStatus.objects.filter(user=answerpaper.user, + course=answerpaper.course) + if course_status.exists(): + course_status.first().set_grade() if answerpaper_id is not None and question_id is not None: answerpaper = get_object_or_404(AnswerPaper, pk=answerpaper_id) details.append(answerpaper.regrade(question_id)) + course_status = CourseStatus.objects.filter(user=answerpaper.user, + course=answerpaper.course) + if course_status.exists(): + course_status.first().set_grade() + return grader(request, extra_context={'details': details}) @@ -2723,6 +2740,12 @@ def course_modules(request, course_id, msg=None): learning_modules = course.get_learning_modules() context = {"course": course, "learning_modules": learning_modules, "user": user, "msg": msg} + course_status = CourseStatus.objects.filter(course=course, user=user) + if course_status.exists(): + course_status = course_status.first() + if not course_status.grade: + course_status.set_grade() + context['grade'] = course_status.get_grade() return my_render_to_response('yaksh/course_modules.html', context) |