def populate(cls, course_descriptor): """ Returns a fully populated CourseDetails model given the course descriptor """ course_key = course_descriptor.id course_details = cls(course_key.org, course_key.course, course_key.run) course_details.start_date = course_descriptor.start course_details.end_date = course_descriptor.end course_details.enrollment_start = course_descriptor.enrollment_start course_details.enrollment_end = course_descriptor.enrollment_end course_details.enable_enrollment_email = course_descriptor.enable_enrollment_email course_details.pre_requisite_courses = course_descriptor.pre_requisite_courses course_details.course_image_name = course_descriptor.course_image course_details.course_image_asset_path = course_image_url(course_descriptor, 'course_image') course_details.banner_image_name = course_descriptor.banner_image course_details.banner_image_asset_path = course_image_url(course_descriptor, 'banner_image') course_details.video_thumbnail_image_name = course_descriptor.video_thumbnail_image course_details.video_thumbnail_image_asset_path = course_image_url(course_descriptor, 'video_thumbnail_image') course_details.language = course_descriptor.language course_details.self_paced = course_descriptor.self_paced course_details.learning_info = course_descriptor.learning_info course_details.instructor_info = course_descriptor.instructor_info # Default course license is "All Rights Reserved" course_details.license = getattr(course_descriptor, "license", "all-rights-reserved") course_details.intro_video = cls.fetch_youtube_video_id(course_key) for attribute in ABOUT_ATTRIBUTES: value = cls.fetch_about_attribute(course_key, attribute) if value is not None: setattr(course_details, attribute, value) return course_details
def fetch(cls, course_key): """ Fetch the course details for the given course from persistence and return a CourseDetails model. """ descriptor = modulestore().get_course(course_key) course_details = cls(course_key.org, course_key.course, course_key.run) course_details.start_date = descriptor.start course_details.end_date = descriptor.end course_details.enrollment_start = descriptor.enrollment_start course_details.enrollment_end = descriptor.enrollment_end course_details.pre_requisite_courses = descriptor.pre_requisite_courses course_details.course_image_name = descriptor.course_image course_details.course_image_asset_path = course_image_url(descriptor, 'course_image') course_details.banner_image_name = descriptor.banner_image course_details.banner_image_asset_path = course_image_url(descriptor, 'banner_image') course_details.video_thumbnail_image_name = descriptor.video_thumbnail_image course_details.video_thumbnail_image_asset_path = course_image_url(descriptor, 'video_thumbnail_image') course_details.language = descriptor.language course_details.self_paced = descriptor.self_paced # Default course license is "All Rights Reserved" course_details.license = getattr(descriptor, "license", "all-rights-reserved") course_details.intro_video = cls.fetch_youtube_video_id(course_key) for attribute in ABOUT_ATTRIBUTES: value = cls.fetch_about_attribute(course_key, attribute) if value is not None: setattr(course_details, attribute, value) return course_details
def _assert_image_urls_all_default(self, modulestore_type, raw_course_image_name, expected_url=None): """ Helper for asserting that all image_urls are defaulting to a particular value. Returns the CourseOverview created. This function is useful when you know that the thumbnail generation process is going to fail in some way (e.g. unspecified source image, disabled config, runtime error) and want to verify that all the image URLs are a certain expected value (either the source image, or the fallback URL). """ with self.store.default_store(modulestore_type): course = CourseFactory.create( default_store=modulestore_type, course_image=raw_course_image_name ) if expected_url is None: expected_url = course_image_url(course) course_overview = CourseOverview.get_from_id(course.id) # All the URLs that come back should be for the expected_url self.assertEqual( course_overview.image_urls, { 'raw': expected_url, 'small': expected_url, 'large': expected_url, } ) return course_overview
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 verify_success(self, response): """ Verifies user course enrollment response for success """ super(TestUserEnrollmentApi, self).verify_success(response) courses = response.data self.assertEqual(len(courses), 1) found_course = courses[0]['course'] self.assertIn('courses/{}/about'.format(self.course.id), found_course['course_about']) self.assertIn('course_info/{}/updates'.format(self.course.id), found_course['course_updates']) self.assertIn('course_info/{}/handouts'.format(self.course.id), found_course['course_handouts']) self.assertIn('video_outlines/courses/{}'.format(self.course.id), found_course['video_outline']) self.assertEqual(found_course['id'], unicode(self.course.id)) self.assertEqual(courses[0]['mode'], CourseMode.DEFAULT_MODE_SLUG) self.assertEqual(courses[0]['course']['subscription_id'], self.course.clean_id(padding_char='_')) expected_course_image_url = course_image_url(self.course) self.assertIsNotNone(expected_course_image_url) self.assertIn(expected_course_image_url, found_course['course_image']) self.assertIn(expected_course_image_url, found_course['media']['course_image']['uri'])
def test_static_asset_path_course_image_default(self): """ Test that without course_image being set, but static_asset_path being set that we get the right course_image url. """ course = CourseFactory.create(static_asset_path="foo") self.assertEquals(course_image_url(course), "/static/foo/images/course_image.jpg")
def test_spaces_in_image_name(self): # Verify that image names with spaces in them are cleaned course = CourseFactory.create(course_image=u"before after.jpg") self.assertEquals( course_image_url(course), "/c4x/{org}/{course}/asset/before_after.jpg".format(org=course.location.org, course=course.location.course), )
def _get_course_email_context(course): """ Returns context arguments to apply to all emails, independent of recipient. """ course_id = text_type(course.id) course_title = course.display_name course_end_date = get_default_time_display(course.end) course_root = reverse('course_root', kwargs={'course_id': course_id}) course_url = '{}{}'.format( settings.LMS_ROOT_URL, course_root ) image_url = u'{}{}'.format(settings.LMS_ROOT_URL, course_image_url(course)) email_context = { 'course_title': course_title, 'course_root': course_root, 'course_language': course.language, 'course_url': course_url, 'course_image_url': image_url, 'course_end_date': course_end_date, 'account_settings_url': '{}{}'.format(settings.LMS_ROOT_URL, reverse('account_settings')), 'email_settings_url': '{}{}'.format(settings.LMS_ROOT_URL, reverse('dashboard')), 'platform_name': configuration_helpers.get_value('PLATFORM_NAME', settings.PLATFORM_NAME), } return email_context
def get_expected_course_data( self, course, enrollment_start, enrollment_end, instructor, staff, expected_pacing_type ): return { 'id': str(course.id), 'title': course.display_name, 'schedule': { 'start': serialize_datetime(self.course_start), 'end': serialize_datetime(self.course_end), 'enrollment_start': enrollment_start, 'enrollment_end': enrollment_end, }, 'team': [ { 'user': instructor.username, 'role': 'instructor', }, { 'user': staff.username, 'role': 'staff', }, ], 'images': { 'card_image': self.request.build_absolute_uri(course_image_url(course)), }, 'pacing_type': expected_pacing_type, }
def course_infos(course): d = { 'course_image_url': course_image_url(course) } for section in ['title', 'university']: d[section] = get_about_section(course, section) return d
def test_static_asset_path_course_image_set(self): """ Test that with course_image and static_asset_path both being set, that we get the right course_image url. """ course = CourseFactory.create(course_image=u"things_stuff.jpg", static_asset_path="foo") self.assertEquals(course_image_url(course), "/static/foo/things_stuff.jpg")
def test_images_upload(self): # http://www.django-rest-framework.org/api-guide/parsers/#fileuploadparser course_run = CourseFactory() expected_filename = 'course_image.png' content_key = StaticContent.compute_location(course_run.id, expected_filename) assert course_run.course_image != expected_filename try: contentstore().find(content_key) self.fail('No image should be associated with a new course run.') except NotFoundError: pass url = reverse('api:v1:course_run-images', kwargs={'pk': str(course_run.id)}) # PNG. Single black pixel content = b'\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x01\x00\x00\x00\x01\x08\x02\x00\x00\x00\x90wS' \ b'\xde\x00\x00\x00\x0cIDATx\x9cc```\x00\x00\x00\x04\x00\x01\xf6\x178U\x00\x00\x00\x00IEND\xaeB`\x82' # We are intentionally passing the incorrect JPEG extension here upload = SimpleUploadedFile('card_image.jpg', content, content_type='image/png') response = self.client.post(url, {'card_image': upload}, format='multipart') assert response.status_code == 200 course_run = modulestore().get_course(course_run.id) assert course_run.course_image == expected_filename expected = {'card_image': RequestFactory().get('').build_absolute_uri(course_image_url(course_run))} assert response.data == expected # There should now be an image stored contentstore().find(content_key)
def test_non_ascii_image_name(self): # Verify that non-ascii image names are cleaned course = CourseFactory.create(course_image=u"before_\N{SNOWMAN}_after.jpg") self.assertEquals( course_image_url(course), "/c4x/{org}/{course}/asset/before___after.jpg".format( org=course.location.org, course=course.location.course ), )
def test_disabled_with_prior_data(self, modulestore_type): """ Test behavior when entries have been created but we are disabled. This might happen because a strange bug was introduced -- e.g. we corrupt the images somehow when making thumbnails. Expectations: 1. We ignore whatever was created for the thumbnails, and image_urls returns the same as if no thumbnails had ever been generated. So basically, we return the raw source image for every resolution. 2. We keep the CourseOverviewImageSet data around for debugging purposes. """ course_image = "my_course.jpg" broken_small_url = "I am small!" broken_large_url = "I am big!" with self.store.default_store(modulestore_type): course = CourseFactory.create( default_store=modulestore_type, course_image=course_image ) course_overview_before = CourseOverview.get_from_id(course.id) # This initial seeding should create an entry for the image_set. self.assertTrue(hasattr(course_overview_before, 'image_set')) # Now just throw in some fake data to this image set, something that # couldn't possibly work. course_overview_before.image_set.small_url = broken_small_url course_overview_before.image_set.large_url = broken_large_url course_overview_before.image_set.save() # Now disable the thumbnail feature self.set_config(False) # Fetch a new CourseOverview course_overview_after = CourseOverview.get_from_id(course.id) # Assert that the data still exists for debugging purposes self.assertTrue(hasattr(course_overview_after, 'image_set')) image_set = course_overview_after.image_set self.assertEqual(image_set.small_url, broken_small_url) self.assertEqual(image_set.large_url, broken_large_url) # But because we've disabled it, asking for image_urls should give us # the raw source image for all resolutions, and not our broken images. expected_url = course_image_url(course) self.assertEqual( course_overview_after.image_urls, { 'raw': expected_url, 'small': expected_url, 'large': expected_url } )
def test_data(self, expected_pacing_type, self_paced): start = datetime.datetime.now(pytz.UTC) end = start + datetime.timedelta(days=30) enrollment_start = start - datetime.timedelta(days=7) enrollment_end = end - datetime.timedelta(days=14) course = CourseFactory( start=start, end=end, enrollment_start=enrollment_start, enrollment_end=enrollment_end, self_paced=self_paced ) instructor = UserFactory() CourseInstructorRole(course.id).add_users(instructor) staff = UserFactory() CourseStaffRole(course.id).add_users(staff) request = RequestFactory().get('') serializer = CourseRunSerializer(course, context={'request': request}) expected = { 'id': str(course.id), 'title': course.display_name, 'schedule': { 'start': serialize_datetime(start), 'end': serialize_datetime(end), 'enrollment_start': serialize_datetime(enrollment_start), 'enrollment_end': serialize_datetime(enrollment_end), }, 'team': [ { 'user': instructor.username, 'role': 'instructor', }, { 'user': staff.username, 'role': 'staff', }, ], 'images': { 'card_image': request.build_absolute_uri(course_image_url(course)), }, 'pacing_type': expected_pacing_type, } assert serializer.data == expected
def verify_success(self, response): """ Verifies user course enrollment response for success """ super(TestUserEnrollmentApi, self).verify_success(response) courses = response.data self.assertEqual(len(courses), 1) found_course = courses[0]["course"] self.assertIn("courses/{}/about".format(self.course.id), found_course["course_about"]) self.assertIn("course_info/{}/updates".format(self.course.id), found_course["course_updates"]) self.assertIn("course_info/{}/handouts".format(self.course.id), found_course["course_handouts"]) self.assertIn("video_outlines/courses/{}".format(self.course.id), found_course["video_outline"]) self.assertEqual(found_course["id"], unicode(self.course.id)) self.assertEqual(courses[0]["mode"], CourseMode.DEFAULT_MODE_SLUG) self.assertEqual(courses[0]["course"]["subscription_id"], self.course.clean_id(padding_char="_")) expected_course_image_url = course_image_url(self.course) self.assertIsNotNone(expected_course_image_url) self.assertIn(expected_course_image_url, found_course["course_image"]) self.assertIn(expected_course_image_url, found_course["media"]["course_image"]["uri"])
def _get_course_email_context(course): """ Returns context arguments to apply to all emails, independent of recipient. """ course_id = course.id.to_deprecated_string() course_title = course.display_name course_end_date = get_default_time_display(course.end) course_url = 'https://{}{}'.format( settings.SITE_NAME, reverse('course_root', kwargs={'course_id': course_id}) ) image_url = u'https://{}{}'.format(settings.SITE_NAME, course_image_url(course)) email_context = { 'course_title': course_title, 'course_url': course_url, 'course_image_url': image_url, 'course_end_date': course_end_date, 'account_settings_url': 'https://{}{}'.format(settings.SITE_NAME, reverse('account_settings')), 'email_settings_url': 'https://{}{}'.format(settings.SITE_NAME, reverse('dashboard')), 'platform_name': settings.PLATFORM_NAME, } return email_context
def _update_course_context(request, context, course, 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)
def get_attribute(self, instance): return course_image_url(instance)
def index_about_information(cls, modulestore, course): """ Add the given course to the course discovery index Arguments: modulestore - modulestore object to use for operations course - course object from which to take properties, locate about information """ searcher = SearchEngine.get_search_engine(cls.INDEX_NAME) if not searcher: return course_id = text_type(course.id) course_info = { 'id': course_id, 'course': course_id, 'content': {}, 'image_url': course_image_url(course), } # load data for all of the 'about' modules for this course into a dictionary about_dictionary = { item.location.block_id: item.data for item in modulestore.get_items(course.id, qualifiers={"category": "about"}) } about_context = { "course": course, "about_dictionary": about_dictionary, } for about_information in cls.ABOUT_INFORMATION_TO_INCLUDE: # Broad exception handler so that a single bad property does not scupper the collection of others try: section_content = about_information.get_value(**about_context) except: # pylint: disable=bare-except section_content = None log.warning( u"Course discovery could not collect property %s for course %s", about_information.property_name, course_id, exc_info=True, ) if section_content: if about_information.index_flags & AboutInfo.ANALYSE: analyse_content = section_content if isinstance(section_content, string_types): analyse_content = strip_html_content_to_text(section_content) course_info['content'][about_information.property_name] = analyse_content if about_information.index_flags & AboutInfo.PROPERTY: course_info[about_information.property_name] = section_content # Broad exception handler to protect around and report problems with indexing try: searcher.index(cls.DISCOVERY_DOCUMENT_TYPE, [course_info]) except: log.exception( u"Course discovery indexing error encountered, course discovery index may be out of date %s", course_id, ) raise log.debug( u"Successfully added %s course to the course discovery index", course_id )
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('Updating course overview for %s.', unicode(course.id)) course_overview = course_overview.first() else: log.info('Creating course overview for %s.', unicode(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 course_overview.language = course.language return course_overview
def get_course_image_url(self, course): """ Builds course image url """ return course.course_image_url if isinstance( course, CourseOverview) else course_image_url(course)
def test_spaces_in_image_name(self): # Verify that image names with spaces in them are cleaned course = CourseFactory.create(course_image='before after.jpg') assert course_image_url(course) == f'/c4x/{course.location.org}/{course.location.course}/asset/before_after.jpg' # pylint: disable=line-too-long
def test_get_image_url(self): """Test image URL formatting.""" course = CourseFactory.create(org='edX', course='999') self.assertEquals(course_image_url(course), '/c4x/edX/999/asset/{0}'.format(course.course_image))
def index_about_information(cls, modulestore, course): """ Add the given course to the course discovery index Arguments: modulestore - modulestore object to use for operations course - course object from which to take properties, locate about information """ searcher = SearchEngine.get_search_engine(cls.INDEX_NAME) if not searcher: return course_id = unicode(course.id) course_info = { 'id': course_id, 'course': course_id, 'content': {}, 'image_url': course_image_url(course), } # load data for all of the 'about' modules for this course into a dictionary about_dictionary = { item.location.name: item.data for item in modulestore.get_items(course.id, qualifiers={"category": "about"}) } about_context = { "course": course, "about_dictionary": about_dictionary, } for about_information in cls.ABOUT_INFORMATION_TO_INCLUDE: # Broad exception handler so that a single bad property does not scupper the collection of others try: section_content = about_information.get_value(**about_context) except: # pylint: disable=bare-except section_content = None log.warning( "Course discovery could not collect property %s for course %s", about_information.property_name, course_id, exc_info=True, ) if section_content: if about_information.index_flags & AboutInfo.ANALYSE: analyse_content = section_content if isinstance(section_content, basestring): analyse_content = strip_html_content_to_text(section_content) course_info['content'][about_information.property_name] = analyse_content if about_information.property_name == "more_info": course_info[about_information.property_name] = analyse_content if about_information.index_flags & AboutInfo.PROPERTY: course_info[about_information.property_name] = section_content # Broad exception handler to protect around and report problems with indexing try: searcher.index(cls.DISCOVERY_DOCUMENT_TYPE, [course_info]) except: # pylint: disable=bare-except log.exception( "Course discovery indexing error encountered, course discovery index may be out of date %s", course_id, ) raise log.debug( "Successfully added %s course to the course discovery index", course_id )
def test_non_ascii_image_name(self): course = self.process_xml(xml.CourseFactory.build(course_image=u'before_\N{SNOWMAN}_after.jpg')) self.assertEquals(course_image_url(course), u'/static/xml_test_course/before_\N{SNOWMAN}_after.jpg')
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 test_get_image_url(self): """Test image URL formatting.""" course = self.process_xml(xml.CourseFactory.build()) self.assertEquals(course_image_url(course), '/static/xml_test_course/images/course_image.jpg')
def test_non_ascii_image_name(self): # Verify that non-ascii image names are cleaned course = CourseFactory.create( course_image='before_\N{SNOWMAN}_after.jpg') assert course_image_url(course) == f'/c4x/{course.location.org}/{course.location.course}/asset/before___after.jpg' # pylint: disable=line-too-long
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', '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', ()), ('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 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 _base_view(self, context=None): context = context is None and {'is_author_view': False} or context annoto_auth = self.get_annoto_settings() horizontal, vertical = self.get_position() translator = self.runtime.service(self, 'i18n').translator lang = getattr( translator, 'get_language', lambda: translator.info().get( 'language', settings.LANGUAGE_CODE))() rtl = getattr(translator, 'get_language_bidi', lambda: lang in settings.LANGUAGES_BIDI)() course = self.get_course_obj() course_overview = CourseOverview.objects.get(id=self.course_id) course_id = str(self.course_id) course_display_name = course.display_name user = self._get_user() if not context[ 'is_author_view'] and user and self.discussions_scope == 'cohort': from openedx.core.djangoapps.course_groups.cohorts import get_cohort cohort = get_cohort(user, self.course_id) if cohort: course_id = u'{}_{}'.format(course_id, cohort.id) course_display_name = u'{} [{}]'.format( course_display_name, cohort.name) js_params = { 'objectType': self.object_type, 'clientId': annoto_auth.get('client_id'), 'horizontal': horizontal, 'vertical': vertical, 'tabs': self.tabs, 'overlayVideo': self.overlay_video, 'initialState': self.initial_state, 'privateThread': self.discussions_scope != 'site', 'mediaTitle': self.get_parent().display_name, 'language': lang, 'rtl': rtl, 'courseId': course_id, 'courseDisplayName': course_display_name, 'courseDescription': course_overview.short_description, 'courseImage': course_image_url(course), 'demoMode': not bool(annoto_auth.get('client_id')), 'isLive': self.video_type == 'stream', 'comments': 'comments' in self.features, 'privateNotes': 'notes' in self.features, 'videoBlockID': self.video_block_id, } context['error'] = {} if not annoto_auth.get('client_id'): context['error']['type'] = 'warning' context['error']['messages'] = [ self.i18n_service.gettext( 'You did not provide annoto credentials. And you view it in demo mode.' ), self.i18n_service.gettext( 'Please add "annoto-auth:<CLIENT_ID>:<CLIENT_SECRET>" to "Advanced Settings" > "LTI Passports"' ), ] else: try: jwt.PyJWS().decode(annoto_auth.get('client_id'), verify=False) except: context['error']['type'] = 'error' context['error']['messages'] = [ self.i18n_service.gettext( '"CLIENT_ID" is not a valid JWT token.'), self.i18n_service.gettext( 'Please provide valid "CLIENT_ID" in ' '"Advanced Settings" > "LTI Passports" > "annoto-auth:<CLIENT_ID>:<CLIENT_SECRET>"' ), ] else: if not annoto_auth.get('client_secret'): context['error']['type'] = 'error' context['error']['messages'] = [ self.i18n_service.gettext( '"CLIENT_SECRET" is required when "CLIENT_ID" provided.' ), self.i18n_service.gettext( 'Please add "CLIENT_SECRET" to ' '"Advanced Settings" > "LTI Passports" > "annoto-auth:<CLIENT_ID>:<CLIENT_SECRET>"' ), ] template = Template(self.resource_string("static/html/annoto.html")) html = template.render(Context(context)) frag = Fragment(html) frag.add_css(self.resource_string("static/css/annoto.css")) frag.add_javascript(self.resource_string("static/js/src/annoto.js")) frag.initialize_js('AnnotoXBlock', json_args=js_params) return frag
def test_spaces_in_image_name(self): course = self.process_xml( xml.CourseFactory.build(course_image=u'before after.jpg')) self.assertEquals(course_image_url(course), u'/static/xml_test_course/before after.jpg')
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 test_get_image_url(self): """Test image URL formatting.""" course = CourseFactory.create(org="edX", course="999") self.assertEquals(course_image_url(course), "/c4x/edX/999/asset/{0}".format(course.course_image))
def handle(self, *args, **options): def get_detail(course_key, attribute): usage_key = course_key.make_usage_key('about', attribute) try: value = modulestore().get_item(usage_key).data except ItemNotFoundError: value = None return value def iso_date(thing): if isinstance(thing, datetime.datetime): return thing.isoformat() return thing exclusion_list = [] inclusion_list = [] if options['exclude_file']: try: with open(options['exclude_file'],'rb') as exclusion_file: data = exclusion_file.readlines() exclusion_list = [x.strip() for x in data] except IOError: raise CommandError("Could not read exclusion list from '{0}'".format(options['exclude_file'])) if options['include_file']: try: with open(options['include_file'],'rb') as inclusion_file: data = inclusion_file.readlines() inclusion_list = [x.strip() for x in data] except IOError: raise CommandError("Could not read inclusion list from '{0}'".format(options['include_file'])) store = modulestore() epoch = int(time.time()) blob = { 'epoch': epoch, 'courses': [], } # For course TOC we need a user and a request. Find the first superuser defined, # that will be our user. request_user = User.objects.filter(is_superuser=True).first() factory = RequestFactory() for course in store.get_courses(): course_id_string = course.id.to_deprecated_string() if options['single_course']: if course_id_string not in [options['single_course'].strip()]: continue elif inclusion_list: if not course_id_string in inclusion_list: continue elif exclusion_list: if course_id_string in exclusion_list: continue print "Processing {}".format(course_id_string) students = CourseEnrollment.objects.users_enrolled_in(course.id) # The method of getting a table of contents for a course is quite obtuse. # We have to go all the way to simulating a request. request = factory.get('/') request.user = request_user raw_blocks = get_blocks(request, store.make_course_usage_key(course.id), request_user, requested_fields=['id', 'type', 'display_name', 'children', 'lms_web_url']) # We got the block structure. Now we need to massage it so we get the proper jump urls without site domain. # Because on the test server the site domain is wrong. blocks = {} for block_key, block in raw_blocks['blocks'].items(): try: direct_url = '/courses/' + block.get('lms_web_url').split('/courses/')[1] except IndexError: direct_url = '' blocks[block_key] = { 'id': block.get('id', ''), 'display_name': block.get('display_name', ''), 'type': block.get('type', ''), 'children_ids': block.get('children', []), 'url': direct_url } # Then we need to recursively stitch it into a tree. # We're only interested in three layers of the hierarchy for now: 'course', 'chapter', 'sequential', 'vertical'. # Everything else is the individual blocks and problems we don't care about right now. INTERESTING_BLOCKS = ['course', 'chapter', 'sequential', 'vertical'] def _get_children(parent): children = [blocks.get(n) for n in parent['children_ids'] if blocks.get(n)] # and blocks.get(n)['type'] in INTERESTING_BLOCKS] for child in children: child['children'] = _get_children(child) parent['children'] = children del parent['children_ids'] return children block_tree = _get_children(blocks[raw_blocks['root']]) course_block = { 'id': course_id_string, 'meta_data': { 'about': { 'display_name': course.display_name, 'media': { 'course_image': course_image_url(course), } }, 'block_tree': block_tree, # Yes, I'm duplicating them for now, because the about section is shot. 'display_name': course.display_name, 'banner': course_image_url(course), 'id_org': course.org, 'id_number': course.number, 'graded': course.graded, 'hidden': course.visible_to_staff_only, 'ispublic': not (course.visible_to_staff_only or False), # course.ispublic was removed in dogwood. 'grading_policy': course.grading_policy, 'advanced_modules': course.advanced_modules, 'lowest_passing_grade': course.lowest_passing_grade, 'start': iso_date(course.start), 'advertised_start': iso_date(course.advertised_start), 'end': iso_date(course.end), 'enrollment_end': iso_date(course.enrollment_end), 'enrollment_start': iso_date(course.enrollment_start), 'has_started': course.has_started(), 'has_ended': course.has_ended(), 'overview': get_detail(course.id,'overview'), 'short_description': get_detail(course.id,'short_description'), 'pre_requisite_courses': get_detail(course.id,'pre_requisite_courses'), 'video': get_detail(course.id,'video'), }, 'students': [x.username for x in students], 'global_anonymous_id': { x.username:anonymous_id_for_user(x, None) for x in students }, 'local_anonymous_id': { x.username:anonymous_id_for_user(x, course.id) for x in students }, } if not options['meta_only']: blob['grading_data_epoch'] = epoch course_block['grading_data'] = [] # Grab grades for all students that have ever had anything to do with the course. graded_students = User.objects.filter(pk__in=CourseEnrollment.objects.filter(course_id=course.id).values_list('user',flat=True)) print "{0} graded students in course {1}".format(graded_students.count(),course_id_string) if graded_students.count(): for student, gradeset, error_message \ in iterate_grades_for(course.id, graded_students): if gradeset: course_block['grading_data'].append({ 'username': student.username, 'grades': gradeset, }) else: print error_message blob['courses'].append(course_block) if options['output']: # Ensure the dump is atomic. with tempfile.NamedTemporaryFile('w', dir=os.path.dirname(options['output']), delete=False) as output_file: json.dump(blob, output_file, default=json_util.default) tempname = output_file.name os.rename(tempname, options['output']) else: print "Blob output:" print json.dumps(blob, indent=2, ensure_ascii=False, default=json_util.default)
def handle(self, *args, **options): def get_detail(course_key, attribute): usage_key = course_key.make_usage_key("about", attribute) try: value = modulestore().get_item(usage_key).data except ItemNotFoundError: value = None return value def iso_date(thing): if isinstance(thing, datetime.datetime): return thing.isoformat() return thing exclusion_list = [] inclusion_list = [] if options["exclude_file"]: try: with open(options["exclude_file"], "rb") as exclusion_file: data = exclusion_file.readlines() exclusion_list = [x.strip() for x in data] except IOError: raise CommandError("Could not read exclusion list from '{0}'".format(options["exclude_file"])) if options["include_file"]: try: with open(options["include_file"], "rb") as inclusion_file: data = inclusion_file.readlines() inclusion_list = [x.strip() for x in data] except IOError: raise CommandError("Could not read inclusion list from '{0}'".format(options["include_file"])) store = modulestore() epoch = int(time.time()) blob = {"epoch": epoch, "courses": []} for course in store.get_courses(): course_id_string = course.id.to_deprecated_string() if options["single_course"]: if course_id_string not in [options["single_course"].strip()]: continue elif inclusion_list: if not course_id_string in inclusion_list: continue elif exclusion_list: if course_id_string in exclusion_list: continue print "Processing {}".format(course_id_string) students = CourseEnrollment.objects.users_enrolled_in(course.id) course_block = { "id": course_id_string, "meta_data": { "about": {"display_name": course.display_name, "media": {"course_image": course_image_url(course)}}, # Yes, I'm duplicating them for now, because the about section is shot. "display_name": course.display_name, "banner": course_image_url(course), "id_org": course.org, "id_number": course.number, "graded": course.graded, "hidden": course.visible_to_staff_only, "ispublic": not (course.visible_to_staff_only or False), # course.ispublic was removed in dogwood. "grading_policy": course.grading_policy, "advanced_modules": course.advanced_modules, "lowest_passing_grade": course.lowest_passing_grade, "start": iso_date(course.start), "advertised_start": iso_date(course.advertised_start), "end": iso_date(course.end), "enrollment_end": iso_date(course.enrollment_end), "enrollment_start": iso_date(course.enrollment_start), "has_started": course.has_started(), "has_ended": course.has_ended(), "overview": get_detail(course.id, "overview"), "short_description": get_detail(course.id, "short_description"), "pre_requisite_courses": get_detail(course.id, "pre_requisite_courses"), "video": get_detail(course.id, "video"), }, "students": [x.username for x in students], "global_anonymous_id": {x.username: anonymous_id_for_user(x, None) for x in students}, "local_anonymous_id": {x.username: anonymous_id_for_user(x, course.id) for x in students}, } if not options["meta_only"]: blob["grading_data_epoch"] = epoch course_block["grading_data"] = [] # Grab grades for all students that have ever had anything to do with the course. graded_students = User.objects.filter( pk__in=CourseEnrollment.objects.filter(course_id=course.id).values_list("user", flat=True) ) print "{0} graded students in course {1}".format(graded_students.count(), course_id_string) if graded_students.count(): for student, gradeset, error_message in iterate_grades_for(course.id, graded_students): if gradeset: course_block["grading_data"].append({"username": student.username, "grades": gradeset}) else: print error_message blob["courses"].append(course_block) if options["output"]: # Ensure the dump is atomic. with tempfile.NamedTemporaryFile("w", dir=os.path.dirname(options["output"]), delete=False) as output_file: json.dump(blob, output_file) tempname = output_file.name os.rename(tempname, options["output"]) else: print "Blob output:" print json.dumps(blob, indent=2, ensure_ascii=False)
def get_image_url(self, course): """ Get the course image URL """ return course_image_url(course)
def test_spaces_in_image_name(self): course = self.process_xml(xml.CourseFactory.build(course_image=u'before after.jpg')) self.assertEquals(course_image_url(course), u'/static/xml_test_course/before after.jpg')
def test_get_image_url(self): """Test image URL formatting.""" course = CourseFactory.create(org='edX', course='999') assert course_image_url( course) == f'/c4x/edX/999/asset/{course.course_image}'