summaryrefslogtreecommitdiff
path: root/grades
diff options
context:
space:
mode:
Diffstat (limited to 'grades')
-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
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})