EDUCATOR-2016 In proctored exams, learner will not receive certificate when marked suspicious

This commit is contained in:
rabia23
2018-01-01 18:24:05 +00:00
parent 17dd276d16
commit 222794ee59
11 changed files with 164 additions and 33 deletions

View File

@@ -5,6 +5,8 @@ Signal handlers are connected here.
"""
from django.apps import AppConfig
from django.conf import settings
from edx_proctoring.runtime import set_runtime_service
class CertificatesConfig(AppConfig):
@@ -20,3 +22,6 @@ class CertificatesConfig(AppConfig):
# Can't import models at module level in AppConfigs, and models get
# included from the signal handlers
from . import signals # pylint: disable=unused-variable
if settings.FEATURES.get('ENABLE_SPECIAL_EXAMS'):
from .services import CertificateService
set_runtime_service('certificates', CertificateService())

View File

@@ -0,0 +1,41 @@
"""
Certificate service
"""
import logging
from django.core.exceptions import ObjectDoesNotExist
from lms.djangoapps.utils import _get_key
from opaque_keys.edx.keys import CourseKey
from .models import GeneratedCertificate
log = logging.getLogger(__name__)
class CertificateService(object):
"""
User Certificate service
"""
def invalidate_certificate(self, user_id, course_key_or_id):
"""
Invalidate the user certificate in a given course if it exists.
"""
course_key = _get_key(course_key_or_id, CourseKey)
try:
generated_certificate = GeneratedCertificate.objects.get(
user=user_id,
course_id=course_key
)
generated_certificate.invalidate()
log.info(
u'Certificate invalidated for user %d in course %s',
user_id,
course_key
)
except ObjectDoesNotExist:
log.warning(
u'Invalidation failed because a certificate for user %d in course %s does not exist.',
user_id,
course_key
)

View File

@@ -24,6 +24,7 @@ class GeneratedCertificateFactory(DjangoModelFactory):
mode = GeneratedCertificate.MODES.honor
name = ''
verify_uuid = uuid4().hex
grade = ''
class CertificateWhitelistFactory(DjangoModelFactory):

View File

@@ -0,0 +1,60 @@
"""
Unit Tests for the Certificate service
"""
from certificates.models import CertificateStatuses, GeneratedCertificate
from certificates.services import CertificateService
from certificates.tests.factories import GeneratedCertificateFactory
from student.tests.factories import UserFactory
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory
class CertificateServiceTests(ModuleStoreTestCase):
"""
Tests for the Certificate service
"""
def setUp(self):
super(CertificateServiceTests, self).setUp()
self.service = CertificateService()
self.course = CourseFactory()
self.user = UserFactory()
self.user_id = self.user.id
self.course_id = self.course.id
GeneratedCertificateFactory.create(
status=CertificateStatuses.downloadable,
user=self.user,
course_id=self.course.id,
grade=1.0
)
def generated_certificate_to_dict(self, generated_certificate):
"""
Converts a Generated Certificate instance to a Python dictionary
"""
return {
'verify_uuid': generated_certificate.verify_uuid,
'download_uuid': generated_certificate.download_uuid,
'download_url': generated_certificate.download_url,
'grade': generated_certificate.grade,
'status': generated_certificate.status
}
def test_invalidate_certificate(self):
"""
Verify that CertificateService invalidates the user certificate
"""
self.service.invalidate_certificate(self.user_id, self.course_id)
invalid_generated_certificate = GeneratedCertificate.objects.get(
user=self.user_id,
course_id=self.course_id
)
self.assertDictEqual(
self.generated_certificate_to_dict(invalid_generated_certificate),
{
'verify_uuid': '',
'download_uuid': '',
'download_url': '',
'grade': '',
'status': CertificateStatuses.unavailable
}
)

View File

@@ -1,3 +1,6 @@
"""
Course Grade Factory Class
"""
from collections import namedtuple
from logging import getLogger
@@ -152,7 +155,7 @@ class CourseGradeFactory(object):
course_data,
persistent_grade.percent_grade,
persistent_grade.letter_grade,
persistent_grade.passed_timestamp is not None,
persistent_grade.letter_grade is not u''
)
@staticmethod

View File

@@ -1,7 +1,11 @@
"""
Grade service
"""
from datetime import datetime
import pytz
from lms.djangoapps.utils import _get_key
from opaque_keys.edx.keys import CourseKey, UsageKey
from track.event_transaction_utils import create_new_event_transaction_id, set_event_transaction_type
@@ -12,18 +16,6 @@ from .models import PersistentSubsectionGrade, PersistentSubsectionGradeOverride
from .signals.signals import SUBSECTION_OVERRIDE_CHANGED
def _get_key(key_or_id, key_cls):
"""
Helper method to get a course/usage key either from a string or a key_cls,
where the key_cls (CourseKey or UsageKey) will simply be returned.
"""
return (
key_cls.from_string(key_or_id)
if isinstance(key_or_id, basestring)
else key_or_id
)
class GradesService(object):
"""
Course grade service

