Commit a361d44b by Awais Qureshi Committed by GitHub

Merge pull request #12666 from edx/awais786/ECOM-4217-certs-invalidate-by-instructor

Adding logic to check if cert is invalidate then user cannot see the …
parents 6bc0fa2b 112aa71f
......@@ -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()
......@@ -209,6 +213,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. """
@classmethod
......
......@@ -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,83 @@ 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 = views.progress(self.request, course_id=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 = views.progress(self.request, course_id=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 = views.progress(self.request, course_id=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 don't show the generate button.
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">
......
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