Commit 2d028f82 by Peter Fogg

Add an explicit cutoff date for audit cert granting.

The previous logic was a convoluted way of doing the same thing, and
has already led to one bug. Instead of hoping that the bugs are ironed
out now and that future devs maintain this logic properly, let's just
set a real cutoff date.
parent 7ccca469
...@@ -326,8 +326,8 @@ class XQueueCertInterface(object): ...@@ -326,8 +326,8 @@ class XQueueCertInterface(object):
# analytics. Only do this if the certificate is new, or # analytics. Only do this if the certificate is new, or
# already marked as ineligible -- we don't want to mark # already marked as ineligible -- we don't want to mark
# existing audit certs as ineligible. # existing audit certs as ineligible.
if (created or cert.status in (CertificateStatuses.audit_passing, CertificateStatuses.audit_notpassing)) \ cutoff = settings.AUDIT_CERT_CUTOFF_DATE
and not is_eligible_for_certificate: if (cutoff and cert.created_date >= cutoff) and not is_eligible_for_certificate:
cert.status = CertificateStatuses.audit_passing if passing else CertificateStatuses.audit_notpassing cert.status = CertificateStatuses.audit_passing if passing else CertificateStatuses.audit_notpassing
cert.save() cert.save()
LOGGER.info( LOGGER.info(
......
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
"""Tests for the XQueue certificates interface. """ """Tests for the XQueue certificates interface. """
from contextlib import contextmanager from contextlib import contextmanager
from datetime import datetime, timedelta
import ddt import ddt
import json import json
from mock import patch, Mock from mock import patch, Mock
...@@ -8,6 +9,8 @@ from nose.plugins.attrib import attr ...@@ -8,6 +9,8 @@ from nose.plugins.attrib import attr
from django.test import TestCase from django.test import TestCase
from django.test.utils import override_settings from django.test.utils import override_settings
import freezegun
import pytz
from course_modes.models import CourseMode from course_modes.models import CourseMode
from opaque_keys.edx.locator import CourseLocator from opaque_keys.edx.locator import CourseLocator
...@@ -81,6 +84,7 @@ class XQueueCertInterfaceAddCertificateTest(ModuleStoreTestCase): ...@@ -81,6 +84,7 @@ class XQueueCertInterfaceAddCertificateTest(ModuleStoreTestCase):
self.assertIsNotNone(certificate.verify_uuid) self.assertIsNotNone(certificate.verify_uuid)
@ddt.data('honor', 'audit') @ddt.data('honor', 'audit')
@override_settings(AUDIT_CERT_CUTOFF_DATE=datetime.now(pytz.UTC) - timedelta(days=1))
def test_add_cert_with_honor_certificates(self, mode): def test_add_cert_with_honor_certificates(self, mode):
"""Test certificates generations for honor and audit modes.""" """Test certificates generations for honor and audit modes."""
template_name = 'certificate-template-{id.org}-{id.course}.pdf'.format( template_name = 'certificate-template-{id.org}-{id.course}.pdf'.format(
...@@ -206,13 +210,45 @@ class XQueueCertInterfaceAddCertificateTest(ModuleStoreTestCase): ...@@ -206,13 +210,45 @@ class XQueueCertInterfaceAddCertificateTest(ModuleStoreTestCase):
self.assertFalse(mock_send.called) self.assertFalse(mock_send.called)
@ddt.data( @ddt.data(
(CertificateStatuses.downloadable, 'Pass', CertificateStatuses.generating), # Eligible and should stay that way
(CertificateStatuses.audit_passing, 'Pass', CertificateStatuses.audit_passing), (
(CertificateStatuses.audit_notpassing, 'Pass', CertificateStatuses.audit_passing), CertificateStatuses.downloadable,
(CertificateStatuses.audit_notpassing, None, CertificateStatuses.audit_notpassing), datetime.now(pytz.UTC) - timedelta(days=2),
'Pass',
CertificateStatuses.generating
),
# Ensure that certs in the wrong state can be fixed by regeneration
(
CertificateStatuses.downloadable,
datetime.now(pytz.UTC) - timedelta(hours=1),
'Pass',
CertificateStatuses.audit_passing
),
# Ineligible and should stay that way
(
CertificateStatuses.audit_passing,
datetime.now(pytz.UTC) - timedelta(hours=1),
'Pass',
CertificateStatuses.audit_passing
),
# As above
(
CertificateStatuses.audit_notpassing,
datetime.now(pytz.UTC) - timedelta(hours=1),
'Pass',
CertificateStatuses.audit_passing
),
# As above
(
CertificateStatuses.audit_notpassing,
datetime.now(pytz.UTC) - timedelta(hours=1),
None,
CertificateStatuses.audit_notpassing
),
) )
@ddt.unpack @ddt.unpack
def test_regen_audit_certs_eligibility(self, status, grade, expected_status): @override_settings(AUDIT_CERT_CUTOFF_DATE=datetime.now(pytz.UTC) - timedelta(days=1))
def test_regen_audit_certs_eligibility(self, status, created_date, grade, expected_status):
""" """
Test that existing audit certificates remain eligible even if cert Test that existing audit certificates remain eligible even if cert
generation is re-run. generation is re-run.
...@@ -224,13 +260,14 @@ class XQueueCertInterfaceAddCertificateTest(ModuleStoreTestCase): ...@@ -224,13 +260,14 @@ class XQueueCertInterfaceAddCertificateTest(ModuleStoreTestCase):
is_active=True, is_active=True,
mode=CourseMode.AUDIT, mode=CourseMode.AUDIT,
) )
GeneratedCertificateFactory( with freezegun.freeze_time(created_date):
user=self.user_2, GeneratedCertificateFactory(
course_id=self.course.id, user=self.user_2,
grade='1.0', course_id=self.course.id,
status=status, grade='1.0',
mode=GeneratedCertificate.MODES.audit, status=status,
) mode=GeneratedCertificate.MODES.audit,
)
# Run grading/cert generation again # Run grading/cert generation again
with patch('courseware.grades.grade', Mock(return_value={'grade': grade, 'percent': 0.75})): with patch('courseware.grades.grade', Mock(return_value={'grade': grade, 'percent': 0.75})):
......
...@@ -19,6 +19,8 @@ Common traits: ...@@ -19,6 +19,8 @@ Common traits:
import datetime import datetime
import json import json
import dateutil
from .common import * from .common import *
from openedx.core.lib.logsettings import get_logger_config from openedx.core.lib.logsettings import get_logger_config
import os import os
...@@ -756,3 +758,7 @@ MICROSITE_DATABASE_TEMPLATE_CACHE_TTL = ENV_TOKENS.get( ...@@ -756,3 +758,7 @@ MICROSITE_DATABASE_TEMPLATE_CACHE_TTL = ENV_TOKENS.get(
# Course Content Bookmarks Settings # Course Content Bookmarks Settings
MAX_BOOKMARKS_PER_COURSE = ENV_TOKENS.get('MAX_BOOKMARKS_PER_COURSE', MAX_BOOKMARKS_PER_COURSE) MAX_BOOKMARKS_PER_COURSE = ENV_TOKENS.get('MAX_BOOKMARKS_PER_COURSE', MAX_BOOKMARKS_PER_COURSE)
# Cutoff date for granting audit certificates
if ENV_TOKENS.get('AUDIT_CERT_CUTOFF_DATE', None):
AUDIT_CERT_CUTOFF_DATE = dateutil.parser.parse(ENV_TOKENS.get('AUDIT_CERT_CUTOFF_DATE'))
...@@ -2755,6 +2755,10 @@ MOBILE_APP_USER_AGENT_REGEXES = [ ...@@ -2755,6 +2755,10 @@ MOBILE_APP_USER_AGENT_REGEXES = [
DEPRECATED_ADVANCED_COMPONENT_TYPES = [] DEPRECATED_ADVANCED_COMPONENT_TYPES = []
# Cutoff date for granting audit certificates
AUDIT_CERT_CUTOFF_DATE = None
################################ Settings for Credentials Service ################################ ################################ Settings for Credentials Service ################################
CREDENTIALS_SERVICE_USERNAME = 'credentials_service_user' CREDENTIALS_SERVICE_USERNAME = 'credentials_service_user'
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