Disable audit certificates for new audit enrollments.
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
This commit is contained in:
@@ -592,6 +592,18 @@ class CourseMode(models.Model):
|
||||
modes = cls.modes_for_course(course_id)
|
||||
return min(mode.min_price for mode in modes if mode.currency.lower() == currency.lower())
|
||||
|
||||
@classmethod
|
||||
def is_eligible_for_certificate(cls, mode_slug):
|
||||
"""
|
||||
Returns whether or not the given mode_slug is eligible for a
|
||||
certificate. Currently all modes other than 'audit' grant a
|
||||
certificate. Note that audit enrollments which existed prior
|
||||
to December 2015 *were* given certificates, so there will be
|
||||
GeneratedCertificate records with mode='audit' which are
|
||||
eligible.
|
||||
"""
|
||||
return mode_slug != cls.AUDIT
|
||||
|
||||
def to_tuple(self):
|
||||
"""
|
||||
Takes a mode model and turns it into a model named tuple.
|
||||
|
||||
@@ -430,3 +430,16 @@ class CourseModeModelTest(TestCase):
|
||||
verified_mode.expiration_datetime = None
|
||||
self.assertFalse(verified_mode.expiration_datetime_is_explicit)
|
||||
self.assertIsNone(verified_mode.expiration_datetime)
|
||||
|
||||
@ddt.data(
|
||||
(CourseMode.AUDIT, False),
|
||||
(CourseMode.HONOR, True),
|
||||
(CourseMode.VERIFIED, True),
|
||||
(CourseMode.CREDIT_MODE, True),
|
||||
(CourseMode.PROFESSIONAL, True),
|
||||
(CourseMode.NO_ID_PROFESSIONAL_MODE, True),
|
||||
)
|
||||
@ddt.unpack
|
||||
def test_eligible_for_cert(self, mode_slug, expected_eligibility):
|
||||
"""Verify that non-audit modes are eligible for a cert."""
|
||||
self.assertEqual(CourseMode.is_eligible_for_certificate(mode_slug), expected_eligibility)
|
||||
|
||||
@@ -97,7 +97,9 @@ class Command(BaseCommand):
|
||||
cert_grades = {
|
||||
cert.user.username: cert.grade
|
||||
for cert in list(
|
||||
GeneratedCertificate.objects.filter(course_id=course_key).prefetch_related('user')
|
||||
GeneratedCertificate.objects.filter( # pylint: disable=no-member
|
||||
course_id=course_key
|
||||
).prefetch_related('user')
|
||||
)
|
||||
}
|
||||
print "Grading students"
|
||||
|
||||
@@ -314,6 +314,8 @@ def _cert_info(user, course_overview, cert_status, course_mode): # pylint: disa
|
||||
CertificateStatuses.notpassing: 'notpassing',
|
||||
CertificateStatuses.restricted: 'restricted',
|
||||
CertificateStatuses.auditing: 'auditing',
|
||||
CertificateStatuses.audit_passing: 'auditing',
|
||||
CertificateStatuses.audit_notpassing: 'auditing',
|
||||
}
|
||||
|
||||
default_status = 'processing'
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -412,7 +412,7 @@ CREATE TABLE `auth_permission` (
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `content_type_id` (`content_type_id`,`codename`),
|
||||
CONSTRAINT `auth__content_type_id_508cf46651277a81_fk_django_content_type_id` FOREIGN KEY (`content_type_id`) REFERENCES `django_content_type` (`id`)
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=737 DEFAULT CHARSET=utf8;
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=740 DEFAULT CHARSET=utf8;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
DROP TABLE IF EXISTS `auth_registration`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
@@ -1600,7 +1600,7 @@ CREATE TABLE `django_content_type` (
|
||||
`model` varchar(100) NOT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `django_content_type_app_label_45f3b1d93ec8c61c_uniq` (`app_label`,`model`)
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=245 DEFAULT CHARSET=utf8;
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=246 DEFAULT CHARSET=utf8;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
DROP TABLE IF EXISTS `django_migrations`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
@@ -1611,7 +1611,7 @@ CREATE TABLE `django_migrations` (
|
||||
`name` varchar(255) NOT NULL,
|
||||
`applied` datetime(6) NOT NULL,
|
||||
PRIMARY KEY (`id`)
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=102 DEFAULT CHARSET=utf8;
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=103 DEFAULT CHARSET=utf8;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
DROP TABLE IF EXISTS `django_openid_auth_association`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
@@ -2718,6 +2718,18 @@ CREATE TABLE `programs_programsapiconfig` (
|
||||
CONSTRAINT `programs_programsa_changed_by_id_b7c3b49d5c0dcd3_fk_auth_user_id` FOREIGN KEY (`changed_by_id`) REFERENCES `auth_user` (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
DROP TABLE IF EXISTS `rss_proxy_whitelistedrssurl`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!40101 SET character_set_client = utf8 */;
|
||||
CREATE TABLE `rss_proxy_whitelistedrssurl` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`created` datetime(6) NOT NULL,
|
||||
`modified` datetime(6) NOT NULL,
|
||||
`url` varchar(255) NOT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `url` (`url`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
DROP TABLE IF EXISTS `self_paced_selfpacedconfiguration`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!40101 SET character_set_client = utf8 */;
|
||||
|
||||
Binary file not shown.
@@ -78,7 +78,7 @@ def get_certificates_for_user(username):
|
||||
else None
|
||||
),
|
||||
}
|
||||
for cert in GeneratedCertificate.objects.filter(user__username=username).order_by("course_id")
|
||||
for cert in GeneratedCertificate.eligible_certificates.filter(user__username=username).order_by("course_id")
|
||||
]
|
||||
|
||||
|
||||
@@ -109,11 +109,14 @@ def generate_user_certificates(student, course_key, course=None, insecure=False,
|
||||
if insecure:
|
||||
xqueue.use_https = False
|
||||
generate_pdf = not has_html_certificates_enabled(course_key, course)
|
||||
status, cert = xqueue.add_cert(student, course_key,
|
||||
course=course,
|
||||
generate_pdf=generate_pdf,
|
||||
forced_grade=forced_grade)
|
||||
if status in [CertificateStatuses.generating, CertificateStatuses.downloadable]:
|
||||
cert = xqueue.add_cert(
|
||||
student,
|
||||
course_key,
|
||||
course=course,
|
||||
generate_pdf=generate_pdf,
|
||||
forced_grade=forced_grade
|
||||
)
|
||||
if cert.status in [CertificateStatuses.generating, CertificateStatuses.downloadable]:
|
||||
emit_certificate_event('created', student, course_key, course, {
|
||||
'user_id': student.id,
|
||||
'course_id': unicode(course_key),
|
||||
@@ -121,7 +124,7 @@ def generate_user_certificates(student, course_key, course=None, insecure=False,
|
||||
'enrollment_mode': cert.mode,
|
||||
'generation_mode': generation_mode
|
||||
})
|
||||
return status
|
||||
return cert.status
|
||||
|
||||
|
||||
def regenerate_user_certificates(student, course_key, course=None,
|
||||
@@ -385,7 +388,7 @@ def get_certificate_url(user_id=None, course_id=None, uuid=None):
|
||||
)
|
||||
return url
|
||||
try:
|
||||
user_certificate = GeneratedCertificate.objects.get(
|
||||
user_certificate = GeneratedCertificate.eligible_certificates.get(
|
||||
user=user_id,
|
||||
course_id=course_id
|
||||
)
|
||||
|
||||
@@ -76,7 +76,7 @@ class Command(BaseCommand):
|
||||
status = options.get('status', CertificateStatuses.downloadable)
|
||||
grade = options.get('grade', '')
|
||||
|
||||
cert, created = GeneratedCertificate.objects.get_or_create(
|
||||
cert, created = GeneratedCertificate.eligible_certificates.get_or_create(
|
||||
user=user,
|
||||
course_id=course_key
|
||||
)
|
||||
|
||||
@@ -42,8 +42,9 @@ class Command(BaseCommand):
|
||||
def handle(self, *args, **options):
|
||||
course_id = options['course']
|
||||
print "Fetching ungraded students for {0}".format(course_id)
|
||||
ungraded = GeneratedCertificate.objects.filter(
|
||||
course_id__exact=course_id).filter(grade__exact='')
|
||||
ungraded = GeneratedCertificate.objects.filter( # pylint: disable=no-member
|
||||
course_id__exact=course_id
|
||||
).filter(grade__exact='')
|
||||
course = courses.get_course_by_id(course_id)
|
||||
factory = RequestFactory()
|
||||
request = factory.get('/')
|
||||
|
||||
@@ -70,14 +70,17 @@ class Command(BaseCommand):
|
||||
enrolled_total = User.objects.filter(
|
||||
courseenrollment__course_id=course_id
|
||||
)
|
||||
verified_enrolled = GeneratedCertificate.objects.filter(
|
||||
course_id__exact=course_id, mode__exact='verified'
|
||||
verified_enrolled = GeneratedCertificate.objects.filter( # pylint: disable=no-member
|
||||
course_id__exact=course_id,
|
||||
mode__exact='verified'
|
||||
)
|
||||
honor_enrolled = GeneratedCertificate.objects.filter(
|
||||
course_id__exact=course_id, mode__exact='honor'
|
||||
honor_enrolled = GeneratedCertificate.objects.filter( # pylint: disable=no-member
|
||||
course_id__exact=course_id,
|
||||
mode__exact='honor'
|
||||
)
|
||||
audit_enrolled = GeneratedCertificate.objects.filter(
|
||||
course_id__exact=course_id, mode__exact='audit'
|
||||
audit_enrolled = GeneratedCertificate.objects.filter( # pylint: disable=no-member
|
||||
course_id__exact=course_id,
|
||||
mode__exact='audit'
|
||||
)
|
||||
|
||||
cert_data[course_id] = {
|
||||
@@ -88,7 +91,7 @@ class Command(BaseCommand):
|
||||
'audit_enrolled': audit_enrolled.count()
|
||||
}
|
||||
|
||||
status_tally = GeneratedCertificate.objects.filter(
|
||||
status_tally = GeneratedCertificate.objects.filter( # pylint: disable=no-member
|
||||
course_id__exact=course_id
|
||||
).values('status').annotate(
|
||||
dcount=Count('status')
|
||||
@@ -100,7 +103,7 @@ class Command(BaseCommand):
|
||||
}
|
||||
)
|
||||
|
||||
mode_tally = GeneratedCertificate.objects.filter(
|
||||
mode_tally = GeneratedCertificate.objects.filter( # pylint: disable=no-member
|
||||
course_id__exact=course_id,
|
||||
status__exact='downloadable'
|
||||
).values('mode').annotate(
|
||||
|
||||
@@ -81,7 +81,7 @@ class Command(BaseCommand):
|
||||
# Retrieve the IDs of generated certificates with
|
||||
# error status in the set of courses we're considering.
|
||||
queryset = (
|
||||
GeneratedCertificate.objects.select_related('user')
|
||||
GeneratedCertificate.objects.select_related('user') # pylint: disable=no-member
|
||||
).filter(status=CertificateStatuses.error)
|
||||
if only_course_keys:
|
||||
queryset = queryset.filter(course_id__in=only_course_keys)
|
||||
|
||||
@@ -86,6 +86,8 @@ class CertificateStatuses(object):
|
||||
restricted = 'restricted'
|
||||
unavailable = 'unavailable'
|
||||
auditing = 'auditing'
|
||||
audit_passing = 'audit_passing'
|
||||
audit_notpassing = 'audit_notpassing'
|
||||
|
||||
readable_statuses = {
|
||||
downloadable: "already received",
|
||||
@@ -143,7 +145,7 @@ class CertificateWhitelist(models.Model):
|
||||
if student:
|
||||
white_list = white_list.filter(user=student)
|
||||
result = []
|
||||
generated_certificates = GeneratedCertificate.objects.filter(
|
||||
generated_certificates = GeneratedCertificate.eligible_certificates.filter(
|
||||
course_id=course_id,
|
||||
user__in=[exception.user for exception in white_list],
|
||||
status=CertificateStatuses.downloadable
|
||||
@@ -168,11 +170,42 @@ class CertificateWhitelist(models.Model):
|
||||
return result
|
||||
|
||||
|
||||
class EligibleCertificateManager(models.Manager):
|
||||
"""
|
||||
A manager for `GeneratedCertificate` models that automatically
|
||||
filters out ineligible certs.
|
||||
|
||||
The idea is to prevent accidentally granting certificates to
|
||||
students who have not enrolled in a cert-granting mode. The
|
||||
alternative is to filter by eligible_for_certificate=True every
|
||||
time certs are searched for, which is verbose and likely to be
|
||||
forgotten.
|
||||
"""
|
||||
|
||||
def get_queryset(self):
|
||||
"""
|
||||
Return a queryset for `GeneratedCertificate` models, filtering out
|
||||
ineligible certificates.
|
||||
"""
|
||||
return super(EligibleCertificateManager, self).get_queryset().exclude(
|
||||
status__in=(CertificateStatuses.audit_passing, CertificateStatuses.audit_notpassing)
|
||||
)
|
||||
|
||||
|
||||
class GeneratedCertificate(models.Model):
|
||||
"""
|
||||
Base model for generated certificates
|
||||
"""
|
||||
|
||||
# Only returns eligible certificates. This should be used in
|
||||
# preference to the default `objects` manager in most cases.
|
||||
eligible_certificates = EligibleCertificateManager()
|
||||
|
||||
# Normal object manager, which should only be used when ineligible
|
||||
# certificates (i.e. new audit certs) should be included in the
|
||||
# results. Django requires us to explicitly declare this.
|
||||
objects = models.Manager()
|
||||
|
||||
MODES = Choices('verified', 'honor', 'audit', 'professional', 'no-id-professional')
|
||||
|
||||
VERIFIED_CERTS_MODES = [CourseMode.VERIFIED, CourseMode.CREDIT_MODE]
|
||||
@@ -410,7 +443,7 @@ def certificate_status_for_student(student, course_id):
|
||||
'''
|
||||
|
||||
try:
|
||||
generated_certificate = GeneratedCertificate.objects.get(
|
||||
generated_certificate = GeneratedCertificate.objects.get( # pylint: disable=no-member
|
||||
user=student, course_id=course_id)
|
||||
cert_status = {
|
||||
'status': generated_certificate.status,
|
||||
|
||||
@@ -20,6 +20,7 @@ from student.models import UserProfile, CourseEnrollment
|
||||
from lms.djangoapps.verify_student.models import SoftwareSecurePhotoVerification
|
||||
|
||||
from certificates.models import (
|
||||
CertificateStatuses,
|
||||
GeneratedCertificate,
|
||||
certificate_status_for_student,
|
||||
CertificateStatuses as status,
|
||||
@@ -120,14 +121,14 @@ class XQueueCertInterface(object):
|
||||
Change the certificate status to unavailable (if it exists) and request
|
||||
grading. Passing grades will put a certificate request on the queue.
|
||||
|
||||
Return the status object.
|
||||
Return the certificate.
|
||||
"""
|
||||
# TODO: when del_cert is implemented and plumbed through certificates
|
||||
# repo also, do a deletion followed by a creation r/t a simple
|
||||
# recreation. XXX: this leaves orphan cert files laying around in
|
||||
# AWS. See note in the docstring too.
|
||||
try:
|
||||
certificate = GeneratedCertificate.objects.get(user=student, course_id=course_id)
|
||||
certificate = GeneratedCertificate.eligible_certificates.get(user=student, course_id=course_id)
|
||||
|
||||
LOGGER.info(
|
||||
(
|
||||
@@ -183,8 +184,7 @@ class XQueueCertInterface(object):
|
||||
raise NotImplementedError
|
||||
|
||||
# pylint: disable=too-many-statements
|
||||
def add_cert(self, student, course_id, course=None, forced_grade=None, template_file=None,
|
||||
title='None', generate_pdf=True):
|
||||
def add_cert(self, student, course_id, course=None, forced_grade=None, template_file=None, generate_pdf=True):
|
||||
"""
|
||||
Request a new certificate for a student.
|
||||
|
||||
@@ -211,7 +211,7 @@ class XQueueCertInterface(object):
|
||||
If a student does not have a passing grade the status
|
||||
will change to status.notpassing
|
||||
|
||||
Returns the student's status and newly created certificate instance
|
||||
Returns the newly created certificate instance
|
||||
"""
|
||||
|
||||
valid_statuses = [
|
||||
@@ -222,10 +222,11 @@ class XQueueCertInterface(object):
|
||||
status.notpassing,
|
||||
status.downloadable,
|
||||
status.auditing,
|
||||
status.audit_passing,
|
||||
status.audit_notpassing,
|
||||
]
|
||||
|
||||
cert_status = certificate_status_for_student(student, course_id)['status']
|
||||
new_status = cert_status
|
||||
cert = None
|
||||
|
||||
if cert_status not in valid_statuses:
|
||||
@@ -240,169 +241,191 @@ class XQueueCertInterface(object):
|
||||
cert_status,
|
||||
unicode(valid_statuses)
|
||||
)
|
||||
return None
|
||||
|
||||
# The caller can optionally pass a course in to avoid
|
||||
# re-fetching it from Mongo. If they have not provided one,
|
||||
# get it from the modulestore.
|
||||
if course is None:
|
||||
course = modulestore().get_course(course_id, depth=0)
|
||||
|
||||
profile = UserProfile.objects.get(user=student)
|
||||
profile_name = profile.name
|
||||
|
||||
# Needed for access control in grading.
|
||||
self.request.user = student
|
||||
self.request.session = {}
|
||||
|
||||
is_whitelisted = self.whitelist.filter(user=student, course_id=course_id, whitelist=True).exists()
|
||||
grade = grades.grade(student, self.request, course)
|
||||
enrollment_mode, __ = CourseEnrollment.enrollment_mode_for_user(student, course_id)
|
||||
mode_is_verified = enrollment_mode in GeneratedCertificate.VERIFIED_CERTS_MODES
|
||||
user_is_verified = SoftwareSecurePhotoVerification.user_is_verified(student)
|
||||
cert_mode = enrollment_mode
|
||||
is_eligible_for_certificate = is_whitelisted or CourseMode.is_eligible_for_certificate(enrollment_mode)
|
||||
|
||||
# For credit mode generate verified certificate
|
||||
if cert_mode == CourseMode.CREDIT_MODE:
|
||||
cert_mode = CourseMode.VERIFIED
|
||||
|
||||
if template_file is not None:
|
||||
template_pdf = template_file
|
||||
elif mode_is_verified and user_is_verified:
|
||||
template_pdf = "certificate-template-{id.org}-{id.course}-verified.pdf".format(id=course_id)
|
||||
elif mode_is_verified and not user_is_verified:
|
||||
template_pdf = "certificate-template-{id.org}-{id.course}.pdf".format(id=course_id)
|
||||
cert_mode = GeneratedCertificate.MODES.honor
|
||||
else:
|
||||
# grade the student
|
||||
# honor code and audit students
|
||||
template_pdf = "certificate-template-{id.org}-{id.course}.pdf".format(id=course_id)
|
||||
if forced_grade:
|
||||
grade['grade'] = forced_grade
|
||||
|
||||
# re-use the course passed in optionally so we don't have to re-fetch everything
|
||||
# for every student
|
||||
if course is None:
|
||||
course = modulestore().get_course(course_id, depth=0)
|
||||
profile = UserProfile.objects.get(user=student)
|
||||
profile_name = profile.name
|
||||
cert, created = GeneratedCertificate.objects.get_or_create(user=student, course_id=course_id) # pylint: disable=no-member
|
||||
|
||||
# Needed
|
||||
self.request.user = student
|
||||
self.request.session = {}
|
||||
cert.mode = cert_mode
|
||||
cert.user = student
|
||||
cert.grade = grade['percent']
|
||||
cert.course_id = course_id
|
||||
cert.name = profile_name
|
||||
cert.download_url = ''
|
||||
|
||||
course_name = course.display_name or unicode(course_id)
|
||||
is_whitelisted = self.whitelist.filter(user=student, course_id=course_id, whitelist=True).exists()
|
||||
grade = grades.grade(student, self.request, course)
|
||||
enrollment_mode, __ = CourseEnrollment.enrollment_mode_for_user(student, course_id)
|
||||
mode_is_verified = enrollment_mode in GeneratedCertificate.VERIFIED_CERTS_MODES
|
||||
user_is_verified = SoftwareSecurePhotoVerification.user_is_verified(student)
|
||||
cert_mode = enrollment_mode
|
||||
# Strip HTML from grade range label
|
||||
grade_contents = grade.get('grade', None)
|
||||
try:
|
||||
grade_contents = lxml.html.fromstring(grade_contents).text_content()
|
||||
passing = True
|
||||
except (TypeError, XMLSyntaxError, ParserError) as exc:
|
||||
LOGGER.info(
|
||||
(
|
||||
u"Could not retrieve grade for student %s "
|
||||
u"in the course '%s' "
|
||||
u"because an exception occurred while parsing the "
|
||||
u"grade contents '%s' as HTML. "
|
||||
u"The exception was: '%s'"
|
||||
),
|
||||
student.id,
|
||||
unicode(course_id),
|
||||
grade_contents,
|
||||
unicode(exc)
|
||||
)
|
||||
|
||||
# For credit mode generate verified certificate
|
||||
if cert_mode == CourseMode.CREDIT_MODE:
|
||||
cert_mode = CourseMode.VERIFIED
|
||||
|
||||
if mode_is_verified and user_is_verified:
|
||||
template_pdf = "certificate-template-{id.org}-{id.course}-verified.pdf".format(id=course_id)
|
||||
elif mode_is_verified and not user_is_verified:
|
||||
template_pdf = "certificate-template-{id.org}-{id.course}.pdf".format(id=course_id)
|
||||
cert_mode = GeneratedCertificate.MODES.honor
|
||||
# Log if the student is whitelisted
|
||||
if is_whitelisted:
|
||||
LOGGER.info(
|
||||
u"Student %s is whitelisted in '%s'",
|
||||
student.id,
|
||||
unicode(course_id)
|
||||
)
|
||||
passing = True
|
||||
else:
|
||||
# honor code and audit students
|
||||
template_pdf = "certificate-template-{id.org}-{id.course}.pdf".format(id=course_id)
|
||||
if forced_grade:
|
||||
grade['grade'] = forced_grade
|
||||
passing = False
|
||||
|
||||
cert, __ = GeneratedCertificate.objects.get_or_create(user=student, course_id=course_id)
|
||||
# If this user's enrollment is not eligible to receive a
|
||||
# certificate, mark it as such for reporting and
|
||||
# analytics. Only do this if the certificate is new -- we
|
||||
# don't want to mark existing audit certs as ineligible.
|
||||
if created and not is_eligible_for_certificate:
|
||||
cert.status = CertificateStatuses.audit_passing if passing else CertificateStatuses.audit_notpassing
|
||||
cert.save()
|
||||
LOGGER.info(
|
||||
u"Student %s with enrollment mode %s is not eligible for a certificate.",
|
||||
student.id,
|
||||
enrollment_mode
|
||||
)
|
||||
return cert
|
||||
# If they are not passing, short-circuit and don't generate cert
|
||||
elif not passing:
|
||||
cert.status = status.notpassing
|
||||
cert.save()
|
||||
|
||||
cert.mode = cert_mode
|
||||
cert.user = student
|
||||
cert.grade = grade['percent']
|
||||
cert.course_id = course_id
|
||||
cert.name = profile_name
|
||||
cert.download_url = ''
|
||||
# Strip HTML from grade range label
|
||||
grade_contents = grade.get('grade', None)
|
||||
LOGGER.info(
|
||||
(
|
||||
u"Student %s does not have a grade for '%s', "
|
||||
u"so their certificate status has been set to '%s'. "
|
||||
u"No certificate generation task was sent to the XQueue."
|
||||
),
|
||||
student.id,
|
||||
unicode(course_id),
|
||||
cert.status
|
||||
)
|
||||
return cert
|
||||
|
||||
# Check to see whether the student is on the the embargoed
|
||||
# country restricted list. If so, they should not receive a
|
||||
# certificate -- set their status to restricted and log it.
|
||||
if self.restricted.filter(user=student).exists():
|
||||
cert.status = status.restricted
|
||||
cert.save()
|
||||
|
||||
LOGGER.info(
|
||||
(
|
||||
u"Student %s is in the embargoed country restricted "
|
||||
u"list, so their certificate status has been set to '%s' "
|
||||
u"for the course '%s'. "
|
||||
u"No certificate generation task was sent to the XQueue."
|
||||
),
|
||||
student.id,
|
||||
cert.status,
|
||||
unicode(course_id)
|
||||
)
|
||||
return cert
|
||||
|
||||
# Finally, generate the certificate and send it off.
|
||||
return self._generate_cert(cert, course, student, grade_contents, template_pdf, generate_pdf)
|
||||
|
||||
def _generate_cert(self, cert, course, student, grade_contents, template_pdf, generate_pdf):
|
||||
"""
|
||||
Generate a certificate for the student. If `generate_pdf` is True,
|
||||
sends a request to XQueue.
|
||||
"""
|
||||
course_id = unicode(course.id)
|
||||
|
||||
key = make_hashkey(random.random())
|
||||
cert.key = key
|
||||
contents = {
|
||||
'action': 'create',
|
||||
'username': student.username,
|
||||
'course_id': course_id,
|
||||
'course_name': course.display_name or course_id,
|
||||
'name': cert.name,
|
||||
'grade': grade_contents,
|
||||
'template_pdf': template_pdf,
|
||||
}
|
||||
if generate_pdf:
|
||||
cert.status = status.generating
|
||||
else:
|
||||
cert.status = status.downloadable
|
||||
cert.verify_uuid = uuid4().hex
|
||||
|
||||
cert.save()
|
||||
|
||||
if generate_pdf:
|
||||
try:
|
||||
grade_contents = lxml.html.fromstring(grade_contents).text_content()
|
||||
except (TypeError, XMLSyntaxError, ParserError) as exc:
|
||||
LOGGER.info(
|
||||
(
|
||||
u"Could not retrieve grade for student %s "
|
||||
u"in the course '%s' "
|
||||
u"because an exception occurred while parsing the "
|
||||
u"grade contents '%s' as HTML. "
|
||||
u"The exception was: '%s'"
|
||||
),
|
||||
student.id,
|
||||
unicode(course_id),
|
||||
grade_contents,
|
||||
unicode(exc)
|
||||
)
|
||||
|
||||
# Despite blowing up the xml parser, bad values here are fine
|
||||
grade_contents = None
|
||||
|
||||
if is_whitelisted or grade_contents is not None:
|
||||
|
||||
if is_whitelisted:
|
||||
LOGGER.info(
|
||||
u"Student %s is whitelisted in '%s'",
|
||||
student.id,
|
||||
unicode(course_id)
|
||||
)
|
||||
|
||||
# check to see whether the student is on the
|
||||
# the embargoed country restricted list
|
||||
# otherwise, put a new certificate request
|
||||
# on the queue
|
||||
|
||||
if self.restricted.filter(user=student).exists():
|
||||
new_status = status.restricted
|
||||
cert.status = new_status
|
||||
cert.save()
|
||||
|
||||
LOGGER.info(
|
||||
(
|
||||
u"Student %s is in the embargoed country restricted "
|
||||
u"list, so their certificate status has been set to '%s' "
|
||||
u"for the course '%s'. "
|
||||
u"No certificate generation task was sent to the XQueue."
|
||||
),
|
||||
student.id,
|
||||
new_status,
|
||||
unicode(course_id)
|
||||
)
|
||||
else:
|
||||
key = make_hashkey(random.random())
|
||||
cert.key = key
|
||||
contents = {
|
||||
'action': 'create',
|
||||
'username': student.username,
|
||||
'course_id': unicode(course_id),
|
||||
'course_name': course_name,
|
||||
'name': profile_name,
|
||||
'grade': grade_contents,
|
||||
'template_pdf': template_pdf,
|
||||
}
|
||||
if template_file:
|
||||
contents['template_pdf'] = template_file
|
||||
if generate_pdf:
|
||||
new_status = status.generating
|
||||
else:
|
||||
new_status = status.downloadable
|
||||
cert.verify_uuid = uuid4().hex
|
||||
|
||||
cert.status = new_status
|
||||
cert.save()
|
||||
|
||||
if generate_pdf:
|
||||
try:
|
||||
self._send_to_xqueue(contents, key)
|
||||
except XQueueAddToQueueError as exc:
|
||||
new_status = ExampleCertificate.STATUS_ERROR
|
||||
cert.status = new_status
|
||||
cert.error_reason = unicode(exc)
|
||||
cert.save()
|
||||
LOGGER.critical(
|
||||
(
|
||||
u"Could not add certificate task to XQueue. "
|
||||
u"The course was '%s' and the student was '%s'."
|
||||
u"The certificate task status has been marked as 'error' "
|
||||
u"and can be re-submitted with a management command."
|
||||
), course_id, student.id
|
||||
)
|
||||
else:
|
||||
LOGGER.info(
|
||||
(
|
||||
u"The certificate status has been set to '%s'. "
|
||||
u"Sent a certificate grading task to the XQueue "
|
||||
u"with the key '%s'. "
|
||||
),
|
||||
new_status,
|
||||
key
|
||||
)
|
||||
else:
|
||||
new_status = status.notpassing
|
||||
cert.status = new_status
|
||||
self._send_to_xqueue(contents, key)
|
||||
except XQueueAddToQueueError as exc:
|
||||
cert.status = ExampleCertificate.STATUS_ERROR
|
||||
cert.error_reason = unicode(exc)
|
||||
cert.save()
|
||||
|
||||
LOGGER.critical(
|
||||
(
|
||||
u"Could not add certificate task to XQueue. "
|
||||
u"The course was '%s' and the student was '%s'."
|
||||
u"The certificate task status has been marked as 'error' "
|
||||
u"and can be re-submitted with a management command."
|
||||
), course_id, student.id
|
||||
)
|
||||
else:
|
||||
LOGGER.info(
|
||||
(
|
||||
u"Student %s does not have a grade for '%s', "
|
||||
u"so their certificate status has been set to '%s'. "
|
||||
u"No certificate generation task was sent to the XQueue."
|
||||
u"The certificate status has been set to '%s'. "
|
||||
u"Sent a certificate grading task to the XQueue "
|
||||
u"with the key '%s'. "
|
||||
),
|
||||
student.id,
|
||||
unicode(course_id),
|
||||
new_status
|
||||
cert.status,
|
||||
key
|
||||
)
|
||||
|
||||
return new_status, cert
|
||||
return cert
|
||||
|
||||
def add_example_cert(self, example_cert):
|
||||
"""Add a task to create an example certificate.
|
||||
|
||||
@@ -231,7 +231,7 @@ class GenerateUserCertificatesTest(EventTestMixin, WebCertificateTestMixin, Modu
|
||||
certs_api.generate_user_certificates(self.student, self.course.id)
|
||||
|
||||
# Verify that the certificate has status 'generating'
|
||||
cert = GeneratedCertificate.objects.get(user=self.student, course_id=self.course.id)
|
||||
cert = GeneratedCertificate.eligible_certificates.get(user=self.student, course_id=self.course.id)
|
||||
self.assertEqual(cert.status, CertificateStatuses.generating)
|
||||
self.assert_event_emitted(
|
||||
'edx.certificate.created',
|
||||
@@ -249,7 +249,7 @@ class GenerateUserCertificatesTest(EventTestMixin, WebCertificateTestMixin, Modu
|
||||
certs_api.generate_user_certificates(self.student, self.course.id)
|
||||
|
||||
# Verify that the certificate has been marked with status error
|
||||
cert = GeneratedCertificate.objects.get(user=self.student, course_id=self.course.id)
|
||||
cert = GeneratedCertificate.eligible_certificates.get(user=self.student, course_id=self.course.id)
|
||||
self.assertEqual(cert.status, 'error')
|
||||
self.assertIn(self.ERROR_REASON, cert.error_reason)
|
||||
|
||||
@@ -263,7 +263,7 @@ class GenerateUserCertificatesTest(EventTestMixin, WebCertificateTestMixin, Modu
|
||||
certs_api.generate_user_certificates(self.student, self.course.id)
|
||||
|
||||
# Verify that the certificate has status 'downloadable'
|
||||
cert = GeneratedCertificate.objects.get(user=self.student, course_id=self.course.id)
|
||||
cert = GeneratedCertificate.eligible_certificates.get(user=self.student, course_id=self.course.id)
|
||||
self.assertEqual(cert.status, CertificateStatuses.downloadable)
|
||||
|
||||
@patch.dict(settings.FEATURES, {'CERTIFICATES_HTML_VIEW': False})
|
||||
|
||||
@@ -6,6 +6,7 @@ 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
|
||||
@@ -30,16 +31,17 @@ class CertificateManagementTest(ModuleStoreTestCase):
|
||||
for __ in range(3)
|
||||
]
|
||||
|
||||
def _create_cert(self, course_key, user, status):
|
||||
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
|
||||
course_id=course_key,
|
||||
mode=mode
|
||||
)
|
||||
|
||||
# Create the certificate
|
||||
GeneratedCertificate.objects.create(
|
||||
GeneratedCertificate.eligible_certificates.create(
|
||||
user=user,
|
||||
course_id=course_key,
|
||||
status=status
|
||||
@@ -52,7 +54,7 @@ class CertificateManagementTest(ModuleStoreTestCase):
|
||||
|
||||
def _assert_cert_status(self, course_key, user, expected_status):
|
||||
"""Check the status of a certificate. """
|
||||
cert = GeneratedCertificate.objects.get(user=user, course_id=course_key)
|
||||
cert = GeneratedCertificate.eligible_certificates.get(user=user, course_id=course_key)
|
||||
self.assertEqual(cert.status, expected_status)
|
||||
|
||||
|
||||
@@ -61,9 +63,10 @@ class CertificateManagementTest(ModuleStoreTestCase):
|
||||
class ResubmitErrorCertificatesTest(CertificateManagementTest):
|
||||
"""Tests for the resubmit_error_certificates management command. """
|
||||
|
||||
def test_resubmit_error_certificate(self):
|
||||
@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)
|
||||
self._create_cert(self.courses[0].id, self.user, CertificateStatuses.error, mode)
|
||||
|
||||
# Re-submit all certificates with status 'error'
|
||||
with check_mongo_calls(1):
|
||||
@@ -198,7 +201,7 @@ class RegenerateCertificatesTest(CertificateManagementTest):
|
||||
username=self.user.email, course=unicode(key), noop=False, insecure=True, template_file=None,
|
||||
grade_value=None
|
||||
)
|
||||
certificate = GeneratedCertificate.objects.get(
|
||||
certificate = GeneratedCertificate.eligible_certificates.get(
|
||||
user=self.user,
|
||||
course_id=key
|
||||
)
|
||||
@@ -236,7 +239,7 @@ class UngenerateCertificatesTest(CertificateManagementTest):
|
||||
course=unicode(key), noop=False, insecure=True, force=False
|
||||
)
|
||||
self.assertTrue(mock_send_to_queue.called)
|
||||
certificate = GeneratedCertificate.objects.get(
|
||||
certificate = GeneratedCertificate.eligible_certificates.get(
|
||||
user=self.user,
|
||||
course_id=key
|
||||
)
|
||||
|
||||
@@ -28,7 +28,7 @@ class CreateFakeCertTest(TestCase):
|
||||
cert_mode='verified',
|
||||
grade='0.89'
|
||||
)
|
||||
cert = GeneratedCertificate.objects.get(user=self.user, course_id=self.COURSE_KEY)
|
||||
cert = GeneratedCertificate.eligible_certificates.get(user=self.user, course_id=self.COURSE_KEY)
|
||||
self.assertEqual(cert.status, 'downloadable')
|
||||
self.assertEqual(cert.mode, 'verified')
|
||||
self.assertEqual(cert.grade, '0.89')
|
||||
@@ -41,7 +41,7 @@ class CreateFakeCertTest(TestCase):
|
||||
unicode(self.COURSE_KEY),
|
||||
cert_mode='honor'
|
||||
)
|
||||
cert = GeneratedCertificate.objects.get(user=self.user, course_id=self.COURSE_KEY)
|
||||
cert = GeneratedCertificate.eligible_certificates.get(user=self.user, course_id=self.COURSE_KEY)
|
||||
self.assertEqual(cert.mode, 'honor')
|
||||
|
||||
def test_too_few_args(self):
|
||||
|
||||
@@ -8,13 +8,21 @@ from django.test.utils import override_settings
|
||||
from nose.plugins.attrib import attr
|
||||
from path import Path as path
|
||||
|
||||
from opaque_keys.edx.locator import CourseLocator
|
||||
from certificates.models import (
|
||||
ExampleCertificate,
|
||||
ExampleCertificateSet,
|
||||
CertificateHtmlViewConfiguration,
|
||||
CertificateTemplateAsset,
|
||||
BadgeImageConfiguration)
|
||||
BadgeImageConfiguration,
|
||||
EligibleCertificateManager,
|
||||
GeneratedCertificate,
|
||||
CertificateStatuses,
|
||||
)
|
||||
from certificates.tests.factories import GeneratedCertificateFactory
|
||||
from opaque_keys.edx.locator import CourseLocator
|
||||
from student.tests.factories import UserFactory
|
||||
from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase
|
||||
from xmodule.modulestore.tests.factories import CourseFactory
|
||||
|
||||
FEATURES_INVALID_FILE_PATH = settings.FEATURES.copy()
|
||||
FEATURES_INVALID_FILE_PATH['CERTS_HTML_VIEW_CONFIG_PATH'] = 'invalid/path/to/config.json'
|
||||
@@ -234,3 +242,42 @@ class CertificateTemplateAssetTest(TestCase):
|
||||
|
||||
certificate_template_asset = CertificateTemplateAsset.objects.get(id=1)
|
||||
self.assertEqual(certificate_template_asset.asset, 'certificate_template_assets/1/picture2.jpg')
|
||||
|
||||
|
||||
@attr('shard_1')
|
||||
class EligibleCertificateManagerTest(SharedModuleStoreTestCase):
|
||||
"""
|
||||
Test the GeneratedCertificate model's object manager for filtering
|
||||
out ineligible certs.
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super(EligibleCertificateManagerTest, cls).setUpClass()
|
||||
cls.courses = (CourseFactory(), CourseFactory())
|
||||
|
||||
def setUp(self):
|
||||
super(EligibleCertificateManagerTest, self).setUp()
|
||||
self.user = UserFactory()
|
||||
self.eligible_cert = GeneratedCertificateFactory.create(
|
||||
status=CertificateStatuses.downloadable,
|
||||
user=self.user,
|
||||
course_id=self.courses[0].id # pylint: disable=no-member
|
||||
)
|
||||
self.ineligible_cert = GeneratedCertificateFactory.create(
|
||||
status=CertificateStatuses.audit_passing,
|
||||
user=self.user,
|
||||
course_id=self.courses[1].id # pylint: disable=no-member
|
||||
)
|
||||
|
||||
def test_filter_ineligible_certificates(self):
|
||||
"""
|
||||
Verify that the EligibleCertificateManager filters out
|
||||
certificates marked as ineligible, and that the default object
|
||||
manager for GeneratedCertificate does not filter them out.
|
||||
"""
|
||||
self.assertEqual(list(GeneratedCertificate.eligible_certificates.filter(user=self.user)), [self.eligible_cert])
|
||||
self.assertEqual(
|
||||
list(GeneratedCertificate.objects.filter(user=self.user)), # pylint: disable=no-member
|
||||
[self.eligible_cert, self.ineligible_cert]
|
||||
)
|
||||
|
||||
@@ -9,6 +9,7 @@ from nose.plugins.attrib import attr
|
||||
from django.test import TestCase
|
||||
from django.test.utils import override_settings
|
||||
|
||||
from course_modes.models import CourseMode
|
||||
from opaque_keys.edx.locator import CourseLocator
|
||||
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
|
||||
from student.tests.factories import UserFactory, CourseEnrollmentFactory
|
||||
@@ -22,13 +23,14 @@ from xmodule.modulestore.tests.factories import CourseFactory
|
||||
# in our `XQueueCertInterface` implementation.
|
||||
from capa.xqueue_interface import XQueueInterface
|
||||
|
||||
from certificates.queue import XQueueCertInterface
|
||||
from certificates.models import (
|
||||
ExampleCertificateSet,
|
||||
ExampleCertificate,
|
||||
GeneratedCertificate,
|
||||
CertificateStatuses,
|
||||
)
|
||||
from certificates.queue import XQueueCertInterface
|
||||
from certificates.tests.factories import CertificateWhitelistFactory, GeneratedCertificateFactory
|
||||
from lms.djangoapps.verify_student.tests.factories import SoftwareSecurePhotoVerificationFactory
|
||||
|
||||
|
||||
@@ -74,7 +76,7 @@ class XQueueCertInterfaceAddCertificateTest(ModuleStoreTestCase):
|
||||
|
||||
# Verify that add_cert method does not add message to queue
|
||||
self.assertFalse(mock_send.called)
|
||||
certificate = GeneratedCertificate.objects.get(user=self.user, course_id=self.course.id)
|
||||
certificate = GeneratedCertificate.eligible_certificates.get(user=self.user, course_id=self.course.id)
|
||||
self.assertEqual(certificate.status, CertificateStatuses.downloadable)
|
||||
self.assertIsNotNone(certificate.verify_uuid)
|
||||
|
||||
@@ -84,7 +86,11 @@ class XQueueCertInterfaceAddCertificateTest(ModuleStoreTestCase):
|
||||
template_name = 'certificate-template-{id.org}-{id.course}.pdf'.format(
|
||||
id=self.course.id
|
||||
)
|
||||
self.assert_queue_response(mode, mode, template_name)
|
||||
mock_send = self.add_cert_to_queue(mode)
|
||||
if CourseMode.is_eligible_for_certificate(mode):
|
||||
self.assert_certificate_generated(mock_send, mode, template_name)
|
||||
else:
|
||||
self.assert_ineligible_certificate_generated(mock_send, mode)
|
||||
|
||||
@ddt.data('credit', 'verified')
|
||||
def test_add_cert_with_verified_certificates(self, mode):
|
||||
@@ -95,10 +101,40 @@ class XQueueCertInterfaceAddCertificateTest(ModuleStoreTestCase):
|
||||
id=self.course.id
|
||||
)
|
||||
|
||||
self.assert_queue_response(mode, 'verified', template_name)
|
||||
mock_send = self.add_cert_to_queue(mode)
|
||||
self.assert_certificate_generated(mock_send, 'verified', template_name)
|
||||
|
||||
def assert_queue_response(self, mode, expected_mode, expected_template_name):
|
||||
"""Dry method for course enrollment and adding request to queue."""
|
||||
def test_ineligible_cert_whitelisted(self):
|
||||
"""Test that audit mode students can receive a certificate if they are whitelisted."""
|
||||
# Enroll as audit
|
||||
CourseEnrollmentFactory(
|
||||
user=self.user_2,
|
||||
course_id=self.course.id,
|
||||
is_active=True,
|
||||
mode='audit'
|
||||
)
|
||||
# Whitelist student
|
||||
CertificateWhitelistFactory(course_id=self.course.id, user=self.user_2)
|
||||
|
||||
# Generate certs
|
||||
with patch('courseware.grades.grade', Mock(return_value={'grade': 'Pass', 'percent': 0.75})):
|
||||
with patch.object(XQueueInterface, 'send_to_queue') as mock_send:
|
||||
mock_send.return_value = (0, None)
|
||||
self.xqueue.add_cert(self.user_2, self.course.id)
|
||||
|
||||
# Assert cert generated correctly
|
||||
self.assertTrue(mock_send.called)
|
||||
certificate = GeneratedCertificate.certificate_for_student(self.user_2, self.course.id)
|
||||
self.assertIsNotNone(certificate)
|
||||
self.assertEqual(certificate.mode, 'audit')
|
||||
|
||||
def add_cert_to_queue(self, mode):
|
||||
"""
|
||||
Dry method for course enrollment and adding request to
|
||||
queue. Returns a mock object containing information about the
|
||||
`XQueueInterface.send_to_queue` method, which can be used in other
|
||||
assertions.
|
||||
"""
|
||||
CourseEnrollmentFactory(
|
||||
user=self.user_2,
|
||||
course_id=self.course.id,
|
||||
@@ -109,19 +145,97 @@ class XQueueCertInterfaceAddCertificateTest(ModuleStoreTestCase):
|
||||
with patch.object(XQueueInterface, 'send_to_queue') as mock_send:
|
||||
mock_send.return_value = (0, None)
|
||||
self.xqueue.add_cert(self.user_2, self.course.id)
|
||||
return mock_send
|
||||
|
||||
def assert_certificate_generated(self, mock_send, expected_mode, expected_template_name):
|
||||
"""
|
||||
Assert that a certificate was generated with the correct mode and
|
||||
template type.
|
||||
"""
|
||||
# Verify that the task was sent to the queue with the correct callback URL
|
||||
self.assertTrue(mock_send.called)
|
||||
__, kwargs = mock_send.call_args_list[0]
|
||||
|
||||
actual_header = json.loads(kwargs['header'])
|
||||
self.assertIn('https://edx.org/update_certificate?key=', actual_header['lms_callback_url'])
|
||||
certificate = GeneratedCertificate.objects.get(user=self.user_2, course_id=self.course.id)
|
||||
self.assertEqual(certificate.mode, expected_mode)
|
||||
|
||||
body = json.loads(kwargs['body'])
|
||||
self.assertIn(expected_template_name, body['template_pdf'])
|
||||
|
||||
certificate = GeneratedCertificate.eligible_certificates.get(user=self.user_2, course_id=self.course.id)
|
||||
self.assertEqual(certificate.mode, expected_mode)
|
||||
|
||||
def assert_ineligible_certificate_generated(self, mock_send, expected_mode):
|
||||
"""
|
||||
Assert that an ineligible certificate was generated with the
|
||||
correct mode.
|
||||
"""
|
||||
# Ensure the certificate was not generated
|
||||
self.assertFalse(mock_send.called)
|
||||
|
||||
certificate = GeneratedCertificate.objects.get( # pylint: disable=no-member
|
||||
user=self.user_2,
|
||||
course_id=self.course.id
|
||||
)
|
||||
|
||||
self.assertIn(certificate.status, (CertificateStatuses.audit_passing, CertificateStatuses.audit_notpassing))
|
||||
self.assertEqual(certificate.mode, expected_mode)
|
||||
|
||||
@ddt.data(
|
||||
(CertificateStatuses.restricted, False),
|
||||
(CertificateStatuses.deleting, False),
|
||||
(CertificateStatuses.generating, True),
|
||||
(CertificateStatuses.unavailable, True),
|
||||
(CertificateStatuses.deleted, True),
|
||||
(CertificateStatuses.error, True),
|
||||
(CertificateStatuses.notpassing, True),
|
||||
(CertificateStatuses.downloadable, True),
|
||||
(CertificateStatuses.auditing, True),
|
||||
)
|
||||
@ddt.unpack
|
||||
def test_add_cert_statuses(self, status, should_generate):
|
||||
"""
|
||||
Test that certificates can or cannot be generated with the given
|
||||
certificate status.
|
||||
"""
|
||||
with patch('certificates.queue.certificate_status_for_student', Mock(return_value={'status': status})):
|
||||
mock_send = self.add_cert_to_queue('verified')
|
||||
if should_generate:
|
||||
self.assertTrue(mock_send.called)
|
||||
else:
|
||||
self.assertFalse(mock_send.called)
|
||||
|
||||
def test_regen_audit_certs_eligibility(self):
|
||||
"""
|
||||
Test that existing audit certificates remain eligible even if cert
|
||||
generation is re-run.
|
||||
"""
|
||||
# Create an existing audit enrollment and certificate
|
||||
CourseEnrollmentFactory(
|
||||
user=self.user_2,
|
||||
course_id=self.course.id,
|
||||
is_active=True,
|
||||
mode=CourseMode.AUDIT,
|
||||
)
|
||||
GeneratedCertificateFactory(
|
||||
user=self.user_2,
|
||||
course_id=self.course.id,
|
||||
grade='1.0',
|
||||
status=CertificateStatuses.downloadable,
|
||||
mode=GeneratedCertificate.MODES.audit,
|
||||
)
|
||||
|
||||
# Run grading/cert generation again
|
||||
with patch('courseware.grades.grade', Mock(return_value={'grade': 'Pass', 'percent': 0.75})):
|
||||
with patch.object(XQueueInterface, 'send_to_queue') as mock_send:
|
||||
mock_send.return_value = (0, None)
|
||||
self.xqueue.add_cert(self.user_2, self.course.id)
|
||||
|
||||
self.assertEqual(
|
||||
GeneratedCertificate.objects.get(user=self.user_2, course_id=self.course.id).status, # pylint: disable=no-member
|
||||
CertificateStatuses.generating
|
||||
)
|
||||
|
||||
|
||||
@attr('shard_1')
|
||||
@override_settings(CERT_QUEUE='certificates')
|
||||
|
||||
@@ -71,7 +71,7 @@ class CertificateSupportTestCase(ModuleStoreTestCase):
|
||||
)
|
||||
|
||||
# Create certificates for the student
|
||||
self.cert = GeneratedCertificate.objects.create(
|
||||
self.cert = GeneratedCertificate.eligible_certificates.create(
|
||||
user=self.student,
|
||||
course_id=self.CERT_COURSE_KEY,
|
||||
grade=self.CERT_GRADE,
|
||||
@@ -259,7 +259,7 @@ class CertificateRegenerateTests(CertificateSupportTestCase):
|
||||
# Check that the user's certificate was updated
|
||||
# Since the student hasn't actually passed the course,
|
||||
# we'd expect that the certificate status will be "notpassing"
|
||||
cert = GeneratedCertificate.objects.get(user=self.student)
|
||||
cert = GeneratedCertificate.eligible_certificates.get(user=self.student)
|
||||
self.assertEqual(cert.status, CertificateStatuses.notpassing)
|
||||
|
||||
def test_regenerate_certificate_missing_params(self):
|
||||
@@ -298,7 +298,7 @@ class CertificateRegenerateTests(CertificateSupportTestCase):
|
||||
|
||||
def test_regenerate_user_has_no_certificate(self):
|
||||
# Delete the user's certificate
|
||||
GeneratedCertificate.objects.all().delete()
|
||||
GeneratedCertificate.eligible_certificates.all().delete()
|
||||
|
||||
# Should be able to regenerate
|
||||
response = self._regenerate(
|
||||
@@ -308,7 +308,7 @@ class CertificateRegenerateTests(CertificateSupportTestCase):
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
# A new certificate is created
|
||||
num_certs = GeneratedCertificate.objects.filter(user=self.student).count()
|
||||
num_certs = GeneratedCertificate.eligible_certificates.filter(user=self.student).count()
|
||||
self.assertEqual(num_certs, 1)
|
||||
|
||||
def _regenerate(self, course_key=None, username=None):
|
||||
@@ -412,7 +412,7 @@ class CertificateGenerateTests(CertificateSupportTestCase):
|
||||
|
||||
def test_generate_user_has_no_certificate(self):
|
||||
# Delete the user's certificate
|
||||
GeneratedCertificate.objects.all().delete()
|
||||
GeneratedCertificate.eligible_certificates.all().delete()
|
||||
|
||||
# Should be able to generate
|
||||
response = self._generate(
|
||||
@@ -422,7 +422,7 @@ class CertificateGenerateTests(CertificateSupportTestCase):
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
# A new certificate is created
|
||||
num_certs = GeneratedCertificate.objects.filter(user=self.student).count()
|
||||
num_certs = GeneratedCertificate.eligible_certificates.filter(user=self.student).count()
|
||||
self.assertEqual(num_certs, 1)
|
||||
|
||||
def _generate(self, course_key=None, username=None):
|
||||
|
||||
@@ -210,7 +210,7 @@ class MicrositeCertificatesViewsTests(ModuleStoreTestCase):
|
||||
self.user.profile.name = "Joe User"
|
||||
self.user.profile.save()
|
||||
self.client.login(username=self.user.username, password='foo')
|
||||
self.cert = GeneratedCertificate.objects.create(
|
||||
self.cert = GeneratedCertificate.eligible_certificates.create(
|
||||
user=self.user,
|
||||
course_id=self.course_id,
|
||||
download_uuid=uuid4(),
|
||||
|
||||
@@ -13,6 +13,7 @@ from django.core.urlresolvers import reverse
|
||||
from django.test.client import Client
|
||||
from django.test.utils import override_settings
|
||||
|
||||
from course_modes.models import CourseMode
|
||||
from openedx.core.lib.tests.assertions.events import assert_event_matches
|
||||
from student.tests.factories import UserFactory, CourseEnrollmentFactory
|
||||
from student.roles import CourseStaffRole
|
||||
@@ -96,7 +97,8 @@ class CertificatesViewsTests(ModuleStoreTestCase, EventTrackingTestCase):
|
||||
)
|
||||
CourseEnrollmentFactory.create(
|
||||
user=self.user,
|
||||
course_id=self.course_id
|
||||
course_id=self.course_id,
|
||||
mode=CourseMode.HONOR,
|
||||
)
|
||||
CertificateHtmlViewConfigurationFactory.create()
|
||||
LinkedInAddToProfileConfigurationFactory.create()
|
||||
@@ -378,6 +380,38 @@ class CertificatesViewsTests(ModuleStoreTestCase, EventTrackingTestCase):
|
||||
self.assertIn("Cannot Find Certificate", response.content)
|
||||
self.assertIn("We cannot find a certificate with this URL or ID number.", response.content)
|
||||
|
||||
@ddt.data(
|
||||
(CertificateStatuses.downloadable, True),
|
||||
(CertificateStatuses.audit_passing, False),
|
||||
(CertificateStatuses.audit_notpassing, False),
|
||||
)
|
||||
@ddt.unpack
|
||||
@override_settings(FEATURES=FEATURES_WITH_CERTS_ENABLED)
|
||||
def test_audit_certificate_display(self, status, eligible_for_certificate):
|
||||
"""
|
||||
Ensure that audit-mode certs are only shown in the web view if they
|
||||
are eligible for a certificate.
|
||||
"""
|
||||
# Convert the cert to audit, with the specified eligibility
|
||||
self.cert.mode = 'audit'
|
||||
self.cert.status = status
|
||||
self.cert.save()
|
||||
|
||||
self._add_course_certificates(count=1, signatory_count=2)
|
||||
test_url = get_certificate_url(
|
||||
user_id=self.user.id,
|
||||
course_id=unicode(self.course.id)
|
||||
)
|
||||
response = self.client.get(test_url)
|
||||
|
||||
if eligible_for_certificate:
|
||||
self.assertIn(str(self.cert.verify_uuid), response.content)
|
||||
else:
|
||||
self.assertIn("Invalid Certificate", response.content)
|
||||
self.assertIn("Cannot Find Certificate", response.content)
|
||||
self.assertIn("We cannot find a certificate with this URL or ID number.", response.content)
|
||||
self.assertNotIn(str(self.cert.verify_uuid), response.content)
|
||||
|
||||
@override_settings(FEATURES=FEATURES_WITH_CERTS_ENABLED)
|
||||
def test_html_view_for_invalid_certificate(self):
|
||||
"""
|
||||
@@ -533,7 +567,7 @@ class CertificatesViewsTests(ModuleStoreTestCase, EventTrackingTestCase):
|
||||
course_id=unicode(self.course.id)
|
||||
)
|
||||
self.cert.delete()
|
||||
self.assertEqual(len(GeneratedCertificate.objects.all()), 0)
|
||||
self.assertEqual(len(GeneratedCertificate.eligible_certificates.all()), 0)
|
||||
|
||||
response = self.client.get(test_url)
|
||||
self.assertIn('invalid', response.content)
|
||||
@@ -556,7 +590,7 @@ class CertificatesViewsTests(ModuleStoreTestCase, EventTrackingTestCase):
|
||||
preview mode. Either the certificate is marked active or not.
|
||||
"""
|
||||
self.cert.delete()
|
||||
self.assertEqual(len(GeneratedCertificate.objects.all()), 0)
|
||||
self.assertEqual(len(GeneratedCertificate.eligible_certificates.all()), 0)
|
||||
self._add_course_certificates(count=1, signatory_count=2)
|
||||
test_url = get_certificate_url(
|
||||
user_id=self.user.id,
|
||||
|
||||
@@ -342,7 +342,7 @@ def _get_user_certificate(request, user, course_key, course, preview_mode=None):
|
||||
else:
|
||||
# certificate is being viewed by learner or public
|
||||
try:
|
||||
user_certificate = GeneratedCertificate.objects.get(
|
||||
user_certificate = GeneratedCertificate.eligible_certificates.get(
|
||||
user=user,
|
||||
course_id=course_key,
|
||||
status=CertificateStatuses.downloadable
|
||||
@@ -459,7 +459,7 @@ def render_cert_by_uuid(request, certificate_uuid):
|
||||
This public view generates an HTML representation of the specified certificate
|
||||
"""
|
||||
try:
|
||||
certificate = GeneratedCertificate.objects.get(
|
||||
certificate = GeneratedCertificate.eligible_certificates.get(
|
||||
verify_uuid=certificate_uuid,
|
||||
status=CertificateStatuses.downloadable
|
||||
)
|
||||
|
||||
@@ -75,7 +75,7 @@ def update_certificate(request):
|
||||
try:
|
||||
course_key = SlashSeparatedCourseKey.from_deprecated_string(xqueue_body['course_id'])
|
||||
|
||||
cert = GeneratedCertificate.objects.get(
|
||||
cert = GeneratedCertificate.eligible_certificates.get(
|
||||
user__username=xqueue_body['username'],
|
||||
course_id=course_key,
|
||||
key=xqueue_header['lms_key'])
|
||||
|
||||
@@ -619,7 +619,7 @@ class CertificateExceptionViewInstructorApiTest(SharedModuleStoreTestCase):
|
||||
# Verify that certificate exception successfully removed from CertificateWhitelist and GeneratedCertificate
|
||||
with self.assertRaises(ObjectDoesNotExist):
|
||||
CertificateWhitelist.objects.get(user=self.user2, course_id=self.course.id)
|
||||
GeneratedCertificate.objects.get(
|
||||
GeneratedCertificate.eligible_certificates.get(
|
||||
user=self.user2, course_id=self.course.id, status__not=CertificateStatuses.unavailable
|
||||
)
|
||||
|
||||
@@ -1010,7 +1010,7 @@ class CertificateInvalidationViewTests(SharedModuleStoreTestCase):
|
||||
self.fail("The certificate is not invalidated.")
|
||||
|
||||
# Validate generated certificate was invalidated
|
||||
generated_certificate = GeneratedCertificate.objects.get(
|
||||
generated_certificate = GeneratedCertificate.eligible_certificates.get(
|
||||
user=self.enrolled_user_1,
|
||||
course_id=self.course.id,
|
||||
)
|
||||
|
||||
@@ -2875,7 +2875,7 @@ def add_certificate_exception(course_key, student, certificate_exception):
|
||||
}
|
||||
)
|
||||
|
||||
generated_certificate = GeneratedCertificate.objects.filter(
|
||||
generated_certificate = GeneratedCertificate.eligible_certificates.filter(
|
||||
user=student,
|
||||
course_id=course_key,
|
||||
status=CertificateStatuses.downloadable,
|
||||
@@ -2912,7 +2912,10 @@ def remove_certificate_exception(course_key, student):
|
||||
)
|
||||
|
||||
try:
|
||||
generated_certificate = GeneratedCertificate.objects.get(user=student, course_id=course_key)
|
||||
generated_certificate = GeneratedCertificate.objects.get( # pylint: disable=no-member
|
||||
user=student,
|
||||
course_id=course_key
|
||||
)
|
||||
generated_certificate.invalidate()
|
||||
except ObjectDoesNotExist:
|
||||
# Certificate has not been generated yet, so just remove the certificate exception from white list
|
||||
|
||||
@@ -185,7 +185,7 @@ def issued_certificates(course_key, features):
|
||||
|
||||
report_run_date = datetime.date.today().strftime("%B %d, %Y")
|
||||
certificate_features = [x for x in CERTIFICATE_FEATURES if x in features]
|
||||
generated_certificates = list(GeneratedCertificate.objects.filter(
|
||||
generated_certificates = list(GeneratedCertificate.eligible_certificates.filter(
|
||||
course_id=course_key,
|
||||
status=CertificateStatuses.downloadable
|
||||
).values(*certificate_features).annotate(total_issued_certificate=Count('mode')))
|
||||
|
||||
@@ -1584,7 +1584,7 @@ def invalidate_generated_certificates(course_id, enrolled_students, certificate_
|
||||
:param enrolled_students: (queryset or list) students enrolled in the course
|
||||
:param certificate_statuses: certificates statuses for whom to remove generated certificate
|
||||
"""
|
||||
certificates = GeneratedCertificate.objects.filter(
|
||||
certificates = GeneratedCertificate.objects.filter( # pylint: disable=no-member
|
||||
user__in=enrolled_students,
|
||||
course_id=course_id,
|
||||
status__in=certificate_statuses,
|
||||
|
||||
@@ -1802,7 +1802,7 @@ class TestCertificateGeneration(InstructorTaskModuleTestCase):
|
||||
},
|
||||
result
|
||||
)
|
||||
generated_certificates = GeneratedCertificate.objects.filter(
|
||||
generated_certificates = GeneratedCertificate.eligible_certificates.filter(
|
||||
user__in=students,
|
||||
course_id=self.course.id,
|
||||
mode='honor'
|
||||
@@ -1912,7 +1912,7 @@ class TestCertificateGeneration(InstructorTaskModuleTestCase):
|
||||
result
|
||||
)
|
||||
|
||||
generated_certificates = GeneratedCertificate.objects.filter(
|
||||
generated_certificates = GeneratedCertificate.eligible_certificates.filter(
|
||||
user__in=students,
|
||||
course_id=self.course.id,
|
||||
mode='honor'
|
||||
|
||||
Reference in New Issue
Block a user