Two new certificate statuses are introduced, 'audit_passing' and 'audit_notpassing'. These signal that the GeneratedCertificate is not to be displayed as a cert to the user, and that they either passed or did not. This allows us to retain existing grading logic, as well as maintaining correctness in analytics and reporting. Ineligible certificates are hidden by using the `eligible_certificates` manager on GeneratedCertificate. Some places in the coe (largely reporting, analytics, and management commands) use the default `objects` manager, since they need access to all certificates. ECOM-3040 ECOM-3515
255 lines
10 KiB
Python
255 lines
10 KiB
Python
"""Tests for the resubmit_error_certificates management command. """
|
|
import ddt
|
|
from contextlib import contextmanager
|
|
from django.core.management.base import CommandError
|
|
from nose.plugins.attrib import attr
|
|
from django.test.utils import override_settings
|
|
from mock import patch
|
|
|
|
from course_modes.models import CourseMode
|
|
from opaque_keys.edx.locator import CourseLocator
|
|
from certificates.tests.factories import BadgeAssertionFactory
|
|
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
|
|
from xmodule.modulestore.tests.factories import CourseFactory, check_mongo_calls
|
|
from student.tests.factories import UserFactory, CourseEnrollmentFactory
|
|
from certificates.management.commands import resubmit_error_certificates, regenerate_user, ungenerated_certs
|
|
from certificates.models import GeneratedCertificate, CertificateStatuses, BadgeAssertion
|
|
|
|
|
|
class CertificateManagementTest(ModuleStoreTestCase):
|
|
"""
|
|
Base test class for Certificate Management command tests.
|
|
"""
|
|
# Override with the command module you wish to test.
|
|
command = resubmit_error_certificates
|
|
|
|
def setUp(self):
|
|
super(CertificateManagementTest, self).setUp()
|
|
self.user = UserFactory.create()
|
|
self.courses = [
|
|
CourseFactory.create()
|
|
for __ in range(3)
|
|
]
|
|
|
|
def _create_cert(self, course_key, user, status, mode=CourseMode.HONOR):
|
|
"""Create a certificate entry. """
|
|
# Enroll the user in the course
|
|
CourseEnrollmentFactory.create(
|
|
user=user,
|
|
course_id=course_key,
|
|
mode=mode
|
|
)
|
|
|
|
# Create the certificate
|
|
GeneratedCertificate.eligible_certificates.create(
|
|
user=user,
|
|
course_id=course_key,
|
|
status=status
|
|
)
|
|
|
|
def _run_command(self, *args, **kwargs):
|
|
"""Run the management command to generate a fake cert. """
|
|
command = self.command.Command()
|
|
return command.handle(*args, **kwargs)
|
|
|
|
def _assert_cert_status(self, course_key, user, expected_status):
|
|
"""Check the status of a certificate. """
|
|
cert = GeneratedCertificate.eligible_certificates.get(user=user, course_id=course_key)
|
|
self.assertEqual(cert.status, expected_status)
|
|
|
|
|
|
@attr('shard_1')
|
|
@ddt.ddt
|
|
class ResubmitErrorCertificatesTest(CertificateManagementTest):
|
|
"""Tests for the resubmit_error_certificates management command. """
|
|
|
|
@ddt.data(CourseMode.HONOR, CourseMode.VERIFIED)
|
|
def test_resubmit_error_certificate(self, mode):
|
|
# Create a certificate with status 'error'
|
|
self._create_cert(self.courses[0].id, self.user, CertificateStatuses.error, mode)
|
|
|
|
# Re-submit all certificates with status 'error'
|
|
with check_mongo_calls(1):
|
|
self._run_command()
|
|
|
|
# Expect that the certificate was re-submitted
|
|
self._assert_cert_status(self.courses[0].id, self.user, CertificateStatuses.notpassing)
|
|
|
|
def test_resubmit_error_certificate_in_a_course(self):
|
|
# Create a certificate with status 'error'
|
|
# in three courses.
|
|
for idx in range(3):
|
|
self._create_cert(self.courses[idx].id, self.user, CertificateStatuses.error)
|
|
|
|
# Re-submit certificates for two of the courses
|
|
self._run_command(course_key_list=[
|
|
unicode(self.courses[0].id),
|
|
unicode(self.courses[1].id)
|
|
])
|
|
|
|
# Expect that the first two courses have been re-submitted,
|
|
# but not the third course.
|
|
self._assert_cert_status(self.courses[0].id, self.user, CertificateStatuses.notpassing)
|
|
self._assert_cert_status(self.courses[1].id, self.user, CertificateStatuses.notpassing)
|
|
self._assert_cert_status(self.courses[2].id, self.user, CertificateStatuses.error)
|
|
|
|
@ddt.data(
|
|
CertificateStatuses.deleted,
|
|
CertificateStatuses.deleting,
|
|
CertificateStatuses.downloadable,
|
|
CertificateStatuses.generating,
|
|
CertificateStatuses.notpassing,
|
|
CertificateStatuses.regenerating,
|
|
CertificateStatuses.restricted,
|
|
CertificateStatuses.unavailable,
|
|
)
|
|
def test_resubmit_error_certificate_skips_non_error_certificates(self, other_status):
|
|
# Create certificates with an error status and some other status
|
|
self._create_cert(self.courses[0].id, self.user, CertificateStatuses.error)
|
|
self._create_cert(self.courses[1].id, self.user, other_status)
|
|
|
|
# Re-submit certificates for all courses
|
|
self._run_command()
|
|
|
|
# Only the certificate with status "error" should have been re-submitted
|
|
self._assert_cert_status(self.courses[0].id, self.user, CertificateStatuses.notpassing)
|
|
self._assert_cert_status(self.courses[1].id, self.user, other_status)
|
|
|
|
def test_resubmit_error_certificate_none_found(self):
|
|
self._create_cert(self.courses[0].id, self.user, CertificateStatuses.downloadable)
|
|
self._run_command()
|
|
self._assert_cert_status(self.courses[0].id, self.user, CertificateStatuses.downloadable)
|
|
|
|
def test_course_caching(self):
|
|
# Create multiple certificates for the same course
|
|
self._create_cert(self.courses[0].id, UserFactory.create(), CertificateStatuses.error)
|
|
self._create_cert(self.courses[0].id, UserFactory.create(), CertificateStatuses.error)
|
|
self._create_cert(self.courses[0].id, UserFactory.create(), CertificateStatuses.error)
|
|
|
|
# Verify that we make only one Mongo query
|
|
# because the course is cached.
|
|
with check_mongo_calls(1):
|
|
self._run_command()
|
|
|
|
def test_invalid_course_key(self):
|
|
invalid_key = u"invalid/"
|
|
with self.assertRaisesRegexp(CommandError, invalid_key):
|
|
self._run_command(course_key_list=[invalid_key])
|
|
|
|
def test_course_does_not_exist(self):
|
|
phantom_course = CourseLocator(org='phantom', course='phantom', run='phantom')
|
|
self._create_cert(phantom_course, self.user, 'error')
|
|
self._run_command()
|
|
|
|
# Expect that the certificate was NOT resubmitted
|
|
# since the course doesn't actually exist.
|
|
self._assert_cert_status(phantom_course, self.user, CertificateStatuses.error)
|
|
|
|
|
|
@attr('shard_1')
|
|
class RegenerateCertificatesTest(CertificateManagementTest):
|
|
"""
|
|
Tests for regenerating certificates.
|
|
"""
|
|
command = regenerate_user
|
|
|
|
def setUp(self):
|
|
"""
|
|
We just need one course here.
|
|
"""
|
|
super(RegenerateCertificatesTest, self).setUp()
|
|
self.course = self.courses[0]
|
|
|
|
@override_settings(CERT_QUEUE='test-queue')
|
|
@patch('certificates.api.XQueueCertInterface', spec=True)
|
|
def test_clear_badge(self, xqueue):
|
|
"""
|
|
Given that I have a user with a badge
|
|
If I run regeneration for a user
|
|
Then certificate generation will be requested
|
|
And the badge will be deleted
|
|
"""
|
|
key = self.course.location.course_key
|
|
BadgeAssertionFactory(user=self.user, course_id=key, data={})
|
|
self._create_cert(key, self.user, CertificateStatuses.downloadable)
|
|
self.assertTrue(BadgeAssertion.objects.filter(user=self.user, course_id=key))
|
|
self._run_command(
|
|
username=self.user.email, course=unicode(key), noop=False, insecure=False, template_file=None,
|
|
grade_value=None
|
|
)
|
|
xqueue.return_value.regen_cert.assert_called_with(
|
|
self.user,
|
|
key,
|
|
course=self.course,
|
|
forced_grade=None,
|
|
template_file=None,
|
|
generate_pdf=True
|
|
)
|
|
self.assertFalse(BadgeAssertion.objects.filter(user=self.user, course_id=key))
|
|
|
|
@override_settings(CERT_QUEUE='test-queue')
|
|
@patch('capa.xqueue_interface.XQueueInterface.send_to_queue', spec=True)
|
|
def test_regenerating_certificate(self, mock_send_to_queue):
|
|
"""
|
|
Given that I have a user who has not passed course
|
|
If I run regeneration for that user
|
|
Then certificate generation will be not be requested
|
|
"""
|
|
key = self.course.location.course_key
|
|
self._create_cert(key, self.user, CertificateStatuses.downloadable)
|
|
self._run_command(
|
|
username=self.user.email, course=unicode(key), noop=False, insecure=True, template_file=None,
|
|
grade_value=None
|
|
)
|
|
certificate = GeneratedCertificate.eligible_certificates.get(
|
|
user=self.user,
|
|
course_id=key
|
|
)
|
|
self.assertEqual(certificate.status, CertificateStatuses.notpassing)
|
|
self.assertFalse(mock_send_to_queue.called)
|
|
|
|
|
|
@attr('shard_1')
|
|
class UngenerateCertificatesTest(CertificateManagementTest):
|
|
"""
|
|
Tests for generating certificates.
|
|
"""
|
|
command = ungenerated_certs
|
|
|
|
def setUp(self):
|
|
"""
|
|
We just need one course here.
|
|
"""
|
|
super(UngenerateCertificatesTest, self).setUp()
|
|
self.course = self.courses[0]
|
|
|
|
@override_settings(CERT_QUEUE='test-queue')
|
|
@patch('capa.xqueue_interface.XQueueInterface.send_to_queue', spec=True)
|
|
def test_ungenerated_certificate(self, mock_send_to_queue):
|
|
"""
|
|
Given that I have ended course
|
|
If I run ungenerated certs command
|
|
Then certificates should be generated for all users who passed course
|
|
"""
|
|
mock_send_to_queue.return_value = (0, "Successfully queued")
|
|
key = self.course.location.course_key
|
|
self._create_cert(key, self.user, CertificateStatuses.unavailable)
|
|
with self._mock_passing_grade():
|
|
self._run_command(
|
|
course=unicode(key), noop=False, insecure=True, force=False
|
|
)
|
|
self.assertTrue(mock_send_to_queue.called)
|
|
certificate = GeneratedCertificate.eligible_certificates.get(
|
|
user=self.user,
|
|
course_id=key
|
|
)
|
|
self.assertEqual(certificate.status, CertificateStatuses.generating)
|
|
|
|
@contextmanager
|
|
def _mock_passing_grade(self):
|
|
"""Mock the grading function to always return a passing grade. """
|
|
symbol = 'courseware.grades.grade'
|
|
with patch(symbol) as mock_grade:
|
|
mock_grade.return_value = {'grade': 'Pass', 'percent': 0.75}
|
|
yield
|