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 | 13 | ||||
-rw-r--r-- | grades/migrations/0001_initial.py | 45 | ||||
-rw-r--r-- | grades/migrations/__init__.py | 0 | ||||
-rw-r--r-- | grades/migrations/default_grading_system.py | 36 | ||||
-rw-r--r-- | grades/models.py | 49 | ||||
-rw-r--r-- | grades/templates/add_grades.html | 28 | ||||
-rw-r--r-- | grades/templates/grading_systems.html | 17 | ||||
-rw-r--r-- | grades/tests/test_models.py | 27 | ||||
-rw-r--r-- | grades/tests/test_views.py | 106 | ||||
-rw-r--r-- | grades/urls.py | 10 | ||||
-rw-r--r-- | grades/views.py | 44 |
14 files changed, 389 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..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}) |