Commit ab713181 by Alex Dusenbery Committed by Alex Dusenbery

EDUCATOR-1316 | Refactor courseware.views.views._get_cert_data and related functions.

parent fd07dea0
...@@ -376,7 +376,7 @@ def _cert_info(user, course_overview, cert_status, course_mode): # pylint: disa ...@@ -376,7 +376,7 @@ def _cert_info(user, course_overview, cert_status, course_mode): # pylint: disa
if status == 'ready': if status == 'ready':
# showing the certificate web view button if certificate is ready state and feature flags are enabled. # showing the certificate web view button if certificate is ready state and feature flags are enabled.
if has_html_certificates_enabled(course_overview.id, course_overview): if has_html_certificates_enabled(course_overview):
if course_overview.has_any_active_web_certificate: if course_overview.has_any_active_web_certificate:
status_dict.update({ status_dict.update({
'show_cert_web_view': True, 'show_cert_web_view': True,
......
...@@ -150,7 +150,12 @@ def generate_user_certificates(student, course_key, course=None, insecure=False, ...@@ -150,7 +150,12 @@ def generate_user_certificates(student, course_key, course=None, insecure=False,
xqueue = XQueueCertInterface() xqueue = XQueueCertInterface()
if insecure: if insecure:
xqueue.use_https = False xqueue.use_https = False
generate_pdf = not has_html_certificates_enabled(course_key, course)
if not course:
course = modulestore().get_course(course_key, depth=0)
generate_pdf = not has_html_certificates_enabled(course)
cert = xqueue.add_cert( cert = xqueue.add_cert(
student, student,
course_key, course_key,
...@@ -198,7 +203,11 @@ def regenerate_user_certificates(student, course_key, course=None, ...@@ -198,7 +203,11 @@ def regenerate_user_certificates(student, course_key, course=None,
if insecure: if insecure:
xqueue.use_https = False xqueue.use_https = False
generate_pdf = not has_html_certificates_enabled(course_key, course) if not course:
course = modulestore().get_course(course_key, depth=0)
generate_pdf = not has_html_certificates_enabled(course)
return xqueue.regen_cert( return xqueue.regen_cert(
student, student,
course_key, course_key,
...@@ -353,44 +362,6 @@ def generate_example_certificates(course_key): ...@@ -353,44 +362,6 @@ def generate_example_certificates(course_key):
xqueue.add_example_cert(cert) xqueue.add_example_cert(cert)
def has_html_certificates_enabled(course_key, course=None):
"""
Determine if a course has html certificates enabled.
Arguments:
course_key (CourseKey|str): A course key or a string representation
of one.
course (CourseDescriptor|CourseOverview): A course.
"""
# If the feature is disabled, then immediately return a False
if not settings.FEATURES.get('CERTIFICATES_HTML_VIEW', False):
return False
# If we don't have a course object, we'll need to assemble one
if not course:
# Initialize a course key if necessary
if not isinstance(course_key, CourseKey):
try:
course_key = CourseKey.from_string(course_key)
except InvalidKeyError:
log.warning(
('Unable to parse course_key "%s"', course_key),
exc_info=True
)
return False
# Pull the course data from the cache
try:
course = CourseOverview.get_from_id(course_key)
except: # pylint: disable=bare-except
log.warning(
('Unable to load CourseOverview object for course_key "%s"', unicode(course_key)),
exc_info=True
)
# Return the flag on the course object
return course.cert_html_view_enabled if course else False
def example_certificates_status(course_key): def example_certificates_status(course_key):
"""Check the status of example certificates for a course. """Check the status of example certificates for a course.
...@@ -425,50 +396,58 @@ def example_certificates_status(course_key): ...@@ -425,50 +396,58 @@ def example_certificates_status(course_key):
return ExampleCertificateSet.latest_status(course_key) return ExampleCertificateSet.latest_status(course_key)
def _safe_course_key(course_key):
if not isinstance(course_key, CourseKey):
return CourseKey.from_string(course_key)
return course_key
def _course_from_key(course_key):
return CourseOverview.get_from_id(_safe_course_key(course_key))
def _certificate_html_url(user_id, course_id, uuid):
if uuid:
return reverse('certificates:render_cert_by_uuid', kwargs={'certificate_uuid': uuid})
elif user_id and course_id:
kwargs = {"user_id": str(user_id), "course_id": unicode(course_id)}
return reverse('certificates:html_view', kwargs=kwargs)
return ''
def _certificate_download_url(user_id, course_id):
try:
user_certificate = GeneratedCertificate.eligible_certificates.get(
user=user_id,
course_id=_safe_course_key(course_id)
)
return user_certificate.download_url
except GeneratedCertificate.DoesNotExist:
log.critical(
'Unable to lookup certificate\n'
'user id: %d\n'
'course: %s', user_id, unicode(course_id)
)
return ''
def has_html_certificates_enabled(course):
if not settings.FEATURES.get('CERTIFICATES_HTML_VIEW', False):
return False
return course.cert_html_view_enabled
def get_certificate_url(user_id=None, course_id=None, uuid=None): def get_certificate_url(user_id=None, course_id=None, uuid=None):
""" url = ''
:return certificate url for web or pdf certs. In case of web certs returns either old
or new cert url based on given parameters. For web certs if `uuid` is it would return course = _course_from_key(course_id)
new uuid based cert url url otherwise old url. if not course:
""" return url
url = ""
if has_html_certificates_enabled(course_id):
if uuid:
url = reverse(
'certificates:render_cert_by_uuid',
kwargs=dict(certificate_uuid=uuid)
)
elif user_id and course_id:
url = reverse(
'certificates:html_view',
kwargs={
"user_id": str(user_id),
"course_id": unicode(course_id),
}
)
else:
if isinstance(course_id, basestring):
try:
course_id = CourseKey.from_string(course_id)
except InvalidKeyError:
log.warning(
('Unable to parse course_id "%s"', course_id),
exc_info=True
)
return url
try:
user_certificate = GeneratedCertificate.eligible_certificates.get(
user=user_id,
course_id=course_id
)
url = user_certificate.download_url
except GeneratedCertificate.DoesNotExist:
log.critical(
'Unable to lookup certificate\n'
'user id: %d\n'
'course: %s', user_id, unicode(course_id)
)
if has_html_certificates_enabled(course):
url = _certificate_html_url(user_id, course_id, uuid)
else:
url = _certificate_download_url(user_id, course_id)
return url return url
......
...@@ -674,7 +674,7 @@ class CertificatesViewsTests(CommonCertificatesTestCase): ...@@ -674,7 +674,7 @@ class CertificatesViewsTests(CommonCertificatesTestCase):
self._add_course_certificates(count=1, signatory_count=0) self._add_course_certificates(count=1, signatory_count=0)
test_url = get_certificate_url( test_url = get_certificate_url(
user_id=self.user.id, user_id=self.user.id,
course_id=unicode(self.course) course_id=unicode(self.course.id)
) )
response = self.client.get(test_url) response = self.client.get(test_url)
self.assertNotIn('Signatory_Name 0', response.content) self.assertNotIn('Signatory_Name 0', response.content)
......
...@@ -26,8 +26,7 @@ from certificates.api import ( ...@@ -26,8 +26,7 @@ from certificates.api import (
get_certificate_footer_context, get_certificate_footer_context,
get_certificate_header_context, get_certificate_header_context,
get_certificate_template, get_certificate_template,
get_certificate_url, get_certificate_url
has_html_certificates_enabled
) )
from certificates.models import ( from certificates.models import (
CertificateGenerationCourseSetting, CertificateGenerationCourseSetting,
...@@ -48,8 +47,7 @@ from student.models import LinkedInAddToProfileConfiguration ...@@ -48,8 +47,7 @@ from student.models import LinkedInAddToProfileConfiguration
from util import organizations_helpers as organization_api from util import organizations_helpers as organization_api
from util.date_utils import strftime_localized from util.date_utils import strftime_localized
from util.views import handle_500 from util.views import handle_500
from xmodule.modulestore.django import modulestore
from xmodule.modulestore.exceptions import ItemNotFoundError
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
...@@ -523,23 +521,18 @@ def render_html_view(request, user_id, course_id): ...@@ -523,23 +521,18 @@ def render_html_view(request, user_id, course_id):
_update_context_with_basic_info(context, course_id, platform_name, configuration) _update_context_with_basic_info(context, course_id, platform_name, configuration)
invalid_template_path = 'certificates/invalid.html' invalid_template_path = 'certificates/invalid.html'
# Kick the user back to the "Invalid" screen if the feature is disabled # Kick the user back to the "Invalid" screen if the feature is disabled globally
if not has_html_certificates_enabled(course_id): if not settings.FEATURES.get('CERTIFICATES_HTML_VIEW', False):
log.info(
"Invalid cert: HTML certificates disabled for %s. User id: %d",
course_id,
user_id,
)
return render_to_response(invalid_template_path, context) return render_to_response(invalid_template_path, context)
# Load the course and user objects # Load the course and user objects
try: try:
course_key = CourseKey.from_string(course_id) course_key = CourseKey.from_string(course_id)
user = User.objects.get(id=user_id) user = User.objects.get(id=user_id)
course = modulestore().get_course(course_key) course = get_course_by_id(course_key)
# For any other expected exceptions, kick the user back to the "Invalid" screen # For any course or user exceptions, kick the user back to the "Invalid" screen
except (InvalidKeyError, ItemNotFoundError, User.DoesNotExist) as exception: except (InvalidKeyError, User.DoesNotExist, Http404) as exception:
error_str = ( error_str = (
"Invalid cert: error finding course %s or user with id " "Invalid cert: error finding course %s or user with id "
"%d. Specific error: %s" "%d. Specific error: %s"
...@@ -547,6 +540,15 @@ def render_html_view(request, user_id, course_id): ...@@ -547,6 +540,15 @@ def render_html_view(request, user_id, course_id):
log.info(error_str, course_id, user_id, str(exception)) log.info(error_str, course_id, user_id, str(exception))
return render_to_response(invalid_template_path, context) return render_to_response(invalid_template_path, context)
# Kick the user back to the "Invalid" screen if the feature is disabled for the course
if not course.cert_html_view_enabled:
log.info(
"Invalid cert: HTML certificates disabled for %s. User id: %d",
course_id,
user_id,
)
return render_to_response(invalid_template_path, context)
# Load user's certificate # Load user's certificate
user_certificate = _get_user_certificate(request, user, course_key, course, preview_mode) user_certificate = _get_user_certificate(request, user, course_key, course, preview_mode)
if not user_certificate: if not user_certificate:
......
...@@ -10,8 +10,13 @@ from HTMLParser import HTMLParser ...@@ -10,8 +10,13 @@ from HTMLParser import HTMLParser
from urllib import quote, urlencode from urllib import quote, urlencode
from uuid import uuid4 from uuid import uuid4
import courseware.views.views as views
import ddt import ddt
from freezegun import freeze_time
from mock import MagicMock, PropertyMock, create_autospec, patch
from nose.plugins.attrib import attr
from pytz import UTC
import courseware.views.views as views
import shoppingcart import shoppingcart
from capa.tests.response_xml_factory import MultipleChoiceResponseXMLFactory from capa.tests.response_xml_factory import MultipleChoiceResponseXMLFactory
from certificates import api as certs_api from certificates import api as certs_api
...@@ -34,13 +39,12 @@ from django.http import Http404, HttpResponseBadRequest ...@@ -34,13 +39,12 @@ from django.http import Http404, HttpResponseBadRequest
from django.test import TestCase from django.test import TestCase
from django.test.client import Client, RequestFactory from django.test.client import Client, RequestFactory
from django.test.utils import override_settings from django.test.utils import override_settings
from freezegun import freeze_time
from lms.djangoapps.commerce.utils import EcommerceService # pylint: disable=import-error from lms.djangoapps.commerce.utils import EcommerceService # pylint: disable=import-error
from lms.djangoapps.grades.config.waffle import waffle as grades_waffle from lms.djangoapps.grades.config.waffle import waffle as grades_waffle
from lms.djangoapps.grades.config.waffle import ASSUME_ZERO_GRADE_IF_ABSENT from lms.djangoapps.grades.config.waffle import ASSUME_ZERO_GRADE_IF_ABSENT
from lms.djangoapps.grades.new.course_grade_factory import CourseGradeFactory
from lms.djangoapps.grades.tests.utils import mock_get_score
from milestones.tests.utils import MilestonesTestCaseMixin from milestones.tests.utils import MilestonesTestCaseMixin
from mock import MagicMock, PropertyMock, create_autospec, patch
from nose.plugins.attrib import attr
from opaque_keys.edx.keys import CourseKey from opaque_keys.edx.keys import CourseKey
from opaque_keys.edx.locations import Location from opaque_keys.edx.locations import Location
from openedx.core.djangoapps.catalog.tests.factories import CourseFactory as CatalogCourseFactory from openedx.core.djangoapps.catalog.tests.factories import CourseFactory as CatalogCourseFactory
...@@ -55,7 +59,6 @@ from openedx.core.djangolib.testing.utils import get_mock_request ...@@ -55,7 +59,6 @@ from openedx.core.djangolib.testing.utils import get_mock_request
from openedx.core.lib.gating import api as gating_api from openedx.core.lib.gating import api as gating_api
from openedx.features.course_experience import COURSE_OUTLINE_PAGE_FLAG from openedx.features.course_experience import COURSE_OUTLINE_PAGE_FLAG
from openedx.features.enterprise_support.tests.mixins.enterprise import EnterpriseTestConsentRequired from openedx.features.enterprise_support.tests.mixins.enterprise import EnterpriseTestConsentRequired
from pytz import UTC
from student.models import CourseEnrollment from student.models import CourseEnrollment
from student.tests.factories import AdminFactory, CourseEnrollmentFactory, UserFactory from student.tests.factories import AdminFactory, CourseEnrollmentFactory, UserFactory
from util.tests.test_date_utils import fake_pgettext, fake_ugettext from util.tests.test_date_utils import fake_pgettext, fake_ugettext
...@@ -1222,7 +1225,7 @@ class ProgressPageBaseTests(ModuleStoreTestCase): ...@@ -1222,7 +1225,7 @@ class ProgressPageBaseTests(ModuleStoreTestCase):
start=datetime(2013, 9, 16, 7, 17, 28), start=datetime(2013, 9, 16, 7, 17, 28),
grade_cutoffs={u'çü†øƒƒ': 0.75, 'Pass': 0.5}, grade_cutoffs={u'çü†øƒƒ': 0.75, 'Pass': 0.5},
end=datetime.now(), end=datetime.now(),
certificate_available_date=datetime.now(), certificate_available_date=datetime.now(UTC),
**options **options
) )
...@@ -1359,10 +1362,6 @@ class ProgressPageTests(ProgressPageBaseTests): ...@@ -1359,10 +1362,6 @@ class ProgressPageTests(ProgressPageBaseTests):
self.assertNotContains(resp, 'Request Certificate') self.assertNotContains(resp, 'Request Certificate')
@patch.dict('django.conf.settings.FEATURES', {'CERTIFICATES_HTML_VIEW': True}) @patch.dict('django.conf.settings.FEATURES', {'CERTIFICATES_HTML_VIEW': True})
@patch(
'lms.djangoapps.grades.new.course_grade.CourseGrade.summary',
PropertyMock(return_value={'grade': 'Pass', 'percent': 0.75, 'section_breakdown': [], 'grade_breakdown': {}}),
)
def test_view_certificate_link(self): def test_view_certificate_link(self):
""" """
If certificate web view is enabled then certificate web view button should appear for user who certificate is If certificate web view is enabled then certificate web view button should appear for user who certificate is
...@@ -1400,28 +1399,30 @@ class ProgressPageTests(ProgressPageBaseTests): ...@@ -1400,28 +1399,30 @@ class ProgressPageTests(ProgressPageBaseTests):
self.course.save() self.course.save()
self.store.update_item(self.course, self.user.id) self.store.update_item(self.course, self.user.id)
resp = self._get_progress_page() with patch('lms.djangoapps.grades.new.course_grade_factory.CourseGradeFactory.create') as mock_create:
self.assertContains(resp, u"View Certificate") course_grade = mock_create.return_value
course_grade.passed = True
course_grade.summary = {'grade': 'Pass', 'percent': 0.75, 'section_breakdown': [], 'grade_breakdown': {}}
self.assertContains(resp, u"earned a certificate for this course") resp = self._get_progress_page()
cert_url = certs_api.get_certificate_url(course_id=self.course.id, uuid=certificate.verify_uuid)
self.assertContains(resp, cert_url)
# when course certificate is not active self.assertContains(resp, u"View Certificate")
certificates[0]['is_active'] = False
self.store.update_item(self.course, self.user.id)
resp = self._get_progress_page() self.assertContains(resp, u"earned a certificate for this course")
self.assertNotContains(resp, u"View Your Certificate") cert_url = certs_api.get_certificate_url(course_id=self.course.id, uuid=certificate.verify_uuid)
self.assertNotContains(resp, u"You can now view your certificate") self.assertContains(resp, cert_url)
self.assertContains(resp, "working on it...")
self.assertContains(resp, "creating your certificate") # when course certificate is not active
certificates[0]['is_active'] = False
self.store.update_item(self.course, self.user.id)
resp = self._get_progress_page()
self.assertNotContains(resp, u"View Your Certificate")
self.assertNotContains(resp, u"You can now view your certificate")
self.assertContains(resp, "working on it...")
self.assertContains(resp, "creating your certificate")
@patch.dict('django.conf.settings.FEATURES', {'CERTIFICATES_HTML_VIEW': False}) @patch.dict('django.conf.settings.FEATURES', {'CERTIFICATES_HTML_VIEW': False})
@patch(
'lms.djangoapps.grades.new.course_grade.CourseGrade.summary',
PropertyMock(return_value={'grade': 'Pass', 'percent': 0.75, 'section_breakdown': [], 'grade_breakdown': {}})
)
def test_view_certificate_link_hidden(self): def test_view_certificate_link_hidden(self):
""" """
If certificate web view is disabled then certificate web view button should not appear for user who certificate If certificate web view is disabled then certificate web view button should not appear for user who certificate
...@@ -1441,8 +1442,13 @@ class ProgressPageTests(ProgressPageBaseTests): ...@@ -1441,8 +1442,13 @@ class ProgressPageTests(ProgressPageBaseTests):
# Enable certificate generation for this course # Enable certificate generation for this course
certs_api.set_cert_generation_enabled(self.course.id, True) certs_api.set_cert_generation_enabled(self.course.id, True)
resp = self._get_progress_page() with patch('lms.djangoapps.grades.new.course_grade_factory.CourseGradeFactory.create') as mock_create:
self.assertContains(resp, u"Download Your Certificate") course_grade = mock_create.return_value
course_grade.passed = True
course_grade.summary = {'grade': 'Pass', 'percent': 0.75, 'section_breakdown': [], 'grade_breakdown': {}}
resp = self._get_progress_page()
self.assertContains(resp, u"Download Your Certificate")
@ddt.data( @ddt.data(
*itertools.product((True, False), (True, False)) *itertools.product((True, False), (True, False))
...@@ -1452,7 +1458,7 @@ class ProgressPageTests(ProgressPageBaseTests): ...@@ -1452,7 +1458,7 @@ class ProgressPageTests(ProgressPageBaseTests):
"""Test that query counts remain the same for self-paced and instructor-paced courses.""" """Test that query counts remain the same for self-paced and instructor-paced courses."""
SelfPacedConfiguration(enabled=self_paced_enabled).save() SelfPacedConfiguration(enabled=self_paced_enabled).save()
self.setup_course(self_paced=self_paced) self.setup_course(self_paced=self_paced)
with self.assertNumQueries(43, table_blacklist=QUERY_COUNT_TABLE_BLACKLIST), check_mongo_calls(2): with self.assertNumQueries(43, table_blacklist=QUERY_COUNT_TABLE_BLACKLIST), check_mongo_calls(1):
self._get_progress_page() self._get_progress_page()
@ddt.data( @ddt.data(
...@@ -1465,20 +1471,16 @@ class ProgressPageTests(ProgressPageBaseTests): ...@@ -1465,20 +1471,16 @@ class ProgressPageTests(ProgressPageBaseTests):
with grades_waffle().override(ASSUME_ZERO_GRADE_IF_ABSENT, active=enable_waffle): with grades_waffle().override(ASSUME_ZERO_GRADE_IF_ABSENT, active=enable_waffle):
with self.assertNumQueries( with self.assertNumQueries(
initial, table_blacklist=QUERY_COUNT_TABLE_BLACKLIST initial, table_blacklist=QUERY_COUNT_TABLE_BLACKLIST
), check_mongo_calls(2): ), check_mongo_calls(1):
self._get_progress_page() self._get_progress_page()
# subsequent accesses to the progress page require fewer queries. # subsequent accesses to the progress page require fewer queries.
for _ in range(2): for _ in range(2):
with self.assertNumQueries( with self.assertNumQueries(
subsequent, table_blacklist=QUERY_COUNT_TABLE_BLACKLIST subsequent, table_blacklist=QUERY_COUNT_TABLE_BLACKLIST
), check_mongo_calls(2): ), check_mongo_calls(1):
self._get_progress_page() self._get_progress_page()
@patch(
'lms.djangoapps.grades.new.course_grade.CourseGrade.summary',
PropertyMock(return_value={'grade': 'Pass', 'percent': 0.75, 'section_breakdown': [], 'grade_breakdown': {}})
)
@ddt.data( @ddt.data(
*itertools.product( *itertools.product(
( (
...@@ -1502,23 +1504,24 @@ class ProgressPageTests(ProgressPageBaseTests): ...@@ -1502,23 +1504,24 @@ class ProgressPageTests(ProgressPageBaseTests):
'lms.djangoapps.verify_student.models.SoftwareSecurePhotoVerification.user_is_verified' 'lms.djangoapps.verify_student.models.SoftwareSecurePhotoVerification.user_is_verified'
) as user_verify: ) as user_verify:
user_verify.return_value = user_verified user_verify.return_value = user_verified
resp = self.client.get( with patch('lms.djangoapps.grades.new.course_grade_factory.CourseGradeFactory.create') as mock_create:
reverse('progress', args=[unicode(self.course.id)]) course_grade = mock_create.return_value
) course_grade.passed = True
course_grade.summary = {
'grade': 'Pass', 'percent': 0.75, 'section_breakdown': [], 'grade_breakdown': {}
}
cert_button_hidden = course_mode is CourseMode.AUDIT or \ resp = self._get_progress_page()
course_mode in CourseMode.VERIFIED_MODES and not user_verified
self.assertEqual( cert_button_hidden = course_mode is CourseMode.AUDIT or \
cert_button_hidden, course_mode in CourseMode.VERIFIED_MODES and not user_verified
'Request Certificate' not in resp.content
) self.assertEqual(
cert_button_hidden,
'Request Certificate' not in resp.content
)
@patch.dict('django.conf.settings.FEATURES', {'CERTIFICATES_HTML_VIEW': True}) @patch.dict('django.conf.settings.FEATURES', {'CERTIFICATES_HTML_VIEW': True})
@patch(
'lms.djangoapps.grades.new.course_grade.CourseGrade.summary',
PropertyMock(return_value={'grade': 'Pass', 'percent': 0.75, 'section_breakdown': [], 'grade_breakdown': {}})
)
def test_page_with_invalidated_certificate_with_html_view(self): def test_page_with_invalidated_certificate_with_html_view(self):
""" """
Verify that for html certs if certificate is marked as invalidated than Verify that for html certs if certificate is marked as invalidated than
...@@ -1545,14 +1548,17 @@ class ProgressPageTests(ProgressPageBaseTests): ...@@ -1545,14 +1548,17 @@ class ProgressPageTests(ProgressPageBaseTests):
self.course.save() self.course.save()
self.store.update_item(self.course, self.user.id) self.store.update_item(self.course, self.user.id)
resp = self._get_progress_page() with patch('lms.djangoapps.grades.new.course_grade_factory.CourseGradeFactory.create') as mock_create:
self.assertContains(resp, u"View Certificate") course_grade = mock_create.return_value
self.assert_invalidate_certificate(generated_certificate) course_grade.passed = True
course_grade.summary = {
'grade': 'Pass', 'percent': 0.75, 'section_breakdown': [], 'grade_breakdown': {}
}
resp = self._get_progress_page()
self.assertContains(resp, u"View Certificate")
self.assert_invalidate_certificate(generated_certificate)
@patch(
'lms.djangoapps.grades.new.course_grade.CourseGrade.summary',
PropertyMock(return_value={'grade': 'Pass', 'percent': 0.75, 'section_breakdown': [], 'grade_breakdown': {}})
)
def test_page_with_invalidated_certificate_with_pdf(self): def test_page_with_invalidated_certificate_with_pdf(self):
""" """
Verify that for pdf certs if certificate is marked as invalidated than Verify that for pdf certs if certificate is marked as invalidated than
...@@ -1562,14 +1568,15 @@ class ProgressPageTests(ProgressPageBaseTests): ...@@ -1562,14 +1568,15 @@ class ProgressPageTests(ProgressPageBaseTests):
"http://www.example.com/certificate.pdf", "honor" "http://www.example.com/certificate.pdf", "honor"
) )
resp = self._get_progress_page() with patch('lms.djangoapps.grades.new.course_grade_factory.CourseGradeFactory.create') as mock_create:
self.assertContains(resp, u'Download Your Certificate') course_grade = mock_create.return_value
self.assert_invalidate_certificate(generated_certificate) course_grade.passed = True
course_grade.summary = {'grade': 'Pass', 'percent': 0.75, 'section_breakdown': [], 'grade_breakdown': {}}
resp = self._get_progress_page()
self.assertContains(resp, u'Download Your Certificate')
self.assert_invalidate_certificate(generated_certificate)
@patch(
'lms.djangoapps.grades.new.course_grade.CourseGrade.summary',
PropertyMock(return_value={'grade': 'Pass', 'percent': 0.75, 'section_breakdown': [], 'grade_breakdown': {}})
)
@patch('courseware.views.views.is_course_passed', PropertyMock(return_value=True)) @patch('courseware.views.views.is_course_passed', PropertyMock(return_value=True))
def test_message_for_audit_mode(self): def test_message_for_audit_mode(self):
""" Verify that message appears on progress page, if learner is enrolled """ Verify that message appears on progress page, if learner is enrolled
...@@ -1578,14 +1585,19 @@ class ProgressPageTests(ProgressPageBaseTests): ...@@ -1578,14 +1585,19 @@ class ProgressPageTests(ProgressPageBaseTests):
user = UserFactory.create() user = UserFactory.create()
self.assertTrue(self.client.login(username=user.username, password='test')) self.assertTrue(self.client.login(username=user.username, password='test'))
CourseEnrollmentFactory(user=user, course_id=self.course.id, mode=CourseMode.AUDIT) CourseEnrollmentFactory(user=user, course_id=self.course.id, mode=CourseMode.AUDIT)
response = self._get_progress_page()
self.assertContains( with patch('lms.djangoapps.grades.new.course_grade_factory.CourseGradeFactory.create') as mock_create:
response, course_grade = mock_create.return_value
u'You are enrolled in the audit track for this course. The audit track does not include a certificate.' course_grade.passed = True
) course_grade.summary = {'grade': 'Pass', 'percent': 0.75, 'section_breakdown': [], 'grade_breakdown': {}}
response = self._get_progress_page()
self.assertContains(
response,
u'You are enrolled in the audit track for this course. The audit track does not include a certificate.'
)
@patch('courseware.views.views.is_course_passed', PropertyMock(return_value=True))
def test_invalidated_cert_data(self): def test_invalidated_cert_data(self):
""" """
Verify that invalidated cert data is returned if cert is invalidated. Verify that invalidated cert data is returned if cert is invalidated.
...@@ -1600,11 +1612,10 @@ class ProgressPageTests(ProgressPageBaseTests): ...@@ -1600,11 +1612,10 @@ class ProgressPageTests(ProgressPageBaseTests):
) )
# Invalidate user certificate # Invalidate user certificate
generated_certificate.invalidate() generated_certificate.invalidate()
response = views._get_cert_data(self.user, self.course, self.course.id, True, CourseMode.HONOR) response = views._get_cert_data(self.user, self.course, CourseMode.HONOR, MagicMock(passed=True))
self.assertEqual(response.cert_status, 'invalidated') self.assertEqual(response.cert_status, 'invalidated')
self.assertEqual(response.title, 'Your certificate has been invalidated') self.assertEqual(response.title, 'Your certificate has been invalidated')
@patch('courseware.views.views.is_course_passed', PropertyMock(return_value=True))
def test_downloadable_get_cert_data(self): def test_downloadable_get_cert_data(self):
""" """
Verify that downloadable cert data is returned if cert is downloadable. Verify that downloadable cert data is returned if cert is downloadable.
...@@ -1614,12 +1625,11 @@ class ProgressPageTests(ProgressPageBaseTests): ...@@ -1614,12 +1625,11 @@ class ProgressPageTests(ProgressPageBaseTests):
) )
with patch('certificates.api.certificate_downloadable_status', with patch('certificates.api.certificate_downloadable_status',
return_value=self.mock_certificate_downloadable_status(is_downloadable=True)): return_value=self.mock_certificate_downloadable_status(is_downloadable=True)):
response = views._get_cert_data(self.user, self.course, self.course.id, True, CourseMode.HONOR) response = views._get_cert_data(self.user, self.course, CourseMode.HONOR, MagicMock(passed=True))
self.assertEqual(response.cert_status, 'downloadable') self.assertEqual(response.cert_status, 'downloadable')
self.assertEqual(response.title, 'Your certificate is available') self.assertEqual(response.title, 'Your certificate is available')
@patch('courseware.views.views.is_course_passed', PropertyMock(return_value=True))
def test_generating_get_cert_data(self): def test_generating_get_cert_data(self):
""" """
Verify that generating cert data is returned if cert is generating. Verify that generating cert data is returned if cert is generating.
...@@ -1629,12 +1639,11 @@ class ProgressPageTests(ProgressPageBaseTests): ...@@ -1629,12 +1639,11 @@ class ProgressPageTests(ProgressPageBaseTests):
) )
with patch('certificates.api.certificate_downloadable_status', with patch('certificates.api.certificate_downloadable_status',
return_value=self.mock_certificate_downloadable_status(is_generating=True)): return_value=self.mock_certificate_downloadable_status(is_generating=True)):
response = views._get_cert_data(self.user, self.course, self.course.id, True, CourseMode.HONOR) response = views._get_cert_data(self.user, self.course, CourseMode.HONOR, MagicMock(passed=True))
self.assertEqual(response.cert_status, 'generating') self.assertEqual(response.cert_status, 'generating')
self.assertEqual(response.title, "We're working on it...") self.assertEqual(response.title, "We're working on it...")
@patch('courseware.views.views.is_course_passed', PropertyMock(return_value=True))
def test_unverified_get_cert_data(self): def test_unverified_get_cert_data(self):
""" """
Verify that unverified cert data is returned if cert is unverified. Verify that unverified cert data is returned if cert is unverified.
...@@ -1644,12 +1653,11 @@ class ProgressPageTests(ProgressPageBaseTests): ...@@ -1644,12 +1653,11 @@ class ProgressPageTests(ProgressPageBaseTests):
) )
with patch('certificates.api.certificate_downloadable_status', with patch('certificates.api.certificate_downloadable_status',
return_value=self.mock_certificate_downloadable_status(is_unverified=True)): return_value=self.mock_certificate_downloadable_status(is_unverified=True)):
response = views._get_cert_data(self.user, self.course, self.course.id, True, CourseMode.HONOR) response = views._get_cert_data(self.user, self.course, CourseMode.HONOR, MagicMock(passed=True))
self.assertEqual(response.cert_status, 'unverified') self.assertEqual(response.cert_status, 'unverified')
self.assertEqual(response.title, "Certificate unavailable") self.assertEqual(response.title, "Certificate unavailable")
@patch('courseware.views.views.is_course_passed', PropertyMock(return_value=True))
def test_request_get_cert_data(self): def test_request_get_cert_data(self):
""" """
Verify that requested cert data is returned if cert is to be requested. Verify that requested cert data is returned if cert is to be requested.
...@@ -1659,7 +1667,7 @@ class ProgressPageTests(ProgressPageBaseTests): ...@@ -1659,7 +1667,7 @@ class ProgressPageTests(ProgressPageBaseTests):
) )
with patch('certificates.api.certificate_downloadable_status', with patch('certificates.api.certificate_downloadable_status',
return_value=self.mock_certificate_downloadable_status()): return_value=self.mock_certificate_downloadable_status()):
response = views._get_cert_data(self.user, self.course, self.course.id, True, CourseMode.HONOR) response = views._get_cert_data(self.user, self.course, CourseMode.HONOR, MagicMock(passed=True))
self.assertEqual(response.cert_status, 'requesting') self.assertEqual(response.cert_status, 'requesting')
self.assertEqual(response.title, "Congratulations, you qualified for a certificate!") self.assertEqual(response.title, "Congratulations, you qualified for a certificate!")
...@@ -2013,54 +2021,6 @@ class VerifyCourseKeyDecoratorTests(TestCase): ...@@ -2013,54 +2021,6 @@ class VerifyCourseKeyDecoratorTests(TestCase):
@attr(shard=1) @attr(shard=1)
class IsCoursePassedTests(ModuleStoreTestCase):
"""
Tests for the is_course_passed helper function
"""
SUCCESS_CUTOFF = 0.5
def setUp(self):
super(IsCoursePassedTests, self).setUp()
self.student = UserFactory()
self.course = CourseFactory.create(
org='edx',
number='verified',
display_name='Verified Course',
grade_cutoffs={'cutoff': 0.75, 'Pass': self.SUCCESS_CUTOFF}
)
self.request = RequestFactory()
self.request.user = self.student
def test_user_fails_if_not_clear_exam(self):
# If user has not grade then false will return
self.assertFalse(views.is_course_passed(self.course, None, self.student, self.request))
@patch('lms.djangoapps.grades.new.course_grade.CourseGrade.summary', PropertyMock(return_value={'percent': 0.9}))
def test_user_pass_if_percent_appears_above_passing_point(self):
# Mocking the grades.grade
# If user has above passing marks then True will return
self.assertTrue(views.is_course_passed(self.course, None, self.student, self.request))
@patch('lms.djangoapps.grades.new.course_grade.CourseGrade.summary', PropertyMock(return_value={'percent': 0.2}))
def test_user_fail_if_percent_appears_below_passing_point(self):
# Mocking the grades.grade
# If user has below passing marks then False will return
self.assertFalse(views.is_course_passed(self.course, None, self.student, self.request))
@patch(
'lms.djangoapps.grades.new.course_grade.CourseGrade.summary',
PropertyMock(return_value={'percent': SUCCESS_CUTOFF})
)
def test_user_with_passing_marks_and_achieved_marks_equal(self):
# Mocking the grades.grade
# If user's achieved passing marks are equal to the required passing
# marks then it will return True
self.assertTrue(views.is_course_passed(self.course, None, self.student, self.request))
@attr(shard=1)
class GenerateUserCertTests(ModuleStoreTestCase): class GenerateUserCertTests(ModuleStoreTestCase):
""" """
Tests for the view function Generated User Certs Tests for the view function Generated User Certs
...@@ -2088,12 +2048,9 @@ class GenerateUserCertTests(ModuleStoreTestCase): ...@@ -2088,12 +2048,9 @@ class GenerateUserCertTests(ModuleStoreTestCase):
self.assertEqual(resp.status_code, HttpResponseBadRequest.status_code) self.assertEqual(resp.status_code, HttpResponseBadRequest.status_code)
self.assertIn("Your certificate will be available when you pass the course.", resp.content) self.assertIn("Your certificate will be available when you pass the course.", resp.content)
@patch( @patch('courseware.views.views.is_course_passed', return_value=True)
'lms.djangoapps.grades.new.course_grade.CourseGrade.summary',
PropertyMock(return_value={'grade': 'Pass', 'percent': 0.75})
)
@override_settings(CERT_QUEUE='certificates', LMS_SEGMENT_KEY="foobar") @override_settings(CERT_QUEUE='certificates', LMS_SEGMENT_KEY="foobar")
def test_user_with_passing_grade(self): def test_user_with_passing_grade(self, mock_is_course_passed):
# If user has above passing grading then json will return cert generating message and # If user has above passing grading then json will return cert generating message and
# status valid code # status valid code
# mocking xqueue and analytics # mocking xqueue and analytics
...@@ -2104,6 +2061,7 @@ class GenerateUserCertTests(ModuleStoreTestCase): ...@@ -2104,6 +2061,7 @@ class GenerateUserCertTests(ModuleStoreTestCase):
with patch('capa.xqueue_interface.XQueueInterface.send_to_queue') as mock_send_to_queue: with patch('capa.xqueue_interface.XQueueInterface.send_to_queue') as mock_send_to_queue:
mock_send_to_queue.return_value = (0, "Successfully queued") mock_send_to_queue.return_value = (0, "Successfully queued")
resp = self.client.post(self.url) resp = self.client.post(self.url)
self.assertEqual(resp.status_code, 200) self.assertEqual(resp.status_code, 200)
...@@ -2123,10 +2081,6 @@ class GenerateUserCertTests(ModuleStoreTestCase): ...@@ -2123,10 +2081,6 @@ class GenerateUserCertTests(ModuleStoreTestCase):
) )
mock_tracker.reset_mock() mock_tracker.reset_mock()
@patch(
'lms.djangoapps.grades.new.course_grade.CourseGrade.summary',
PropertyMock(return_value={'grade': 'Pass', 'percent': 0.75})
)
def test_user_with_passing_existing_generating_cert(self): def test_user_with_passing_existing_generating_cert(self):
# If user has passing grade but also has existing generating cert # If user has passing grade but also has existing generating cert
# then json will return cert generating message with bad request code # then json will return cert generating message with bad request code
...@@ -2136,14 +2090,15 @@ class GenerateUserCertTests(ModuleStoreTestCase): ...@@ -2136,14 +2090,15 @@ class GenerateUserCertTests(ModuleStoreTestCase):
status=CertificateStatuses.generating, status=CertificateStatuses.generating,
mode='verified' mode='verified'
) )
resp = self.client.post(self.url) with patch('lms.djangoapps.grades.new.course_grade_factory.CourseGradeFactory.create') as mock_create:
self.assertEqual(resp.status_code, HttpResponseBadRequest.status_code) course_grade = mock_create.return_value
self.assertIn("Certificate is being created.", resp.content) course_grade.passed = True
course_grade.summary = {'grade': 'Pass', 'percent': 0.75}
resp = self.client.post(self.url)
self.assertEqual(resp.status_code, HttpResponseBadRequest.status_code)
self.assertIn("Certificate is being created.", resp.content)
@patch(
'lms.djangoapps.grades.new.course_grade.CourseGrade.summary',
PropertyMock(return_value={'grade': 'Pass', 'percent': 0.75})
)
@override_settings(CERT_QUEUE='certificates', LMS_SEGMENT_KEY="foobar") @override_settings(CERT_QUEUE='certificates', LMS_SEGMENT_KEY="foobar")
def test_user_with_passing_existing_downloadable_cert(self): def test_user_with_passing_existing_downloadable_cert(self):
# If user has already downloadable certificate # If user has already downloadable certificate
...@@ -2156,9 +2111,14 @@ class GenerateUserCertTests(ModuleStoreTestCase): ...@@ -2156,9 +2111,14 @@ class GenerateUserCertTests(ModuleStoreTestCase):
mode='verified' mode='verified'
) )
resp = self.client.post(self.url) with patch('lms.djangoapps.grades.new.course_grade_factory.CourseGradeFactory.create') as mock_create:
self.assertEqual(resp.status_code, HttpResponseBadRequest.status_code) course_grade = mock_create.return_value
self.assertIn("Certificate has already been created.", resp.content) course_grade.passed = True
course_grade.summay = {'grade': 'Pass', 'percent': 0.75}
resp = self.client.post(self.url)
self.assertEqual(resp.status_code, HttpResponseBadRequest.status_code)
self.assertIn("Certificate has already been created.", resp.content)
def test_user_with_non_existing_course(self): def test_user_with_non_existing_course(self):
# If try to access a course with valid key pattern then it will return # If try to access a course with valid key pattern then it will return
......
...@@ -71,13 +71,14 @@ from markupsafe import escape ...@@ -71,13 +71,14 @@ from markupsafe import escape
from opaque_keys import InvalidKeyError from opaque_keys import InvalidKeyError
from opaque_keys.edx.keys import CourseKey, UsageKey from opaque_keys.edx.keys import CourseKey, UsageKey
from openedx.core.djangoapps.catalog.utils import get_programs, get_programs_with_type from openedx.core.djangoapps.catalog.utils import get_programs, get_programs_with_type
from openedx.core.djangoapps.certificates import api as auto_certs_api
from openedx.core.djangoapps.content.course_overviews.models import CourseOverview from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
from openedx.core.djangoapps.credit.api import ( from openedx.core.djangoapps.credit.api import (
get_credit_requirement_status, get_credit_requirement_status,
is_credit_course, is_credit_course,
is_user_eligible_for_credit is_user_eligible_for_credit
) )
from openedx.core.djangoapps.certificates.config import waffle as certificates_waffle from openedx.core.djangoapps.certificates import api as auto_certs_api
from openedx.core.djangoapps.models.course_details import CourseDetails from openedx.core.djangoapps.models.course_details import CourseDetails
from openedx.core.djangoapps.monitoring_utils import set_custom_metrics_for_course_key from openedx.core.djangoapps.monitoring_utils import set_custom_metrics_for_course_key
from openedx.core.djangoapps.plugin_api.views import EdxFragmentView from openedx.core.djangoapps.plugin_api.views import EdxFragmentView
...@@ -117,6 +118,62 @@ CertData = namedtuple( ...@@ -117,6 +118,62 @@ CertData = namedtuple(
"CertData", ["cert_status", "title", "msg", "download_url", "cert_web_view_url"] "CertData", ["cert_status", "title", "msg", "download_url", "cert_web_view_url"]
) )
AUDIT_PASSING_CERT_DATA = CertData(
CertificateStatuses.audit_passing,
_('Your enrollment: Audit track'),
_('You are enrolled in the audit track for this course. The audit track does not include a certificate.'),
download_url=None,
cert_web_view_url=None
)
GENERATING_CERT_DATA = CertData(
CertificateStatuses.generating,
_("We're working on it..."),
_(
"We're creating your certificate. You can keep working in your courses and a link "
"to it will appear here and on your Dashboard when it is ready."
),
download_url=None,
cert_web_view_url=None
)
INVALID_CERT_DATA = CertData(
CertificateStatuses.invalidated,
_('Your certificate has been invalidated'),
_('Please contact your course team if you have any questions.'),
download_url=None,
cert_web_view_url=None
)
REQUESTING_CERT_DATA = CertData(
CertificateStatuses.requesting,
_('Congratulations, you qualified for a certificate!'),
_("You've earned a certificate for this course."),
download_url=None,
cert_web_view_url=None
)
UNVERIFIED_CERT_DATA = CertData(
CertificateStatuses.unverified,
_('Certificate unavailable'),
_(
'You have not received a certificate because you do not have a current {platform_name} '
'verified identity.'
).format(platform_name=configuration_helpers.get_value('PLATFORM_NAME', settings.PLATFORM_NAME)),
download_url=None,
cert_web_view_url=None
)
def _downloadable_cert_data(download_url=None, cert_web_view_url=None):
return CertData(
CertificateStatuses.downloadable,
_('Your certificate is available'),
_("You've earned a certificate for this course."),
download_url=download_url,
cert_web_view_url=cert_web_view_url
)
def user_groups(user): def user_groups(user):
""" """
...@@ -872,23 +929,22 @@ def _progress(request, course_key, student_id): ...@@ -872,23 +929,22 @@ def _progress(request, course_key, student_id):
course_grade = CourseGradeFactory().create(student, course) course_grade = CourseGradeFactory().create(student, course)
courseware_summary = course_grade.chapter_grades.values() courseware_summary = course_grade.chapter_grades.values()
grade_summary = course_grade.summary
studio_url = get_studio_url(course, 'settings/grading') studio_url = get_studio_url(course, 'settings/grading')
# checking certificate generation configuration # checking certificate generation configuration
enrollment_mode, is_active = CourseEnrollment.enrollment_mode_for_user(student, course_key) enrollment_mode, _ = CourseEnrollment.enrollment_mode_for_user(student, course_key)
context = { context = {
'course': course, 'course': course,
'courseware_summary': courseware_summary, 'courseware_summary': courseware_summary,
'studio_url': studio_url, 'studio_url': studio_url,
'grade_summary': grade_summary, 'grade_summary': course_grade.summary,
'staff_access': staff_access, 'staff_access': staff_access,
'masquerade': masquerade, 'masquerade': masquerade,
'supports_preview_menu': True, 'supports_preview_menu': True,
'student': student, 'student': student,
'credit_course_requirements': _credit_course_requirements(course_key, student), 'credit_course_requirements': _credit_course_requirements(course_key, student),
'certificate_data': _get_cert_data(student, course, course_key, is_active, enrollment_mode, grade_summary), 'certificate_data': _get_cert_data(student, course, enrollment_mode, course_grade),
} }
context.update( context.update(
get_experiment_user_metadata_context( get_experiment_user_metadata_context(
...@@ -903,128 +959,68 @@ def _progress(request, course_key, student_id): ...@@ -903,128 +959,68 @@ def _progress(request, course_key, student_id):
return response return response
def _get_cert_data(student, course, course_key, is_active, enrollment_mode, grade_summary=None): def _downloadable_certificate_message(course, cert_downloadable_status):
if certs_api.has_html_certificates_enabled(course):
if certs_api.get_active_web_certificate(course) is not None:
return _downloadable_cert_data(
download_url=None,
cert_web_view_url=certs_api.get_certificate_url(
course_id=course.id, uuid=cert_downloadable_status['uuid']
)
)
else:
return GENERATING_CERT_DATA
return _downloadable_cert_data(download_url=cert_downloadable_status['download_url'])
def _missing_required_verification(student, enrollment_mode):
return (
enrollment_mode in CourseMode.VERIFIED_MODES and not SoftwareSecurePhotoVerification.user_is_verified(student)
)
def _certificate_message(student, course, enrollment_mode):
if certs_api.is_certificate_invalid(student, course.id):
return INVALID_CERT_DATA
cert_downloadable_status = certs_api.certificate_downloadable_status(student, course.id)
if cert_downloadable_status['is_generating']:
return GENERATING_CERT_DATA
if cert_downloadable_status['is_unverified'] or _missing_required_verification(student, enrollment_mode):
return UNVERIFIED_CERT_DATA
if cert_downloadable_status['is_downloadable']:
return _downloadable_certificate_message(course, cert_downloadable_status)
return REQUESTING_CERT_DATA
def _get_cert_data(student, course, enrollment_mode, course_grade=None):
"""Returns students course certificate related data. """Returns students course certificate related data.
Arguments: Arguments:
student (User): Student for whom certificate to retrieve. student (User): Student for whom certificate to retrieve.
course (Course): Course object for which certificate data to retrieve. course (Course): Course object for which certificate data to retrieve.
course_key (CourseKey): Course identifier for course.
is_active (Bool): Boolean value to check if course is active.
enrollment_mode (String): Course mode in which student is enrolled. enrollment_mode (String): Course mode in which student is enrolled.
grade_summary (dict): Student grade details. course_grade (CourseGrade): Student's course grade record.
Returns: Returns:
returns dict if course certificate is available else None. returns dict if course certificate is available else None.
""" """
from lms.djangoapps.courseware.courses import get_course_by_id
if not CourseMode.is_eligible_for_certificate(enrollment_mode): if not CourseMode.is_eligible_for_certificate(enrollment_mode):
return CertData( return AUDIT_PASSING_CERT_DATA
CertificateStatuses.audit_passing,
_('Your enrollment: Audit track'),
_('You are enrolled in the audit track for this course. The audit track does not include a certificate.'),
download_url=None,
cert_web_view_url=None
)
may_view_certificate = False
if course_key:
may_view_certificate = get_course_by_id(course_key).may_certify()
switches = certificates_waffle.waffle()
switch_enabled = switches.is_enabled(certificates_waffle.AUTO_CERTIFICATE_GENERATION)
student_cert_generation_enabled = switch_enabled or certs_api.cert_generation_enabled(course_key)
# Don't show certificate information if:
# 1) the learner has not passed the course
# 2) the course is not active
# 3) auto-generated certs flags are not enabled, but student cert generation is not enabled either
# 4) the learner may not view the certificate, based on the course's advanced course settings.
if not all([
is_course_passed(course, grade_summary),
is_active,
student_cert_generation_enabled,
may_view_certificate
]):
return None
if certs_api.is_certificate_invalid(student, course_key): certificates_enabled_for_course = certs_api.cert_generation_enabled(course.id)
return CertData( if course_grade is None:
CertificateStatuses.invalidated, course_grade = CourseGradeFactory().create(student, course)
_('Your certificate has been invalidated'),
_('Please contact your course team if you have any questions.'),
download_url=None,
cert_web_view_url=None
)
cert_downloadable_status = certs_api.certificate_downloadable_status(student, course_key) if not auto_certs_api.can_show_certificate_message(course, student, course_grade, certificates_enabled_for_course):
return
generating_certificate_message = CertData( return _certificate_message(student, course, enrollment_mode)
CertificateStatuses.generating,
_("We're working on it..."),
_(
"We're creating your certificate. You can keep working in your courses and a link "
"to it will appear here and on your Dashboard when it is ready."
),
download_url=None,
cert_web_view_url=None
)
if cert_downloadable_status['is_downloadable']:
if certs_api.has_html_certificates_enabled(course_key, course):
if certs_api.get_active_web_certificate(course) is not None:
return CertData(
CertificateStatuses.downloadable,
_('Your certificate is available'),
_("You've earned a certificate for this course."),
download_url=None,
cert_web_view_url=certs_api.get_certificate_url(
course_id=course_key, uuid=cert_downloadable_status['uuid']
)
)
else:
# If there is an error, the user should see the generating certificate message
# until a new certificate is generated.
return generating_certificate_message
return CertData(
CertificateStatuses.downloadable,
_('Your certificate is available'),
_("You've earned a certificate for this course."),
download_url=cert_downloadable_status['download_url'],
cert_web_view_url=None
)
if cert_downloadable_status['is_generating']:
return generating_certificate_message
# If the learner is in verified modes and the student did not have
# their ID verified, we need to show message to ask learner to verify their ID first
missing_required_verification = (
enrollment_mode in CourseMode.VERIFIED_MODES and not SoftwareSecurePhotoVerification.user_is_verified(student)
)
if missing_required_verification or cert_downloadable_status['is_unverified']:
platform_name = configuration_helpers.get_value('PLATFORM_NAME', settings.PLATFORM_NAME)
return CertData(
CertificateStatuses.unverified,
_('Certificate unavailable'),
_(
'You have not received a certificate because you do not have a current {platform_name} '
'verified identity.'
).format(platform_name=platform_name),
download_url=None,
cert_web_view_url=None
)
return CertData(
CertificateStatuses.requesting,
_('Congratulations, you qualified for a certificate!'),
_("You've earned a certificate for this course."),
download_url=None,
cert_web_view_url=None
)
def _credit_course_requirements(course_key, student): def _credit_course_requirements(course_key, student):
...@@ -1281,26 +1277,21 @@ def course_survey(request, course_id): ...@@ -1281,26 +1277,21 @@ def course_survey(request, course_id):
) )
def is_course_passed(course, grade_summary=None, student=None, request=None): def is_course_passed(student, course, course_grade=None):
""" """
check user's course passing status. return True if passed check user's course passing status. return True if passed
Arguments: Arguments:
course : course object
grade_summary (dict) : contains student grade details.
student : user object student : user object
request (HttpRequest) course : course object
course_grade (CourseGrade) : contains student grade details.
Returns: Returns:
returns bool value returns bool value
""" """
nonzero_cutoffs = [cutoff for cutoff in course.grade_cutoffs.values() if cutoff > 0] if course_grade is None:
success_cutoff = min(nonzero_cutoffs) if nonzero_cutoffs else None course_grade = CourseGradeFactory().create(student, course)
return course_grade.passed
if grade_summary is None:
grade_summary = CourseGradeFactory().create(student, course).summary
return success_cutoff and grade_summary['percent'] >= success_cutoff
# Grades can potentially be written - if so, let grading manage the transaction. # Grades can potentially be written - if so, let grading manage the transaction.
...@@ -1343,7 +1334,7 @@ def generate_user_cert(request, course_id): ...@@ -1343,7 +1334,7 @@ def generate_user_cert(request, course_id):
if not course: if not course:
return HttpResponseBadRequest(_("Course is not valid")) return HttpResponseBadRequest(_("Course is not valid"))
if not is_course_passed(course, None, student, request): if not is_course_passed(student, course):
return HttpResponseBadRequest(_("Your certificate will be available when you pass the course.")) return HttpResponseBadRequest(_("Your certificate will be available when you pass the course."))
certificate_status = certs_api.certificate_downloadable_status(student, course.id) certificate_status = certs_api.certificate_downloadable_status(student, course.id)
......
...@@ -331,7 +331,7 @@ def _section_certificates(course): ...@@ -331,7 +331,7 @@ def _section_certificates(course):
""" """
example_cert_status = None example_cert_status = None
html_cert_enabled = certs_api.has_html_certificates_enabled(course.id, course) html_cert_enabled = certs_api.has_html_certificates_enabled(course)
if html_cert_enabled: if html_cert_enabled:
can_enable_for_course = True can_enable_for_course = True
else: else:
......
...@@ -1976,7 +1976,7 @@ class TestCertificateGeneration(InstructorTaskModuleTestCase): ...@@ -1976,7 +1976,7 @@ class TestCertificateGeneration(InstructorTaskModuleTestCase):
'failed': 3, 'failed': 3,
'skipped': 2 'skipped': 2
} }
with self.assertNumQueries(171): with self.assertNumQueries(176):
self.assertCertificatesGenerated(task_input, expected_results) self.assertCertificatesGenerated(task_input, expected_results)
expected_results = { expected_results = {
......
...@@ -2,8 +2,12 @@ ...@@ -2,8 +2,12 @@
The public API for certificates. The public API for certificates.
""" """
from datetime import datetime from datetime import datetime
from pytz import UTC from pytz import UTC
from course_modes.models import CourseMode
from openedx.core.djangoapps.certificates.config import waffle from openedx.core.djangoapps.certificates.config import waffle
from student.models import CourseEnrollment
SWITCHES = waffle.waffle() SWITCHES = waffle.waffle()
...@@ -19,6 +23,48 @@ def _enabled_and_instructor_paced(course): ...@@ -19,6 +23,48 @@ def _enabled_and_instructor_paced(course):
return False return False
def certificates_viewable_for_course(course):
"""
Returns True if certificates are viewable for any student enrolled in the course, False otherwise.
"""
if course.self_paced:
return True
if (
course.certificates_display_behavior in ('early_with_info', 'early_no_info')
or course.certificates_show_before_end
):
return True
if (
course.certificate_available_date
and course.certificate_available_date <= datetime.now(UTC)
):
return True
if (
course.certificate_available_date is None
and course.has_ended()
):
return True
return False
def is_certificate_valid(certificate):
"""
Returns True if the student has a valid, verified certificate for this course, False otherwise.
"""
return CourseEnrollment.is_enrolled_as_verified(certificate.user, certificate.course_id) and certificate.is_valid()
def can_show_certificate_message(course, student, course_grade, certificates_enabled_for_course):
if not (
(auto_certificate_generation_enabled() or certificates_enabled_for_course) and
CourseEnrollment.is_enrolled(student, course.id) and
certificates_viewable_for_course(course) and
course_grade.passed
):
return False
return True
def can_show_certificate_available_date_field(course): def can_show_certificate_available_date_field(course):
return _enabled_and_instructor_paced(course) return _enabled_and_instructor_paced(course)
......
"""
Openedx Certificates Application Configuration
"""
from django.apps import AppConfig
class OpenedxCertificatesConfig(AppConfig):
"""
Application Configuration for Openedx Certificates.
"""
name = 'openedx.core.djangoapps.certificates'
label = 'openedx_certificates'
from contextlib import contextmanager from contextlib import contextmanager
from datetime import datetime, timedelta
import itertools import itertools
from unittest import TestCase from unittest import TestCase
import ddt import ddt
import pytz
import waffle import waffle
from course_modes.models import CourseMode
from openedx.core.djangoapps.certificates import api from openedx.core.djangoapps.certificates import api
from openedx.core.djangoapps.certificates.config import waffle as certs_waffle from openedx.core.djangoapps.certificates.config import waffle as certs_waffle
from openedx.core.djangoapps.content.course_overviews.tests.factories import CourseOverviewFactory from openedx.core.djangoapps.content.course_overviews.tests.factories import CourseOverviewFactory
from student.tests.factories import CourseEnrollmentFactory, UserFactory
# TODO: Copied from lms.djangoapps.certificates.models,
# to be resolved per https://openedx.atlassian.net/browse/EDUCATOR-1318
class CertificateStatuses(object):
"""
Enum for certificate statuses
"""
deleted = 'deleted'
deleting = 'deleting'
downloadable = 'downloadable'
error = 'error'
generating = 'generating'
notpassing = 'notpassing'
restricted = 'restricted'
unavailable = 'unavailable'
auditing = 'auditing'
audit_passing = 'audit_passing'
audit_notpassing = 'audit_notpassing'
unverified = 'unverified'
invalidated = 'invalidated'
requesting = 'requesting'
ALL_STATUSES = (
deleted, deleting, downloadable, error, generating, notpassing, restricted, unavailable, auditing,
audit_passing, audit_notpassing, unverified, invalidated, requesting
)
class MockGeneratedCertificate(object):
"""
We can't import GeneratedCertificate from LMS here, so we roll
our own minimal Certificate model for testing.
"""
def __init__(self, user=None, course_id=None, mode=None, status=None):
self.user = user
self.course_id = course_id
self.mode = mode
self.status = status
def is_valid(self):
"""
Return True if certificate is valid else return False.
"""
return self.status == CertificateStatuses.downloadable
@contextmanager @contextmanager
...@@ -15,18 +64,29 @@ def configure_waffle_namespace(feature_enabled): ...@@ -15,18 +64,29 @@ def configure_waffle_namespace(feature_enabled):
namespace = certs_waffle.waffle() namespace = certs_waffle.waffle()
with namespace.override(certs_waffle.AUTO_CERTIFICATE_GENERATION, active=feature_enabled): with namespace.override(certs_waffle.AUTO_CERTIFICATE_GENERATION, active=feature_enabled):
yield yield
@ddt.ddt @ddt.ddt
class FeatureEnabledTestCase(TestCase): class CertificatesApiTestCase(TestCase):
def setUp(self): def setUp(self):
super(FeatureEnabledTestCase, self).setUp() super(CertificatesApiTestCase, self).setUp()
self.course = CourseOverviewFactory.create() self.course = CourseOverviewFactory.create(
start=datetime(2017, 1, 1, tzinfo=pytz.UTC),
def tearDown(self): end=datetime(2017, 1, 31, tzinfo=pytz.UTC),
super(FeatureEnabledTestCase, self).tearDown() certificate_available_date=None
self.course.self_paced = False )
self.user = UserFactory.create()
self.enrollment = CourseEnrollmentFactory(
user=self.user,
course_id=self.course.id,
is_active=True,
mode='audit',
)
self.certificate = MockGeneratedCertificate(
user=self.user,
course_id=self.course.id
)
@ddt.data(True, False) @ddt.data(True, False)
def test_auto_certificate_generation_enabled(self, feature_enabled): def test_auto_certificate_generation_enabled(self, feature_enabled):
...@@ -46,3 +106,18 @@ class FeatureEnabledTestCase(TestCase): ...@@ -46,3 +106,18 @@ class FeatureEnabledTestCase(TestCase):
self.course.self_paced = is_self_paced self.course.self_paced = is_self_paced
with configure_waffle_namespace(feature_enabled): with configure_waffle_namespace(feature_enabled):
self.assertEqual(expected_value, api.can_show_certificate_available_date_field(self.course)) self.assertEqual(expected_value, api.can_show_certificate_available_date_field(self.course))
@ddt.data(
(CourseMode.VERIFIED, CertificateStatuses.downloadable, True),
(CourseMode.VERIFIED, CertificateStatuses.notpassing, False),
(CourseMode.AUDIT, CertificateStatuses.downloadable, False)
)
@ddt.unpack
def test_is_certificate_valid(self, enrollment_mode, certificate_status, expected_value):
self.enrollment.mode = enrollment_mode
self.enrollment.save()
self.certificate.mode = CourseMode.VERIFIED
self.certificate.status = certificate_status
self.assertEqual(expected_value, api.is_certificate_valid(self.certificate))
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