def test_search_single(self): org = OrganizationFactory() course = CourseFactory(authoring_organizations=[org]) person1 = PersonFactory(partner=self.partner) person2 = PersonFactory(partner=self.partner) PersonFactory(partner=self.partner) CourseRunFactory(staff=[person1, person2], course=course) facet_name = 'organizations_exact:{org_key}'.format(org_key=org.key) self.reindex_people(person1) self.reindex_people(person2) query = {'selected_facets': facet_name} qs = urllib.parse.urlencode(query) url = '{path}?{qs}'.format(path=self.path, qs=qs) response = self.client.get(url) self.assertEqual(response.status_code, 200) response_data = response.json() self.assertEqual(response_data['objects']['count'], 2) query = {'selected_facets': facet_name, 'q': person1.uuid} qs = urllib.parse.urlencode(query) url = '{path}?{qs}'.format(path=self.path, qs=qs) response = self.client.get(url) self.assertEqual(response.status_code, 200) response_data = response.json() self.assertEqual(response_data['objects']['count'], 1) self.assertEqual(response_data['objects']['results'][0]['uuid'], str(person1.uuid)) self.assertEqual(response_data['objects']['results'][0]['full_name'], person1.full_name)
def test_run_handles_pagination(self): """ Verify that run supports paginated queries. """ course_1 = CourseFactory() for _ in range(5): CourseRunFactory(title='foo', course=course_1) query = DistinctCountsSearchQuery() query.aggregation_key = 'aggregation_key' query.add_filter(SQ(title='foo')) query.add_model(CourseRun) query.run() all_results = query._results assert len(all_results) == 5 query._reset() query.set_limits(low=1, high=3) query.run() paginated_results = query._results assert len(paginated_results) == 2 expected = sorted([run.key for run in all_results[1:3]]) actual = sorted([run.key for run in paginated_results]) assert expected == actual
def test_exclude_unavailable_program_types(self, path, serializer, result_location_keys, program_status, expected_queries): """ Verify that unavailable programs do not show in the program_types representation. """ course_run = CourseRunFactory(course__partner=self.partner, course__title='Software Testing', status=CourseRunStatus.Published) active_program = ProgramFactory(courses=[course_run.course], status=ProgramStatus.Active) ProgramFactory(courses=[course_run.course], status=program_status) self.reindex_courses(active_program) with self.assertNumQueries( expected_queries, threshold=1): # travis sometimes adds a query response = self.get_response('software', path=path) assert response.status_code == 200 response_data = response.data # Validate the search results expected = { 'count': 1, 'results': [ self.serialize_course_run_search(course_run, serializer=serializer) ] } self.assertDictContainsSubset(expected, response_data) # Check that the program is indeed the active one. for key in result_location_keys: response_data = response_data[key] assert response_data == active_program.type.name
def assert_successful_search(self, path=None, serializer=None): """ Asserts the search functionality returns results for a generated query. """ # Generate data that should be indexed and returned by the query course_run = CourseRunFactory(course__partner=self.partner, course__title='Software Testing', status=CourseRunStatus.Published) response = self.get_response('software', path=path) assert response.status_code == 200 response_data = response.data # Validate the search results expected = { 'count': 1, 'results': [ self.serialize_course_run_search(course_run, serializer=serializer) ] } actual = response_data[ 'objects'] if path == self.faceted_path else response_data self.assertDictContainsSubset(expected, actual) return course_run, response_data
def create_program(self, orgs): program = ProgramFactory( authoring_organizations=orgs, type=self.program_type ) curr = CurriculumFactory(program=program) course1_draft = CourseFactory(draft=True) course1 = CourseFactory(draft_version=course1_draft) _run1a = CourseRunFactory(course=course1) _run1b = CourseRunFactory(course=course1) course2 = CourseFactory() _run2a = CourseRunFactory(course=course2) run2b = CourseRunFactory(course=course2) _mem1 = CurriculumCourseMembershipFactory(curriculum=curr, course=course1) mem2 = CurriculumCourseMembershipFactory(curriculum=curr, course=course2) _ex = CurriculumCourseRunExclusionFactory(course_membership=mem2, course_run=run2b) return program
def test_title_synonyms(self): """ Test that synonyms work for terms in the title """ CourseRunFactory(title='HTML', course__partner=self.partner) ProgramFactory(title='HTML', partner=self.partner) response1 = self.process_response({'q': 'HTML5'}) response2 = self.process_response({'q': 'HTML'}) self.assertDictEqual(response1, response2)
def test_from_queryset(self): """ Verify that a DistinctCountsSearchQuerySet can be built from an existing SearchQuerySet.""" course_1 = CourseFactory() CourseRunFactory(title='foo', course=course_1) CourseRunFactory(title='foo', course=course_1) course_2 = CourseFactory() CourseRunFactory(title='foo', course=course_2) CourseRunFactory(title='bar', course=course_2) queryset = DSLFacetedSearch( index=CourseRunDocument._index._name).filter('term', title='foo') dc_queryset = DistinctCountsSearchQuerySet.from_queryset(queryset) expected = sorted([run.key for run in queryset]) actual = sorted([run.key for run in dc_queryset]) assert expected == actual
def test_create_run(self): run = CourseRunFactory() self.api.create_course_run_in_studio(run) expected_data = self.make_studio_data(run) self.assertEqual(self.client.course_runs.post.call_args_list[0][0][0], expected_data)
def test_create_run(self): run = CourseRunFactory() expected_data = self.make_studio_data(run) responses.add(responses.POST, f'{self.studio_url}api/v1/course_runs/', match=[responses.matchers.json_params_matcher(expected_data)]) self.api.create_course_run_in_studio(run)
def test_update_run(self): run = CourseRunFactory() expected_data = self.make_studio_data(run, add_pacing=False, add_schedule=False) responses.add(responses.PATCH, f'{self.studio_url}api/v1/course_runs/{run.key}/', match=[responses.matchers.json_params_matcher(expected_data)]) self.api.update_course_run_details_in_studio(run)
def setUp(self): super().setUp() self.user = UserFactory(is_staff=True, is_superuser=True) self.client.force_authenticate(self.user) self.course = CourseFactory(partner=self.partner, key='simple_key') self.course_run = CourseRunFactory(course=self.course) self.url_base = reverse('api:v1:catalog-query_contains') self.error_message = 'CatalogQueryContains endpoint requires query and identifiers list(s)'
def test_update_course_run_image_in_studio_without_course_image(self): run = CourseRunFactory(course__image=None) with mock.patch( 'course_discovery.apps.api.utils.logger') as mock_logger: self.api.update_course_run_image_in_studio(run) mock_logger.warning.assert_called_with( 'Card image for course run [%d] cannot be updated. The related course [%d] has no image defined.', run.id, run.course.id)
def test_results_only_include_published_objects(self): """ Verify the search results only include items with status set to 'Published'. """ # These items should NOT be in the results CourseRunFactory(course__partner=self.partner, status=CourseRunStatus.Unpublished) ProgramFactory(partner=self.partner, status=ProgramStatus.Unpublished) course_run = CourseRunFactory(course__partner=self.partner, status=CourseRunStatus.Published) program = ProgramFactory(partner=self.partner, status=ProgramStatus.Active) response = self.get_response() assert response.status_code == 200 response_data = response.json() assert response_data['objects']['results'] == \ [self.serialize_program_search(program), self.serialize_course_run_search(course_run)]
def test_course_availability_empty_if_no_published_runs(self): course = AlgoliaProxyCourseFactory(partner=self.__class__.edxPartner) CourseRunFactory( course=course, status=CourseRunStatus.Unpublished, ) assert course.availability_level == []
def test_distinct_count_runs_query_when_cache_is_empty(self): """ Verify that distinct_count runs the query, caches, and returns the distinct_count when cache is empty.""" course_1 = CourseFactory() CourseRunFactory(title='foo', course=course_1) CourseRunFactory(title='foo', course=course_1) course_2 = CourseFactory() CourseRunFactory(title='foo', course=course_2) CourseRunFactory(title='bar', course=course_2) queryset = SearchQuerySet().filter(title='foo').models(CourseRun) dc_queryset = DistinctCountsSearchQuerySet.from_queryset( queryset).with_distinct_counts('aggregation_key') assert dc_queryset._distinct_result_count is None # pylint: disable=protected-access assert dc_queryset.distinct_count() == 2 assert dc_queryset._distinct_result_count == 2 # pylint: disable=protected-access
def test_get_draft(self): extra = CourseRunFactory(course=self.draft.course) with pytest.raises(CourseRun.DoesNotExist): CourseRun.objects.get_draft(hidden=True) with pytest.raises(CourseRun.MultipleObjectsReturned): CourseRun.objects.get_draft(course=extra.course) assert CourseRun.objects.get_draft(uuid=self.draft.uuid) == self.draft
def test_ensure_draft_world_draft_obj_given(self): course_run = CourseRunFactory(draft=True) ensured_draft_course_run = utils.ensure_draft_world(course_run) self.assertEqual(ensured_draft_course_run, course_run) self.assertEqual(ensured_draft_course_run.id, course_run.id) self.assertEqual(ensured_draft_course_run.uuid, course_run.uuid) self.assertEqual(ensured_draft_course_run.draft, course_run.draft)
def test_results_ordered_by_start_date(self, ordering): """ Verify the search results can be ordered by start date """ now = datetime.datetime.now(pytz.UTC) archived = CourseRunFactory(course__partner=self.partner, start=now - datetime.timedelta(weeks=2)) current = CourseRunFactory(course__partner=self.partner, start=now - datetime.timedelta(weeks=1)) starting_soon = CourseRunFactory(course__partner=self.partner, start=now + datetime.timedelta(weeks=3)) upcoming = CourseRunFactory(course__partner=self.partner, start=now + datetime.timedelta(weeks=4)) course_run_keys = [course_run.key for course_run in [archived, current, starting_soon, upcoming]] with self.assertNumQueries(6): response = self.get_response({"ordering": ordering}) assert response.status_code == 200 assert response.data['objects']['count'] == 4 course_runs = CourseRun.objects.filter(key__in=course_run_keys).order_by(ordering) expected = [self.serialize_course_run_search(course_run) for course_run in course_runs] assert response.data['objects']['results'] == expected
def test_marketable_seats_exclusions(self, has_seats): """ Verify that the method excludes CourseRuns without seats. """ course_run = CourseRunFactory() if has_seats: SeatFactory(course_run=course_run) assert CourseRun.objects.marketable().exists() == has_seats
def test_facet_counts_caches_results(self): """ Verify that facet_counts cache results when it is forced to run the query.""" course = CourseFactory() runs = [ CourseRunFactory(title='foo', pacing_type='self_paced', hidden=True, course=course), CourseRunFactory(title='foo', pacing_type='self_paced', hidden=True, course=course), CourseRunFactory(title='foo', pacing_type='instructor_paced', hidden=False, course=course), ] queryset = SearchQuerySet().filter(title='foo').models(CourseRun) queryset = queryset.facet('pacing_type').query_facet( 'hidden', 'hidden:true') dc_queryset = DistinctCountsSearchQuerySet.from_queryset( queryset).with_distinct_counts('aggregation_key') # This should force the query to run and the results to be cached facet_counts = dc_queryset.facet_counts() with mock.patch.object(DistinctCountsSearchQuery, 'run') as mock_run: # Calling facet_counts again shouldn't result in an additional query cached_facet_counts = dc_queryset.facet_counts() assert not mock_run.called assert facet_counts == cached_facet_counts # Calling count shouldn't result in another query, as we should have already cached it with the # first request. count = dc_queryset.count() assert not mock_run.called assert count == len(runs) # Fetching the results shouldn't result in another query, as we should have already cached them # with the initial request. results = dc_queryset[:] assert not mock_run.called expected = {run.key for run in runs} actual = {run.key for run in results} assert expected == actual
def test_availability_faceting(self): """ Verify the endpoint returns availability facets with the results. """ now = datetime.datetime.now(pytz.UTC) archived = CourseRunFactory(course__partner=self.partner, start=now - datetime.timedelta(weeks=2), end=now - datetime.timedelta(weeks=1), status=CourseRunStatus.Published) current = CourseRunFactory(course__partner=self.partner, start=now - datetime.timedelta(weeks=2), end=now + datetime.timedelta(weeks=1), status=CourseRunStatus.Published) starting_soon = CourseRunFactory(course__partner=self.partner, start=now + datetime.timedelta(days=10), end=now + datetime.timedelta(days=90), status=CourseRunStatus.Published) upcoming = CourseRunFactory(course__partner=self.partner, start=now + datetime.timedelta(days=61), end=now + datetime.timedelta(days=90), status=CourseRunStatus.Published) response = self.get_response(path=self.faceted_path) assert response.status_code == 200 response_data = response.json() # Verify all course runs are returned assert response_data['objects']['count'] == 4 for run in [archived, current, starting_soon, upcoming]: serialized = self.serialize_course_run_search(run) # Force execution of lazy function. serialized['availability'] = serialized['availability'].strip() assert serialized in response_data['objects']['results'] self.assert_response_includes_availability_facets(response_data) # Verify the results can be filtered based on availability url = '{path}?page=1&selected_query_facets={facet}'.format( path=self.faceted_path, facet='availability_archived') response = self.client.get(url) assert response.status_code == 200 response_data = response.json() assert response_data['objects']['results'] == [ self.serialize_course_run_search(archived) ]
def test_marketable_exclusions(self): """ Verify the method excludes CourseRuns without a slug. """ course_run = CourseRunFactory() SeatFactory(course_run=course_run) course_run.slug = '' # blank out auto-generated slug course_run.save() self.assertEqual(CourseRun.objects.marketable().exists(), False)
def test_delete_orphans(self): """ Verify the delete_orphans method deletes orphaned instances. """ orphan = VideoFactory() used = CourseRunFactory().video delete_orphans(Video) assert used.__class__.objects.filter(pk=used.pk).exists() assert not orphan.__class__.objects.filter(pk=orphan.pk).exists()
def test_courses(self): """ Verify the endpoint returns the list of courses contained in the catalog. """ url = reverse('api:v1:catalog-courses', kwargs={'id': self.catalog.id}) courses = [self.course] # These courses/course runs should not be returned because they are no longer open for enrollment. enrollment_end = datetime.datetime.now( pytz.UTC) - datetime.timedelta(days=30) CourseRunFactory(enrollment_end=enrollment_end, course__title='ABC Test Course 2') CourseRunFactory(enrollment_end=enrollment_end, course=self.course) with self.assertNumQueries(42): response = self.client.get(url) self.assertEqual(response.status_code, 200) self.assertListEqual( response.data['results'], self.serialize_catalog_course(courses, many=True))
def test_data(self): request = make_request() course_run = CourseRunFactory() SeatFactory(course_run=course_run) serializer_context = {'request': request} serializer = FlattenedCourseRunWithCourseSerializer( course_run, context=serializer_context) expected = self.get_expected_data(request, course_run) self.assertDictEqual(serializer.data, expected)
def test_generate_data_for_studio_api_without_team(self): run = CourseRunFactory() with mock.patch('course_discovery.apps.api.utils.logger.warning') as mock_logger: assert StudioAPI.generate_data_for_studio_api(run, True) == self.make_studio_data(run) mock_logger.assert_called_with( 'No course team admin specified for course [%s]. This may result in a Studio course run ' 'being created without a course team.', run.key.split('/')[1] )
def test_unpublished_and_hidden_courses(self): """ Verify that typeahead does not return unpublished or hidden courses or programs that are not active. """ title = "supply" course_run = CourseRunFactory(title=title, course__partner=self.partner) CourseRunFactory(title=title + "unpublished", status=CourseRunStatus.Unpublished, course__partner=self.partner) CourseRunFactory(title=title + "hidden", hidden=True, course__partner=self.partner) program = ProgramFactory(title=title, status=ProgramStatus.Active, partner=self.partner) ProgramFactory(title=title + "unpublished", status=ProgramStatus.Unpublished, partner=self.partner) query = "suppl" response = self.get_response({'q': query}) self.assertEqual(response.status_code, 200) response_data = response.json() expected_response_data = { 'course_runs': [self.serialize_course_run_search(course_run)], 'programs': [self.serialize_program_search(program)] } self.assertDictEqual(response_data, expected_response_data)
def test_course_ordering(self): """ Verify that courses in a program are ordered by ascending run start date, with ties broken by earliest run enrollment start date. """ request = make_request() course_list = CourseFactory.create_batch(3) # Create a course run with arbitrary start and empty enrollment_start. CourseRunFactory( course=course_list[2], enrollment_start=None, start=datetime(2014, 2, 1), ) # Create a second run with matching start, but later enrollment_start. CourseRunFactory( course=course_list[1], enrollment_start=datetime(2014, 1, 2), start=datetime(2014, 2, 1), ) # Create a third run with later start and enrollment_start. CourseRunFactory( course=course_list[0], enrollment_start=datetime(2014, 2, 1), start=datetime(2014, 3, 1), ) program = ProgramFactory(courses=course_list) serializer = self.serializer_class(program, context={'request': request}) expected = MinimalProgramCourseSerializer( # The expected ordering is the reverse of course_list. course_list[::-1], many=True, context={ 'request': request, 'program': program, 'course_runs': list(program.course_runs) }).data self.assertEqual(serializer.data['courses'], expected)
def test_courses(self): """ Verify the endpoint returns the list of available courses contained in the catalog, and that courses appearing in the response always have at least one serialized run. """ url = reverse('api:v1:catalog-courses', kwargs={'id': self.catalog.id}) for state in self.states(): Course.objects.all().delete() course_run = CourseRunFactory(course__title='ABC Test Course') for function in state: function(course_run) course_run.save() if state in self.available_states: course = course_run.course # This run has no seats, but we still expect its parent course # to be included. filtered_course_run = CourseRunFactory(course=course) with self.assertNumQueries(19): response = self.client.get(url) assert response.status_code == 200 # Emulate prefetching behavior. filtered_course_run.delete() assert response.data[ 'results'] == self.serialize_catalog_course([course], many=True) # Any course appearing in the response must have at least one serialized run. assert len(response.data['results'][0]['course_runs']) > 0 else: with self.assertNumQueries(3): response = self.client.get(url) assert response.status_code == 200 assert response.data['results'] == []
def test_marketable_course_runs_only(self, marketable_course_runs_only): """ Verify that a client requesting marketable_course_runs_only only receives course runs that are published, have seats, and can still be enrolled in. """ # Published course run with a seat, no enrollment start or end, and an end date in the future. enrollable_course_run = CourseRunFactory( status=CourseRunStatus.Published, end=datetime.datetime.now(pytz.UTC) + datetime.timedelta(days=10), enrollment_start=None, enrollment_end=None, course=self.course) SeatFactory(course_run=enrollable_course_run) # Unpublished course run with a seat. unpublished_course_run = CourseRunFactory( status=CourseRunStatus.Unpublished, course=self.course) SeatFactory(course_run=unpublished_course_run) # Published course run with no seats. no_seats_course_run = CourseRunFactory( status=CourseRunStatus.Published, course=self.course) # Published course run with a seat and an end date in the past. closed_course_run = CourseRunFactory( status=CourseRunStatus.Published, end=datetime.datetime.now(pytz.UTC) - datetime.timedelta(days=10), course=self.course) SeatFactory(course_run=closed_course_run) url = reverse('api:v1:course-detail', kwargs={'key': self.course.key}) url = '{}?marketable_course_runs_only={}'.format( url, marketable_course_runs_only) response = self.client.get(url) assert response.status_code == 200 if marketable_course_runs_only: # Emulate prefetching behavior. for course_run in (unpublished_course_run, no_seats_course_run, closed_course_run): course_run.delete() assert response.data == self.serialize_course(self.course)