def __init__( self, async_send_task, site, course_key, target_datetime, day_offset, override_recipient_email=None ): access_duration = MIN_DURATION discovery_course_details = get_course_run_details(course_key, ['weeks_to_complete']) expected_weeks = discovery_course_details.get('weeks_to_complete') if expected_weeks: access_duration = timedelta(weeks=expected_weeks) access_duration = max(MIN_DURATION, min(MAX_DURATION, access_duration)) self.course_key = course_key super(ExpiryReminderResolver, self).__init__( async_send_task, site, target_datetime - access_duration, day_offset - access_duration.days, 0, override_recipient_email, )
def _get_catalog_data_for_course(course_key): """ Retrieve data from the Discovery service necessary for rendering a certificate for a specific course. """ course_certificate_settings = CertificateGenerationCourseSetting.get( course_key) if not course_certificate_settings: return {} catalog_data = {} course_run_fields = [] if course_certificate_settings.language_specific_templates_enabled: course_run_fields.append('content_language') if course_certificate_settings.include_hours_of_effort: course_run_fields.extend(['weeks_to_complete', 'max_effort']) if course_run_fields: course_run_data = get_course_run_details(course_key, course_run_fields) if course_run_data.get('weeks_to_complete') and course_run_data.get( 'max_effort'): try: weeks_to_complete = int(course_run_data['weeks_to_complete']) max_effort = int(course_run_data['max_effort']) catalog_data[ 'hours_of_effort'] = weeks_to_complete * max_effort except ValueError: log.exception( 'Error occurred while parsing course run details') catalog_data['content_language'] = course_run_data.get( 'content_language') log.info( "catalog data received for course:{course_key} is : {catalog_data}". format(course_key=course_key, catalog_data=catalog_data)) return catalog_data
def get_user_course_duration(user, course): """ Return a timedelta measuring the duration of the course for a particular user. Business Logic: - Course access duration is bounded by the min and max duration. - If course fields are missing, default course access duration to MIN_DURATION. """ access_duration = MIN_DURATION verified_mode = CourseMode.verified_mode_for_course(course=course, include_expired=True) if not verified_mode: return None enrollment = CourseEnrollment.get_enrollment(user, course.id) if enrollment is None or enrollment.mode != CourseMode.AUDIT: return None # The user course expiration date is the content availability date # plus the weeks_to_complete field from course-discovery. discovery_course_details = get_course_run_details(course.id, ['weeks_to_complete']) expected_weeks = discovery_course_details.get('weeks_to_complete') if expected_weeks: access_duration = timedelta(weeks=expected_weeks) # Course access duration is bounded by the min and max duration. access_duration = max(MIN_DURATION, min(MAX_DURATION, access_duration)) return access_duration
def _update_course_context(request, context, course, course_key, platform_name): """ Updates context dictionary with course info. """ context['full_course_image_url'] = request.build_absolute_uri(course_image_url(course)) course_title_from_cert = context['certificate_data'].get('course_title', '') accomplishment_copy_course_name = course_title_from_cert if course_title_from_cert else course.display_name context['accomplishment_copy_course_name'] = accomplishment_copy_course_name course_number = course.display_coursenumber if course.display_coursenumber else course.number context['course_number'] = course_number if context['organization_long_name']: # Translators: This text represents the description of course context['accomplishment_copy_course_description'] = _('a course of study offered by {partner_short_name}, ' 'an online learning initiative of ' '{partner_long_name}.').format( partner_short_name=context['organization_short_name'], partner_long_name=context['organization_long_name'], platform_name=platform_name) else: # Translators: This text represents the description of course context['accomplishment_copy_course_description'] = _('a course of study offered by ' '{partner_short_name}.').format( partner_short_name=context['organization_short_name'], platform_name=platform_name) # If language specific templates are enabled for the course, add course_run specific information to the context if CertificateGenerationCourseSetting.is_language_specific_templates_enabled_for_course(course_key): fields = ['start', 'end', 'max_effort', 'language'] course_run_data = get_course_run_details(course_key, fields) context.update(course_run_data)
def _get_catalog_data_for_course(course_key): """ Retrieve data from the Discovery service necessary for rendering a certificate for a specific course. """ course_certificate_settings = CertificateGenerationCourseSetting.get(course_key) if not course_certificate_settings: return {} catalog_data = {} course_run_fields = [] if course_certificate_settings.language_specific_templates_enabled: course_run_fields.append('content_language') if course_certificate_settings.include_hours_of_effort: course_run_fields.extend(['weeks_to_complete', 'max_effort']) if course_run_fields: course_run_data = get_course_run_details(course_key, course_run_fields) if course_run_data.get('weeks_to_complete') and course_run_data.get('max_effort'): try: weeks_to_complete = int(course_run_data['weeks_to_complete']) max_effort = int(course_run_data['max_effort']) catalog_data['hours_of_effort'] = weeks_to_complete * max_effort except ValueError: log.exception('Error occurred while parsing course run details') catalog_data['content_language'] = course_run_data.get('content_language') log.info( u"catalog data received for course: %s is : %s", course_key, catalog_data, ) return catalog_data
def __init__( self, async_send_task, site, course_key, target_datetime, day_offset, override_recipient_email=None ): access_duration = MIN_DURATION discovery_course_details = get_course_run_details(course_key, ['weeks_to_complete']) expected_weeks = discovery_course_details.get('weeks_to_complete') if expected_weeks: access_duration = timedelta(weeks=expected_weeks) access_duration = max(MIN_DURATION, min(MAX_DURATION, access_duration)) self.course_key = course_key super(ExpiryReminderResolver, self).__init__( async_send_task, site, target_datetime - access_duration, day_offset - access_duration.days, 0, override_recipient_email, )
def _update_context_with_catalog_data(context, course_key): """ Updates context dictionary with relevant course run info from Discovery. """ course_certificate_settings = CertificateGenerationCourseSetting.get( course_key) if course_certificate_settings: course_run_fields = [] if course_certificate_settings.language_specific_templates_enabled: course_run_fields.append('content_language') if course_certificate_settings.include_hours_of_effort: course_run_fields.extend(['weeks_to_complete', 'max_effort']) if course_run_fields: course_run_data = get_course_run_details(course_key, course_run_fields) if course_run_data.get( 'weeks_to_complete') and course_run_data.get('max_effort'): try: weeks_to_complete = int( course_run_data['weeks_to_complete']) max_effort = int(course_run_data['max_effort']) context['hours_of_effort'] = weeks_to_complete * max_effort except ValueError: log.exception( 'Error occurred while parsing course run details') context['content_language'] = course_run_data.get( 'content_language')
def _get_pseudo_course_overview(self, course_key): """ Returns a pseudo course overview object for deleted courses. """ course_run = get_course_run_details(course_key, ['title']) return CourseOverview(display_name=course_run.get('title'), display_org_with_default=course_key.org, certificates_show_before_end=True)
def date(self): if self.course.self_paced and RELATIVE_DATES_FLAG.is_enabled(self.course_id): weeks_to_complete = get_course_run_details(self.course.id, ['weeks_to_complete']).get('weeks_to_complete') if weeks_to_complete: course_duration = datetime.timedelta(weeks=weeks_to_complete) if self.course.end < (self.current_time + course_duration): return self.course.end return None return self.course.end
def test_get_course_run_details(self, mock_get_edx_api_data): """ Test retrieval of details about a specific course run """ course_run = CourseRunFactory() course_run_details = { 'content_language': course_run['content_language'], 'weeks_to_complete': course_run['weeks_to_complete'], 'max_effort': course_run['max_effort'] } mock_get_edx_api_data.return_value = course_run_details data = get_course_run_details(course_run['key'], ['content_language', 'weeks_to_complete', 'max_effort']) self.assertTrue(mock_get_edx_api_data.called) self.assertEqual(data, course_run_details)
def test_get_course_run_details(self, mock_get_edx_api_data): """ Test retrieval of details about a specific course run """ course_run = CourseRunFactory() course_run_details = { 'content_language': course_run['content_language'], 'weeks_to_complete': course_run['weeks_to_complete'], 'max_effort': course_run['max_effort'] } mock_get_edx_api_data.return_value = course_run_details data = get_course_run_details(course_run['key'], ['content_language', 'weeks_to_complete', 'max_effort']) self.assertTrue(mock_get_edx_api_data.called) self.assertEqual(data, course_run_details)
def get_user_course_expiration_date(user, course): """ Return expiration date for given user course pair. Return None if the course does not expire. Business Logic: - Course access duration is bounded by the min and max duration. - If course fields are missing, default course access duration to MIN_DURATION. """ access_duration = MIN_DURATION verified_mode = CourseMode.verified_mode_for_course(course=course, include_expired=True) if not verified_mode: return None enrollment = CourseEnrollment.get_enrollment(user, course.id) if enrollment is None or enrollment.mode != CourseMode.AUDIT: return None try: # Content availability date is equivalent to max(enrollment date, course start date) # for most people. Using the schedule date will provide flexibility to deal with # more complex business rules in the future. content_availability_date = enrollment.schedule.start # We have anecdotally observed a case where the schedule.start was # equal to the course start, but should have been equal to the enrollment start # https://openedx.atlassian.net/browse/PROD-58 # This section is meant to address that case if enrollment.created and course.start: if (content_availability_date.date() == course.start.date() and course.start < enrollment.created < timezone.now()): content_availability_date = enrollment.created except CourseEnrollment.schedule.RelatedObjectDoesNotExist: content_availability_date = max(enrollment.created, course.start) # The user course expiration date is the content availability date # plus the weeks_to_complete field from course-discovery. discovery_course_details = get_course_run_details(course.id, ['weeks_to_complete']) expected_weeks = discovery_course_details.get('weeks_to_complete') if expected_weeks: access_duration = timedelta(weeks=expected_weeks) # Course access duration is bounded by the min and max duration. access_duration = max(MIN_DURATION, min(MAX_DURATION, access_duration)) return content_availability_date + access_duration
def _update_course_context(request, context, course, course_key, platform_name): """ Updates context dictionary with course info. """ context['full_course_image_url'] = request.build_absolute_uri( course_image_url(course)) course_title_from_cert = context['certificate_data'].get( 'course_title', '') accomplishment_copy_course_name = course_title_from_cert if course_title_from_cert else course.display_name context[ 'accomplishment_copy_course_name'] = accomplishment_copy_course_name course_number = course.display_coursenumber if course.display_coursenumber else course.number context['course_number'] = course_number if context['organization_long_name']: # Translators: This text represents the description of course context['accomplishment_copy_course_description'] = _( 'a course of study offered by {partner_short_name}, ' 'an online learning initiative of ' '{partner_long_name}.').format( partner_short_name=context['organization_short_name'], partner_long_name=context['organization_long_name'], platform_name=platform_name) else: # Translators: This text represents the description of course context['accomplishment_copy_course_description'] = _( 'a course of study offered by ' '{partner_short_name}.').format( partner_short_name=context['organization_short_name'], platform_name=platform_name) # If language specific templates are enabled for the course, add course_run specific information to the context if CertificateGenerationCourseSetting.is_language_specific_templates_enabled_for_course( course_key): fields = ['start', 'end', 'max_effort', 'content_language'] course_run_data = get_course_run_details(course_key, fields) if course_run_data['start'] and course_run_data[ 'end'] and course_run_data['max_effort']: # Calculate duration of the course run in weeks, multiplied by max_effort for total Hours of Effort try: start = parser.parse(course_run_data['start']) end = parser.parse(course_run_data['end']) max_effort = int(course_run_data['max_effort']) context['hours_of_effort'] = ( (end - start).days / 7) * max_effort except ValueError: log.exception( 'Error occurred while parsing course run details') context['content_language'] = course_run_data['content_language']
def get_user_course_expiration_date(user, course): """ Return expiration date for given user course pair. Return None if the course does not expire. Business Logic: - Course access duration is bounded by the min and max duration. - If course fields are missing, default course access duration to MIN_DURATION. """ access_duration = MIN_DURATION verified_mode = CourseMode.verified_mode_for_course(course=course, include_expired=True) if not verified_mode: return None enrollment = CourseEnrollment.get_enrollment(user, course.id) if enrollment is None or enrollment.mode != CourseMode.AUDIT: return None try: # Content availability date is equivalent to max(enrollment date, course start date) # for most people. Using the schedule date will provide flexibility to deal with # more complex business rules in the future. content_availability_date = enrollment.schedule.start # We have anecdotally observed a case where the schedule.start was # equal to the course start, but should have been equal to the enrollment start # https://openedx.atlassian.net/browse/PROD-58 # This section is meant to address that case if enrollment.created and course.start: if (content_availability_date.date() == course.start.date() and course.start < enrollment.created < timezone.now()): content_availability_date = enrollment.created except CourseEnrollment.schedule.RelatedObjectDoesNotExist: content_availability_date = max(enrollment.created, course.start) # The user course expiration date is the content availability date # plus the weeks_to_complete field from course-discovery. discovery_course_details = get_course_run_details(course.id, ['weeks_to_complete']) expected_weeks = discovery_course_details.get('weeks_to_complete') if expected_weeks: access_duration = timedelta(weeks=expected_weeks) # Course access duration is bounded by the min and max duration. access_duration = max(MIN_DURATION, min(MAX_DURATION, access_duration)) return content_availability_date + access_duration
def get_user_course_expiration_date(user, course): """ Return expiration date for given user course pair. Return None if the course does not expire. Business Logic: - Course access duration is bounded by the min and max duration. - If course fields are missing, default course access duration to MIN_DURATION. """ access_duration = MIN_DURATION if not CourseMode.verified_mode_for_course(course.id): return None CourseEnrollment = apps.get_model('student.CourseEnrollment') enrollment = CourseEnrollment.get_enrollment(user, course.id) if enrollment is None or enrollment.mode != 'audit': return None # if the user is a beta tester their access should not expire if CourseBetaTesterRole(course.id).has_user(user): return None try: # Content availability date is equivalent to max(enrollment date, course start date) # for most people. Using the schedule date will provide flexibility to deal with # more complex business rules in the future. content_availability_date = enrollment.schedule.start except CourseEnrollment.schedule.RelatedObjectDoesNotExist: content_availability_date = max(enrollment.created, course.start) # The user course expiration date is the content availability date # plus the weeks_to_complete field from course-discovery. discovery_course_details = get_course_run_details(course.id, ['weeks_to_complete']) expected_weeks = discovery_course_details.get('weeks_to_complete') if expected_weeks: access_duration = timedelta(weeks=expected_weeks) # Course access duration is bounded by the min and max duration. access_duration = max(MIN_DURATION, min(MAX_DURATION, access_duration)) return content_availability_date + access_duration
def get_expected_duration(course_id): """ Return a `datetime.timedelta` defining the expected length of the supplied course. """ access_duration = MIN_DURATION # The user course expiration date is the content availability date # plus the weeks_to_complete field from course-discovery. discovery_course_details = get_course_run_details(course_id, ['weeks_to_complete']) expected_weeks = discovery_course_details.get('weeks_to_complete') if expected_weeks: access_duration = timedelta(weeks=expected_weeks) # Course access duration is bounded by the min and max duration. access_duration = max(MIN_DURATION, min(MAX_DURATION, access_duration)) return access_duration
def get_user_course_expiration_date(user, course): """ Return expiration date for given user course pair. Return None if the course does not expire. Business Logic: - Course access duration is bounded by the min and max duration. - If course fields are missing, default course access duration to MIN_DURATION. """ access_duration = MIN_DURATION CourseEnrollment = apps.get_model('student.CourseEnrollment') enrollment = CourseEnrollment.get_enrollment(user, course.id) if enrollment is None or enrollment.mode != 'audit': return None try: # Content availability date is equivalent to max(enrollment date, course start date) # for most people. Using the schedule date will provide flexibility to deal with # more complex business rules in the future. content_availability_date = enrollment.schedule.start except CourseEnrollment.schedule.RelatedObjectDoesNotExist: content_availability_date = max(enrollment.created, course.start) if course.self_paced: # The user course expiration date for self paced courses is the # content availability date plus the weeks_to_complete field from course-discovery. discovery_course_details = get_course_run_details( course.id, ['weeks_to_complete']) expected_weeks = discovery_course_details['weeks_to_complete'] or int( MIN_DURATION.days / 7) access_duration = timedelta(weeks=expected_weeks) elif not course.self_paced and course.end and course.start: # The user course expiration date for instructor paced courses is the # content availability date plus the duration of the course (course end date minus course start date). access_duration = course.end - course.start # Course access duration is bounded by the min and max duration. access_duration = max(MIN_DURATION, min(MAX_DURATION, access_duration)) return content_availability_date + access_duration
def _update_context_with_catalog_data(context, course_key): """ Updates context dictionary with relevant course run info from Discovery. """ course_certificate_settings = CertificateGenerationCourseSetting.get(course_key) if course_certificate_settings: course_run_fields = [] if course_certificate_settings.language_specific_templates_enabled: course_run_fields.append('content_language') if course_certificate_settings.include_hours_of_effort: course_run_fields.extend(['weeks_to_complete', 'max_effort']) if course_run_fields: course_run_data = get_course_run_details(course_key, course_run_fields) if course_run_data.get('weeks_to_complete') and course_run_data.get('max_effort'): try: weeks_to_complete = int(course_run_data['weeks_to_complete']) max_effort = int(course_run_data['max_effort']) context['hours_of_effort'] = weeks_to_complete * max_effort except ValueError: log.exception('Error occurred while parsing course run details') context['content_language'] = course_run_data.get('content_language')
def get_user_course_expiration_date(user, course): """ Return expiration date for given user course pair. Return None if the course does not expire. Business Logic: - Course access duration is bounded by the min and max duration. - If course fields are missing, default course access duration to MIN_DURATION. """ access_duration = MIN_DURATION if not CourseMode.verified_mode_for_course(course.id): return None CourseEnrollment = apps.get_model('student.CourseEnrollment') enrollment = CourseEnrollment.get_enrollment(user, course.id) if enrollment is None or enrollment.mode != 'audit': return None try: # Content availability date is equivalent to max(enrollment date, course start date) # for most people. Using the schedule date will provide flexibility to deal with # more complex business rules in the future. content_availability_date = enrollment.schedule.start except CourseEnrollment.schedule.RelatedObjectDoesNotExist: content_availability_date = max(enrollment.created, course.start) # The user course expiration date is the content availability date # plus the weeks_to_complete field from course-discovery. discovery_course_details = get_course_run_details(course.id, ['weeks_to_complete']) expected_weeks = discovery_course_details.get('weeks_to_complete') if expected_weeks: access_duration = timedelta(weeks=expected_weeks) # Course access duration is bounded by the min and max duration. access_duration = max(MIN_DURATION, min(MAX_DURATION, access_duration)) return content_availability_date + access_duration
def _update_course_context(request, context, course, course_key, platform_name): """ Updates context dictionary with course info. """ context['full_course_image_url'] = request.build_absolute_uri( course_image_url(course)) course_title_from_cert = context['certificate_data'].get( 'course_title', '') accomplishment_copy_course_name = course_title_from_cert if course_title_from_cert else course.display_name context[ 'accomplishment_copy_course_name'] = accomplishment_copy_course_name course_number = course.display_coursenumber if course.display_coursenumber else course.number context['course_number'] = course_number if context['organization_long_name']: # Translators: This text represents the description of course context['accomplishment_copy_course_description'] = _( 'a course of study offered by {partner_short_name}, ' 'an online learning initiative of ' '{partner_long_name}.').format( partner_short_name=context['organization_short_name'], partner_long_name=context['organization_long_name'], platform_name=platform_name) else: # Translators: This text represents the description of course context['accomplishment_copy_course_description'] = _( 'a course of study offered by ' '{partner_short_name}.').format( partner_short_name=context['organization_short_name'], platform_name=platform_name) # If language specific templates are enabled for the course, add course_run specific information to the context if CertificateGenerationCourseSetting.is_language_specific_templates_enabled_for_course( course_key): fields = ['start', 'end', 'max_effort', 'language'] course_run_data = get_course_run_details(course_key, fields) context.update(course_run_data)