summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.travis.yml1
-rw-r--r--grades/__init__.py0
-rw-r--r--grades/admin.py9
-rw-r--r--grades/apps.py5
-rw-r--r--grades/forms.py13
-rw-r--r--grades/migrations/0001_initial.py45
-rw-r--r--grades/migrations/__init__.py0
-rw-r--r--grades/migrations/default_grading_system.py36
-rw-r--r--grades/models.py49
-rw-r--r--grades/templates/add_grades.html28
-rw-r--r--grades/templates/grading_systems.html17
-rw-r--r--grades/tests/test_models.py27
-rw-r--r--grades/tests/test_views.py106
-rw-r--r--grades/urls.py10
-rw-r--r--grades/views.py44
-rw-r--r--online_test/settings.py1
-rw-r--r--online_test/urls.py1
-rw-r--r--yaksh/forms.py2
-rw-r--r--yaksh/models.py34
-rw-r--r--yaksh/templates/manage.html3
-rw-r--r--yaksh/templates/yaksh/course_modules.html1
-rw-r--r--yaksh/views.py6
22 files changed, 434 insertions, 4 deletions
diff --git a/.travis.yml b/.travis.yml
index b1a8402..86fcd0c 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.tests
- 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..ab38f6b
--- /dev/null
+++ b/grades/admin.py
@@ -0,0 +1,9 @@
+from django.contrib import admin
+from grades.models import GradingSystem, GradeRange
+
+# Register your models here.
+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..f8c800a
--- /dev/null
+++ b/grades/forms.py
@@ -0,0 +1,13 @@
+from grades.models import GradingSystem
+from django import forms
+
+class GradingSystemForm(forms.ModelForm):
+ def __init__(self, *args, ** kwargs):
+ super(GradingSystemForm, self).__init__(*args, **kwargs)
+ system = getattr(self, 'instance', None)
+ if system.name == 'default':
+ self.fields['name'].widget.attrs['readonly'] = True
+
+ class Meta:
+ model = GradingSystem
+ fields = ['name', 'description', 'can_be_used']
diff --git a/grades/migrations/0001_initial.py b/grades/migrations/0001_initial.py
new file mode 100644
index 0000000..65d711e
--- /dev/null
+++ b/grades/migrations/0001_initial.py
@@ -0,0 +1,45 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.9.5 on 2018-02-02 06:20
+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')),
+ ('order', models.IntegerField(default=0)),
+ ('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!')),
+ ('can_be_used', models.BooleanField(default=False)),
+ ('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..1629d29
--- /dev/null
+++ b/grades/migrations/default_grading_system.py
@@ -0,0 +1,36 @@
+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',
+ can_be_used=True)
+ GradeRange.objects.using(db).create(system=default_system, order=1, lower_limit=0,
+ upper_limit=40, grade='F', description='Fail')
+ GradeRange.objects.using(db).create(system=default_system, order=2, lower_limit=40,
+ upper_limit=55, grade='P', description='Pass')
+ GradeRange.objects.using(db).create(system=default_system, order=3, lower_limit=55,
+ upper_limit=60, grade='C', description='Average')
+ GradeRange.objects.using(db).create(system=default_system, order=4, lower_limit=60,
+ upper_limit=75, grade='B', description='Satisfactory')
+ GradeRange.objects.using(db).create(system=default_system, order=5, lower_limit=75,
+ upper_limit=90, grade='A', description='Good')
+ GradeRange.objects.using(db).create(system=default_system, order=6, lower_limit=90,
+ upper_limit=101, grade='A+', description='Excellent')
+
+
+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..33895bb
--- /dev/null
+++ b/grades/models.py
@@ -0,0 +1,49 @@
+from django.db import models
+from django.contrib.auth.models import User
+
+# Create your models here.
+
+class GradingSystem(models.Model):
+ name = models.CharField(max_length=255, unique=True)
+ description = models.TextField(default='About the grading system!')
+ can_be_used = models.BooleanField(default=False)
+ 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)
+ order = models.IntegerField(default=0)
+ 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..f2f0051
--- /dev/null
+++ b/grades/templates/add_grades.html
@@ -0,0 +1,28 @@
+<html>
+<a href="{% url 'grades:grading_systems'%}"> Back to Grading Systems </a>
+<p><b>Note: For grade range lower limit is inclusive and upper limit is exclusive</b></p>
+{% 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>
+ {{ grade_form }}
+ </table>
+ {{ formset.management_form }}
+ <br>
+ <b><u>Grade Ranges</u></b>
+ <hr>
+ {% for form in formset %}
+ <div>
+ {{ form }}
+ </div>
+ <hr>
+ {% endfor %}
+
+ <input type="submit" id="add" name="add" value="Add">
+ <input type="submit" id="save" name="save" value="Save">
+
+</form>
+</html>
diff --git a/grades/templates/grading_systems.html b/grades/templates/grading_systems.html
new file mode 100644
index 0000000..143b037
--- /dev/null
+++ b/grades/templates/grading_systems.html
@@ -0,0 +1,17 @@
+<html>
+ <b>Default Grading System:</b>
+ <ul>
+ <li><a href="{% url 'grades:edit_grade' default_grading_system.id %}">{{ default_grading_system.name }}</a></li>
+ </ul>
+ <b> My grading System: </b>
+ {% if grading_systems %}
+ <ul>
+ {% for system in grading_systems %}
+ <li><a href="{% url 'grades:edit_grade' system.id %}">{{ system.name }}</a></li>
+ {% endfor %}
+ </ul>
+ {% else %}
+ <p> None. You can add one.</p>
+ {% endif %}
+ <a href="{% url 'grades:add_grade' %}"> Add a Grading System </a>
+</html>
diff --git a/grades/tests/test_models.py b/grades/tests/test_models.py
new file mode 100644
index 0000000..89708e2
--- /dev/null
+++ b/grades/tests/test_models.py
@@ -0,0 +1,27 @@
+from django.test import TestCase
+from grades.models import GradingSystem, GradeRange
+
+# Create your tests here.
+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
+ 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..2c29ae5
--- /dev/null
+++ b/grades/tests/test_views.py
@@ -0,0 +1,106 @@
+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 the grading system!'],
+ 'graderange_set-MIN_NUM_FORMS': ['0'],
+ 'graderange_set-TOTAL_FORMS': ['0'], 'can_be_used': ['on'],
+ '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-order': ['0'],
+ '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'], 'can_be_used': ['on'],
+ '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..86803c9
--- /dev/null
+++ b/grades/views.py
@@ -0,0 +1,44 @@
+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
+
+# Create your views here.
+@login_required
+def grading_systems(request):
+ user = request.user
+ default_grading_system = GradingSystem.objects.get(name='default')
+ grading_systems = GradingSystem.objects.filter(creator=user).exclude(
+ name='default')
+ 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)
+
+ 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})
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 258a1ee..6e70d46 100644
--- a/yaksh/forms.py
+++ b/yaksh/forms.py
@@ -279,7 +279,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 f065190..f76feed 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"),
@@ -311,7 +311,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)
@@ -551,6 +552,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):
@@ -737,6 +740,33 @@ class CourseStatus(models.Model):
grade = models.CharField(max_length=255, null=True, blank=True)
total_marks = models.FloatField(default=0.0)
+ def set_grade(self):
+ grade = self.course.grading_system.get_grade(self.total_marks)
+ self.grade = grade
+
+ def calculate_total_marks(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.total_marks = (sum/total_weightage)*100
+ self.set_grade()
+
+
+ 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
###############################################################################
class ConcurrentUser(models.Model):
diff --git a/yaksh/templates/manage.html b/yaksh/templates/manage.html
index 17ce23e..2590655 100644
--- a/yaksh/templates/manage.html
+++ b/yaksh/templates/manage.html
@@ -18,7 +18,8 @@
<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"> Grader </a></li>
+ <li><a href="{% url 'grades:grading_systems'%}"> Grading Systems </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_modules.html b/yaksh/templates/yaksh/course_modules.html
index fad1be0..eba7409 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 %}
{% for module in learning_modules %}
<div class="row well">
diff --git a/yaksh/views.py b/yaksh/views.py
index 011b417..30b454b 100644
--- a/yaksh/views.py
+++ b/yaksh/views.py
@@ -2710,6 +2710,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 course_status.is_course_complete() and not course_status.grade:
+ course_status.calculate_total_marks()
+ context['grade'] = course_status.grade
return my_render_to_response('yaksh/course_modules.html', context)