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
if status == 'ready':
# 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:
status_dict.update({
'show_cert_web_view': True,
......
......@@ -150,7 +150,12 @@ def generate_user_certificates(student, course_key, course=None, insecure=False,
xqueue = XQueueCertInterface()
if insecure:
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(
student,
course_key,
......@@ -198,7 +203,11 @@ def regenerate_user_certificates(student, course_key, course=None,
if insecure:
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(
student,
course_key,
......@@ -353,44 +362,6 @@ def generate_example_certificates(course_key):
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):
"""Check the status of example certificates for a course.
......@@ -425,50 +396,58 @@ def example_certificates_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):
"""
: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
new uuid based cert url url otherwise old 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)
)
url = ''
course = _course_from_key(course_id)
if not course:
return url
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
......
......@@ -674,7 +674,7 @@ class CertificatesViewsTests(CommonCertificatesTestCase):
self._add_course_certificates(count=1, signatory_count=0)
test_url = get_certificate_url(
user_id=self.user.id,
course_id=unicode(self.course)
course_id=unicode(self.course.id)
)
response = self.client.get(test_url)
self.assertNotIn('Signatory_Name 0', response.content)
......
......@@ -26,8 +26,7 @@ from certificates.api import (
get_certificate_footer_context,
get_certificate_header_context,
get_certificate_template,
get_certificate_url,
has_html_certificates_enabled
get_certificate_url
)
from certificates.models import (
CertificateGenerationCourseSetting,
......@@ -48,8 +47,7 @@ from student.models import LinkedInAddToProfileConfiguration
from util import organizations_helpers as organization_api
from util.date_utils import strftime_localized
from util.views import handle_500
from xmodule.modulestore.django import modulestore
from xmodule.modulestore.exceptions import ItemNotFoundError
log = logging.getLogger(__name__)
......@@ -523,23 +521,18 @@ def render_html_view(request, user_id, course_id):
_update_context_with_basic_info(context, course_id, platform_name, configuration)
invalid_template_path = 'certificates/invalid.html'
# Kick the user back to the "Invalid" screen if the feature is disabled
if not has_html_certificates_enabled(course_id):
log.info(
"Invalid cert: HTML certificates disabled for %s. User id: %d",
course_id,
user_id,
)
# Kick the user back to the "Invalid" screen if the feature is disabled globally
if not settings.FEATURES.get('CERTIFICATES_HTML_VIEW', False):
return render_to_response(invalid_template_path, context)
# Load the course and user objects
try:
course_key = CourseKey.from_string(course_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
except (InvalidKeyError, ItemNotFoundError, User.DoesNotExist) as exception:
# For any course or user exceptions, kick the user back to the "Invalid" screen
except (InvalidKeyError, User.DoesNotExist, Http404) as exception:
error_str = (
"Invalid cert: error finding course %s or user with id "
"%d. Specific error: %s"
......@@ -547,6 +540,15 @@ def render_html_view(request, user_id, course_id):
log.info(error_str, course_id, user_id, str(exception))
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
user_certificate = _get_user_certificate(request, user, course_key, course, preview_mode)
if not user_certificate:
......
......@@ -10,8 +10,13 @@ from HTMLParser import HTMLParser
from urllib import quote, urlencode
from uuid import uuid4
import courseware.views.views as views
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
from capa.tests.response_xml_factory import MultipleChoiceResponseXMLFactory
from certificates import api as certs_api
......@@ -34,13 +39,12 @@ from django.http import Http404, HttpResponseBadRequest
from django.test import TestCase
from django.test.client import Client, RequestFactory
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.grades.config.waffle import waffle as grades_waffle
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 mock import MagicMock, PropertyMock, create_autospec, patch
from nose.plugins.attrib import attr
from opaque_keys.edx.keys import CourseKey
from opaque_keys.edx.locations import Location
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
from openedx.core.lib.gating import api as gating_api
from openedx.features.course_experience import COURSE_OUTLINE_PAGE_FLAG
from openedx.features.enterprise_support.tests.mixins.enterprise import EnterpriseTestConsentRequired
from pytz import UTC
from student.models import CourseEnrollment
from student.tests.factories import AdminFactory, CourseEnrollmentFactory, UserFactory
from util.tests.test_date_utils import fake_pgettext, fake_ugettext
......@@ -1222,7 +1225,7 @@ class ProgressPageBaseTests(ModuleStoreTestCase):
start=datetime(2013, 9, 16, 7, 17, 28),
grade_cutoffs={u'çü†øƒƒ': 0.75, 'Pass': 0.5},
end=datetime.now(),
certificate_available_date=datetime.now(),
certificate_available_date=datetime.now(UTC),
**options
)
......@@ -1359,10 +1362,6 @@ class ProgressPageTests(ProgressPageBaseTests):
self.assertNotContains(resp, 'Request Certificate')
@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):
"""
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):
self.course.save()
self.store.update_item(self.course, self.user.id)
resp = self._get_progress_page()
self.assertContains(resp, u"View Certificate")
with patch('lms.djangoapps.grades.new.course_grade_factory.CourseGradeFactory.create') as mock_create:
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")
cert_url = certs_api.get_certificate_url(course_id=self.course.id, uuid=certificate.verify_uuid)
self.assertContains(resp, cert_url)
resp = self._get_progress_page()
# when course certificate is not active
certificates[0]['is_active'] = False
self.store.update_item(self.course, self.user.id)
self.assertContains(resp, u"View Certificate")
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")
self.assertContains(resp, u"earned a certificate for this course")
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
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(
'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):
"""
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):
# Enable certificate generation for this course
certs_api.set_cert_generation_enabled(self.course.id, True)
resp = self._get_progress_page()
self.assertContains(resp, u"Download Your Certificate")
with patch('lms.djangoapps.grades.new.course_grade_factory.CourseGradeFactory.create') as mock_create:
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(
*itertools.product((True, False), (True, False))
......@@ -1452,7 +1458,7 @@ class ProgressPageTests(ProgressPageBaseTests):
"""Test that query counts remain the same for self-paced and instructor-paced courses."""
SelfPacedConfiguration(enabled=self_paced_enabled).save()
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()
@ddt.data(
......@@ -1465,20 +1471,16 @@ class ProgressPageTests(ProgressPageBaseTests):
with grades_waffle().override(ASSUME_ZERO_GRADE_IF_ABSENT, active=enable_waffle):
with self.assertNumQueries(
initial, table_blacklist=QUERY_COUNT_TABLE_BLACKLIST
), check_mongo_calls(2):
), check_mongo_calls(1):
self._get_progress_page()
# subsequent accesses to the progress page require fewer queries.
for _ in range(2):
with self.assertNumQueries(
subsequent, table_blacklist=QUERY_COUNT_TABLE_BLACKLIST
), check_mongo_calls(2):
), check_mongo_calls(1):
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(
*itertools.product(
(
......@@ -1502,23 +1504,24 @@ class ProgressPageTests(ProgressPageBaseTests):
'lms.djangoapps.verify_student.models.SoftwareSecurePhotoVerification.user_is_verified'
) as user_verify:
user_verify.return_value = user_verified
resp = self.client.get(
reverse('progress', args=[unicode(self.course.id)])
)
with patch('lms.djangoapps.grades.new.course_grade_factory.CourseGradeFactory.create') as mock_create:
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 \
course_mode in CourseMode.VERIFIED_MODES and not user_verified
resp = self._get_progress_page()
self.assertEqual(
cert_button_hidden,
'Request Certificate' not in resp.content
)
cert_button_hidden = course_mode is CourseMode.AUDIT or \
course_mode in CourseMode.VERIFIED_MODES and not user_verified
self.assertEqual(
cert_button_hidden,
'Request Certificate' not in resp.content
)
@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):
"""
Verify that for html certs if certificate is marked as invalidated than
......@@ -1545,14 +1548,17 @@ class ProgressPageTests(ProgressPageBaseTests):
self.course.save()
self.store.update_item(self.course, self.user.id)
resp = self._get_progress_page()
self.assertContains(resp, u"View Certificate")
self.assert_invalidate_certificate(generated_certificate)
with patch('lms.djangoapps.grades.new.course_grade_factory.CourseGradeFactory.create') as mock_create:
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"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):
"""
Verify that for pdf certs if certificate is marked as invalidated than
......@@ -1562,14 +1568,15 @@ class ProgressPageTests(ProgressPageBaseTests):
"http://www.example.com/certificate.pdf", "honor"
)
resp = self._get_progress_page()
self.assertContains(resp, u'Download Your Certificate')
self.assert_invalidate_certificate(generated_certificate)
with patch('lms.djangoapps.grades.new.course_grade_factory.CourseGradeFactory.create') as mock_create:
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')
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))
def test_message_for_audit_mode(self):
""" Verify that message appears on progress page, if learner is enrolled
......@@ -1578,14 +1585,19 @@ class ProgressPageTests(ProgressPageBaseTests):
user = UserFactory.create()
self.assertTrue(self.client.login(username=user.username, password='test'))
CourseEnrollmentFactory(user=user, course_id=self.course.id, mode=CourseMode.AUDIT)
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.'
)
with patch('lms.djangoapps.grades.new.course_grade_factory.CourseGradeFactory.create') as mock_create:
course_grade = mock_create.return_value
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):
"""
Verify that invalidated cert data is returned if cert is invalidated.
......@@ -1600,11 +1612,10 @@ class ProgressPageTests(ProgressPageBaseTests):
)
# Invalidate user certificate
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.title, 'Your certificate has been invalidated')
@patch('courseware.views.views.is_course_passed', PropertyMock(return_value=True))
def test_downloadable_get_cert_data(self):
"""
Verify that downloadable cert data is returned if cert is downloadable.
......@@ -1614,12 +1625,11 @@ class ProgressPageTests(ProgressPageBaseTests):
)
with patch('certificates.api.certificate_downloadable_status',
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.title, 'Your certificate is available')
@patch('courseware.views.views.is_course_passed', PropertyMock(return_value=True))
def test_generating_get_cert_data(self):
"""
Verify that generating cert data is returned if cert is generating.
......@@ -1629,12 +1639,11 @@ class ProgressPageTests(ProgressPageBaseTests):
)
with patch('certificates.api.certificate_downloadable_status',
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.title, "We're working on it...")
@patch('courseware.views.views.is_course_passed', PropertyMock(return_value=True))
def test_unverified_get_cert_data(self):
"""
Verify that unverified cert data is returned if cert is unverified.
......@@ -1644,12 +1653,11 @@ class ProgressPageTests(ProgressPageBaseTests):
)
with patch('certificates.api.certificate_downloadable_status',
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.title, "Certificate unavailable")
@patch('courseware.views.views.is_course_passed', PropertyMock(return_value=True))
def test_request_get_cert_data(self):
"""
Verify that requested cert data is returned if cert is to be requested.
......@@ -1659,7 +1667,7 @@ class ProgressPageTests(ProgressPageBaseTests):
)
with patch('certificates.api.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.title, "Congratulations, you qualified for a certificate!")
......@@ -2013,54 +2021,6 @@ class VerifyCourseKeyDecoratorTests(TestCase):
@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):
"""
Tests for the view function Generated User Certs
......@@ -2088,12 +2048,9 @@ class GenerateUserCertTests(ModuleStoreTestCase):
self.assertEqual(resp.status_code, HttpResponseBadRequest.status_code)
self.assertIn("Your certificate will be available when you pass the course.", resp.content)
@patch(
'lms.djangoapps.grades.new.course_grade.CourseGrade.summary',
PropertyMock(return_value={'grade': 'Pass', 'percent': 0.75})
)
@patch('courseware.views.views.is_course_passed', return_value=True)
@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
# status valid code
# mocking xqueue and analytics
......@@ -2104,6 +2061,7 @@ class GenerateUserCertTests(ModuleStoreTestCase):
with patch('capa.xqueue_interface.XQueueInterface.send_to_queue') as mock_send_to_queue:
mock_send_to_queue.return_value = (0, "Successfully queued")
resp = self.client.post(self.url)
self.assertEqual(resp.status_code, 200)
......@@ -2123,10 +2081,6 @@ class GenerateUserCertTests(ModuleStoreTestCase):
)
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):
# If user has passing grade but also has existing generating cert
# then json will return cert generating message with bad request code
......@@ -2136,14 +2090,15 @@ class GenerateUserCertTests(ModuleStoreTestCase):
status=CertificateStatuses.generating,
mode='verified'
)
resp = self.client.post(self.url)
self.assertEqual(resp.status_code, HttpResponseBadRequest.status_code)
self.assertIn("Certificate is being created.", resp.content)
with patch('lms.djangoapps.grades.new.course_grade_factory.CourseGradeFactory.create') as mock_create:
course_grade = mock_create.return_value
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")
def test_user_with_passing_existing_downloadable_cert(self):
# If user has already downloadable certificate
......@@ -2156,9 +2111,14 @@ class GenerateUserCertTests(ModuleStoreTestCase):
mode='verified'
)
resp = self.client.post(self.url)
self.assertEqual(resp.status_code, HttpResponseBadRequest.status_code)
self.assertIn("Certificate has already been created.", resp.content)
with patch('lms.djangoapps.grades.new.course_grade_factory.CourseGradeFactory.create') as mock_create:
course_grade = mock_create.return_value
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):
# If try to access a course with valid key pattern then it will return
......
......@@ -71,13 +71,14 @@ from markupsafe import escape
from opaque_keys import InvalidKeyError
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.certificates import api as auto_certs_api
from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
from openedx.core.djangoapps.credit.api import (
get_credit_requirement_status,
is_credit_course,
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.monitoring_utils import set_custom_metrics_for_course_key
from openedx.core.djangoapps.plugin_api.views import EdxFragmentView
......@@ -117,6 +118,62 @@ CertData = namedtuple(
"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):
"""
......@@ -872,23 +929,22 @@ def _progress(request, course_key, student_id):
course_grade = CourseGradeFactory().create(student, course)
courseware_summary = course_grade.chapter_grades.values()
grade_summary = course_grade.summary
studio_url = get_studio_url(course, 'settings/grading')
# 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 = {
'course': course,
'courseware_summary': courseware_summary,
'studio_url': studio_url,
'grade_summary': grade_summary,
'grade_summary': course_grade.summary,
'staff_access': staff_access,
'masquerade': masquerade,
'supports_preview_menu': True,
'student': 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(
get_experiment_user_metadata_context(
......@@ -903,128 +959,68 @@ def _progress(request, course_key, student_id):
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.
Arguments:
student (User): Student for whom certificate 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.
grade_summary (dict): Student grade details.
course_grade (CourseGrade): Student's course grade record.
Returns:
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):
return 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
)
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
return AUDIT_PASSING_CERT_DATA
if certs_api.is_certificate_invalid(student, course_key):
return 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
)
certificates_enabled_for_course = certs_api.cert_generation_enabled(course.id)
if course_grade is None:
course_grade = CourseGradeFactory().create(student, course)
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(
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
)
return _certificate_message(student, course, enrollment_mode)
def _credit_course_requirements(course_key, student):
......@@ -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
Arguments:
course : course object
grade_summary (dict) : contains student grade details.
student : user object
request (HttpRequest)
course : course object
course_grade (CourseGrade) : contains student grade details.
Returns:
returns bool value
"""
nonzero_cutoffs = [cutoff for cutoff in course.grade_cutoffs.values() if cutoff > 0]
success_cutoff = min(nonzero_cutoffs) if nonzero_cutoffs else None
if grade_summary is None:
grade_summary = CourseGradeFactory().create(student, course).summary
return success_cutoff and grade_summary['percent'] >= success_cutoff
if course_grade is None:
course_grade = CourseGradeFactory().create(student, course)
return course_grade.passed
# Grades can potentially be written - if so, let grading manage the transaction.
......@@ -1343,7 +1334,7 @@ def generate_user_cert(request, course_id):
if not course:
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."))
certificate_status = certs_api.certificate_downloadable_status(student, course.id)
......
......@@ -331,7 +331,7 @@ def _section_certificates(course):
"""
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:
can_enable_for_course = True
else:
......
......@@ -1976,7 +1976,7 @@ class TestCertificateGeneration(InstructorTaskModuleTestCase):
'failed': 3,
'skipped': 2
}
with self.assertNumQueries(171):
with self.assertNumQueries(176):
self.assertCertificatesGenerated(task_input, expected_results)
expected_results = {
......
......@@ -2,8 +2,12 @@
The public API for certificates.
"""
from datetime import datetime
from pytz import UTC
from course_modes.models import CourseMode
from openedx.core.djangoapps.certificates.config import waffle
from student.models import CourseEnrollment
SWITCHES = waffle.waffle()
......@@ -19,6 +23,48 @@ def _enabled_and_instructor_paced(course):
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):
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 datetime import datetime, timedelta
import itertools
from unittest import TestCase
import ddt
import pytz
import waffle
from course_modes.models import CourseMode
from openedx.core.djangoapps.certificates import api
from openedx.core.djangoapps.certificates.config import waffle as certs_waffle
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
......@@ -15,18 +64,29 @@ def configure_waffle_namespace(feature_enabled):
namespace = certs_waffle.waffle()
with namespace.override(certs_waffle.AUTO_CERTIFICATE_GENERATION, active=feature_enabled):
yield
yield
@ddt.ddt
class FeatureEnabledTestCase(TestCase):
class CertificatesApiTestCase(TestCase):
def setUp(self):
super(FeatureEnabledTestCase, self).setUp()
self.course = CourseOverviewFactory.create()
def tearDown(self):
super(FeatureEnabledTestCase, self).tearDown()
self.course.self_paced = False
super(CertificatesApiTestCase, self).setUp()
self.course = CourseOverviewFactory.create(
start=datetime(2017, 1, 1, tzinfo=pytz.UTC),
end=datetime(2017, 1, 31, tzinfo=pytz.UTC),
certificate_available_date=None
)
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)
def test_auto_certificate_generation_enabled(self, feature_enabled):
......@@ -46,3 +106,18 @@ class FeatureEnabledTestCase(TestCase):
self.course.self_paced = is_self_paced
with configure_waffle_namespace(feature_enabled):
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