View File

@@ -386,7 +386,7 @@ class PersistentCourseGradesTest(GradesModelTestCase):
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
# If the grade later reverts to a failing grade, passed_timestamp remains the same.
self.params.update({
u'percent_grade': 20.0,
u'letter_grade': u'',

View File

@@ -1,11 +1,13 @@
"""
Grades Service Tests
"""
from datetime import datetime
import ddt
import pytz
from datetime import datetime
from freezegun import freeze_time
from lms.djangoapps.grades.models import PersistentSubsectionGrade, PersistentSubsectionGradeOverride
from lms.djangoapps.grades.services import GradesService, _get_key
from lms.djangoapps.grades.services import GradesService
from mock import patch, call
from opaque_keys.edx.keys import CourseKey, UsageKey
from student.tests.factories import UserFactory
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
@@ -14,7 +16,7 @@ from ..config.waffle import REJECTED_EXAM_OVERRIDES_GRADE
from ..constants import ScoreDatabaseTableEnum
class MockWaffleFlag():
class MockWaffleFlag(object):
def __init__(self, state):
self.state = state
@@ -27,7 +29,7 @@ class GradesServiceTests(ModuleStoreTestCase):
"""
Tests for the Grades service
"""
def setUp(self, **kwargs):
def setUp(self):
super(GradesServiceTests, self).setUp()
self.service = GradesService()
self.course = CourseFactory.create(org='edX', number='DemoX', display_name='Demo_Course')
@@ -215,20 +217,6 @@ class GradesServiceTests(ModuleStoreTestCase):
)
)
@ddt.data(
['edX/DemoX/Demo_Course', CourseKey.from_string('edX/DemoX/Demo_Course'), CourseKey],
['course-v1:edX+DemoX+Demo_Course', CourseKey.from_string('course-v1:edX+DemoX+Demo_Course'), CourseKey],
[CourseKey.from_string('course-v1:edX+DemoX+Demo_Course'),
CourseKey.from_string('course-v1:edX+DemoX+Demo_Course'), CourseKey],
['block-v1:edX+DemoX+Demo_Course+type@sequential+block@workflow',
UsageKey.from_string('block-v1:edX+DemoX+Demo_Course+type@sequential+block@workflow'), UsageKey],
[UsageKey.from_string('block-v1:edX+DemoX+Demo_Course+type@sequential+block@workflow'),
UsageKey.from_string('block-v1:edX+DemoX+Demo_Course+type@sequential+block@workflow'), UsageKey],
)
@ddt.unpack
def test_get_key(self, input_key, output_key, key_cls):
self.assertEqual(_get_key(input_key, key_cls), output_key)
def test_should_override_grade_on_rejected_exam(self):
self.assertTrue(self.service.should_override_grade_on_rejected_exam('course-v1:edX+DemoX+Demo_Course'))
self.mock_waffle_flags.return_value = {

View File

View File

@@ -0,0 +1,26 @@
"""
Unit Tests for Utils Class
"""
from unittest import TestCase
import ddt
from lms.djangoapps.utils import _get_key
from opaque_keys.edx.keys import CourseKey, UsageKey
@ddt.ddt
class UtilsTests(TestCase):
@ddt.data(
['edX/DemoX/Demo_Course', CourseKey.from_string('edX/DemoX/Demo_Course'), CourseKey],
['course-v1:edX+DemoX+Demo_Course', CourseKey.from_string('course-v1:edX+DemoX+Demo_Course'), CourseKey],
[CourseKey.from_string('course-v1:edX+DemoX+Demo_Course'),
CourseKey.from_string('course-v1:edX+DemoX+Demo_Course'), CourseKey],
['block-v1:edX+DemoX+Demo_Course+type@sequential+block@workflow',
UsageKey.from_string('block-v1:edX+DemoX+Demo_Course+type@sequential+block@workflow'), UsageKey],
[UsageKey.from_string('block-v1:edX+DemoX+Demo_Course+type@sequential+block@workflow'),
UsageKey.from_string('block-v1:edX+DemoX+Demo_Course+type@sequential+block@workflow'), UsageKey],
)
@ddt.unpack
def test_get_key(self, input_key, output_key, key_cls):
self.assertEqual(_get_key(input_key, key_cls), output_key)

15
lms/djangoapps/utils.py Normal file
View File

@@ -0,0 +1,15 @@
"""
Helper Methods
"""
def _get_key(key_or_id, key_cls):
"""
Helper method to get a course/usage key either from a string or a key_cls,
where the key_cls (CourseKey or UsageKey) will simply be returned.
"""
return (
key_cls.from_string(key_or_id)
if isinstance(key_or_id, basestring)
else key_or_id
)