Adding logic to check if cert is invalidate then user cannot see the regen button.
ECOM-4217
This commit is contained in:
@@ -21,6 +21,7 @@ from util.organizations_helpers import get_course_organizations
|
||||
from certificates.models import (
|
||||
CertificateGenerationConfiguration,
|
||||
CertificateGenerationCourseSetting,
|
||||
CertificateInvalidation,
|
||||
CertificateStatuses,
|
||||
CertificateTemplate,
|
||||
CertificateTemplateAsset,
|
||||
@@ -273,6 +274,26 @@ def set_cert_generation_enabled(course_key, is_enabled):
|
||||
log.info(u"Disabled self-generated certificates for course '%s'.", unicode(course_key))
|
||||
|
||||
|
||||
def is_certificate_invalid(student, course_key):
|
||||
"""Check that whether the student in the course has been invalidated
|
||||
for receiving certificates.
|
||||
|
||||
Arguments:
|
||||
student (user object): logged-in user
|
||||
course_key (CourseKey): The course identifier.
|
||||
|
||||
Returns:
|
||||
Boolean denoting whether the student in the course is invalidated
|
||||
to receive certificates
|
||||
"""
|
||||
is_invalid = False
|
||||
certificate = GeneratedCertificate.certificate_for_student(student, course_key)
|
||||
if certificate is not None:
|
||||
is_invalid = CertificateInvalidation.has_certificate_invalidation(student, course_key)
|
||||
|
||||
return is_invalid
|
||||
|
||||
|
||||
def cert_generation_enabled(course_key):
|
||||
"""Check whether certificate generation is enabled for a course.
|
||||
|
||||
|
||||
@@ -436,6 +436,25 @@ class CertificateInvalidation(TimeStampedModel):
|
||||
})
|
||||
return data
|
||||
|
||||
@classmethod
|
||||
def has_certificate_invalidation(cls, student, course_key):
|
||||
"""Check that whether the student in the course has been invalidated
|
||||
for receiving certificates.
|
||||
|
||||
Arguments:
|
||||
student (user): logged-in user
|
||||
course_key (CourseKey): The course associated with the certificate.
|
||||
|
||||
Returns:
|
||||
Boolean denoting whether the student in the course is invalidated
|
||||
to receive certificates
|
||||
"""
|
||||
return cls.objects.filter(
|
||||
generated_certificate__course_id=course_key,
|
||||
active=True,
|
||||
generated_certificate__user=student
|
||||
).exists()
|
||||
|
||||
|
||||
@receiver(COURSE_CERT_AWARDED, sender=GeneratedCertificate)
|
||||
def handle_course_cert_awarded(sender, user, course_key, **kwargs): # pylint: disable=unused-argument
|
||||
|
||||
@@ -13,6 +13,7 @@ from opaque_keys.edx.locator import CourseLocator
|
||||
from config_models.models import cache
|
||||
from course_modes.models import CourseMode
|
||||
from course_modes.tests.factories import CourseModeFactory
|
||||
from courseware.tests.factories import GlobalStaffFactory
|
||||
from microsite_configuration import microsite
|
||||
from student.models import CourseEnrollment
|
||||
from student.tests.factories import UserFactory
|
||||
@@ -32,7 +33,10 @@ from certificates.models import (
|
||||
certificate_status_for_student,
|
||||
)
|
||||
from certificates.queue import XQueueCertInterface, XQueueAddToQueueError
|
||||
from certificates.tests.factories import GeneratedCertificateFactory
|
||||
from certificates.tests.factories import (
|
||||
CertificateInvalidationFactory,
|
||||
GeneratedCertificateFactory
|
||||
)
|
||||
|
||||
|
||||
FEATURES_WITH_CERTS_ENABLED = settings.FEATURES.copy()
|
||||
@@ -208,6 +212,118 @@ class CertificateDownloadableStatusTests(WebCertificateTestMixin, ModuleStoreTes
|
||||
)
|
||||
|
||||
|
||||
@attr('shard_1')
|
||||
@ddt.ddt
|
||||
class CertificateisInvalid(WebCertificateTestMixin, ModuleStoreTestCase):
|
||||
"""Tests for the `is_certificate_invalid` helper function. """
|
||||
|
||||
def setUp(self):
|
||||
super(CertificateisInvalid, self).setUp()
|
||||
|
||||
self.student = UserFactory()
|
||||
self.course = CourseFactory.create(
|
||||
org='edx',
|
||||
number='verified',
|
||||
display_name='Verified Course'
|
||||
)
|
||||
self.global_staff = GlobalStaffFactory()
|
||||
self.request_factory = RequestFactory()
|
||||
|
||||
def test_method_with_no_certificate(self):
|
||||
""" Test the case when there is no certificate for a user for a specific course. """
|
||||
course = CourseFactory.create(
|
||||
org='edx',
|
||||
number='honor',
|
||||
display_name='Course 1'
|
||||
)
|
||||
# Also check query count for 'is_certificate_invalid' method.
|
||||
with self.assertNumQueries(1):
|
||||
self.assertFalse(
|
||||
certs_api.is_certificate_invalid(self.student, course.id)
|
||||
)
|
||||
|
||||
@ddt.data(
|
||||
CertificateStatuses.generating,
|
||||
CertificateStatuses.downloadable,
|
||||
CertificateStatuses.notpassing,
|
||||
CertificateStatuses.error,
|
||||
CertificateStatuses.unverified,
|
||||
CertificateStatuses.deleted,
|
||||
CertificateStatuses.unavailable,
|
||||
)
|
||||
def test_method_with_invalidated_cert(self, status):
|
||||
""" Verify that if certificate is marked as invalid than method will return
|
||||
True. """
|
||||
generated_cert = self._generate_cert(status)
|
||||
self._invalidate_certificate(generated_cert, True)
|
||||
self.assertTrue(
|
||||
certs_api.is_certificate_invalid(self.student, self.course.id)
|
||||
)
|
||||
|
||||
@ddt.data(
|
||||
CertificateStatuses.generating,
|
||||
CertificateStatuses.downloadable,
|
||||
CertificateStatuses.notpassing,
|
||||
CertificateStatuses.error,
|
||||
CertificateStatuses.unverified,
|
||||
CertificateStatuses.deleted,
|
||||
CertificateStatuses.unavailable,
|
||||
)
|
||||
def test_method_with_inactive_invalidated_cert(self, status):
|
||||
""" Verify that if certificate is valid but it's invalidated status is
|
||||
false than method will return false. """
|
||||
generated_cert = self._generate_cert(status)
|
||||
self._invalidate_certificate(generated_cert, False)
|
||||
self.assertFalse(
|
||||
certs_api.is_certificate_invalid(self.student, self.course.id)
|
||||
)
|
||||
|
||||
@ddt.data(
|
||||
CertificateStatuses.generating,
|
||||
CertificateStatuses.downloadable,
|
||||
CertificateStatuses.notpassing,
|
||||
CertificateStatuses.error,
|
||||
CertificateStatuses.unverified,
|
||||
CertificateStatuses.deleted,
|
||||
CertificateStatuses.unavailable,
|
||||
)
|
||||
def test_method_with_all_statues(self, status):
|
||||
""" Verify method return True if certificate has valid status but it is
|
||||
marked as invalid in CertificateInvalidation table. """
|
||||
|
||||
certificate = self._generate_cert(status)
|
||||
CertificateInvalidationFactory.create(
|
||||
generated_certificate=certificate,
|
||||
invalidated_by=self.global_staff,
|
||||
active=True
|
||||
)
|
||||
# Also check query count for 'is_certificate_invalid' method.
|
||||
with self.assertNumQueries(2):
|
||||
self.assertTrue(
|
||||
certs_api.is_certificate_invalid(self.student, self.course.id)
|
||||
)
|
||||
|
||||
def _invalidate_certificate(self, certificate, active):
|
||||
""" Dry method to mark certificate as invalid. """
|
||||
CertificateInvalidationFactory.create(
|
||||
generated_certificate=certificate,
|
||||
invalidated_by=self.global_staff,
|
||||
active=active
|
||||
)
|
||||
# Invalidate user certificate
|
||||
certificate.invalidate()
|
||||
self.assertFalse(certificate.is_valid())
|
||||
|
||||
def _generate_cert(self, status):
|
||||
""" Dry method to generate certificate. """
|
||||
return GeneratedCertificateFactory.create(
|
||||
user=self.student,
|
||||
course_id=self.course.id,
|
||||
status=status,
|
||||
mode='verified'
|
||||
)
|
||||
|
||||
|
||||
@attr('shard_1')
|
||||
class CertificateGetTests(SharedModuleStoreTestCase):
|
||||
"""Tests for the `test_get_certificate_for_user` helper function. """
|
||||
|
||||
@@ -15,11 +15,15 @@ from certificates.models import (
|
||||
ExampleCertificateSet,
|
||||
CertificateHtmlViewConfiguration,
|
||||
CertificateTemplateAsset,
|
||||
CertificateInvalidation,
|
||||
GeneratedCertificate,
|
||||
CertificateStatuses,
|
||||
CertificateGenerationHistory,
|
||||
)
|
||||
from certificates.tests.factories import GeneratedCertificateFactory
|
||||
from certificates.tests.factories import (
|
||||
CertificateInvalidationFactory,
|
||||
GeneratedCertificateFactory
|
||||
)
|
||||
from instructor_task.tests.factories import InstructorTaskFactory
|
||||
from opaque_keys.edx.locator import CourseLocator
|
||||
from student.tests.factories import AdminFactory, UserFactory
|
||||
@@ -300,3 +304,50 @@ class TestCertificateGenerationHistory(TestCase):
|
||||
certificate_generation_history.get_task_name(),
|
||||
expected
|
||||
)
|
||||
|
||||
|
||||
@attr('shard_1')
|
||||
class CertificateInvalidationTest(SharedModuleStoreTestCase):
|
||||
"""
|
||||
Test for the Certificate Invalidation model.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
super(CertificateInvalidationTest, self).setUp()
|
||||
self.course = CourseFactory()
|
||||
self.user = UserFactory()
|
||||
self.course_id = self.course.id # pylint: disable=no-member
|
||||
self.certificate = GeneratedCertificateFactory.create(
|
||||
status=CertificateStatuses.downloadable,
|
||||
user=self.user,
|
||||
course_id=self.course_id
|
||||
)
|
||||
|
||||
def test_is_certificate_invalid_method(self):
|
||||
""" Verify that method return false if certificate is valid. """
|
||||
|
||||
self.assertFalse(
|
||||
CertificateInvalidation.has_certificate_invalidation(self.user, self.course_id)
|
||||
)
|
||||
|
||||
def test_is_certificate_invalid_with_invalid_cert(self):
|
||||
""" Verify that method return true if certificate is invalid. """
|
||||
|
||||
invalid_cert = CertificateInvalidationFactory.create(
|
||||
generated_certificate=self.certificate,
|
||||
invalidated_by=self.user
|
||||
)
|
||||
# Invalidate user certificate
|
||||
self.certificate.invalidate()
|
||||
self.assertTrue(
|
||||
CertificateInvalidation.has_certificate_invalidation(self.user, self.course_id)
|
||||
)
|
||||
|
||||
# mark the entry as in-active.
|
||||
invalid_cert.active = False
|
||||
invalid_cert.save()
|
||||
|
||||
# After making the certificate valid method will return false.
|
||||
self.assertFalse(
|
||||
CertificateInvalidation.has_certificate_invalidation(self.user, self.course_id)
|
||||
)
|
||||
|
||||
@@ -32,7 +32,10 @@ import courseware.views.views as views
|
||||
import shoppingcart
|
||||
from certificates import api as certs_api
|
||||
from certificates.models import CertificateStatuses, CertificateGenerationConfiguration
|
||||
from certificates.tests.factories import GeneratedCertificateFactory
|
||||
from certificates.tests.factories import (
|
||||
CertificateInvalidationFactory,
|
||||
GeneratedCertificateFactory
|
||||
)
|
||||
from commerce.models import CommerceConfiguration
|
||||
from course_modes.models import CourseMode
|
||||
from course_modes.tests.factories import CourseModeFactory
|
||||
@@ -1387,6 +1390,89 @@ class ProgressPageTests(ModuleStoreTestCase):
|
||||
cert_button_hidden,
|
||||
'Request Certificate' not in resp.content)
|
||||
|
||||
@patch.dict('django.conf.settings.FEATURES', {'CERTIFICATES_HTML_VIEW': True})
|
||||
@patch('courseware.grades.grade', Mock(return_value={'grade': 'Pass', 'percent': 0.75, 'section_breakdown': [],
|
||||
'grade_breakdown': []}))
|
||||
def test_page_with_invalidated_certificate_with_html_view(self):
|
||||
"""
|
||||
Verify that for html certs if certificate is marked as invalidated than
|
||||
re-generate button should not appear on progress page.
|
||||
"""
|
||||
generated_certificate = self.generate_certificate(
|
||||
"http://www.example.com/certificate.pdf", "honor"
|
||||
)
|
||||
CertificateGenerationConfiguration(enabled=True).save()
|
||||
certs_api.set_cert_generation_enabled(self.course.id, True)
|
||||
|
||||
# Course certificate configurations
|
||||
certificates = [
|
||||
{
|
||||
'id': 1,
|
||||
'name': 'dummy',
|
||||
'description': 'dummy description',
|
||||
'course_title': 'dummy title',
|
||||
'signatories': [],
|
||||
'version': 1,
|
||||
'is_active': True
|
||||
}
|
||||
]
|
||||
self.course.certificates = {'certificates': certificates}
|
||||
self.course.cert_html_view_enabled = True
|
||||
self.course.save()
|
||||
self.store.update_item(self.course, self.user.id)
|
||||
|
||||
resp = self.client.get(
|
||||
reverse('progress', args=[unicode(self.course.id)])
|
||||
)
|
||||
self.assertContains(resp, u"View Certificate")
|
||||
self.assert_invalidate_certificate(generated_certificate)
|
||||
|
||||
@patch('courseware.grades.grade', Mock(return_value={'grade': 'Pass', 'percent': 0.75, 'section_breakdown': [],
|
||||
'grade_breakdown': []}))
|
||||
def test_page_with_invalidated_certificate_with_pdf(self):
|
||||
"""
|
||||
Verify that for pdf certs if certificate is marked as invalidated than
|
||||
re-generate button should not appear on progress page.
|
||||
"""
|
||||
generated_certificate = self.generate_certificate(
|
||||
"http://www.example.com/certificate.pdf", "honor"
|
||||
)
|
||||
|
||||
CertificateGenerationConfiguration(enabled=True).save()
|
||||
certs_api.set_cert_generation_enabled(self.course.id, True)
|
||||
resp = self.client.get(
|
||||
reverse('progress', args=[unicode(self.course.id)])
|
||||
)
|
||||
self.assertContains(resp, u'Download Your Certificate')
|
||||
self.assert_invalidate_certificate(generated_certificate)
|
||||
|
||||
def assert_invalidate_certificate(self, certificate):
|
||||
""" Dry method to mark certificate as invalid. And assert the response. """
|
||||
CertificateInvalidationFactory.create(
|
||||
generated_certificate=certificate,
|
||||
invalidated_by=self.user
|
||||
)
|
||||
# Invalidate user certificate
|
||||
certificate.invalidate()
|
||||
resp = self.client.get(
|
||||
reverse('progress', args=[unicode(self.course.id)])
|
||||
)
|
||||
self.assertNotContains(resp, u'Request Certificate')
|
||||
self.assertContains(resp, u'Your certificate has been invalidated.')
|
||||
self.assertContains(resp, u'Please contact your course team if you have any questions.')
|
||||
self.assertNotContains(resp, u'View Your Certificate')
|
||||
self.assertNotContains(resp, u'Download Your Certificate')
|
||||
|
||||
def generate_certificate(self, url, mode):
|
||||
""" Dry method to generate certificate. """
|
||||
return GeneratedCertificateFactory.create(
|
||||
user=self.user,
|
||||
course_id=self.course.id,
|
||||
status=CertificateStatuses.downloadable,
|
||||
download_url=url,
|
||||
mode=mode
|
||||
)
|
||||
|
||||
|
||||
@attr('shard_1')
|
||||
class VerifyCourseKeyDecoratorTests(TestCase):
|
||||
|
||||
@@ -753,10 +753,17 @@ def _progress(request, course_key, student_id):
|
||||
'passed': is_course_passed(course, grade_summary),
|
||||
'show_generate_cert_btn': show_generate_cert_btn,
|
||||
'credit_course_requirements': _credit_course_requirements(course_key, student),
|
||||
'missing_required_verification': missing_required_verification
|
||||
'missing_required_verification': missing_required_verification,
|
||||
'certificate_invalidated': False,
|
||||
}
|
||||
|
||||
if show_generate_cert_btn:
|
||||
# If current certificate is invalidated by instructor
|
||||
# then show the certificate invalidated message.
|
||||
context.update({
|
||||
'certificate_invalidated': certs_api.is_certificate_invalid(student, course_key)
|
||||
})
|
||||
|
||||
cert_status = certs_api.certificate_downloadable_status(student, course_key)
|
||||
context.update(cert_status)
|
||||
# showing the certificate web view button if feature flags are enabled.
|
||||
|
||||
@@ -55,7 +55,10 @@ from django.utils.http import urlquote_plus
|
||||
<div class="auto-cert-message" id="course-success">
|
||||
<div class="has-actions">
|
||||
<% post_url = reverse('generate_user_cert', args=[unicode(course.id)]) %>
|
||||
% if is_downloadable:
|
||||
## If current certificate is invalidated by instructor then don't show the generate button.
|
||||
% if certificate_invalidated:
|
||||
<p class="copy">${_("Your certificate has been invalidated. Please contact your course team if you have any questions.")}</p>
|
||||
%elif is_downloadable:
|
||||
<div class="msg-content">
|
||||
<h2 class="title">${_("Your certificate is available")}</h2>
|
||||
<p class="copy">
|
||||
|
||||
Reference in New Issue
Block a user