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:
......
...@@ -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