Commit 2bc35396 by J. Cliff Dyer

Add date_passed column to PersistentCourseGrade model.

The column is populated with the current timestamp when a passing grade
is persisted for the first time.  "Passing grade" is defined as having a
non-blank value for letter_grade.

TNL-5888
parent 92995b42
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('grades', '0006_persistent_course_grades'),
]
operations = [
migrations.AddField(
model_name='persistentcoursegrade',
name='passed_timestamp',
field=models.DateTimeField(null=True, verbose_name='Date learner earned a passing grade', blank=True),
),
migrations.AlterIndexTogether(
name='persistentcoursegrade',
index_together=set([('passed_timestamp', 'course_id')]),
),
]
...@@ -16,6 +16,7 @@ from lazy import lazy ...@@ -16,6 +16,7 @@ from lazy import lazy
import logging import logging
from django.db import models from django.db import models
from django.utils.timezone import now
from model_utils.models import TimeStampedModel from model_utils.models import TimeStampedModel
from coursewarehistoryextended.fields import UnsignedBigIntAutoField from coursewarehistoryextended.fields import UnsignedBigIntAutoField
...@@ -378,9 +379,13 @@ class PersistentCourseGrade(TimeStampedModel): ...@@ -378,9 +379,13 @@ class PersistentCourseGrade(TimeStampedModel):
# (course_id, user_id) for individual grades # (course_id, user_id) for individual grades
# (course_id) for instructors to see all course grades, implicitly created via the unique_together constraint # (course_id) for instructors to see all course grades, implicitly created via the unique_together constraint
# (user_id) for course dashboard; explicitly declared as an index below # (user_id) for course dashboard; explicitly declared as an index below
# (passed_timestamp, course_id) for tracking when users first earned a passing grade.
unique_together = [ unique_together = [
('course_id', 'user_id'), ('course_id', 'user_id'),
] ]
index_together = [
('passed_timestamp', 'course_id'),
]
# primary key will need to be large for this table # primary key will need to be large for this table
id = UnsignedBigIntAutoField(primary_key=True) # pylint: disable=invalid-name id = UnsignedBigIntAutoField(primary_key=True) # pylint: disable=invalid-name
...@@ -396,18 +401,21 @@ class PersistentCourseGrade(TimeStampedModel): ...@@ -396,18 +401,21 @@ class PersistentCourseGrade(TimeStampedModel):
percent_grade = models.FloatField(blank=False) percent_grade = models.FloatField(blank=False)
letter_grade = models.CharField(u'Letter grade for course', blank=False, max_length=255) letter_grade = models.CharField(u'Letter grade for course', blank=False, max_length=255)
# Information related to course completion
passed_timestamp = models.DateTimeField(u'Date learner earned a passing grade', blank=True, null=True)
def __unicode__(self): def __unicode__(self):
""" """
Returns a string representation of this model. Returns a string representation of this model.
""" """
return u"{} user: {}, course version: {}, grading policy: {}, percent grade {}%, letter grade {}".format( return u', '.join([
type(self).__name__, u"{} user: {}".format(type(self).__name__, self.user_id),
self.user_id, u"course version: {}".format(self.course_version),
self.course_version, u"grading policy: {}".format(self.grading_policy_hash),
self.grading_policy_hash, u"percent grade: {}%".format(self.percent_grade),
self.percent_grade, u"letter grade: {}".format(self.letter_grade),
self.letter_grade, u"passed_timestamp: {}".format(self.passed_timestamp),
) ])
@classmethod @classmethod
def read_course_grade(cls, user_id, course_id): def read_course_grade(cls, user_id, course_id):
...@@ -428,6 +436,7 @@ class PersistentCourseGrade(TimeStampedModel): ...@@ -428,6 +436,7 @@ class PersistentCourseGrade(TimeStampedModel):
Creates a course grade in the database. Creates a course grade in the database.
Returns a PersistedCourseGrade object. Returns a PersistedCourseGrade object.
""" """
passed = kwargs.pop('passed')
if kwargs.get('course_version', None) is None: if kwargs.get('course_version', None) is None:
kwargs['course_version'] = "" kwargs['course_version'] = ""
...@@ -436,4 +445,7 @@ class PersistentCourseGrade(TimeStampedModel): ...@@ -436,4 +445,7 @@ class PersistentCourseGrade(TimeStampedModel):
course_id=course_id, course_id=course_id,
defaults=kwargs defaults=kwargs
) )
if passed and not grade.passed_timestamp:
grade.passed_timestamp = now()
grade.save()
return grade return grade
...@@ -3,10 +3,12 @@ CourseGrade Class ...@@ -3,10 +3,12 @@ CourseGrade Class
""" """
from collections import defaultdict from collections import defaultdict
from logging import getLogger
from django.conf import settings from django.conf import settings
from django.core.exceptions import PermissionDenied from django.core.exceptions import PermissionDenied
from lazy import lazy from lazy import lazy
from logging import getLogger
from lms.djangoapps.course_blocks.api import get_course_blocks from lms.djangoapps.course_blocks.api import get_course_blocks
from lms.djangoapps.grades.config.models import PersistentGradesEnabledFlag from lms.djangoapps.grades.config.models import PersistentGradesEnabledFlag
from openedx.core.djangoapps.signals.signals import GRADES_UPDATED from openedx.core.djangoapps.signals.signals import GRADES_UPDATED
...@@ -163,6 +165,7 @@ class CourseGrade(object): ...@@ -163,6 +165,7 @@ class CourseGrade(object):
grading_policy_hash=grading_policy_hash, grading_policy_hash=grading_policy_hash,
percent_grade=self.percent, percent_grade=self.percent,
letter_grade=self.letter_grade or "", letter_grade=self.letter_grade or "",
passed=self.passed,
) )
self._signal_listeners_when_grade_computed() self._signal_listeners_when_grade_computed()
......
...@@ -10,6 +10,8 @@ import json ...@@ -10,6 +10,8 @@ import json
from django.db.utils import IntegrityError from django.db.utils import IntegrityError
from django.test import TestCase from django.test import TestCase
from django.utils.timezone import now
from freezegun import freeze_time
from opaque_keys.edx.locator import CourseLocator, BlockUsageLocator from opaque_keys.edx.locator import CourseLocator, BlockUsageLocator
from lms.djangoapps.grades.models import ( from lms.djangoapps.grades.models import (
...@@ -271,10 +273,11 @@ class PersistentCourseGradesTest(GradesModelTestCase): ...@@ -271,10 +273,11 @@ class PersistentCourseGradesTest(GradesModelTestCase):
), ),
"percent_grade": 77.7, "percent_grade": 77.7,
"letter_grade": "Great job", "letter_grade": "Great job",
"passed": True
} }
def test_update(self): def test_update(self):
created_grade = PersistentCourseGrade.objects.create(**self.params) created_grade = PersistentCourseGrade.update_or_create_course_grade(**self.params)
self.params["percent_grade"] = 88.8 self.params["percent_grade"] = 88.8
self.params["letter_grade"] = "Better job" self.params["letter_grade"] = "Better job"
updated_grade = PersistentCourseGrade.update_or_create_course_grade(**self.params) updated_grade = PersistentCourseGrade.update_or_create_course_grade(**self.params)
...@@ -282,11 +285,61 @@ class PersistentCourseGradesTest(GradesModelTestCase): ...@@ -282,11 +285,61 @@ class PersistentCourseGradesTest(GradesModelTestCase):
self.assertEqual(updated_grade.letter_grade, "Better job") self.assertEqual(updated_grade.letter_grade, "Better job")
self.assertEqual(created_grade.id, updated_grade.id) self.assertEqual(created_grade.id, updated_grade.id)
def test_passed_timestamp(self):
# When the user has not passed, passed_timestamp is None
self.params.update({
u'percent_grade': 25.0,
u'letter_grade': u'',
u'passed': False,
})
grade = PersistentCourseGrade.update_or_create_course_grade(**self.params)
self.assertIsNone(grade.passed_timestamp)
# After the user earns a passing grade, the passed_timestamp is set
self.params.update({
u'percent_grade': 75.0,
u'letter_grade': u'C',
u'passed': True,
})
grade = PersistentCourseGrade.update_or_create_course_grade(**self.params)
passed_timestamp = grade.passed_timestamp
self.assertEqual(grade.letter_grade, u'C')
self.assertIsInstance(passed_timestamp, datetime)
# After the user improves their score, the new grade is reflected, but
# the passed_timestamp remains the same.
self.params.update({
u'percent_grade': 95.0,
u'letter_grade': u'A',
u'passed': True,
})
grade = PersistentCourseGrade.update_or_create_course_grade(**self.params)
self.assertEqual(grade.letter_grade, u'A')
self.assertEqual(grade.passed_timestamp, passed_timestamp)
# If the grade later reverts to a failing grade, they keep their passed_timestamp
self.params.update({
u'percent_grade': 20.0,
u'letter_grade': u'',
u'passed': False,
})
grade = PersistentCourseGrade.update_or_create_course_grade(**self.params)
self.assertEqual(grade.letter_grade, u'')
self.assertEqual(grade.passed_timestamp, passed_timestamp)
@freeze_time(now())
def test_passed_timestamp_is_now(self):
grade = PersistentCourseGrade.update_or_create_course_grade(**self.params)
self.assertEqual(now(), grade.passed_timestamp)
def test_create_and_read_grade(self): def test_create_and_read_grade(self):
created_grade = PersistentCourseGrade.update_or_create_course_grade(**self.params) created_grade = PersistentCourseGrade.update_or_create_course_grade(**self.params)
read_grade = PersistentCourseGrade.read_course_grade(self.params["user_id"], self.params["course_id"]) read_grade = PersistentCourseGrade.read_course_grade(self.params["user_id"], self.params["course_id"])
for param in self.params: for param in self.params:
if param == u'passed':
continue # passed/passed_timestamp takes special handling, and is tested separately
self.assertEqual(self.params[param], getattr(created_grade, param)) self.assertEqual(self.params[param], getattr(created_grade, param))
self.assertIsInstance(created_grade.passed_timestamp, datetime)
self.assertEqual(created_grade, read_grade) self.assertEqual(created_grade, read_grade)
def test_course_version_optional(self): def test_course_version_optional(self):
......
...@@ -129,6 +129,13 @@ class TestCourseGradeFactory(GradeTestBase): ...@@ -129,6 +129,13 @@ class TestCourseGradeFactory(GradeTestBase):
self.assertEqual(course_grade.letter_grade, u'Pass') self.assertEqual(course_grade.letter_grade, u'Pass')
self.assertEqual(course_grade.percent, 0.5) self.assertEqual(course_grade.percent, 0.5)
def test_zero_course_grade(self):
grade_factory = CourseGradeFactory(self.request.user)
with mock_get_score(0, 2):
course_grade = grade_factory.create(self.course)
self.assertIsNone(course_grade.letter_grade)
self.assertEqual(course_grade.percent, 0.0)
def test_get_persisted(self): def test_get_persisted(self):
grade_factory = CourseGradeFactory(self.request.user) grade_factory = CourseGradeFactory(self.request.user)
# first, create a grade in the database # first, create a grade in the database
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment