def test_program_completion_with_no_id_professional( self, mock_get_certificates_for_user, mock_get_programs): """ Verify that 'no-id-professional' certificates are treated as if they were 'professional' certificates when determining program completion. """ # Create serialized course runs like the ones we expect to receive from # the discovery service's API. These runs are of type 'professional'. course_runs = CourseRunFactory.create_batch(2, type='professional') program = ProgramFactory( courses=[CourseFactory(course_runs=course_runs)]) mock_get_programs.return_value = [program] # Verify that the test program is not complete. meter = ProgramProgressMeter(self.site, self.user) self.assertEqual(meter.completed_programs, []) # Grant a 'no-id-professional' certificate for one of the course runs, # thereby completing the program. mock_get_certificates_for_user.return_value = [ self._make_certificate_result(status='downloadable', type='no-id-professional', course_key=course_runs[0]['key']) ] # Verify that the program is complete. meter = ProgramProgressMeter(self.site, self.user) self.assertEqual(meter.completed_programs, [program['uuid']])
def test_completed_programs(self, mock_completed_course_runs, mock_get_programs): """Verify that completed programs are correctly identified.""" data = ProgramFactory.create_batch(3) mock_get_programs.return_value = data program_uuids = [] course_run_keys = [] for program in data: program_uuids.append(program['uuid']) for course in program['courses']: for course_run in course['course_runs']: course_run_keys.append(course_run['key']) # Verify that no programs are complete. meter = ProgramProgressMeter(self.user) self.assertEqual(meter.completed_programs, []) # Complete all programs. self._create_enrollments(*course_run_keys) mock_completed_course_runs.return_value = [ {'course_run_id': course_run_key, 'type': MODES.verified} for course_run_key in course_run_keys ] # Verify that all programs are complete. meter = ProgramProgressMeter(self.user) self.assertEqual(meter.completed_programs, program_uuids)
def test_completed_programs_no_id_professional(self, mock_completed_course_runs, mock_get_programs): """ Verify the method treats no-id-professional enrollments as professional enrollments. """ course_runs = CourseRunFactory.create_batch(2, type='no-id-professional') program = ProgramFactory( courses=[CourseFactory(course_runs=course_runs)]) mock_get_programs.return_value = [program] # Verify that no programs are complete. meter = ProgramProgressMeter(self.user) self.assertEqual(meter.completed_programs, []) # Complete all programs. for course_run in course_runs: CourseEnrollmentFactory(user=self.user, course_id=course_run['key'], mode='no-id-professional') mock_completed_course_runs.return_value = [{ 'course_run_id': course_run['key'], 'type': MODES.professional } for course_run in course_runs] # Verify that all programs are complete. meter = ProgramProgressMeter(self.user) self.assertEqual(meter.completed_programs, [program['uuid']])
def handle(self, *args, **options): """ Command's entry point. """ should_commit = not options['no_commit'] email_sent_records = [] site = Site.objects.get_current() course_to_users_maps = self.get_passed_course_to_users_maps() for completed_course_id, users in course_to_users_maps.items(): course_linked_programs = get_programs(course=completed_course_id) course_linked_programs = self.sort_programs(course_linked_programs) if course_linked_programs: for user in users: meter = ProgramProgressMeter(site=site, user=user, include_course_entitlements=False) programs_progress = meter.progress(programs=course_linked_programs, count_only=False) suggested_program_progress, suggested_course_run = self.get_course_run_to_suggest( programs_progress, completed_course_id ) if suggested_course_run: suggested_program = self.get_program(course_linked_programs, suggested_program_progress) completed_course_run = self.get_course_run(suggested_program, completed_course_id) if should_commit: self.emit_event(user, suggested_program, suggested_course_run, completed_course_run) email_sent_records.append( f'User: {user.username}, Completed Course: {completed_course_id}, ' f'Suggested Course: {suggested_course_run["key"]}' ) LOGGER.info( '[Program Course Nudge Email] %s Emails sent. Records: %s', len(email_sent_records), email_sent_records, )
def test_completed_course_runs(self, mock_get_certificates_for_user, _mock_get_programs): """ Verify that the method can find course run certificates when not mocked out. """ mock_get_certificates_for_user.return_value = [ self._make_certificate_result(status='downloadable', type='verified', course_key='downloadable-course'), self._make_certificate_result(status='generating', type='honor', course_key='generating-course'), self._make_certificate_result(status='unknown', course_key='unknown-course'), ] meter = ProgramProgressMeter(self.site, self.user) self.assertEqual(meter.completed_course_runs, [ { 'course_run_id': 'downloadable-course', 'type': 'verified' }, { 'course_run_id': 'generating-course', 'type': 'honor' }, ]) mock_get_certificates_for_user.assert_called_with(self.user.username)
def render_to_fragment(self, request, **kwargs): """ Render the program listing fragment. """ user = request.user try: mobile_only = json.loads(request.GET.get('mobile_only', 'false')) except ValueError: mobile_only = False programs_config = kwargs.get( 'programs_config') or ProgramsApiConfig.current() if not programs_config.enabled or not user.is_authenticated: raise Http404 meter = ProgramProgressMeter(request.site, user, mobile_only=mobile_only) context = { 'marketing_url': get_program_marketing_url(programs_config, mobile_only), 'programs': meter.engaged_programs, 'progress': meter.progress() } html = render_to_string('learner_dashboard/programs_fragment.html', context) programs_fragment = Fragment(html) self.add_fragment_resource_urls(programs_fragment) return programs_fragment
def program_listing(request): """View a list of programs in which the user is engaged.""" programs_config = ProgramsApiConfig.current() if not programs_config.show_program_listing: raise Http404 meter = ProgramProgressMeter(request.user) engaged_programs = [ munge_catalog_program(program) for program in meter.engaged_programs ] progress = [ munge_progress_map(progress_map) for progress_map in meter.progress ] context = { 'credentials': get_programs_credentials(request.user), 'disable_courseware_js': True, 'marketing_url': get_program_marketing_url(programs_config), 'nav_hidden': True, 'programs': engaged_programs, 'progress': progress, 'show_program_listing': programs_config.show_program_listing, 'uses_pattern_library': True, } return render_to_response('learner_dashboard/programs.html', context)
def test_course_progress(self, mock_get_programs): """ Verify that the progress meter can represent progress in terms of serialized courses. """ course_run_key = generate_course_run_key() data = [ ProgramFactory( courses=[ CourseFactory(course_runs=[ CourseRunFactory(key=course_run_key), ]), ] ) ] mock_get_programs.return_value = data self._create_enrollments(course_run_key) meter = ProgramProgressMeter(self.user) program = data[0] expected = [ ProgressFactory( uuid=program['uuid'], completed=[], in_progress=[program['courses'][0]], not_started=[] ) ] self.assertEqual(meter.progress(count_only=False), expected)
def test_course_grade_results(self, mock_get_programs): grade_percent = .8 with mock_passing_grade(percent=grade_percent): course_run_key = generate_course_run_key() data = [ ProgramFactory( courses=[ CourseFactory(course_runs=[ CourseRunFactory(key=course_run_key), ]), ] ) ] mock_get_programs.return_value = data self._create_enrollments(course_run_key) meter = ProgramProgressMeter(self.site, self.user) program = data[0] expected = [ ProgressFactory( uuid=program['uuid'], completed=[], in_progress=[program['courses'][0]], not_started=[], grades={course_run_key: grade_percent}, ) ] self.assertEqual(meter.progress(count_only=False), expected)
def test_single_program_engagement(self, mock_get_programs): """ Verify that correct program is returned when the user is enrolled in a course run appearing in one program. """ course_run_key = generate_course_run_key() data = [ ProgramFactory( courses=[ CourseFactory(course_runs=[ CourseRunFactory(key=course_run_key), ]), ] ), ProgramFactory(), ] mock_get_programs.return_value = data self._create_enrollments(course_run_key) meter = ProgramProgressMeter(self.user) self._attach_detail_url(data) program = data[0] self.assertEqual(meter.engaged_programs, [program]) self._assert_progress( meter, ProgressFactory(uuid=program['uuid'], in_progress=1) ) self.assertEqual(meter.completed_programs, [])
def test_no_id_professional_in_progress(self, mock_get_programs): """ Verify that the progress meter treats no-id-professional enrollments as professional. """ course_run_key = generate_course_run_key() data = [ ProgramFactory( courses=[ CourseFactory(course_runs=[ CourseRunFactory(key=course_run_key, type=CourseMode.PROFESSIONAL), ]), ] ) ] mock_get_programs.return_value = data CourseEnrollmentFactory( user=self.user, course_id=course_run_key, mode=CourseMode.NO_ID_PROFESSIONAL_MODE ) meter = ProgramProgressMeter(self.user) program = data[0] expected = [ ProgressFactory( uuid=program['uuid'], completed=[], in_progress=[program['courses'][0]], not_started=[] ) ] self.assertEqual(meter.progress(count_only=False), expected)
def view_programs(request): """View programs in which the user is engaged.""" show_program_listing = ProgramsApiConfig.current().show_program_listing if not show_program_listing: raise Http404 enrollments = list(get_course_enrollments(request.user, None, [])) meter = ProgramProgressMeter(request.user, enrollments) programs = meter.engaged_programs # TODO: Pull 'xseries' string from configuration model. marketing_root = urljoin(settings.MKTG_URLS.get('ROOT'), 'xseries').strip('/') for program in programs: program['display_category'] = get_display_category(program) program['marketing_url'] = '{root}/{slug}'.format( root=marketing_root, slug=program['marketing_slug'] ) context = { 'programs': programs, 'progress': meter.progress, 'xseries_url': marketing_root if ProgramsApiConfig.current().show_xseries_ad else None, 'nav_hidden': True, 'show_program_listing': show_program_listing, 'credentials': get_programs_credentials(request.user, category='xseries'), 'disable_courseware_js': True, 'uses_pattern_library': True } return render_to_response('learner_dashboard/programs.html', context)
def test_nonverified_course_run_completion(self, mock_completed_course_runs, mock_get_programs): """ Course runs aren't necessarily of type verified. Verify that a program can still be completed when this is the case. """ course_run_key = generate_course_run_key() data = [ ProgramFactory( courses=[ CourseFactory(course_runs=[ CourseRunFactory(key=course_run_key, type='honor'), CourseRunFactory(), ]), ] ), ProgramFactory(), ] mock_get_programs.return_value = data self._create_enrollments(course_run_key) mock_completed_course_runs.return_value = [ {'course_run_id': course_run_key, 'type': MODES.honor}, ] meter = ProgramProgressMeter(self.user) program, program_uuid = data[0], data[0]['uuid'] self._assert_progress( meter, ProgressFactory(uuid=program_uuid, completed=1) ) self.assertEqual(meter.completed_programs, [program_uuid])
def test_mutiple_program_engagement(self, mock_get_programs): """ Verify that correct programs are returned in the correct order when the user is enrolled in course runs appearing in programs. """ newer_course_run_key, older_course_run_key = ( generate_course_run_key() for __ in range(2)) data = [ ProgramFactory(courses=[ CourseFactory(course_runs=[ CourseRunFactory(key=newer_course_run_key), ]), ]), ProgramFactory(courses=[ CourseFactory(course_runs=[ CourseRunFactory(key=older_course_run_key), ]), ]), ProgramFactory(), ] mock_get_programs.return_value = data # The creation time of the enrollments matters to the test. We want # the first_course_run_key to represent the newest enrollment. self._create_enrollments(older_course_run_key, newer_course_run_key) meter = ProgramProgressMeter(self.user) self._attach_detail_url(data) programs = data[:2] self.assertEqual(meter.engaged_programs, programs) self._assert_progress( meter, *(ProgressFactory(uuid=program['uuid'], in_progress=1) for program in programs)) self.assertEqual(meter.completed_programs, [])
def test_completed_course_runs(self, mock_get_certificates_for_user, _mock_get_programs): """ Verify that the method can find course run certificates when not mocked out. """ def make_certificate_result(**kwargs): """Helper to create dummy results from the certificates API.""" result = { 'username': '******', 'course_key': 'dummy-course', 'type': 'dummy-type', 'status': 'dummy-status', 'download_url': 'http://www.example.com/cert.pdf', 'grade': '0.98', 'created': '2015-07-31T00:00:00Z', 'modified': '2015-07-31T00:00:00Z', } result.update(**kwargs) return result mock_get_certificates_for_user.return_value = [ make_certificate_result(status='downloadable', type='verified', course_key='downloadable-course'), make_certificate_result(status='generating', type='honor', course_key='generating-course'), make_certificate_result(status='unknown', course_key='unknown-course'), ] meter = ProgramProgressMeter(self.user) self.assertEqual( meter.completed_course_runs, [ {'course_run_id': 'downloadable-course', 'type': 'verified'}, {'course_run_id': 'generating-course', 'type': 'honor'}, ] ) mock_get_certificates_for_user.assert_called_with(self.user.username)
def related_programs(self): """ Returns related program data if the effective_user is enrolled. Note: related programs can be None depending on the course. """ if self.effective_user.is_anonymous: return meter = ProgramProgressMeter(self.request.site, self.effective_user) inverted_programs = meter.invert_programs() related_programs = inverted_programs.get(str(self.course_key)) if related_programs is None: return related_progress = meter.progress(programs=related_programs) progress_by_program = { progress['uuid']: progress for progress in related_progress } programs = [{ 'progress': progress_by_program[program['uuid']], 'title': program['title'], 'slug': program['type_attrs']['slug'], 'url': program['detail_url'], 'uuid': program['uuid'] } for program in related_programs] return programs
def test_empty_programs(self, mock_get_programs): """Verify that programs with no courses do not count as completed.""" program = ProgramFactory() program['courses'] = [] meter = ProgramProgressMeter(self.site, self.user) program_complete = meter._is_program_complete(program) self.assertFalse(program_complete)
def render_to_fragment(self, request, program_uuid, **kwargs): """View details about a specific program.""" programs_config = kwargs.get( 'programs_config') or ProgramsApiConfig.current() if not programs_config.enabled or not request.user.is_authenticated(): raise Http404 meter = ProgramProgressMeter(request.site, request.user, uuid=program_uuid) program_data = meter.programs[0] if not program_data: raise Http404 try: mobile_only = json.loads(request.GET.get('mobile_only', 'false')) except ValueError: mobile_only = False program_data = ProgramDataExtender(program_data, request.user, mobile_only=mobile_only).extend() course_data = meter.progress(programs=[program_data], count_only=False)[0] certificate_data = get_certificates(request.user, program_data) program_data.pop('courses') skus = program_data.get('skus') ecommerce_service = EcommerceService() 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'), 'buy_button_url': ecommerce_service.get_checkout_page_url(*skus) } context = { 'urls': urls, 'user_preferences': get_user_preferences(request.user), 'program_data': program_data, 'course_data': course_data, 'certificate_data': certificate_data } html = render_to_string( 'learner_dashboard/program_details_fragment.html', context) program_details_fragment = Fragment(html) self.add_fragment_resource_urls(program_details_fragment) return program_details_fragment
def test_no_enrollments(self, mock_get_programs): """Verify behavior when programs exist, but no relevant enrollments do.""" data = [ProgramFactory()] mock_get_programs.return_value = data meter = ProgramProgressMeter(self.user) self.assertEqual(meter.engaged_programs, []) self._assert_progress(meter) self.assertEqual(meter.completed_programs, [])
def render_to_fragment(self, request, program_uuid, **kwargs): """View details about a specific program.""" programs_config = kwargs.get('programs_config') or ProgramsApiConfig.current() if not programs_config.enabled or not request.user.is_authenticated: raise Http404 meter = ProgramProgressMeter(request.site, request.user, uuid=program_uuid) program_data = meter.programs[0] if not program_data: raise Http404 try: mobile_only = json.loads(request.GET.get('mobile_only', 'false')) except ValueError: mobile_only = False program_data = ProgramDataExtender(program_data, request.user, mobile_only=mobile_only).extend() course_data = meter.progress(programs=[program_data], count_only=False)[0] certificate_data = get_certificates(request.user, program_data) program_data.pop('courses') skus = program_data.get('skus') ecommerce_service = EcommerceService() # TODO: Don't have business logic of course-certificate==record-available here in LMS. # Eventually, the UI should ask Credentials if there is a record available and get a URL from it. # But this is here for now so that we can gate this URL behind both this business logic and # a waffle flag. This feature is in active developoment. program_record_url = get_credentials_records_url(program_uuid=program_uuid) if not certificate_data: program_record_url = None 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'), 'buy_button_url': ecommerce_service.get_checkout_page_url(*skus), 'program_record_url': program_record_url, } context = { 'urls': urls, 'user_preferences': get_user_preferences(request.user), 'program_data': program_data, 'course_data': course_data, 'certificate_data': certificate_data } html = render_to_string('learner_dashboard/program_details_fragment.html', context) program_details_fragment = Fragment(html) self.add_fragment_resource_urls(program_details_fragment) return program_details_fragment
def program_details(request, program_uuid): """View details about a specific program.""" programs_config = ProgramsApiConfig.current() if not programs_config.enabled: raise Http404 meter = ProgramProgressMeter(request.user, uuid=program_uuid) program_data = meter.programs[0] 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 = { '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) } if waffle.switch_is_active('new_program_progress'): course_data = meter.progress(programs=[program_data], count_only=False)[0] certificate_data = get_certificates(request.user, program_data) program_data.pop('courses') context.update({ 'program_data': program_data, 'course_data': course_data, 'certificate_data': certificate_data, }) return render_to_response( 'learner_dashboard/program_details_2017.html', context) else: context.update({'program_data': program_data}) return render_to_response('learner_dashboard/program_details.html', context)
def test_no_programs(self, mock_get_programs): """Verify behavior when enrollments exist, but no matching programs do.""" mock_get_programs.return_value = [] course_run_id = generate_course_run_key() self._create_enrollments(course_run_id) meter = ProgramProgressMeter(self.user) self.assertEqual(meter.engaged_programs, []) self._assert_progress(meter) self.assertEqual(meter.completed_programs, [])
def get_completed_programs(student): """ Given a set of completed courses, determine which programs are completed. Args: student (User): Representing the student whose completed programs to check for. Returns: list of program UUIDs """ meter = ProgramProgressMeter(student) return meter.completed_programs
def get_completed_programs(site, student): """ Given a set of completed courses, determine which programs are completed. Args: site (Site): Site for which data should be retrieved. student (User): Representing the student whose completed programs to check for. Returns: dict of {program_UUIDs: visible_dates} """ meter = ProgramProgressMeter(site, student) return meter.completed_programs_with_available_dates
def get_completed_programs(site, student): """ Given a set of completed courses, determine which programs are completed. Args: site (Site): Site for which data should be retrieved. student (User): Representing the student whose completed programs to check for. Returns: list of program UUIDs """ meter = ProgramProgressMeter(site, student) return meter.completed_programs
def test_shared_enrollment_engagement(self, mock_get_programs): """ Verify that correct programs are returned when the user is enrolled in a single course run appearing in multiple programs. """ shared_course_run_key, solo_course_run_key = (generate_course_run_key() for __ in range(2)) batch = [ ProgramFactory( courses=[ CourseFactory(course_runs=[ CourseRunFactory(key=shared_course_run_key), ]), ] ) for __ in range(2) ] joint_programs = sorted(batch, key=lambda program: program['title']) data = joint_programs + [ ProgramFactory( courses=[ CourseFactory(course_runs=[ CourseRunFactory(key=solo_course_run_key), ]), ] ), ProgramFactory(), ] mock_get_programs.return_value = data # Enrollment for the shared course run created last (most recently). self._create_enrollments(solo_course_run_key, shared_course_run_key) meter = ProgramProgressMeter(self.site, self.user) self._attach_detail_url(data) programs = data[:3] self.assertEqual(meter.engaged_programs, programs) grades = { solo_course_run_key: 0.0, shared_course_run_key: 0.0, } self._assert_progress( meter, *(ProgressFactory(uuid=program['uuid'], in_progress=1, grades=grades) for program in programs) ) self.assertEqual(meter.completed_programs, [])
def test_credit_course_counted_complete_for_verified(self, mock_completed_course_runs, mock_get_programs): """ Verify that 'credit' course certificate type are treated as if they were "verified" when checking for course completion status. """ course_run_key = generate_course_run_key() course = CourseFactory(course_runs=[ CourseRunFactory(key=course_run_key, type='credit'), ]) program = ProgramFactory(courses=[course]) mock_get_programs.return_value = [program] self._create_enrollments(course_run_key) meter = ProgramProgressMeter(self.user) mock_completed_course_runs.return_value = [{'course_run_id': course_run_key, 'type': 'verified'}] self.assertEqual(meter._is_course_complete(course), True)
def program_details(request, program_uuid): """View details about a specific program.""" programs_config = ProgramsApiConfig.current() if not programs_config.enabled: raise Http404 meter = ProgramProgressMeter(request.site, request.user, uuid=program_uuid) program_data = meter.programs[0] if not program_data: raise Http404 program_data = ProgramDataExtender(program_data, request.user).extend() course_data = meter.progress(programs=[program_data], count_only=False)[0] certificate_data = get_certificates(request.user, program_data) program_data.pop('courses') skus = program_data.get('skus') ecommerce_service = EcommerceService() 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'), 'buy_button_url': ecommerce_service.get_checkout_page_url(*skus) } context = { 'urls': urls, 'show_program_listing': programs_config.enabled, 'show_dashboard_tabs': True, 'nav_hidden': True, 'disable_courseware_js': True, 'uses_pattern_library': True, 'user_preferences': get_user_preferences(request.user), 'program_data': program_data, 'course_data': course_data, 'certificate_data': certificate_data } return render_to_response('learner_dashboard/program_details.html', context)
def get_inverted_programs(student): """ Get programs keyed by course run ID. Args: student (User): Representing the student whose programs to check for. Returns: dict, programs keyed by course run ID """ inverted_programs = {} for site in Site.objects.all(): meter = ProgramProgressMeter(site, student) inverted_programs.update(meter.invert_programs()) return inverted_programs
def program_listing(request): """View a list of programs in which the user is engaged.""" programs_config = ProgramsApiConfig.current() if not programs_config.enabled: raise Http404 meter = ProgramProgressMeter(request.user) context = { 'disable_courseware_js': True, 'marketing_url': get_program_marketing_url(programs_config), 'nav_hidden': True, 'programs': meter.engaged_programs, 'progress': meter.progress(), 'show_program_listing': programs_config.enabled, 'uses_pattern_library': True, } return render_to_response('learner_dashboard/programs.html', context)