diff options
Diffstat (limited to 'grades')
-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 |
15 files changed, 449 insertions, 0 deletions
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}) |