def __init__(self, site, user, enrollments=None, uuid=None, mobile_only=False): self.site = site self.user = user self.mobile_only = mobile_only self.enrollments = enrollments or list(CourseEnrollment.enrollments_for_user(self.user)) self.enrollments.sort(key=lambda e: e.created, reverse=True) self.enrolled_run_modes = {} self.course_run_ids = [] for enrollment in self.enrollments: # enrollment.course_id is really a CourseKey (╯ಠ_ಠ)╯︵ ┻━┻ enrollment_id = unicode(enrollment.course_id) mode = enrollment.mode if mode == CourseMode.NO_ID_PROFESSIONAL_MODE: mode = CourseMode.PROFESSIONAL self.enrolled_run_modes[enrollment_id] = mode # We can't use dict.keys() for this because the course run ids need to be ordered self.course_run_ids.append(enrollment_id) self.entitlements = list(CourseEntitlement.unexpired_entitlements_for_user(self.user)) self.course_uuids = [str(entitlement.course_uuid) for entitlement in self.entitlements] self.course_grade_factory = CourseGradeFactory() if uuid: self.programs = [get_programs(uuid=uuid)] else: self.programs = attach_program_detail_url(get_programs(self.site), self.mobile_only)
def test_get_programs_anonymous_user(self, _mock_cache, mock_get_catalog_data): programs = [factories.Program() for __ in range(3)] mock_get_catalog_data.return_value = programs anonymous_user = AnonymousUserFactory() # The user is an Anonymous user but the Catalog Service User has not been created yet. data = utils.get_programs(anonymous_user) # This should not return programs. self.assertEqual(data, []) UserFactory(username="******") # After creating the service user above, data = utils.get_programs(anonymous_user) # the programs should be returned successfully. self.assertEqual(data, programs)
def test_programs_unavailable(self, mock_get_edx_api_data): mock_get_edx_api_data.return_value = [] data = get_programs() self.assert_contract(mock_get_edx_api_data.call_args) self.assertEqual(data, [])
def program_details(request, program_uuid): """View details about a specific program.""" programs_config = ProgramsApiConfig.current() if not programs_config.enabled: raise Http404 program_data = get_programs(uuid=program_uuid) if not program_data: raise Http404 program_data = ProgramDataExtender(program_data, request.user).extend() urls = { 'program_listing_url': reverse('program_listing_view'), 'track_selection_url': strip_course_id( reverse('course_modes_choose', kwargs={'course_id': FAKE_COURSE_KEY}) ), 'commerce_api_url': reverse('commerce_api:v0:baskets:create'), } context = { 'program_data': program_data, 'urls': urls, 'show_program_listing': programs_config.enabled, 'nav_hidden': True, 'disable_courseware_js': True, 'uses_pattern_library': True, 'user_preferences': get_user_preferences(request.user) } return render_to_response('learner_dashboard/program_details.html', context)
def test_programs_unavailable(self, _mock_cache, mock_get_catalog_data): mock_get_catalog_data.return_value = [] data = utils.get_programs(self.user) self.assert_contract(mock_get_catalog_data.call_args) self.assertEqual(data, [])
def get_user_by_program_id(external_user_id, program_uuid): """ Returns a User model for an external_user_id with a social auth entry. Args: external_user_id: external user id used for social auth program_uuid: a program this user is/will be enrolled in Returns: A User object or None, if no user with the given external id for the given organization exists. Raises: ProgramDoesNotExistException if no such program exists. OrganizationDoesNotExistException if no organization exists. ProviderDoesNotExistException if there is no SAML provider configured for the related organization. """ program = get_programs(uuid=program_uuid) if program is None: log.error(u'Unable to find catalog program matching uuid [%s]', program_uuid) raise ProgramDoesNotExistException try: org_key = program['authoring_organizations'][0]['key'] organization = Organization.objects.get(short_name=org_key) except (KeyError, IndexError): log.error(u'Cannot determine authoring organization key for catalog program [%s]', program_uuid) raise OrganizationDoesNotExistException except Organization.DoesNotExist: log.error(u'Unable to find organization for short_name [%s]', org_key) raise OrganizationDoesNotExistException return get_user_by_organization(external_user_id, organization)
def test_get_many_with_missing(self, mock_cache, mock_warning, mock_info): programs = ProgramFactory.create_batch(3) all_programs = { PROGRAM_CACHE_KEY_TPL.format(uuid=program['uuid']): program for program in programs } partial_programs = { PROGRAM_CACHE_KEY_TPL.format(uuid=program['uuid']): program for program in programs[:2] } def fake_get_many(keys): if len(keys) == 1: return {PROGRAM_CACHE_KEY_TPL.format(uuid=programs[-1]['uuid']): programs[-1]} else: return partial_programs mock_cache.get.return_value = [program['uuid'] for program in programs] mock_cache.get_many.side_effect = fake_get_many actual_programs = get_programs(self.site) # All 3 cached programs should be returned. An info message should be # logged about the one that was initially missing, but the code should # be able to stitch together all the details. self.assertEqual( set(program['uuid'] for program in actual_programs), set(program['uuid'] for program in all_programs.values()) ) self.assertFalse(mock_warning.called) mock_info.assert_called_with('Failed to get details for 1 programs. Retrying.') for program in actual_programs: key = PROGRAM_CACHE_KEY_TPL.format(uuid=program['uuid']) self.assertEqual(program, all_programs[key])
def test_get_one_program(self, mock_get_edx_api_data): program = ProgramFactory() mock_get_edx_api_data.return_value = program data = get_programs(uuid=self.uuid) self.assert_contract(mock_get_edx_api_data.call_args, program_uuid=self.uuid) self.assertEqual(data, program)
def test_get_programs_by_types(self, mock_get_edx_api_data): programs = ProgramFactory.create_batch(2) mock_get_edx_api_data.return_value = programs data = get_programs(types=self.types) self.assert_contract(mock_get_edx_api_data.call_args, types=self.types) self.assertEqual(data, programs)
def _load_course_runs(self): """Find all course runs which are part of a program.""" programs = [] for site in Site.objects.all(): logger.info('Loading programs from the catalog for site %s.', site.domain) programs.extend(get_programs(site)) self.course_runs = self._flatten(programs)
def test_get_programs(self, mock_get_edx_api_data): programs = [ProgramFactory() for __ in range(3)] mock_get_edx_api_data.return_value = programs data = get_programs() self.assert_contract(mock_get_edx_api_data.call_args) self.assertEqual(data, programs)
def test_get_one_program(self, _mock_cache, mock_get_catalog_data): program = factories.Program() mock_get_catalog_data.return_value = program data = utils.get_programs(self.user, uuid=self.uuid) self.assert_contract(mock_get_catalog_data.call_args, program_uuid=self.uuid) self.assertEqual(data, program)
def test_get_programs(self, _mock_cache, mock_get_catalog_data): programs = [factories.Program() for __ in range(3)] mock_get_catalog_data.return_value = programs data = utils.get_programs(self.user) self.assert_contract(mock_get_catalog_data.call_args) self.assertEqual(data, programs)
def program(self): """ The program specified by the `program_uuid` URL parameter. """ program = get_programs(uuid=self.kwargs['program_uuid']) if program is None: raise Http404() return program
def test_config_missing(self, _mock_get_edx_api_data): """ Verify that no errors occur if this method is called when catalog config is missing. """ CatalogIntegration.objects.all().delete() data = get_programs() self.assertEqual(data, [])
def __init__(self, user, enrollments=None): self.user = user self.enrollments = enrollments or list(CourseEnrollment.enrollments_for_user(self.user)) self.enrollments.sort(key=lambda e: e.created, reverse=True) # enrollment.course_id is really a CourseKey (╯ಠ_ಠ)╯︵ ┻━┻ self.course_run_ids = [unicode(e.course_id) for e in self.enrollments] self.programs = attach_program_detail_url(get_programs())
def test_get_one(self, mock_warning, _mock_info): expected_program = ProgramFactory() expected_uuid = expected_program['uuid'] self.assertEqual(get_programs(self.site, uuid=expected_uuid), None) mock_warning.assert_called_once_with( 'Failed to get details for program {uuid} from the cache.'.format(uuid=expected_uuid) ) mock_warning.reset_mock() cache.set( PROGRAM_CACHE_KEY_TPL.format(uuid=expected_uuid), expected_program, None ) actual_program = get_programs(self.site, uuid=expected_uuid) self.assertEqual(actual_program, expected_program) self.assertFalse(mock_warning.called)
def test_service_user_missing(self, _mock_get_edx_api_data): """ Verify that no errors occur if this method is called when the catalog service user is missing. """ # Note: Deleting the service user would be ideal, but causes mysterious # errors on Jenkins. self.create_catalog_integration(service_username='******') data = get_programs() self.assertEqual(data, [])
def test_get_from_course(self, mock_warning, _mock_info): expected_program = ProgramFactory() expected_course = expected_program['courses'][0]['course_runs'][0]['key'] self.assertEqual(get_programs(course=expected_course), []) cache.set( COURSE_PROGRAMS_CACHE_KEY_TPL.format(course_run_id=expected_course), [expected_program['uuid']], None ) cache.set( PROGRAM_CACHE_KEY_TPL.format(uuid=expected_program['uuid']), expected_program, None ) actual_program = get_programs(course=expected_course) self.assertEqual(actual_program, [expected_program]) self.assertFalse(mock_warning.called)
def is_course_run_in_a_program(course_run_key): """ Returns true if the given course key is in any program at all. """ # We don't have an easy way to go from course_run_key to a specific site that owns it. So just search each site. sites = Site.objects.all() str_key = str(course_run_key) for site in sites: for program in get_programs(site): for course in program['courses']: for course_run in course['course_runs']: if str_key == course_run['key']: return True return False
def wrapped_function(self, request, **kwargs): """ Wraps the given view_function. """ program_uuid = kwargs['program_uuid'] program = get_programs(uuid=program_uuid) if not program: raise self.api_error( status_code=status.HTTP_404_NOT_FOUND, developer_message='no program exists with given key', error_code='program_does_not_exist' ) return view_func(self, request, **kwargs)
def __init__(self, user, enrollments=None, uuid=None): self.user = user self.enrollments = enrollments or list(CourseEnrollment.enrollments_for_user(self.user)) self.enrollments.sort(key=lambda e: e.created, reverse=True) self.enrolled_run_modes = {} self.course_run_ids = [] for enrollment in self.enrollments: # enrollment.course_id is really a CourseKey (╯ಠ_ಠ)╯︵ ┻━┻ enrollment_id = unicode(enrollment.course_id) mode = enrollment.mode if mode == CourseMode.NO_ID_PROFESSIONAL_MODE: mode = CourseMode.PROFESSIONAL self.enrolled_run_modes[enrollment_id] = mode # We can't use dict.keys() for this because the course run ids need to be ordered self.course_run_ids.append(enrollment_id) if uuid: self.programs = [get_programs(uuid=uuid)] else: self.programs = attach_program_detail_url(get_programs())
def test_get_programs_with_status_filtering(self, mock_get_edx_api_data): """ The function should request active and retired programs when the Waffle switch is enabled. """ programs = ProgramFactory.create_batch(3) mock_get_edx_api_data.return_value = programs Switch.objects.get_or_create(name='display_retired_programs_on_learner_dashboard', defaults={'active': True}) data = get_programs() expected_querystring = { 'exclude_utm': 1, 'status': ('active', 'retired',) } self.assert_contract(mock_get_edx_api_data.call_args, expected_querystring=expected_querystring) self.assertEqual(data, programs)
def get_program_context(course, user_enrollments, audit_enrollments): """ Return a context dictionary with program information. """ program_key = None if PROGRAM_INFO_FLAG.is_enabled(): programs = get_programs(course=course.id) if programs: # A course can be in multiple programs, but we're just grabbing the first one program = programs[0] complete_enrollment = False has_courses_left_to_purchase = False total_courses = None courses = program.get('courses') courses_left_to_purchase_price = None courses_left_to_purchase_url = None program_uuid = program.get('uuid') is_eligible_for_one_click_purchase = program.get('is_program_eligible_for_one_click_purchase') if courses is not None: total_courses = len(courses) complete_enrollment = is_enrolled_in_all_courses(courses, user_enrollments) # Get the price and purchase URL of the program courses the user has yet to purchase. Say a # program has 3 courses (A, B and C), and the user previously purchased a certificate for A. # The user is enrolled in audit mode for B. The "left to purchase price" should be the price of # B+C. non_audit_enrollments = [en for en in user_enrollments if en not in audit_enrollments] courses_left_to_purchase = get_unenrolled_courses(courses, non_audit_enrollments) if courses_left_to_purchase: has_courses_left_to_purchase = True if courses_left_to_purchase and is_eligible_for_one_click_purchase: courses_left_to_purchase_price, courses_left_to_purchase_skus = \ get_program_price_and_skus(courses_left_to_purchase) if courses_left_to_purchase_skus: courses_left_to_purchase_url = EcommerceService().get_checkout_page_url( *courses_left_to_purchase_skus, program_uuid=program_uuid) program_key = { 'uuid': program_uuid, 'title': program.get('title'), 'marketing_url': program.get('marketing_url'), 'status': program.get('status'), 'is_eligible_for_one_click_purchase': is_eligible_for_one_click_purchase, 'total_courses': total_courses, 'complete_enrollment': complete_enrollment, 'has_courses_left_to_purchase': has_courses_left_to_purchase, 'courses_left_to_purchase_price': courses_left_to_purchase_price, 'courses_left_to_purchase_url': courses_left_to_purchase_url, } return program_key
def get(self, request): """ How to respond to a GET request to this endpoint """ program_enrollments = ProgramEnrollment.objects.filter( user=request.user, status__in=('enrolled', 'pending') ) uuids = [enrollment.program_uuid for enrollment in program_enrollments] catalog_data_of_programs = get_programs(uuids=uuids) or [] programs_in_which_learner_is_enrolled = [{'uuid': program['uuid'], 'slug': program['marketing_slug']} for program in catalog_data_of_programs] return Response(programs_in_which_learner_is_enrolled, status.HTTP_200_OK)
def get_programs_for_credentials(programs_credentials): """ Given a user and an iterable of credentials, get corresponding programs data and return it as a list of dictionaries. Arguments: programs_credentials (list): List of credentials awarded to the user for completion of a program. Returns: list, containing programs dictionaries. """ certified_programs = [] programs = get_programs() for program in programs: for credential in programs_credentials: if program['uuid'] == credential['credential']['program_uuid']: program['credential_url'] = credential['certificate_url'] certified_programs.append(program) return certified_programs
def __init__(self, program_uuid, request): self.program_uuid = program_uuid self.program = get_programs(uuid=self.program_uuid) self.request = request self.configuration = ProgramDiscussionsConfiguration.get( self.program_uuid)
def test_get_many(self, mock_warning, mock_info): programs = ProgramFactory.create_batch(3) # Cache details for 2 of 3 programs. partial_programs = { PROGRAM_CACHE_KEY_TPL.format(uuid=program['uuid']): program for program in programs[:2] } cache.set_many(partial_programs, None) # When called before UUIDs are cached, the function should return an # empty list and log a warning. self.assertEqual(get_programs(self.site), []) mock_warning.assert_called_once_with( 'Failed to get program UUIDs from the cache.') mock_warning.reset_mock() # Cache UUIDs for all 3 programs. cache.set( SITE_PROGRAM_UUIDS_CACHE_KEY_TPL.format(domain=self.site.domain), [program['uuid'] for program in programs], None) actual_programs = get_programs(self.site) # The 2 cached programs should be returned while info and warning # messages should be logged for the missing one. self.assertEqual( set(program['uuid'] for program in actual_programs), set(program['uuid'] for program in partial_programs.values())) mock_info.assert_called_with( 'Failed to get details for 1 programs. Retrying.') mock_warning.assert_called_with( 'Failed to get details for program {uuid} from the cache.'.format( uuid=programs[2]['uuid'])) mock_warning.reset_mock() # We can't use a set comparison here because these values are dictionaries # and aren't hashable. We've already verified that all programs came out # of the cache above, so all we need to do here is verify the accuracy of # the data itself. for program in actual_programs: key = PROGRAM_CACHE_KEY_TPL.format(uuid=program['uuid']) self.assertEqual(program, partial_programs[key]) # Cache details for all 3 programs. all_programs = { PROGRAM_CACHE_KEY_TPL.format(uuid=program['uuid']): program for program in programs } cache.set_many(all_programs, None) actual_programs = get_programs(self.site) # All 3 programs should be returned. self.assertEqual( set(program['uuid'] for program in actual_programs), set(program['uuid'] for program in all_programs.values())) self.assertFalse(mock_warning.called) for program in actual_programs: key = PROGRAM_CACHE_KEY_TPL.format(uuid=program['uuid']) self.assertEqual(program, all_programs[key])
def test_cache_disabled(self, _mock_cache, mock_get_catalog_data): self.catalog_integration = self.create_catalog_integration(cache_ttl=0) utils.get_programs(self.user) self.assert_contract(mock_get_catalog_data.call_args)
def test_config_missing(self, _mock_cache, _mock_get_catalog_data): """Verify that no errors occur if this method is called when catalog config is missing.""" CatalogIntegration.objects.all().delete() data = utils.get_programs(self.user) self.assertEqual(data, [])
def test_get_many(self, mock_warning, mock_info): programs = ProgramFactory.create_batch(3) # Cache details for 2 of 3 programs. partial_programs = { PROGRAM_CACHE_KEY_TPL.format(uuid=program['uuid']): program for program in programs[:2] } cache.set_many(partial_programs, None) # When called before UUIDs are cached, the function should return an # empty list and log a warning. self.assertEqual(get_programs(self.site), []) mock_warning.assert_called_once_with('Failed to get program UUIDs from the cache.') mock_warning.reset_mock() # Cache UUIDs for all 3 programs. cache.set( SITE_PROGRAM_UUIDS_CACHE_KEY_TPL.format(domain=self.site.domain), [program['uuid'] for program in programs], None ) actual_programs = get_programs(self.site) # The 2 cached programs should be returned while info and warning # messages should be logged for the missing one. self.assertEqual( set(program['uuid'] for program in actual_programs), set(program['uuid'] for program in partial_programs.values()) ) mock_info.assert_called_with('Failed to get details for 1 programs. Retrying.') mock_warning.assert_called_with( 'Failed to get details for program {uuid} from the cache.'.format(uuid=programs[2]['uuid']) ) mock_warning.reset_mock() # We can't use a set comparison here because these values are dictionaries # and aren't hashable. We've already verified that all programs came out # of the cache above, so all we need to do here is verify the accuracy of # the data itself. for program in actual_programs: key = PROGRAM_CACHE_KEY_TPL.format(uuid=program['uuid']) self.assertEqual(program, partial_programs[key]) # Cache details for all 3 programs. all_programs = { PROGRAM_CACHE_KEY_TPL.format(uuid=program['uuid']): program for program in programs } cache.set_many(all_programs, None) actual_programs = get_programs(self.site) # All 3 programs should be returned. self.assertEqual( set(program['uuid'] for program in actual_programs), set(program['uuid'] for program in all_programs.values()) ) self.assertFalse(mock_warning.called) for program in actual_programs: key = PROGRAM_CACHE_KEY_TPL.format(uuid=program['uuid']) self.assertEqual(program, all_programs[key])
def _load_course_runs(self): """Find all course runs which are part of a program.""" programs = get_programs() self.course_runs = self._flatten(programs)
def get_experiment_user_metadata_context(course, user): """ Return a context dictionary with the keys used by the user_metadata.html. """ enrollment_mode = None enrollment_time = None enrollment = None # TODO: clean up as part of REVO-28 (START) has_non_audit_enrollments = None # TODO: clean up as part of REVO-28 (END) # TODO: clean up as part of REVEM-199 (START) program_key = None # TODO: clean up as part of REVEM-199 (END) try: # TODO: clean up as part of REVO-28 (START) user_enrollments = CourseEnrollment.objects.select_related( 'course').filter(user_id=user.id) audit_enrollments = user_enrollments.filter(mode='audit') has_non_audit_enrollments = (len(audit_enrollments) != len(user_enrollments)) # TODO: clean up as part of REVO-28 (END) # TODO: clean up as part of REVEM-199 (START) if PROGRAM_INFO_FLAG.is_enabled(): programs = get_programs(course=course.id) if programs: # A course can be in multiple programs, but we're just grabbing the first one program = programs[0] complete_enrollment = False total_courses = None courses = program.get('courses') if courses is not None: total_courses = len(courses) complete_enrollment = is_enrolled_in_all_courses_in_program( courses, user_enrollments) program_key = { 'uuid': program.get('uuid'), 'title': program.get('title'), 'marketing_url': program.get('marketing_url'), 'total_courses': total_courses, 'complete_enrollment': complete_enrollment, } # TODO: clean up as part of REVEM-199 (END) enrollment = CourseEnrollment.objects.select_related('course').get( user_id=user.id, course_id=course.id) if enrollment.is_active: enrollment_mode = enrollment.mode enrollment_time = enrollment.created except CourseEnrollment.DoesNotExist: pass # Not enrolled, used the default None values # upgrade_link and upgrade_date should be None if user has passed their dynamic pacing deadline. upgrade_link, upgrade_date = check_and_get_upgrade_link_and_date( user, enrollment, course) has_staff_access = has_staff_access_to_preview_mode(user, course.id) forum_roles = [] if user.is_authenticated: forum_roles = list( Role.objects.filter( users=user, course_id=course.id).values_list('name').distinct()) # get user partition data if user.is_authenticated(): partition_groups = get_all_partitions_for_course(course) user_partitions = get_user_partition_groups(course.id, partition_groups, user, 'name') else: user_partitions = {} return { 'upgrade_link': upgrade_link, 'upgrade_price': unicode(get_cosmetic_verified_display_price(course)), 'enrollment_mode': enrollment_mode, 'enrollment_time': enrollment_time, 'pacing_type': 'self_paced' if course.self_paced else 'instructor_paced', 'upgrade_deadline': upgrade_date, 'course_key': course.id, 'course_start': course.start, 'course_end': course.end, 'has_staff_access': has_staff_access, 'forum_roles': forum_roles, 'partition_groups': user_partitions, # TODO: clean up as part of REVO-28 (START) 'has_non_audit_enrollments': has_non_audit_enrollments, # TODO: clean up as part of REVO-28 (END) # TODO: clean up as part of REVEM-199 (START) 'program_key_fields': program_key, # TODO: clean up as part of REVEM-199 (END) }
def test_cache_disabled(self, mock_get_edx_api_data): self.catalog_integration = self.create_catalog_integration(cache_ttl=0) get_programs() self.assert_contract(mock_get_edx_api_data.call_args)
def student_dashboard(request): """ Provides the LMS dashboard view TODO: This is lms specific and does not belong in common code. Note: To load the all courses set course_limit=None as parameter in GET. If its not None then default course limit will be used that is set in configuration Arguments: request: The request object. Returns: The dashboard response. """ user = request.user if not UserProfile.objects.filter(user=user).exists(): return redirect(reverse('account_settings')) platform_name = configuration_helpers.get_value("platform_name", settings.PLATFORM_NAME) enable_verified_certificates = configuration_helpers.get_value( 'ENABLE_VERIFIED_CERTIFICATES', settings.FEATURES.get('ENABLE_VERIFIED_CERTIFICATES')) display_course_modes_on_dashboard = configuration_helpers.get_value( 'DISPLAY_COURSE_MODES_ON_DASHBOARD', settings.FEATURES.get('DISPLAY_COURSE_MODES_ON_DASHBOARD', True)) activation_email_support_link = configuration_helpers.get_value( 'ACTIVATION_EMAIL_SUPPORT_LINK', settings.ACTIVATION_EMAIL_SUPPORT_LINK) or settings.SUPPORT_SITE_LINK hide_dashboard_courses_until_activated = configuration_helpers.get_value( 'HIDE_DASHBOARD_COURSES_UNTIL_ACTIVATED', settings.FEATURES.get('HIDE_DASHBOARD_COURSES_UNTIL_ACTIVATED', False)) empty_dashboard_message = configuration_helpers.get_value( 'EMPTY_DASHBOARD_MESSAGE', None) disable_course_limit = request and 'course_limit' in request.GET course_limit = get_dashboard_course_limit( ) if not disable_course_limit else None # Get the org whitelist or the org blacklist for the current site site_org_whitelist, site_org_blacklist = get_org_black_and_whitelist_for_site( ) course_enrollments = list( get_course_enrollments(user, site_org_whitelist, site_org_blacklist, course_limit)) # Get the entitlements for the user and a mapping to all available sessions for that entitlement # If an entitlement has no available sessions, pass through a mock course overview object (course_entitlements, course_entitlement_available_sessions, unfulfilled_entitlement_pseudo_sessions ) = get_filtered_course_entitlements(user, site_org_whitelist, site_org_blacklist) # Record how many courses there are so that we can get a better # understanding of usage patterns on prod. monitoring_utils.accumulate('num_courses', len(course_enrollments)) # Sort the enrollment pairs by the enrollment date course_enrollments.sort(key=lambda x: x.created, reverse=True) # Retrieve the course modes for each course enrolled_course_ids = [ enrollment.course_id for enrollment in course_enrollments ] __, unexpired_course_modes = CourseMode.all_and_unexpired_modes_for_courses( enrolled_course_ids) course_modes_by_course = { course_id: {mode.slug: mode for mode in modes} for course_id, modes in iteritems(unexpired_course_modes) } # Check to see if the student has recently enrolled in a course. # If so, display a notification message confirming the enrollment. enrollment_message = _create_recent_enrollment_message( course_enrollments, course_modes_by_course) course_optouts = Optout.objects.filter(user=user).values_list('course_id', flat=True) # Display activation message activate_account_message = '' if not user.is_active: activate_account_message = Text( _("Check your {email_start}{email}{email_end} inbox for an account activation link from {platform_name}. " "If you need help, contact {link_start}{platform_name} Support{link_end}." ) ).format( platform_name=platform_name, email_start=HTML("<strong>"), email_end=HTML("</strong>"), email=user.email, link_start=HTML( "<a target='_blank' href='{activation_email_support_link}'>"). format( activation_email_support_link=activation_email_support_link, ), link_end=HTML("</a>"), ) enterprise_message = get_dashboard_consent_notification( request, user, course_enrollments) recovery_email_message = recovery_email_activation_message = None if is_secondary_email_feature_enabled_for_user(user=user): try: pending_email = PendingSecondaryEmailChange.objects.get(user=user) except PendingSecondaryEmailChange.DoesNotExist: try: account_recovery_obj = AccountRecovery.objects.get(user=user) except AccountRecovery.DoesNotExist: recovery_email_message = Text( _("Add a recovery email to retain access when single-sign on is not available. " "Go to {link_start}your Account Settings{link_end}.") ).format(link_start=HTML( "<a href='{account_setting_page}'>").format( account_setting_page=reverse('account_settings'), ), link_end=HTML("</a>")) else: recovery_email_activation_message = Text( _("Recovery email is not activated yet. " "Kindly visit your email and follow the instructions to activate it." )) # Disable lookup of Enterprise consent_required_course due to ENT-727 # Will re-enable after fixing WL-1315 consent_required_courses = set() enterprise_customer_name = None # Account activation message account_activation_messages = [ message for message in messages.get_messages(request) if 'account-activation' in message.tags ] # Global staff can see what courses encountered an error on their dashboard staff_access = False errored_courses = {} if has_access(user, 'staff', 'global'): # Show any courses that encountered an error on load staff_access = True errored_courses = modulestore().get_errored_courses() show_courseware_links_for = { enrollment.course_id: has_access(request.user, 'load', enrollment.course_overview) for enrollment in course_enrollments } # Find programs associated with course runs being displayed. This information # is passed in the template context to allow rendering of program-related # information on the dashboard. meter = ProgramProgressMeter(request.site, user, enrollments=course_enrollments) ecommerce_service = EcommerceService() inverted_programs = meter.invert_programs() urls, programs_data = {}, {} bundles_on_dashboard_flag = WaffleFlag(experiments_namespace, u'bundles_on_dashboard') # TODO: Delete this code and the relevant HTML code after testing LEARNER-3072 is complete if bundles_on_dashboard_flag.is_enabled() and inverted_programs and list( inverted_programs.items()): if len(course_enrollments) < 4: for program in inverted_programs.values(): try: program_uuid = program[0]['uuid'] program_data = get_programs(uuid=program_uuid) program_data = ProgramDataExtender(program_data, request.user).extend() skus = program_data.get('skus') checkout_page_url = ecommerce_service.get_checkout_page_url( *skus) program_data[ 'completeProgramURL'] = checkout_page_url + '&bundle=' + program_data.get( 'uuid') programs_data[program_uuid] = program_data except: # pylint: disable=bare-except pass # Construct a dictionary of course mode information # used to render the course list. We re-use the course modes dict # we loaded earlier to avoid hitting the database. course_mode_info = { enrollment.course_id: complete_course_mode_info( enrollment.course_id, enrollment, modes=course_modes_by_course[enrollment.course_id]) for enrollment in course_enrollments } # Determine the per-course verification status # This is a dictionary in which the keys are course locators # and the values are one of: # # VERIFY_STATUS_NEED_TO_VERIFY # VERIFY_STATUS_SUBMITTED # VERIFY_STATUS_APPROVED # VERIFY_STATUS_MISSED_DEADLINE # # Each of which correspond to a particular message to display # next to the course on the dashboard. # # If a course is not included in this dictionary, # there is no verification messaging to display. verify_status_by_course = check_verify_status_by_course( user, course_enrollments) cert_statuses = { enrollment.course_id: cert_info(request.user, enrollment.course_overview) for enrollment in course_enrollments } # only show email settings for Mongo course and when bulk email is turned on show_email_settings_for = frozenset( enrollment.course_id for enrollment in course_enrollments if (is_bulk_email_feature_enabled(enrollment.course_id))) # Verification Attempts # Used to generate the "you must reverify for course x" banner verification_status = IDVerificationService.user_status(user) verification_errors = get_verification_error_reasons_for_display( verification_status['error']) # Gets data for midcourse reverifications, if any are necessary or have failed statuses = ["approved", "denied", "pending", "must_reverify"] reverifications = reverification_info(statuses) block_courses = frozenset( enrollment.course_id for enrollment in course_enrollments if is_course_blocked( request, CourseRegistrationCode.objects.filter( course_id=enrollment.course_id, registrationcoderedemption__redeemed_by=request.user), enrollment.course_id)) enrolled_courses_either_paid = frozenset( enrollment.course_id for enrollment in course_enrollments if enrollment.is_paid_course()) # If there are *any* denied reverifications that have not been toggled off, # we'll display the banner denied_banner = any(item.display for item in reverifications["denied"]) # get list of courses having pre-requisites yet to be completed courses_having_prerequisites = frozenset( enrollment.course_id for enrollment in course_enrollments if enrollment.course_overview.pre_requisite_courses) courses_requirements_not_met = get_pre_requisite_courses_not_completed( user, courses_having_prerequisites) if 'notlive' in request.GET: redirect_message = _( "The course you are looking for does not start until {date}." ).format(date=request.GET['notlive']) elif 'course_closed' in request.GET: redirect_message = _( "The course you are looking for is closed for enrollment as of {date}." ).format(date=request.GET['course_closed']) elif 'access_response_error' in request.GET: # This can be populated in a generalized way with fields from access response errors redirect_message = request.GET['access_response_error'] else: redirect_message = '' valid_verification_statuses = [ 'approved', 'must_reverify', 'pending', 'expired' ] display_sidebar_on_dashboard = verification_status['status'] in valid_verification_statuses and \ verification_status['should_display'] # Filter out any course enrollment course cards that are associated with fulfilled entitlements for entitlement in [ e for e in course_entitlements if e.enrollment_course_run is not None ]: course_enrollments = [ enr for enr in course_enrollments if entitlement.enrollment_course_run.course_id != enr.course_id ] context = { 'urls': urls, 'programs_data': programs_data, 'enterprise_message': enterprise_message, 'consent_required_courses': consent_required_courses, 'enterprise_customer_name': enterprise_customer_name, 'enrollment_message': enrollment_message, 'redirect_message': Text(redirect_message), 'account_activation_messages': account_activation_messages, 'activate_account_message': activate_account_message, 'course_enrollments': course_enrollments, 'course_entitlements': course_entitlements, 'course_entitlement_available_sessions': course_entitlement_available_sessions, 'unfulfilled_entitlement_pseudo_sessions': unfulfilled_entitlement_pseudo_sessions, 'course_optouts': course_optouts, 'staff_access': staff_access, 'errored_courses': errored_courses, 'show_courseware_links_for': show_courseware_links_for, 'all_course_modes': course_mode_info, 'cert_statuses': cert_statuses, 'credit_statuses': _credit_statuses(user, course_enrollments), 'show_email_settings_for': show_email_settings_for, 'reverifications': reverifications, 'verification_display': verification_status['should_display'], 'verification_status': verification_status['status'], 'verification_expiry': verification_status['verification_expiry'], 'verification_status_by_course': verify_status_by_course, 'verification_errors': verification_errors, 'block_courses': block_courses, 'denied_banner': denied_banner, 'billing_email': settings.PAYMENT_SUPPORT_EMAIL, 'user': user, 'logout_url': reverse('logout'), 'platform_name': platform_name, 'enrolled_courses_either_paid': enrolled_courses_either_paid, 'provider_states': [], 'courses_requirements_not_met': courses_requirements_not_met, 'nav_hidden': True, 'inverted_programs': inverted_programs, 'show_program_listing': ProgramsApiConfig.is_enabled(), 'show_dashboard_tabs': True, 'disable_courseware_js': True, 'display_course_modes_on_dashboard': enable_verified_certificates and display_course_modes_on_dashboard, 'display_sidebar_on_dashboard': display_sidebar_on_dashboard, 'display_sidebar_account_activation_message': not (user.is_active or hide_dashboard_courses_until_activated), 'display_dashboard_courses': (user.is_active or not hide_dashboard_courses_until_activated), 'empty_dashboard_message': empty_dashboard_message, 'recovery_email_message': recovery_email_message, 'recovery_email_activation_message': recovery_email_activation_message, 'show_load_all_courses_link': show_load_all_courses_link(user, course_limit, course_enrollments), # TODO START: clean up as part of REVEM-199 (START) 'course_info': get_dashboard_course_info(user, course_enrollments), # TODO START: clean up as part of REVEM-199 (END) } context_from_plugins = get_plugins_view_context( plugin_constants.ProjectType.LMS, COURSE_DASHBOARD_PLUGIN_VIEW_NAME, context) context.update(context_from_plugins) if ecommerce_service.is_enabled(request.user): context.update({ 'use_ecommerce_payment_flow': True, 'ecommerce_payment_page': ecommerce_service.payment_page_url(), }) # Gather urls for course card resume buttons. resume_button_urls = ['' for entitlement in course_entitlements] for url in get_resume_urls_for_enrollments(user, course_enrollments).values(): resume_button_urls.append(url) # There must be enough urls for dashboard.html. Template creates course # cards for "enrollments + entitlements". context.update({'resume_button_urls': resume_button_urls}) return render_to_response('dashboard.html', context)
def get_deprecated_experiment_user_metadata_context(course, user): """ Return a context dictionary with the keys used by the user_metadata.html. This is deprecated and will be removed once we have confirmed that its replacement functions as intended. """ enrollment_mode = None enrollment_time = None enrollment = None # TODO: clean up as part of REVO-28 (START) has_non_audit_enrollments = None # TODO: clean up as part of REVO-28 (END) # TODO: clean up as part of REVEM-199 (START) program_key = None # TODO: clean up as part of REVEM-199 (END) try: # TODO: clean up as part of REVO-28 (START) user_enrollments = CourseEnrollment.objects.select_related('course').filter(user_id=user.id) audit_enrollments = user_enrollments.filter(mode='audit') has_non_audit_enrollments = (len(audit_enrollments) != len(user_enrollments)) # TODO: clean up as part of REVO-28 (END) # TODO: clean up as part of REVEM-199 (START) if PROGRAM_INFO_FLAG.is_enabled(): programs = get_programs(course=course.id) if programs: # A course can be in multiple programs, but we're just grabbing the first one program = programs[0] complete_enrollment = False has_courses_left_to_purchase = False total_courses = None courses = program.get('courses') courses_left_to_purchase_price = None courses_left_to_purchase_url = None program_uuid = program.get('uuid') status = None is_eligible_for_one_click_purchase = None if courses is not None: total_courses = len(courses) complete_enrollment = is_enrolled_in_all_courses(courses, user_enrollments) status = program.get('status') is_eligible_for_one_click_purchase = program.get('is_program_eligible_for_one_click_purchase') # Get the price and purchase URL of the program courses the user has yet to purchase. Say a # program has 3 courses (A, B and C), and the user previously purchased a certificate for A. # The user is enrolled in audit mode for B. The "left to purchase price" should be the price of # B+C. non_audit_enrollments = [en for en in user_enrollments if en not in audit_enrollments] courses_left_to_purchase = get_unenrolled_courses(courses, non_audit_enrollments) if courses_left_to_purchase: has_courses_left_to_purchase = True if is_eligible_for_one_click_purchase: courses_left_to_purchase_price, courses_left_to_purchase_skus = \ get_program_price_and_skus(courses_left_to_purchase) if courses_left_to_purchase_skus: courses_left_to_purchase_url = EcommerceService().get_checkout_page_url( *courses_left_to_purchase_skus, program_uuid=program_uuid) program_key = { 'uuid': program_uuid, 'title': program.get('title'), 'marketing_url': program.get('marketing_url'), 'status': status, 'is_eligible_for_one_click_purchase': is_eligible_for_one_click_purchase, 'total_courses': total_courses, 'complete_enrollment': complete_enrollment, 'has_courses_left_to_purchase': has_courses_left_to_purchase, 'courses_left_to_purchase_price': courses_left_to_purchase_price, 'courses_left_to_purchase_url': courses_left_to_purchase_url, } # TODO: clean up as part of REVEM-199 (END) enrollment = CourseEnrollment.objects.select_related( 'course' ).get(user_id=user.id, course_id=course.id) if enrollment.is_active: enrollment_mode = enrollment.mode enrollment_time = enrollment.created except CourseEnrollment.DoesNotExist: pass # Not enrolled, used the default None values # upgrade_link and upgrade_date should be None if user has passed their dynamic pacing deadline. upgrade_link, upgrade_date = check_and_get_upgrade_link_and_date(user, enrollment, course) has_staff_access = has_staff_access_to_preview_mode(user, course.id) forum_roles = [] if user.is_authenticated: forum_roles = list(Role.objects.filter(users=user, course_id=course.id).values_list('name').distinct()) # get user partition data if user.is_authenticated(): partition_groups = get_all_partitions_for_course(course) user_partitions = get_user_partition_groups(course.id, partition_groups, user, 'name') else: user_partitions = {} return { 'upgrade_link': upgrade_link, 'upgrade_price': six.text_type(get_cosmetic_verified_display_price(course)), 'enrollment_mode': enrollment_mode, 'enrollment_time': enrollment_time, 'pacing_type': 'self_paced' if course.self_paced else 'instructor_paced', 'upgrade_deadline': upgrade_date, 'audit_access_deadline': get_audit_access_expiration(user, course), 'course_key': course.id, 'course_start': course.start, 'course_end': course.end, 'has_staff_access': has_staff_access, 'forum_roles': forum_roles, 'partition_groups': user_partitions, # TODO: clean up as part of REVO-28 (START) 'has_non_audit_enrollments': has_non_audit_enrollments, # TODO: clean up as part of REVO-28 (END) # TODO: clean up as part of REVEM-199 (START) 'program_key_fields': program_key, # TODO: clean up as part of REVEM-199 (END) }
def student_dashboard(request): """ Provides the LMS dashboard view TODO: This is lms specific and does not belong in common code. Arguments: request: The request object. Returns: The dashboard response. """ user = request.user if not UserProfile.objects.filter(user=user).exists(): return redirect(reverse('account_settings')) platform_name = configuration_helpers.get_value("platform_name", settings.PLATFORM_NAME) enable_verified_certificates = configuration_helpers.get_value( 'ENABLE_VERIFIED_CERTIFICATES', settings.FEATURES.get('ENABLE_VERIFIED_CERTIFICATES') ) display_course_modes_on_dashboard = configuration_helpers.get_value( 'DISPLAY_COURSE_MODES_ON_DASHBOARD', settings.FEATURES.get('DISPLAY_COURSE_MODES_ON_DASHBOARD', True) ) activation_email_support_link = configuration_helpers.get_value( 'ACTIVATION_EMAIL_SUPPORT_LINK', settings.ACTIVATION_EMAIL_SUPPORT_LINK ) or settings.SUPPORT_SITE_LINK # Get the org whitelist or the org blacklist for the current site site_org_whitelist, site_org_blacklist = get_org_black_and_whitelist_for_site() course_enrollments = list(get_course_enrollments(user, site_org_whitelist, site_org_blacklist)) # Get the entitlements for the user and a mapping to all available sessions for that entitlement # If an entitlement has no available sessions, pass through a mock course overview object (course_entitlements, course_entitlement_available_sessions, unfulfilled_entitlement_pseudo_sessions) = get_filtered_course_entitlements( user, site_org_whitelist, site_org_blacklist ) # Record how many courses there are so that we can get a better # understanding of usage patterns on prod. monitoring_utils.accumulate('num_courses', len(course_enrollments)) # Sort the enrollment pairs by the enrollment date course_enrollments.sort(key=lambda x: x.created, reverse=True) # Retrieve the course modes for each course enrolled_course_ids = [enrollment.course_id for enrollment in course_enrollments] __, unexpired_course_modes = CourseMode.all_and_unexpired_modes_for_courses(enrolled_course_ids) course_modes_by_course = { course_id: { mode.slug: mode for mode in modes } for course_id, modes in iteritems(unexpired_course_modes) } courses_count = len(course_enrollments) # Check to see if the student has recently enrolled in a course. # If so, display a notification message confirming the enrollment. enrollment_message = _create_recent_enrollment_message( course_enrollments, course_modes_by_course ) course_optouts = Optout.objects.filter(user=user).values_list('course_id', flat=True) sidebar_account_activation_message = '' banner_account_activation_message = '' display_account_activation_message_on_sidebar = configuration_helpers.get_value( 'DISPLAY_ACCOUNT_ACTIVATION_MESSAGE_ON_SIDEBAR', settings.FEATURES.get('DISPLAY_ACCOUNT_ACTIVATION_MESSAGE_ON_SIDEBAR', False) ) # Display activation message in sidebar if DISPLAY_ACCOUNT_ACTIVATION_MESSAGE_ON_SIDEBAR # flag is active. Otherwise display existing message at the top. if display_account_activation_message_on_sidebar and not user.is_active: sidebar_account_activation_message = render_to_string( 'registration/account_activation_sidebar_notice.html', { 'email': user.email, 'platform_name': platform_name, 'activation_email_support_link': activation_email_support_link } ) elif not user.is_active: banner_account_activation_message = render_to_string( 'registration/activate_account_notice.html', {'email': user.email} ) enterprise_message = get_dashboard_consent_notification(request, user, course_enrollments) # Disable lookup of Enterprise consent_required_course due to ENT-727 # Will re-enable after fixing WL-1315 consent_required_courses = set() enterprise_customer_name = None # Account activation message account_activation_messages = [ message for message in messages.get_messages(request) if 'account-activation' in message.tags ] # Global staff can see what courses encountered an error on their dashboard staff_access = False errored_courses = {} if has_access(user, 'staff', 'global'): # Show any courses that encountered an error on load staff_access = True errored_courses = modulestore().get_errored_courses() show_courseware_links_for = frozenset( enrollment.course_id for enrollment in course_enrollments if has_access(request.user, 'load', enrollment.course_overview) ) # Find programs associated with course runs being displayed. This information # is passed in the template context to allow rendering of program-related # information on the dashboard. meter = ProgramProgressMeter(request.site, user, enrollments=course_enrollments) ecommerce_service = EcommerceService() inverted_programs = meter.invert_programs() urls, programs_data = {}, {} bundles_on_dashboard_flag = WaffleFlag(WaffleFlagNamespace(name=u'student.experiments'), u'bundles_on_dashboard') # TODO: Delete this code and the relevant HTML code after testing LEARNER-3072 is complete if bundles_on_dashboard_flag.is_enabled() and inverted_programs and inverted_programs.items(): if len(course_enrollments) < 4: for program in inverted_programs.values(): try: program_uuid = program[0]['uuid'] program_data = get_programs(request.site, uuid=program_uuid) program_data = ProgramDataExtender(program_data, request.user).extend() skus = program_data.get('skus') checkout_page_url = ecommerce_service.get_checkout_page_url(*skus) program_data['completeProgramURL'] = checkout_page_url + '&bundle=' + program_data.get('uuid') programs_data[program_uuid] = program_data except: # pylint: disable=bare-except pass # Construct a dictionary of course mode information # used to render the course list. We re-use the course modes dict # we loaded earlier to avoid hitting the database. course_mode_info = { enrollment.course_id: complete_course_mode_info( enrollment.course_id, enrollment, modes=course_modes_by_course[enrollment.course_id] ) for enrollment in course_enrollments } # Determine the per-course verification status # This is a dictionary in which the keys are course locators # and the values are one of: # # VERIFY_STATUS_NEED_TO_VERIFY # VERIFY_STATUS_SUBMITTED # VERIFY_STATUS_APPROVED # VERIFY_STATUS_MISSED_DEADLINE # # Each of which correspond to a particular message to display # next to the course on the dashboard. # # If a course is not included in this dictionary, # there is no verification messaging to display. verify_status_by_course = check_verify_status_by_course(user, course_enrollments) cert_statuses = { enrollment.course_id: cert_info(request.user, enrollment.course_overview) for enrollment in course_enrollments } # only show email settings for Mongo course and when bulk email is turned on show_email_settings_for = frozenset( enrollment.course_id for enrollment in course_enrollments if ( BulkEmailFlag.feature_enabled(enrollment.course_id) ) ) # Verification Attempts # Used to generate the "you must reverify for course x" banner verification_status = IDVerificationService.user_status(user) verification_errors = get_verification_error_reasons_for_display(verification_status['error']) # Gets data for midcourse reverifications, if any are necessary or have failed statuses = ["approved", "denied", "pending", "must_reverify"] reverifications = reverification_info(statuses) block_courses = frozenset( enrollment.course_id for enrollment in course_enrollments if is_course_blocked( request, CourseRegistrationCode.objects.filter( course_id=enrollment.course_id, registrationcoderedemption__redeemed_by=request.user ), enrollment.course_id ) ) enrolled_courses_either_paid = frozenset( enrollment.course_id for enrollment in course_enrollments if enrollment.is_paid_course() ) # If there are *any* denied reverifications that have not been toggled off, # we'll display the banner denied_banner = any(item.display for item in reverifications["denied"]) # Populate the Order History for the side-bar. order_history_list = order_history( user, course_org_filter=site_org_whitelist, org_filter_out_set=site_org_blacklist ) # get list of courses having pre-requisites yet to be completed courses_having_prerequisites = frozenset( enrollment.course_id for enrollment in course_enrollments if enrollment.course_overview.pre_requisite_courses ) courses_requirements_not_met = get_pre_requisite_courses_not_completed(user, courses_having_prerequisites) if 'notlive' in request.GET: redirect_message = _("The course you are looking for does not start until {date}.").format( date=request.GET['notlive'] ) elif 'course_closed' in request.GET: redirect_message = _("The course you are looking for is closed for enrollment as of {date}.").format( date=request.GET['course_closed'] ) else: redirect_message = '' valid_verification_statuses = ['approved', 'must_reverify', 'pending', 'expired'] display_sidebar_on_dashboard = (len(order_history_list) or (verification_status['status'] in valid_verification_statuses and verification_status['should_display'])) # Filter out any course enrollment course cards that are associated with fulfilled entitlements for entitlement in [e for e in course_entitlements if e.enrollment_course_run is not None]: course_enrollments = [ enr for enr in course_enrollments if entitlement.enrollment_course_run.course_id != enr.course_id ] context = { 'urls': urls, 'programs_data': programs_data, 'enterprise_message': enterprise_message, 'consent_required_courses': consent_required_courses, 'enterprise_customer_name': enterprise_customer_name, 'prev_courses_count': prev_courses_count, 'courses_count': courses_count, 'prev_time': prev_time, 'time': time, 'enrollment_message': enrollment_message, 'redirect_message': redirect_message, 'account_activation_messages': account_activation_messages, 'course_enrollments': course_enrollments, 'course_entitlements': course_entitlements, 'course_entitlement_available_sessions': course_entitlement_available_sessions, 'unfulfilled_entitlement_pseudo_sessions': unfulfilled_entitlement_pseudo_sessions, 'course_optouts': course_optouts, 'banner_account_activation_message': banner_account_activation_message, 'sidebar_account_activation_message': sidebar_account_activation_message, 'staff_access': staff_access, 'errored_courses': errored_courses, 'show_courseware_links_for': show_courseware_links_for, 'all_course_modes': course_mode_info, 'cert_statuses': cert_statuses, 'credit_statuses': _credit_statuses(user, course_enrollments), 'show_email_settings_for': show_email_settings_for, 'reverifications': reverifications, 'verification_display': verification_status['should_display'], 'verification_status': verification_status['status'], 'verification_status_by_course': verify_status_by_course, 'verification_errors': verification_errors, 'block_courses': block_courses, 'denied_banner': denied_banner, 'billing_email': settings.PAYMENT_SUPPORT_EMAIL, 'user': user, 'logout_url': reverse('logout'), 'platform_name': platform_name, 'enrolled_courses_either_paid': enrolled_courses_either_paid, 'provider_states': [], 'order_history_list': order_history_list, 'courses_requirements_not_met': courses_requirements_not_met, 'nav_hidden': True, 'inverted_programs': inverted_programs, 'show_program_listing': ProgramsApiConfig.is_enabled(), 'show_dashboard_tabs': True, 'disable_courseware_js': True, 'display_course_modes_on_dashboard': enable_verified_certificates and display_course_modes_on_dashboard, 'display_sidebar_on_dashboard': display_sidebar_on_dashboard, } if ecommerce_service.is_enabled(request.user): context.update({ 'use_ecommerce_payment_flow': True, 'ecommerce_payment_page': ecommerce_service.payment_page_url(), }) # Gather urls for course card resume buttons. resume_button_urls = _get_urls_for_resume_buttons(user, course_enrollments) # There must be enough urls for dashboard.html. Template creates course # cards for "enrollments + entitlements". resume_button_urls += ['' for entitlement in course_entitlements] context.update({ 'resume_button_urls': resume_button_urls, 'time': pytz.utc.localize(datetime.datetime.now()), }) response = render_to_response('dashboard.html', context) set_user_info_cookie(response, request) return response
def get_experiment_user_metadata_context(course, user): """ Return a context dictionary with the keys used by the user_metadata.html. """ # TODO: call get_base_experiment_metadata_context(), and then add in the bits that are only needed by user_metadata enrollment_mode = None enrollment_time = None enrollment = None # TODO: clean up as part of REVO-28 (START) has_non_audit_enrollments = None # TODO: clean up as part of REVO-28 (END) # TODO: clean up as part of REVEM-199 (START) program_key = None # TODO: clean up as part of REVEM-199 (END) try: # TODO: clean up as part of REVO-28 (START) user_enrollments = CourseEnrollment.objects.select_related('course').filter(user_id=user.id) audit_enrollments = user_enrollments.filter(mode='audit') has_non_audit_enrollments = (len(audit_enrollments) != len(user_enrollments)) # TODO: clean up as part of REVO-28 (END) # TODO: clean up as part of REVEM-199 (START) if PROGRAM_INFO_FLAG.is_enabled(): programs = get_programs(course=course.id) if programs: # A course can be in multiple programs, but we're just grabbing the first one program = programs[0] complete_enrollment = False has_courses_left_to_purchase = False total_courses = None courses = program.get('courses') courses_left_to_purchase_price = None courses_left_to_purchase_url = None program_uuid = program.get('uuid') status = None is_eligible_for_one_click_purchase = None if courses is not None: total_courses = len(courses) complete_enrollment = is_enrolled_in_all_courses(courses, user_enrollments) status = program.get('status') is_eligible_for_one_click_purchase = program.get('is_program_eligible_for_one_click_purchase') # Get the price and purchase URL of the program courses the user has yet to purchase. Say a # program has 3 courses (A, B and C), and the user previously purchased a certificate for A. # The user is enrolled in audit mode for B. The "left to purchase price" should be the price of # B+C. non_audit_enrollments = [en for en in user_enrollments if en not in audit_enrollments] courses_left_to_purchase = get_unenrolled_courses(courses, non_audit_enrollments) if courses_left_to_purchase: has_courses_left_to_purchase = True if is_eligible_for_one_click_purchase: courses_left_to_purchase_price, courses_left_to_purchase_skus = \ get_program_price_and_skus(courses_left_to_purchase) if courses_left_to_purchase_skus: courses_left_to_purchase_url = EcommerceService().get_checkout_page_url( *courses_left_to_purchase_skus, program_uuid=program_uuid) program_key = { 'uuid': program_uuid, 'title': program.get('title'), 'marketing_url': program.get('marketing_url'), 'status': status, 'is_eligible_for_one_click_purchase': is_eligible_for_one_click_purchase, 'total_courses': total_courses, 'complete_enrollment': complete_enrollment, 'has_courses_left_to_purchase': has_courses_left_to_purchase, 'courses_left_to_purchase_price': courses_left_to_purchase_price, 'courses_left_to_purchase_url': courses_left_to_purchase_url, } # TODO: clean up as part of REVEM-199 (END) enrollment = CourseEnrollment.objects.select_related( 'course' ).get(user_id=user.id, course_id=course.id) if enrollment.is_active: enrollment_mode = enrollment.mode enrollment_time = enrollment.created except CourseEnrollment.DoesNotExist: pass # Not enrolled, used the default None values # upgrade_link and upgrade_date should be None if user has passed their dynamic pacing deadline. upgrade_link, upgrade_date = check_and_get_upgrade_link_and_date(user, enrollment, course) has_staff_access = has_staff_access_to_preview_mode(user, course.id) forum_roles = [] if user.is_authenticated: forum_roles = list(Role.objects.filter(users=user, course_id=course.id).values_list('name').distinct()) # get user partition data if user.is_authenticated(): partition_groups = get_all_partitions_for_course(course) user_partitions = get_user_partition_groups(course.id, partition_groups, user, 'name') else: user_partitions = {} return { 'upgrade_link': upgrade_link, 'upgrade_price': unicode(get_cosmetic_verified_display_price(course)), 'enrollment_mode': enrollment_mode, 'enrollment_time': enrollment_time, 'pacing_type': 'self_paced' if course.self_paced else 'instructor_paced', 'upgrade_deadline': upgrade_date, 'course_key': course.id, 'course_start': course.start, 'course_end': course.end, 'has_staff_access': has_staff_access, 'forum_roles': forum_roles, 'partition_groups': user_partitions, # TODO: clean up as part of REVO-28 (START) 'has_non_audit_enrollments': has_non_audit_enrollments, # TODO: clean up as part of REVO-28 (END) # TODO: clean up as part of REVEM-199 (START) 'program_key_fields': program_key, # TODO: clean up as part of REVEM-199 (END) }
def student_dashboard(request): """ Provides the LMS dashboard view TODO: This is lms specific and does not belong in common code. Arguments: request: The request object. Returns: The dashboard response. """ user = request.user if not UserProfile.objects.filter(user=user).exists(): return redirect(reverse('account_settings')) platform_name = configuration_helpers.get_value("platform_name", settings.PLATFORM_NAME) enable_verified_certificates = configuration_helpers.get_value( 'ENABLE_VERIFIED_CERTIFICATES', settings.FEATURES.get('ENABLE_VERIFIED_CERTIFICATES') ) display_course_modes_on_dashboard = configuration_helpers.get_value( 'DISPLAY_COURSE_MODES_ON_DASHBOARD', settings.FEATURES.get('DISPLAY_COURSE_MODES_ON_DASHBOARD', True) ) activation_email_support_link = configuration_helpers.get_value( 'ACTIVATION_EMAIL_SUPPORT_LINK', settings.ACTIVATION_EMAIL_SUPPORT_LINK ) or settings.SUPPORT_SITE_LINK # Get the org whitelist or the org blacklist for the current site site_org_whitelist, site_org_blacklist = get_org_black_and_whitelist_for_site() course_enrollments = list(get_course_enrollments(user, site_org_whitelist, site_org_blacklist)) # Get the entitlements for the user and a mapping to all available sessions for that entitlement # If an entitlement has no available sessions, pass through a mock course overview object (course_entitlements, course_entitlement_available_sessions, unfulfilled_entitlement_pseudo_sessions) = get_filtered_course_entitlements( user, site_org_whitelist, site_org_blacklist ) # Record how many courses there are so that we can get a better # understanding of usage patterns on prod. monitoring_utils.accumulate('num_courses', len(course_enrollments)) # Sort the enrollment pairs by the enrollment date course_enrollments.sort(key=lambda x: x.created, reverse=True) # Retrieve the course modes for each course enrolled_course_ids = [enrollment.course_id for enrollment in course_enrollments] __, unexpired_course_modes = CourseMode.all_and_unexpired_modes_for_courses(enrolled_course_ids) course_modes_by_course = { course_id: { mode.slug: mode for mode in modes } for course_id, modes in iteritems(unexpired_course_modes) } # Check to see if the student has recently enrolled in a course. # If so, display a notification message confirming the enrollment. enrollment_message = _create_recent_enrollment_message( course_enrollments, course_modes_by_course ) course_optouts = Optout.objects.filter(user=user).values_list('course_id', flat=True) sidebar_account_activation_message = '' banner_account_activation_message = '' display_account_activation_message_on_sidebar = configuration_helpers.get_value( 'DISPLAY_ACCOUNT_ACTIVATION_MESSAGE_ON_SIDEBAR', settings.FEATURES.get('DISPLAY_ACCOUNT_ACTIVATION_MESSAGE_ON_SIDEBAR', False) ) # Display activation message in sidebar if DISPLAY_ACCOUNT_ACTIVATION_MESSAGE_ON_SIDEBAR # flag is active. Otherwise display existing message at the top. if display_account_activation_message_on_sidebar and not user.is_active: sidebar_account_activation_message = render_to_string( 'registration/account_activation_sidebar_notice.html', { 'email': user.email, 'platform_name': platform_name, 'activation_email_support_link': activation_email_support_link } ) elif not user.is_active: banner_account_activation_message = render_to_string( 'registration/activate_account_notice.html', {'email': user.email} ) enterprise_message = get_dashboard_consent_notification(request, user, course_enrollments) # Disable lookup of Enterprise consent_required_course due to ENT-727 # Will re-enable after fixing WL-1315 consent_required_courses = set() enterprise_customer_name = None # Account activation message account_activation_messages = [ message for message in messages.get_messages(request) if 'account-activation' in message.tags ] # Global staff can see what courses encountered an error on their dashboard staff_access = False errored_courses = {} if has_access(user, 'staff', 'global'): # Show any courses that encountered an error on load staff_access = True errored_courses = modulestore().get_errored_courses() show_courseware_links_for = frozenset( enrollment.course_id for enrollment in course_enrollments if has_access(request.user, 'load', enrollment.course_overview) ) # Find programs associated with course runs being displayed. This information # is passed in the template context to allow rendering of program-related # information on the dashboard. meter = ProgramProgressMeter(request.site, user, enrollments=course_enrollments) ecommerce_service = EcommerceService() inverted_programs = meter.invert_programs() urls, programs_data = {}, {} bundles_on_dashboard_flag = WaffleFlag(WaffleFlagNamespace(name=u'student.experiments'), u'bundles_on_dashboard') # TODO: Delete this code and the relevant HTML code after testing LEARNER-3072 is complete if bundles_on_dashboard_flag.is_enabled() and inverted_programs and inverted_programs.items(): if len(course_enrollments) < 4: for program in inverted_programs.values(): try: program_uuid = program[0]['uuid'] program_data = get_programs(request.site, uuid=program_uuid) program_data = ProgramDataExtender(program_data, request.user).extend() skus = program_data.get('skus') checkout_page_url = ecommerce_service.get_checkout_page_url(*skus) program_data['completeProgramURL'] = checkout_page_url + '&bundle=' + program_data.get('uuid') programs_data[program_uuid] = program_data except: # pylint: disable=bare-except pass # Construct a dictionary of course mode information # used to render the course list. We re-use the course modes dict # we loaded earlier to avoid hitting the database. course_mode_info = { enrollment.course_id: complete_course_mode_info( enrollment.course_id, enrollment, modes=course_modes_by_course[enrollment.course_id] ) for enrollment in course_enrollments } # Determine the per-course verification status # This is a dictionary in which the keys are course locators # and the values are one of: # # VERIFY_STATUS_NEED_TO_VERIFY # VERIFY_STATUS_SUBMITTED # VERIFY_STATUS_APPROVED # VERIFY_STATUS_MISSED_DEADLINE # # Each of which correspond to a particular message to display # next to the course on the dashboard. # # If a course is not included in this dictionary, # there is no verification messaging to display. verify_status_by_course = check_verify_status_by_course(user, course_enrollments) cert_statuses = { enrollment.course_id: cert_info(request.user, enrollment.course_overview) for enrollment in course_enrollments } # only show email settings for Mongo course and when bulk email is turned on show_email_settings_for = frozenset( enrollment.course_id for enrollment in course_enrollments if ( BulkEmailFlag.feature_enabled(enrollment.course_id) ) ) # Verification Attempts # Used to generate the "you must reverify for course x" banner verification_status = IDVerificationService.user_status(user) verification_errors = get_verification_error_reasons_for_display(verification_status['error']) # Gets data for midcourse reverifications, if any are necessary or have failed statuses = ["approved", "denied", "pending", "must_reverify"] reverifications = reverification_info(statuses) block_courses = frozenset( enrollment.course_id for enrollment in course_enrollments if is_course_blocked( request, CourseRegistrationCode.objects.filter( course_id=enrollment.course_id, registrationcoderedemption__redeemed_by=request.user ), enrollment.course_id ) ) enrolled_courses_either_paid = frozenset( enrollment.course_id for enrollment in course_enrollments if enrollment.is_paid_course() ) # If there are *any* denied reverifications that have not been toggled off, # we'll display the banner denied_banner = any(item.display for item in reverifications["denied"]) # Populate the Order History for the side-bar. order_history_list = order_history( user, course_org_filter=site_org_whitelist, org_filter_out_set=site_org_blacklist ) # get list of courses having pre-requisites yet to be completed courses_having_prerequisites = frozenset( enrollment.course_id for enrollment in course_enrollments if enrollment.course_overview.pre_requisite_courses ) courses_requirements_not_met = get_pre_requisite_courses_not_completed(user, courses_having_prerequisites) if 'notlive' in request.GET: redirect_message = _("The course you are looking for does not start until {date}.").format( date=request.GET['notlive'] ) elif 'course_closed' in request.GET: redirect_message = _("The course you are looking for is closed for enrollment as of {date}.").format( date=request.GET['course_closed'] ) else: redirect_message = '' valid_verification_statuses = ['approved', 'must_reverify', 'pending', 'expired'] display_sidebar_on_dashboard = (len(order_history_list) or (verification_status['status'] in valid_verification_statuses and verification_status['should_display'])) # Filter out any course enrollment course cards that are associated with fulfilled entitlements for entitlement in [e for e in course_entitlements if e.enrollment_course_run is not None]: course_enrollments = [ enr for enr in course_enrollments if entitlement.enrollment_course_run.course_id != enr.course_id ] context = { 'urls': urls, 'programs_data': programs_data, 'enterprise_message': enterprise_message, 'consent_required_courses': consent_required_courses, 'enterprise_customer_name': enterprise_customer_name, 'enrollment_message': enrollment_message, 'redirect_message': redirect_message, 'account_activation_messages': account_activation_messages, 'course_enrollments': course_enrollments, 'course_entitlements': course_entitlements, 'course_entitlement_available_sessions': course_entitlement_available_sessions, 'unfulfilled_entitlement_pseudo_sessions': unfulfilled_entitlement_pseudo_sessions, 'course_optouts': course_optouts, 'banner_account_activation_message': banner_account_activation_message, 'sidebar_account_activation_message': sidebar_account_activation_message, 'staff_access': staff_access, 'errored_courses': errored_courses, 'show_courseware_links_for': show_courseware_links_for, 'all_course_modes': course_mode_info, 'cert_statuses': cert_statuses, 'credit_statuses': _credit_statuses(user, course_enrollments), 'show_email_settings_for': show_email_settings_for, 'reverifications': reverifications, 'verification_display': verification_status['should_display'], 'verification_status': verification_status['status'], 'verification_status_by_course': verify_status_by_course, 'verification_errors': verification_errors, 'block_courses': block_courses, 'denied_banner': denied_banner, 'billing_email': settings.PAYMENT_SUPPORT_EMAIL, 'user': user, 'logout_url': reverse('logout'), 'platform_name': platform_name, 'enrolled_courses_either_paid': enrolled_courses_either_paid, 'provider_states': [], 'order_history_list': order_history_list, 'courses_requirements_not_met': courses_requirements_not_met, 'nav_hidden': True, 'inverted_programs': inverted_programs, 'show_program_listing': ProgramsApiConfig.is_enabled(), 'show_dashboard_tabs': True, 'disable_courseware_js': True, 'display_course_modes_on_dashboard': enable_verified_certificates and display_course_modes_on_dashboard, 'display_sidebar_on_dashboard': display_sidebar_on_dashboard, } if ecommerce_service.is_enabled(request.user): context.update({ 'use_ecommerce_payment_flow': True, 'ecommerce_payment_page': ecommerce_service.payment_page_url(), }) # Gather urls for course card resume buttons. resume_button_urls = _get_urls_for_resume_buttons(user, course_enrollments) # There must be enough urls for dashboard.html. Template creates course # cards for "enrollments + entitlements". resume_button_urls += ['' for entitlement in course_entitlements] context.update({ 'resume_button_urls': resume_button_urls }) response = render_to_response('dashboard.html', context) set_user_info_cookie(response, request) return response
def matriculate_learner(user, uid): """ Update any waiting program enrollments with a user, and enroll the user in any waiting course enrollments. In most cases this will just short-circuit. Enrollments will only be updated for a SAML provider with a linked organization. """ try: provider_slug, external_user_key = uid.split(':') authorizing_org = SAMLProviderConfig.objects.current_set().get( slug=provider_slug).organization if not authorizing_org: return except (AttributeError, ValueError): logger.info('Ignoring non-saml social auth entry for user=%s', user.id) return except SAMLProviderConfig.DoesNotExist: logger.warning( 'Got incoming social auth for provider=%s but no such provider exists', provider_slug) return except SAMLProviderConfig.MultipleObjectsReturned: logger.warning( 'Unable to activate waiting enrollments for user=%s.' ' Multiple active SAML configurations found for slug=%s. Expected one.', user.id, provider_slug) return incomplete_enrollments = fetch_program_enrollments_by_student( external_user_key=external_user_key, waiting_only=True, ).prefetch_related('program_course_enrollments') for enrollment in incomplete_enrollments: try: enrollment_org = get_programs( uuid=enrollment.program_uuid)['authoring_organizations'][0] if enrollment_org['key'] != authorizing_org.short_name: continue except (KeyError, TypeError): logger.warning( 'Failed to complete waiting enrollments for organization=%s.' ' No catalog programs with matching authoring_organization exist.', authorizing_org.short_name) continue enrollment.user = user enrollment.save() for program_course_enrollment in enrollment.program_course_enrollments.all( ): try: program_course_enrollment.enroll(user) except CourseEnrollmentException as e: logger.warning( 'Failed to enroll user=%s with waiting program_course_enrollment=%s: %s', user.id, program_course_enrollment.id, e, ) raise e