def get_course_date_blocks(course, user): """ Return the list of blocks to display on the course info page, sorted by date. """ block_classes = [ CourseEndDate, CourseStartDate, TodaysDate, VerificationDeadlineDate, VerifiedUpgradeDeadlineDate, ] if certs_api.get_active_web_certificate(course): block_classes.insert(0, CertificateAvailableDate) blocks = (cls(course, user) for cls in block_classes) def block_key_fn(block): """ If the block's date is None, return the maximum datetime in order to force it to the end of the list of displayed blocks. """ if block.date is None: return datetime.max.replace(tzinfo=pytz.UTC) return block.date return sorted((b for b in blocks if b.is_enabled), key=block_key_fn)
def get_course_date_blocks(course, user, request=None, include_access=False, include_past_dates=False, num_assignments=None): """ Return the list of blocks to display on the course info page, sorted by date. """ block_classes = [ CourseEndDate, CourseStartDate, TodaysDate, VerificationDeadlineDate, VerifiedUpgradeDeadlineDate, ] if not course.self_paced and certs_api.get_active_web_certificate(course): block_classes.insert(0, CertificateAvailableDate) blocks = [cls(course, user) for cls in block_classes] if RELATIVE_DATES_FLAG.is_enabled(course.id): blocks.append(CourseExpiredDate(course, user)) blocks.extend(get_course_assignment_date_blocks( course, user, request, num_return=num_assignments, include_access=include_access, include_past_dates=include_past_dates, )) return sorted((b for b in blocks if b.date and (b.is_enabled or include_past_dates)), key=date_block_key_fn)
def _create_from_course(course): """ Creates a CourseOverview object from a CourseDescriptor. Does not touch the database, simply constructs and returns an overview from the given course. Arguments: course (CourseDescriptor): any course descriptor object Returns: CourseOverview: overview extracted from the given course """ from lms.djangoapps.certificates.api import get_active_web_certificate from lms.djangoapps.courseware.courses import course_image_url # Workaround for a problem discovered in https://openedx.atlassian.net/browse/TNL-2806. # If the course has a malformed grading policy such that # course._grading_policy['GRADE_CUTOFFS'] = {}, then # course.lowest_passing_grade will raise a ValueError. # Work around this for now by defaulting to None. try: lowest_passing_grade = course.lowest_passing_grade except ValueError: lowest_passing_grade = None return CourseOverview( id=course.id, _location=course.location, display_name=course.display_name, display_number_with_default=course.display_number_with_default, display_org_with_default=course.display_org_with_default, start=course.start, end=course.end, advertised_start=course.advertised_start, course_image_url=course_image_url(course), facebook_url=course.facebook_url, social_sharing_url=course.social_sharing_url, certificates_display_behavior=course.certificates_display_behavior, certificates_show_before_end=course.certificates_show_before_end, cert_html_view_enabled=course.cert_html_view_enabled, has_any_active_web_certificate=(get_active_web_certificate(course) is not None), cert_name_short=course.cert_name_short, cert_name_long=course.cert_name_long, lowest_passing_grade=lowest_passing_grade, end_of_course_survey_url=course.end_of_course_survey_url, days_early_for_beta=course.days_early_for_beta, mobile_available=course.mobile_available, visible_to_staff_only=course.visible_to_staff_only, _pre_requisite_courses_json=json.dumps(course.pre_requisite_courses) )
def _create_from_course(course): """ Creates a CourseOverview object from a CourseDescriptor. Does not touch the database, simply constructs and returns an overview from the given course. Arguments: course (CourseDescriptor): any course descriptor object Returns: CourseOverview: overview extracted from the given course """ from lms.djangoapps.certificates.api import get_active_web_certificate from lms.djangoapps.courseware.courses import course_image_url # Workaround for a problem discovered in https://openedx.atlassian.net/browse/TNL-2806. # If the course has a malformed grading policy such that # course._grading_policy['GRADE_CUTOFFS'] = {}, then # course.lowest_passing_grade will raise a ValueError. # Work around this for now by defaulting to None. try: lowest_passing_grade = course.lowest_passing_grade except ValueError: lowest_passing_grade = None return CourseOverview( id=course.id, _location=course.location, display_name=course.display_name, display_number_with_default=course.display_number_with_default, display_org_with_default=course.display_org_with_default, start=course.start, end=course.end, advertised_start=course.advertised_start, course_image_url=course_image_url(course), facebook_url=course.facebook_url, social_sharing_url=course.social_sharing_url, certificates_display_behavior=course.certificates_display_behavior, certificates_show_before_end=course.certificates_show_before_end, cert_html_view_enabled=course.cert_html_view_enabled, has_any_active_web_certificate=(get_active_web_certificate(course) is not None), cert_name_short=course.cert_name_short, cert_name_long=course.cert_name_long, lowest_passing_grade=lowest_passing_grade, end_of_course_survey_url=course.end_of_course_survey_url, days_early_for_beta=course.days_early_for_beta, mobile_available=course.mobile_available, visible_to_staff_only=course.visible_to_staff_only, _pre_requisite_courses_json=json.dumps( course.pre_requisite_courses))
def _create_from_course(course): """ Creates a CourseOverview object from a CourseDescriptor. Does not touch the database, simply constructs and returns an overview from the given course. Arguments: course (CourseDescriptor): any course descriptor object Returns: CourseOverview: overview extracted from the given course """ from lms.djangoapps.certificates.api import get_active_web_certificate from lms.djangoapps.courseware.courses import course_image_url return CourseOverview( id=course.id, _location=course.location, display_name=course.display_name, display_number_with_default=course.display_number_with_default, display_org_with_default=course.display_org_with_default, start=course.start, end=course.end, advertised_start=course.advertised_start, course_image_url=course_image_url(course), facebook_url=course.facebook_url, social_sharing_url=course.social_sharing_url, certificates_display_behavior=course.certificates_display_behavior, certificates_show_before_end=course.certificates_show_before_end, cert_html_view_enabled=course.cert_html_view_enabled, has_any_active_web_certificate=(get_active_web_certificate(course) is not None), cert_name_short=course.cert_name_short, cert_name_long=course.cert_name_long, lowest_passing_grade=course.lowest_passing_grade, end_of_course_survey_url=course.end_of_course_survey_url, days_early_for_beta=course.days_early_for_beta, mobile_available=course.mobile_available, visible_to_staff_only=course.visible_to_staff_only, _pre_requisite_courses_json=json.dumps(course.pre_requisite_courses) )
def get_course_date_blocks(course, user, request=None, include_access=False, include_past_dates=False, num_assignments=None): """ Return the list of blocks to display on the course info page, sorted by date. """ blocks = [] if RELATIVE_DATES_FLAG.is_enabled(course.id): blocks.append(CourseExpiredDate(course, user)) blocks.extend( get_course_assignment_date_blocks( course, user, request, num_return=num_assignments, include_access=include_access, include_past_dates=include_past_dates, )) # Adding these in after the assignment blocks so in the case multiple blocks have the same date, # these blocks will be sorted to come after the assignments. See https://openedx.atlassian.net/browse/AA-158 default_block_classes = [ CourseEndDate, CourseStartDate, TodaysDate, VerificationDeadlineDate, VerifiedUpgradeDeadlineDate, ] if not course.self_paced and certs_api.get_active_web_certificate(course): block_classes.insert(0, CertificateAvailableDate) blocks.extend([cls(course, user) for cls in default_block_classes]) return sorted( (b for b in blocks if b.date and (b.is_enabled or include_past_dates)), key=date_block_key_fn)
def __init__(self, course, user, course_desc, path_builder=None, preview_mode=None): self.course_id = course.id self.path_builder = path_builder self.certificate_data = get_active_web_certificate( course, preview_mode) self.cert = self._get_user_certificate(user, course, preview_mode=preview_mode) course_title_from_cert = self.certificate_data.get('course_title') self.course_name = course_title_from_cert if course_title_from_cert else course.display_name self.course_desc = course_desc self.organizations = organization_api.get_course_organizations( course_id=self.course_id) self.sponsors = None self.colors = { 'base': (225 / 255.0, 0 / 255.0, 67 / 255.0), 'grey-dark': (66 / 255.0, 74 / 255.0, 82 / 255.0), 'grey-light': (113 / 255.0, 113 / 255.0, 113 / 255.0), } self.temp_file = NamedTemporaryFile(suffix='-cert.pdf') self.is_english = not contains_rtl_text(self.course_name) self.user_profile_name = self._get_user_name(user) self.left_panel_center = 1.6 self.ctx = None self.size = None self.font = None self.font_size = None
def check_course_overview_against_course(self, course): """ Compares a CourseOverview object against its corresponding CourseDescriptor object. Specifically, given a course, test that data within the following three objects match each other: - the CourseDescriptor itself - a CourseOverview that was newly constructed from _create_from_course - a CourseOverview that was loaded from the MySQL database Arguments: course (CourseDescriptor): the course to be checked. """ def get_seconds_since_epoch(date_time): """ Returns the number of seconds between the Unix Epoch and the given datetime. If the given datetime is None, return None. Arguments: date_time (datetime): the datetime in question. """ if date_time is None: return None epoch = datetime.datetime.utcfromtimestamp(0).replace(tzinfo=pytz.utc) return math.floor((date_time - epoch).total_seconds()) # Load the CourseOverview from the cache twice. The first load will be a cache miss (because the cache # is empty) so the course will be newly created with CourseOverviewDescriptor.create_from_course. The second # load will be a cache hit, so the course will be loaded from the cache. course_overview_cache_miss = CourseOverview.get_from_id(course.id) course_overview_cache_hit = CourseOverview.get_from_id(course.id) # Test if value of these attributes match between the three objects fields_to_test = [ 'id', 'display_name', 'display_number_with_default', 'display_org_with_default', 'advertised_start', 'facebook_url', 'social_sharing_url', 'certificates_display_behavior', 'certificates_show_before_end', 'cert_name_short', 'cert_name_long', 'lowest_passing_grade', 'end_of_course_survey_url', 'mobile_available', 'visible_to_staff_only', 'location', 'number', 'url_name', 'display_name_with_default', 'start_date_is_still_default', 'pre_requisite_courses', ] for attribute_name in fields_to_test: course_value = getattr(course, attribute_name) cache_miss_value = getattr(course_overview_cache_miss, attribute_name) cache_hit_value = getattr(course_overview_cache_hit, attribute_name) self.assertEqual(course_value, cache_miss_value) self.assertEqual(cache_miss_value, cache_hit_value) # Test if return values for all methods are equal between the three objects methods_to_test = [ ('clean_id', ()), ('clean_id', ('#',)), ('has_ended', ()), ('has_started', ()), ('start_datetime_text', ('SHORT_DATE',)), ('start_datetime_text', ('DATE_TIME',)), ('end_datetime_text', ('SHORT_DATE',)), ('end_datetime_text', ('DATE_TIME',)), ('may_certify', ()), ] for method_name, method_args in methods_to_test: course_value = getattr(course, method_name)(*method_args) cache_miss_value = getattr(course_overview_cache_miss, method_name)(*method_args) cache_hit_value = getattr(course_overview_cache_hit, method_name)(*method_args) self.assertEqual(course_value, cache_miss_value) self.assertEqual(cache_miss_value, cache_hit_value) # Other values to test # Note: we test the start and end attributes here instead of in # fields_to_test, because I ran into trouble while testing datetimes # for equality. When writing and reading dates from databases, the # resulting values are often off by fractions of a second. So, as a # workaround, we simply test if the start and end times are the same # number of seconds from the Unix epoch. others_to_test = [( course_image_url(course), course_overview_cache_miss.course_image_url, course_overview_cache_hit.course_image_url ), ( get_active_web_certificate(course) is not None, course_overview_cache_miss.has_any_active_web_certificate, course_overview_cache_hit.has_any_active_web_certificate ), ( get_seconds_since_epoch(course.start), get_seconds_since_epoch(course_overview_cache_miss.start), get_seconds_since_epoch(course_overview_cache_hit.start), ), ( get_seconds_since_epoch(course.end), get_seconds_since_epoch(course_overview_cache_miss.end), get_seconds_since_epoch(course_overview_cache_hit.end), )] for (course_value, cache_miss_value, cache_hit_value) in others_to_test: self.assertEqual(course_value, cache_miss_value) self.assertEqual(cache_miss_value, cache_hit_value)
def is_allowed(self): return (can_show_certificate_available_date_field(self.course) and self.has_certificate_modes and get_active_web_certificate(self.course))
def check_course_overview_against_course(self, course): """ Compares a CourseOverview object against its corresponding CourseDescriptor object. Specifically, given a course, test that data within the following three objects match each other: - the CourseDescriptor itself - a CourseOverview that was newly constructed from _create_or_update - a CourseOverview that was loaded from the MySQL database Arguments: course (CourseDescriptor): the course to be checked. """ def get_seconds_since_epoch(date_time): """ Returns the number of seconds between the Unix Epoch and the given datetime. If the given datetime is None, return None. Arguments: date_time (datetime): the datetime in question. """ if date_time is None: return None epoch = datetime.datetime.utcfromtimestamp(0).replace(tzinfo=pytz.utc) return math.floor((date_time - epoch).total_seconds()) # Load the CourseOverview from the cache twice. The first load will be a cache miss (because the cache # is empty) so the course will be newly created with CourseOverview._create_or_update. The second # load will be a cache hit, so the course will be loaded from the cache. course_overview_cache_miss = CourseOverview.get_from_id(course.id) course_overview_cache_hit = CourseOverview.get_from_id(course.id) # Test if value of these attributes match between the three objects fields_to_test = [ 'id', 'display_name', 'display_number_with_default', 'display_org_with_default', 'advertised_start', 'social_sharing_url', 'certificates_display_behavior', 'certificates_show_before_end', 'cert_name_short', 'cert_name_long', 'lowest_passing_grade', 'end_of_course_survey_url', 'mobile_available', 'visible_to_staff_only', 'location', 'number', 'url_name', 'display_name_with_default', 'display_name_with_default_escaped', 'start_date_is_still_default', 'pre_requisite_courses', 'enrollment_domain', 'invitation_only', 'max_student_enrollments_allowed', 'catalog_visibility', ] for attribute_name in fields_to_test: course_value = getattr(course, attribute_name) cache_miss_value = getattr(course_overview_cache_miss, attribute_name) cache_hit_value = getattr(course_overview_cache_hit, attribute_name) self.assertEqual(course_value, cache_miss_value) self.assertEqual(cache_miss_value, cache_hit_value) # Test if return values for all methods are equal between the three objects methods_to_test = [ ('clean_id', ()), ('clean_id', ('#',)), ('has_ended', ()), ('has_started', ()), ('may_certify', ()), ] for method_name, method_args in methods_to_test: course_value = getattr(course, method_name)(*method_args) cache_miss_value = getattr(course_overview_cache_miss, method_name)(*method_args) cache_hit_value = getattr(course_overview_cache_hit, method_name)(*method_args) self.assertEqual(course_value, cache_miss_value) self.assertEqual(cache_miss_value, cache_hit_value) # Other values to test # Note: we test the time-related attributes here instead of in # fields_to_test, because we run into trouble while testing datetimes # for equality. When writing and reading dates from databases, the # resulting values are often off by fractions of a second. So, as a # workaround, we simply test if the start and end times are the same # number of seconds from the Unix epoch. time_field_accessor = lambda object, field_name: get_seconds_since_epoch(getattr(object, field_name)) # The course about fields are accessed through the CourseDetail # class for the course module, and stored as attributes on the # CourseOverview objects. course_about_accessor = lambda object, field_name: CourseDetails.fetch_about_attribute(object.id, field_name) others_to_test = [ ('start', time_field_accessor, time_field_accessor), ('end', time_field_accessor, time_field_accessor), ('enrollment_start', time_field_accessor, time_field_accessor), ('enrollment_end', time_field_accessor, time_field_accessor), ('announcement', time_field_accessor, time_field_accessor), ('short_description', course_about_accessor, getattr), ('effort', course_about_accessor, getattr), ( 'video', lambda c, __: CourseDetails.fetch_video_url(c.id), lambda c, __: c.course_video_url, ), ( 'course_image_url', lambda c, __: course_image_url(c), getattr, ), ( 'has_any_active_web_certificate', lambda c, field_name: get_active_web_certificate(c) is not None, getattr, ), ] for attribute_name, course_accessor, course_overview_accessor in others_to_test: course_value = course_accessor(course, attribute_name) cache_miss_value = course_overview_accessor(course_overview_cache_miss, attribute_name) cache_hit_value = course_overview_accessor(course_overview_cache_hit, attribute_name) self.assertEqual(course_value, cache_miss_value) self.assertEqual(cache_miss_value, cache_hit_value) # test tabs for both cached miss and cached hit courses for course_overview in [course_overview_cache_miss, course_overview_cache_hit]: course_overview_tabs = course_overview.tabs.all() course_resp_tabs = {tab.tab_id for tab in course_overview_tabs} self.assertEqual(self.COURSE_OVERVIEW_TABS, course_resp_tabs)
def _section_certificates(course): """Section information for the certificates panel. The certificates panel allows global staff to generate example certificates and enable self-generated certificates for a course. Arguments: course (Course) Returns: dict """ example_cert_status = None html_cert_enabled = certs_api.has_html_certificates_enabled(course) if html_cert_enabled: can_enable_for_course = True else: example_cert_status = certs_api.example_certificates_status(course.id) # Allow the user to enable self-generated certificates for students # *only* once a set of example certificates has been successfully generated. # If certificates have been misconfigured for the course (for example, if # the PDF template hasn't been uploaded yet), then we don't want # to turn on self-generated certificates for students! can_enable_for_course = ( example_cert_status is not None and all( cert_status['status'] == 'success' for cert_status in example_cert_status ) ) instructor_generation_enabled = settings.FEATURES.get('CERTIFICATES_INSTRUCTOR_GENERATION', False) certificate_statuses_with_count = { certificate['status']: certificate['count'] for certificate in GeneratedCertificate.get_unique_statuses(course_key=course.id) } return { 'section_key': 'certificates', 'section_display_name': _('Certificates'), 'example_certificate_status': example_cert_status, 'can_enable_for_course': can_enable_for_course, 'enabled_for_course': certs_api.cert_generation_enabled(course.id), 'is_self_paced': course.self_paced, 'instructor_generation_enabled': instructor_generation_enabled, 'html_cert_enabled': html_cert_enabled, 'active_certificate': certs_api.get_active_web_certificate(course), 'certificate_statuses_with_count': certificate_statuses_with_count, 'status': CertificateStatuses, 'certificate_generation_history': CertificateGenerationHistory.objects.filter(course_id=course.id).order_by("-created"), 'urls': { 'generate_example_certificates': reverse( 'generate_example_certificates', kwargs={'course_id': course.id} ), 'enable_certificate_generation': reverse( 'enable_certificate_generation', kwargs={'course_id': course.id} ), 'start_certificate_generation': reverse( 'start_certificate_generation', kwargs={'course_id': course.id} ), 'start_certificate_regeneration': reverse( 'start_certificate_regeneration', kwargs={'course_id': course.id} ), 'list_instructor_tasks_url': reverse( 'list_instructor_tasks', kwargs={'course_id': course.id} ), } }
def render_html_view(request, course_id, certificate=None): # pylint: disable=too-many-statements """ This public view generates an HTML representation of the specified user and course If a certificate is not available, we display a "Sorry!" screen instead It can be overridden by setting `OVERRIDE_RENDER_CERTIFICATE_VIEW` to an alternative implementation. """ user = certificate.user if certificate else request.user user_id = user.id preview_mode = request.GET.get('preview', None) platform_name = configuration_helpers.get_value("platform_name", settings.PLATFORM_NAME) configuration = CertificateHtmlViewConfiguration.get_config() # 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_invalid_certificate(request, course_id, platform_name, configuration) # Load the course and user objects try: course_key = CourseKey.from_string(course_id) course = get_course_by_id(course_key) # For any course or user exceptions, kick the user back to the "Invalid" screen except (InvalidKeyError, Http404) as exception: error_str = ( "Invalid cert: error finding course %s " "Specific error: %s" ) log.info(error_str, course_id, str(exception)) return _render_invalid_certificate(request, course_id, platform_name, configuration) course_overview = get_course_overview_or_none(course_key) # 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_invalid_certificate(request, course_id, platform_name, configuration) # Load user's certificate user_certificate = _get_user_certificate(request, user, course_key, course_overview, preview_mode) if not user_certificate: log.info( "Invalid cert: User %d does not have eligible cert for %s.", user_id, course_id, ) return _render_invalid_certificate(request, course_id, platform_name, configuration) # Get the active certificate configuration for this course # If we do not have an active certificate, we'll need to send the user to the "Invalid" screen # Passing in the 'preview' parameter, if specified, will return a configuration, if defined active_configuration = get_active_web_certificate(course, preview_mode) if active_configuration is None: log.info( "Invalid cert: course %s does not have an active configuration. User id: %d", course_id, user_id, ) return _render_invalid_certificate(request, course_id, platform_name, configuration) # Get data from Discovery service that will be necessary for rendering this Certificate. catalog_data = _get_catalog_data_for_course(course_key) # Determine whether to use the standard or custom template to render the certificate. custom_template = None custom_template_language = None if settings.FEATURES.get('CUSTOM_CERTIFICATE_TEMPLATES_ENABLED', False): log.info("Custom certificate for course %s", course_id) custom_template, custom_template_language = _get_custom_template_and_language( course.id, user_certificate.mode, catalog_data.pop('content_language', None) ) # Determine the language that should be used to render the certificate. # For the standard certificate template, use the user language. For custom templates, use # the language associated with the template. user_language = translation.get_language() certificate_language = custom_template_language if custom_template else user_language log.info( "certificate language is: %s for the course: %s", certificate_language, course_key ) # Generate the certificate context in the correct language, then render the template. with translation.override(certificate_language): context = {'user_language': user_language} _update_context_with_basic_info(context, course_id, platform_name, configuration) context['certificate_data'] = active_configuration # Append/Override the existing view context values with any mode-specific ConfigurationModel values context.update(configuration.get(user_certificate.mode, {})) # Append organization info _update_organization_context(context, course) # Append course info _update_course_context(request, context, course, platform_name) # Append course run info from discovery context.update(catalog_data) # Append user info _update_context_with_user_info(context, user, user_certificate) # Append social sharing info _update_social_context(request, context, course, user_certificate, platform_name) # Append/Override the existing view context values with certificate specific values _update_certificate_context(context, course, course_overview, user_certificate, platform_name) # Append badge info _update_badge_context(context, course, user) # Add certificate header/footer data to current context context.update(get_certificate_header_context(is_secure=request.is_secure())) context.update(get_certificate_footer_context()) # Append/Override the existing view context values with any course-specific static values from Advanced Settings context.update(course.cert_html_view_overrides) # Track certificate view events _track_certificate_events(request, course, user, user_certificate) try: # .. filter_implemented_name: CertificateRenderStarted # .. filter_type: org.openedx.learning.certificate.render.started.v1 context, custom_template = CertificateRenderStarted.run_filter( context=context, custom_template=custom_template, ) except CertificateRenderStarted.RenderAlternativeInvalidCertificate as exc: response = _render_invalid_certificate( request, course_id, platform_name, configuration, cert_path=exc.template_name or INVALID_CERTIFICATE_TEMPLATE_PATH, ) except CertificateRenderStarted.RedirectToPage as exc: response = HttpResponseRedirect(exc.redirect_to) except CertificateRenderStarted.RenderCustomResponse as exc: response = exc.response else: response = _render_valid_certificate(request, context, custom_template) # Render the certificate return response
def _create_or_update(cls, course): """ Creates or updates a CourseOverview object from a CourseDescriptor. Does not touch the database, simply constructs and returns an overview from the given course. Arguments: course (CourseDescriptor): any course descriptor object Returns: CourseOverview: created or updated overview extracted from the given course """ from lms.djangoapps.certificates.api import get_active_web_certificate from openedx.core.lib.courses import course_image_url # Workaround for a problem discovered in https://openedx.atlassian.net/browse/TNL-2806. # If the course has a malformed grading policy such that # course._grading_policy['GRADE_CUTOFFS'] = {}, then # course.lowest_passing_grade will raise a ValueError. # Work around this for now by defaulting to None. try: lowest_passing_grade = course.lowest_passing_grade except ValueError: lowest_passing_grade = None display_name = course.display_name start = course.start end = course.end max_student_enrollments_allowed = course.max_student_enrollments_allowed if isinstance(course.id, CCXLocator): from lms.djangoapps.ccx.utils import get_ccx_from_ccx_locator ccx = get_ccx_from_ccx_locator(course.id) display_name = ccx.display_name start = ccx.start end = ccx.due max_student_enrollments_allowed = ccx.max_student_enrollments_allowed course_overview = cls.objects.filter(id=course.id) if course_overview.exists(): log.info(u'Updating course overview for %s.', six.text_type(course.id)) course_overview = course_overview.first() else: log.info(u'Creating course overview for %s.', six.text_type(course.id)) course_overview = cls() course_overview.version = cls.VERSION course_overview.id = course.id course_overview._location = course.location course_overview.org = course.location.org course_overview.display_name = display_name course_overview.display_number_with_default = course.display_number_with_default course_overview.display_org_with_default = course.display_org_with_default course_overview.start = start course_overview.end = end course_overview.advertised_start = course.advertised_start course_overview.announcement = course.announcement course_overview.course_image_url = course_image_url(course) course_overview.social_sharing_url = course.social_sharing_url course_overview.certificates_display_behavior = course.certificates_display_behavior course_overview.certificates_show_before_end = course.certificates_show_before_end course_overview.cert_html_view_enabled = course.cert_html_view_enabled course_overview.has_any_active_web_certificate = ( get_active_web_certificate(course) is not None) course_overview.cert_name_short = course.cert_name_short course_overview.cert_name_long = course.cert_name_long course_overview.certificate_available_date = course.certificate_available_date course_overview.lowest_passing_grade = lowest_passing_grade course_overview.end_of_course_survey_url = course.end_of_course_survey_url course_overview.days_early_for_beta = course.days_early_for_beta course_overview.mobile_available = course.mobile_available course_overview.visible_to_staff_only = course.visible_to_staff_only course_overview._pre_requisite_courses_json = json.dumps( course.pre_requisite_courses) course_overview.enrollment_start = course.enrollment_start course_overview.enrollment_end = course.enrollment_end course_overview.enrollment_domain = course.enrollment_domain course_overview.invitation_only = course.invitation_only course_overview.max_student_enrollments_allowed = max_student_enrollments_allowed course_overview.catalog_visibility = course.catalog_visibility course_overview.short_description = CourseDetails.fetch_about_attribute( course.id, 'short_description') course_overview.effort = CourseDetails.fetch_about_attribute( course.id, 'effort') course_overview.course_video_url = CourseDetails.fetch_video_url( course.id) course_overview.self_paced = course.self_paced if not CatalogIntegration.is_enabled(): course_overview.language = course.language return course_overview
def _create_or_update(cls, course): # lint-amnesty, pylint: disable=too-many-statements """ Creates or updates a CourseOverview object from a CourseBlock. Does not touch the database, simply constructs and returns an overview from the given course. Arguments: course (CourseBlock): any course descriptor object Returns: CourseOverview: created or updated overview extracted from the given course """ from lms.djangoapps.certificates.api import get_active_web_certificate from openedx.core.lib.courses import course_image_url # Workaround for a problem discovered in https://openedx.atlassian.net/browse/TNL-2806. # If the course has a malformed grading policy such that # course._grading_policy['GRADE_CUTOFFS'] = {}, then # course.lowest_passing_grade will raise a ValueError. # Work around this for now by defaulting to None. try: lowest_passing_grade = course.lowest_passing_grade except ValueError: lowest_passing_grade = None display_name = course.display_name start = course.start end = course.end max_student_enrollments_allowed = course.max_student_enrollments_allowed if isinstance(course.id, CCXLocator): from lms.djangoapps.ccx.utils import get_ccx_from_ccx_locator ccx = get_ccx_from_ccx_locator(course.id) display_name = ccx.display_name start = ccx.start end = ccx.due max_student_enrollments_allowed = ccx.max_student_enrollments_allowed course_overview = cls.objects.filter(id=course.id) if course_overview.exists(): log.info('Updating course overview for %s.', str(course.id)) course_overview = course_overview.first() # MySQL ignores casing, but CourseKey doesn't. To prevent multiple # courses with different cased keys from overriding each other, we'll # check for equality here in python. if course_overview.id != course.id: raise CourseOverviewCaseMismatchException( course_overview.id, course.id) else: log.info('Creating course overview for %s.', str(course.id)) course_overview = cls() course_overview.version = cls.VERSION course_overview.id = course.id course_overview._location = course.location # lint-amnesty, pylint: disable=protected-access course_overview.org = course.location.org course_overview.display_name = display_name course_overview.display_number_with_default = course.display_number_with_default course_overview.display_org_with_default = course.display_org_with_default course_overview.start = start course_overview.end = end course_overview.advertised_start = course.advertised_start course_overview.announcement = course.announcement course_overview.banner_image_url = course_image_url( course, 'banner_image') course_overview.course_image_url = course_image_url(course) course_overview.social_sharing_url = course.social_sharing_url updated_available_date, updated_display_behavior = CourseDetails.validate_certificate_settings( course.certificate_available_date, course.certificates_display_behavior) course_overview.certificates_display_behavior = updated_display_behavior course_overview.certificate_available_date = updated_available_date course_overview.certificates_show_before_end = course.certificates_show_before_end course_overview.cert_html_view_enabled = course.cert_html_view_enabled course_overview.has_any_active_web_certificate = ( get_active_web_certificate(course) is not None) course_overview.cert_name_short = course.cert_name_short course_overview.cert_name_long = course.cert_name_long course_overview.lowest_passing_grade = lowest_passing_grade course_overview.end_of_course_survey_url = course.end_of_course_survey_url course_overview.days_early_for_beta = course.days_early_for_beta course_overview.mobile_available = course.mobile_available course_overview.visible_to_staff_only = course.visible_to_staff_only course_overview._pre_requisite_courses_json = json.dumps( course.pre_requisite_courses) # lint-amnesty, pylint: disable=protected-access course_overview.enrollment_start = course.enrollment_start course_overview.enrollment_end = course.enrollment_end course_overview.enrollment_domain = course.enrollment_domain course_overview.invitation_only = course.invitation_only course_overview.max_student_enrollments_allowed = max_student_enrollments_allowed course_overview.catalog_visibility = course.catalog_visibility course_overview.short_description = CourseDetails.fetch_about_attribute( course.id, 'short_description') course_overview.effort = CourseDetails.fetch_about_attribute( course.id, 'effort') course_overview.course_video_url = CourseDetails.fetch_video_url( course.id) course_overview.self_paced = course.self_paced course_overview.has_highlights = cls._get_course_has_highlights(course) course_overview.enable_proctored_exams = course.enable_proctored_exams course_overview.proctoring_provider = course.proctoring_provider course_overview.proctoring_escalation_email = course.proctoring_escalation_email course_overview.allow_proctoring_opt_out = course.allow_proctoring_opt_out course_overview.entrance_exam_enabled = course.entrance_exam_enabled # entrance_exam_id defaults to None in the course object, but '' is more reasonable for a string field course_overview.entrance_exam_id = course.entrance_exam_id or '' # Despite it being a float, the course object defaults to an int. So we will detect that case and update # it to be a float like everything else. if isinstance(course.entrance_exam_minimum_score_pct, int): course_overview.entrance_exam_minimum_score_pct = course.entrance_exam_minimum_score_pct / 100 else: course_overview.entrance_exam_minimum_score_pct = course.entrance_exam_minimum_score_pct if not CatalogIntegration.is_enabled(): course_overview.language = course.language return course_overview
def check_course_overview_against_course(self, course): """ Compares a CourseOverview object against its corresponding CourseDescriptor object. Specifically, given a course, test that data within the following three objects match each other: - the CourseDescriptor itself - a CourseOverview that was newly constructed from _create_from_course - a CourseOverview that was loaded from the MySQL database Arguments: course (CourseDescriptor): the course to be checked. """ def get_seconds_since_epoch(date_time): """ Returns the number of seconds between the Unix Epoch and the given datetime. If the given datetime is None, return None. Arguments: date_time (datetime): the datetime in question. """ if date_time is None: return None epoch = datetime.datetime.utcfromtimestamp(0).replace( tzinfo=pytz.utc) return math.floor((date_time - epoch).total_seconds()) # Load the CourseOverview from the cache twice. The first load will be a cache miss (because the cache # is empty) so the course will be newly created with CourseOverviewDescriptor.create_from_course. The second # load will be a cache hit, so the course will be loaded from the cache. course_overview_cache_miss = CourseOverview.get_from_id(course.id) course_overview_cache_hit = CourseOverview.get_from_id(course.id) # Test if value of these attributes match between the three objects fields_to_test = [ 'id', 'display_name', 'display_number_with_default', 'display_org_with_default', 'advertised_start', 'facebook_url', 'social_sharing_url', 'certificates_display_behavior', 'certificates_show_before_end', 'cert_name_short', 'cert_name_long', 'lowest_passing_grade', 'end_of_course_survey_url', 'mobile_available', 'visible_to_staff_only', 'location', 'number', 'url_name', 'display_name_with_default', 'start_date_is_still_default', 'pre_requisite_courses', ] for attribute_name in fields_to_test: course_value = getattr(course, attribute_name) cache_miss_value = getattr(course_overview_cache_miss, attribute_name) cache_hit_value = getattr(course_overview_cache_hit, attribute_name) self.assertEqual(course_value, cache_miss_value) self.assertEqual(cache_miss_value, cache_hit_value) # Test if return values for all methods are equal between the three objects methods_to_test = [ ('clean_id', ()), ('clean_id', ('#', )), ('has_ended', ()), ('has_started', ()), ('start_datetime_text', ('SHORT_DATE', )), ('start_datetime_text', ('DATE_TIME', )), ('end_datetime_text', ('SHORT_DATE', )), ('end_datetime_text', ('DATE_TIME', )), ('may_certify', ()), ] for method_name, method_args in methods_to_test: course_value = getattr(course, method_name)(*method_args) cache_miss_value = getattr(course_overview_cache_miss, method_name)(*method_args) cache_hit_value = getattr(course_overview_cache_hit, method_name)(*method_args) self.assertEqual(course_value, cache_miss_value) self.assertEqual(cache_miss_value, cache_hit_value) # Other values to test # Note: we test the start and end attributes here instead of in # fields_to_test, because I ran into trouble while testing datetimes # for equality. When writing and reading dates from databases, the # resulting values are often off by fractions of a second. So, as a # workaround, we simply test if the start and end times are the same # number of seconds from the Unix epoch. others_to_test = [ (course_image_url(course), course_overview_cache_miss.course_image_url, course_overview_cache_hit.course_image_url), (get_active_web_certificate(course) is not None, course_overview_cache_miss.has_any_active_web_certificate, course_overview_cache_hit.has_any_active_web_certificate), ( get_seconds_since_epoch(course.start), get_seconds_since_epoch(course_overview_cache_miss.start), get_seconds_since_epoch(course_overview_cache_hit.start), ), ( get_seconds_since_epoch(course.end), get_seconds_since_epoch(course_overview_cache_miss.end), get_seconds_since_epoch(course_overview_cache_hit.end), ) ] for (course_value, cache_miss_value, cache_hit_value) in others_to_test: self.assertEqual(course_value, cache_miss_value) self.assertEqual(cache_miss_value, cache_hit_value)
def render_html_view(request, user_id, course_id): """ This public view generates an HTML representation of the specified user and course If a certificate is not available, we display a "Sorry!" screen instead """ try: user_id = int(user_id) except ValueError: raise Http404 preview_mode = request.GET.get('preview', None) platform_name = configuration_helpers.get_value("platform_name", settings.PLATFORM_NAME) configuration = CertificateHtmlViewConfiguration.get_config() # 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_invalid_certificate(course_id, platform_name, configuration) # Load the course and user objects try: course_key = CourseKey.from_string(course_id) user = User.objects.get(id=user_id) course = get_course_by_id(course_key) # For any course or user exceptions, kick the user back to the "Invalid" screen except (InvalidKeyError, User.DoesNotExist, Http404) as exception: error_str = ( u"Invalid cert: error finding course %s or user with id " u"%d. Specific error: %s" ) log.info(error_str, course_id, user_id, str(exception)) return _render_invalid_certificate(course_id, platform_name, configuration) # 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( u"Invalid cert: HTML certificates disabled for %s. User id: %d", course_id, user_id, ) return _render_invalid_certificate(course_id, platform_name, configuration) # Load user's certificate user_certificate = _get_user_certificate(request, user, course_key, course, preview_mode) if not user_certificate: log.info( u"Invalid cert: User %d does not have eligible cert for %s.", user_id, course_id, ) return _render_invalid_certificate(course_id, platform_name, configuration) # Get the active certificate configuration for this course # If we do not have an active certificate, we'll need to send the user to the "Invalid" screen # Passing in the 'preview' parameter, if specified, will return a configuration, if defined active_configuration = get_active_web_certificate(course, preview_mode) if active_configuration is None: log.info( u"Invalid cert: course %s does not have an active configuration. User id: %d", course_id, user_id, ) return _render_invalid_certificate(course_id, platform_name, configuration) # Get data from Discovery service that will be necessary for rendering this Certificate. catalog_data = _get_catalog_data_for_course(course_key) # Determine whether to use the standard or custom template to render the certificate. custom_template = None custom_template_language = None if settings.FEATURES.get('CUSTOM_CERTIFICATE_TEMPLATES_ENABLED', False): log.info(u"Custom certificate for course %s", course_id) custom_template, custom_template_language = _get_custom_template_and_language( course.id, user_certificate.mode, catalog_data.pop('content_language', None) ) # Determine the language that should be used to render the certificate. # For the standard certificate template, use the user language. For custom templates, use # the language associated with the template. user_language = translation.get_language() certificate_language = custom_template_language if custom_template else user_language log.info( u"certificate language is: %s for the course: %s", certificate_language, course_key ) # Generate the certificate context in the correct language, then render the template. with translation.override(certificate_language): context = {'user_language': user_language} _update_context_with_basic_info(context, course_id, platform_name, configuration) context['certificate_data'] = active_configuration # Append/Override the existing view context values with any mode-specific ConfigurationModel values context.update(configuration.get(user_certificate.mode, {})) # Append organization info _update_organization_context(context, course) # Append course info _update_course_context(request, context, course, course_key, platform_name) # Append course run info from discovery context.update(catalog_data) # Append user info _update_context_with_user_info(context, user, user_certificate) # Append social sharing info _update_social_context(request, context, course, user, user_certificate, platform_name) # Append/Override the existing view context values with certificate specific values _update_certificate_context(context, course, user_certificate, platform_name) # Append badge info _update_badge_context(context, course, user) # Append site configuration overrides _update_configuration_context(context, configuration) # Add certificate header/footer data to current context context.update(get_certificate_header_context(is_secure=request.is_secure())) context.update(get_certificate_footer_context()) # Append/Override the existing view context values with any course-specific static values from Advanced Settings context.update(course.cert_html_view_overrides) # Track certificate view events _track_certificate_events(request, context, course, user, user_certificate) # Render the certificate return _render_valid_certificate(request, context, custom_template)
def render_html_view(request, user_id, course_id): """ This public view generates an HTML representation of the specified user and course If a certificate is not available, we display a "Sorry!" screen instead """ try: user_id = int(user_id) except ValueError: raise Http404 preview_mode = request.GET.get('preview', None) platform_name = configuration_helpers.get_value("platform_name", settings.PLATFORM_NAME) configuration = CertificateHtmlViewConfiguration.get_config() # 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_invalid_certificate(course_id, platform_name, configuration) # Load the course and user objects try: course_key = CourseKey.from_string(course_id) user = User.objects.get(id=user_id) course = get_course_by_id(course_key) # For any course or user exceptions, kick the user back to the "Invalid" screen except (InvalidKeyError, User.DoesNotExist, Http404) as exception: error_str = (u"Invalid cert: error finding course %s or user with id " u"%d. Specific error: %s") log.info(error_str, course_id, user_id, str(exception)) return _render_invalid_certificate(course_id, platform_name, configuration) # 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( u"Invalid cert: HTML certificates disabled for %s. User id: %d", course_id, user_id, ) return _render_invalid_certificate(course_id, platform_name, configuration) # Load user's certificate user_certificate = _get_user_certificate(request, user, course_key, course, preview_mode) if not user_certificate: log.info( u"Invalid cert: User %d does not have eligible cert for %s.", user_id, course_id, ) return _render_invalid_certificate(course_id, platform_name, configuration) # Get the active certificate configuration for this course # If we do not have an active certificate, we'll need to send the user to the "Invalid" screen # Passing in the 'preview' parameter, if specified, will return a configuration, if defined active_configuration = get_active_web_certificate(course, preview_mode) if active_configuration is None: log.info( u"Invalid cert: course %s does not have an active configuration. User id: %d", course_id, user_id, ) return _render_invalid_certificate(course_id, platform_name, configuration) # Get data from Discovery service that will be necessary for rendering this Certificate. catalog_data = _get_catalog_data_for_course(course_key) # Determine whether to use the standard or custom template to render the certificate. custom_template = None custom_template_language = None if settings.FEATURES.get('CUSTOM_CERTIFICATE_TEMPLATES_ENABLED', False): log.info("Custom certificate for course {course_id}".format( course_id=course_id)) custom_template, custom_template_language = _get_custom_template_and_language( course.id, user_certificate.mode, catalog_data.pop('content_language', None)) # Determine the language that should be used to render the certificate. # For the standard certificate template, use the user language. For custom templates, use # the language associated with the template. user_language = translation.get_language() certificate_language = custom_template_language if custom_template else user_language log.info("certificate language is: {language} for the course:{course_key}". format(language=certificate_language, course_key=course_key)) # Generate the certificate context in the correct language, then render the template. with translation.override(certificate_language): context = {'user_language': user_language} _update_context_with_basic_info(context, course_id, platform_name, configuration) context['certificate_data'] = active_configuration # Append/Override the existing view context values with any mode-specific ConfigurationModel values context.update(configuration.get(user_certificate.mode, {})) # Append organization info _update_organization_context(context, course) # Append course info _update_course_context(request, context, course, course_key, platform_name) # Append course run info from discovery context.update(catalog_data) # Append user info _update_context_with_user_info(context, user, user_certificate) # Append social sharing info _update_social_context(request, context, course, user, user_certificate, platform_name) # Append/Override the existing view context values with certificate specific values _update_certificate_context(context, course, user_certificate, platform_name) # Append badge info _update_badge_context(context, course, user) # Append site configuration overrides _update_configuration_context(context, configuration) # Add certificate header/footer data to current context context.update( get_certificate_header_context(is_secure=request.is_secure())) context.update(get_certificate_footer_context()) # Append/Override the existing view context values with any course-specific static values from Advanced Settings context.update(course.cert_html_view_overrides) # Track certificate view events _track_certificate_events(request, context, course, user, user_certificate) # Render the certificate return _render_valid_certificate(request, context, custom_template)
def _create_from_course(cls, course): """ Creates a CourseOverview object from a CourseDescriptor. Does not touch the database, simply constructs and returns an overview from the given course. Arguments: course (CourseDescriptor): any course descriptor object Returns: CourseOverview: overview extracted from the given course """ from lms.djangoapps.certificates.api import get_active_web_certificate from openedx.core.lib.courses import course_image_url log.info('Creating course overview for %s.', unicode(course.id)) # Workaround for a problem discovered in https://openedx.atlassian.net/browse/TNL-2806. # If the course has a malformed grading policy such that # course._grading_policy['GRADE_CUTOFFS'] = {}, then # course.lowest_passing_grade will raise a ValueError. # Work around this for now by defaulting to None. try: lowest_passing_grade = course.lowest_passing_grade except ValueError: lowest_passing_grade = None display_name = course.display_name start = course.start end = course.end max_student_enrollments_allowed = course.max_student_enrollments_allowed if isinstance(course.id, CCXLocator): from lms.djangoapps.ccx.utils import get_ccx_from_ccx_locator ccx = get_ccx_from_ccx_locator(course.id) display_name = ccx.display_name start = ccx.start end = ccx.due max_student_enrollments_allowed = ccx.max_student_enrollments_allowed return cls( version=cls.VERSION, id=course.id, _location=course.location, org=course.location.org, display_name=display_name, display_number_with_default=course.display_number_with_default, display_org_with_default=course.display_org_with_default, start=start, end=end, advertised_start=course.advertised_start, announcement=course.announcement, course_image_url=course_image_url(course), social_sharing_url=course.social_sharing_url, certificates_display_behavior=course.certificates_display_behavior, certificates_show_before_end=course.certificates_show_before_end, cert_html_view_enabled=course.cert_html_view_enabled, has_any_active_web_certificate=(get_active_web_certificate(course) is not None), cert_name_short=course.cert_name_short, cert_name_long=course.cert_name_long, lowest_passing_grade=lowest_passing_grade, end_of_course_survey_url=course.end_of_course_survey_url, days_early_for_beta=course.days_early_for_beta, mobile_available=course.mobile_available, visible_to_staff_only=course.visible_to_staff_only, _pre_requisite_courses_json=json.dumps(course.pre_requisite_courses), enrollment_start=course.enrollment_start, enrollment_end=course.enrollment_end, enrollment_domain=course.enrollment_domain, invitation_only=course.invitation_only, max_student_enrollments_allowed=max_student_enrollments_allowed, catalog_visibility=course.catalog_visibility, short_description=CourseDetails.fetch_about_attribute(course.id, 'short_description'), effort=CourseDetails.fetch_about_attribute(course.id, 'effort'), course_video_url=CourseDetails.fetch_video_url(course.id), self_paced=course.self_paced, )
def _create_from_course(cls, course): """ Creates a CourseOverview object from a CourseDescriptor. Does not touch the database, simply constructs and returns an overview from the given course. Arguments: course (CourseDescriptor): any course descriptor object Returns: CourseOverview: overview extracted from the given course """ from lms.djangoapps.certificates.api import get_active_web_certificate from openedx.core.lib.courses import course_image_url log.info('Creating course overview for %s.', unicode(course.id)) # Workaround for a problem discovered in https://openedx.atlassian.net/browse/TNL-2806. # If the course has a malformed grading policy such that # course._grading_policy['GRADE_CUTOFFS'] = {}, then # course.lowest_passing_grade will raise a ValueError. # Work around this for now by defaulting to None. try: lowest_passing_grade = course.lowest_passing_grade except ValueError: lowest_passing_grade = None display_name = course.display_name start = course.start end = course.end max_student_enrollments_allowed = course.max_student_enrollments_allowed if isinstance(course.id, CCXLocator): from lms.djangoapps.ccx.utils import get_ccx_from_ccx_locator ccx = get_ccx_from_ccx_locator(course.id) display_name = ccx.display_name start = ccx.start end = ccx.due max_student_enrollments_allowed = ccx.max_student_enrollments_allowed return cls( version=cls.VERSION, id=course.id, _location=course.location, org=course.location.org, display_name=display_name, display_number_with_default=course.display_number_with_default, display_org_with_default=course.display_org_with_default, start=start, end=end, advertised_start=course.advertised_start, announcement=course.announcement, course_image_url=course_image_url(course), social_sharing_url=course.social_sharing_url, certificates_display_behavior=course.certificates_display_behavior, certificates_show_before_end=course.certificates_show_before_end, cert_html_view_enabled=course.cert_html_view_enabled, has_any_active_web_certificate=(get_active_web_certificate(course) is not None), cert_name_short=course.cert_name_short, cert_name_long=course.cert_name_long, lowest_passing_grade=lowest_passing_grade, end_of_course_survey_url=course.end_of_course_survey_url, days_early_for_beta=course.days_early_for_beta, mobile_available=course.mobile_available, visible_to_staff_only=course.visible_to_staff_only, _pre_requisite_courses_json=json.dumps( course.pre_requisite_courses), enrollment_start=course.enrollment_start, enrollment_end=course.enrollment_end, enrollment_domain=course.enrollment_domain, invitation_only=course.invitation_only, max_student_enrollments_allowed=max_student_enrollments_allowed, catalog_visibility=course.catalog_visibility, short_description=CourseDetails.fetch_about_attribute( course.id, 'short_description'), effort=CourseDetails.fetch_about_attribute(course.id, 'effort'), course_video_url=CourseDetails.fetch_video_url(course.id), self_paced=course.self_paced, )