EDUCATOR-2016 In proctored exams, learner will not receive certificate when marked suspicious
This commit is contained in:
@@ -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())
|
||||
|
||||
41
lms/djangoapps/certificates/services.py
Normal file
41
lms/djangoapps/certificates/services.py
Normal 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
|
||||
)
|
||||
@@ -24,6 +24,7 @@ class GeneratedCertificateFactory(DjangoModelFactory):
|
||||
mode = GeneratedCertificate.MODES.honor
|
||||
name = ''
|
||||
verify_uuid = uuid4().hex
|
||||
grade = ''
|
||||
|
||||
|
||||
class CertificateWhitelistFactory(DjangoModelFactory):
|
||||
|
||||
60
lms/djangoapps/certificates/tests/test_services.py
Normal file
60
lms/djangoapps/certificates/tests/test_services.py
Normal 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
|
||||
}
|
||||
)
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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'',
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
0
lms/djangoapps/tests/__init__.py
Normal file
0
lms/djangoapps/tests/__init__.py
Normal file
26
lms/djangoapps/tests/test_utils.py
Normal file
26
lms/djangoapps/tests/test_utils.py
Normal 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
15
lms/djangoapps/utils.py
Normal 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
|
||||
)
|
||||
Reference in New Issue
Block a user