def test_models_course_state_future_enrollment_open(self): """ Confirm course state when there is a future course run open for enrollment. """ course = CourseFactory() course_run = self.create_run_future_open(course) with self.assertNumQueries(3): state = course.state expected_state = CourseState(1, course_run.start) self.assertEqual(state, expected_state) # Adding course runs of lower priority states should not change the result and require # only 1 additional database query self.create_run_ongoing_closed(course) self.create_run_future_closed(course) with self.assertNumQueries(1): state = course.state self.assertEqual(state, expected_state)
def test_indexers_courses_get_es_documents_no_enrollment_end(self): """ Course runs with no end of enrollment date should get their end date as date of end of enrollment. """ course = CourseFactory(should_publish=True) course_run = CourseRunFactory(page_parent=course.extended_object, enrollment_end=None, should_publish=True) indexed_courses = list( CoursesIndexer.get_es_documents(index="some_index", action="some_action")) self.assertEqual(len(indexed_courses), 1) self.assertEqual(len(indexed_courses[0]["course_runs"]), 1) self.assertEqual( indexed_courses[0]["course_runs"][0]["enrollment_end"], course_run.end)
def test_models_course_state_future_enrollment_closed(self): """ Confirm course state when there is a future course run but closed for enrollment. """ course = CourseFactory() self.create_run_future_closed(course) with self.assertNumQueries(3): state = course.state expected_state = CourseState(4) self.assertEqual(state, expected_state) # Adding an on-going but closed course run should not change the result and require # only 1 additional database query self.create_run_ongoing_closed(course) with self.assertNumQueries(1): state = course.state self.assertEqual(state, expected_state)
def test_models_course_run_delete_draft(self): """ Deleting a draft course run that is not published should delete all its related translations. """ course = CourseFactory(page_languages=["en", "fr"]) course_run = CourseRunFactory(direct_course=course) CourseRunTranslation.objects.create( master=course_run, language_code="fr", title="mon titre" ) self.assertEqual(CourseRun.objects.count(), 1) self.assertEqual(CourseRunTranslation.objects.count(), 2) course_run.delete() self.assertFalse(CourseRun.objects.exists()) self.assertFalse(CourseRunTranslation.objects.exists())
def test_models_course_state_future_enrollment_open(self): """ Confirm course state when there is a future course run open for enrollment. """ course = CourseFactory() course_run = self.create_run_future_open(course) with self.assertNumQueries(2): state = course.state expected_state = CourseState(1, "enroll now", "starting on", course_run.start) self.assertEqual(state, expected_state) # Adding courses in less priorietary states should not change the result self.create_run_ongoing_closed(course) self.create_run_future_closed(course) with self.assertNumQueries(2): state = course.state self.assertEqual(state, expected_state)
def test_models_course_state_future_enrollment_not_yet_open(self): """ Confirm course state when there is a future course run but not yet open for enrollment. """ course = CourseFactory() course_run = self.create_run_future_not_yet_open(course) with self.assertNumQueries(2): state = course.state expected_state = CourseState(2, "see details", "starting on", course_run.start) self.assertEqual(state, expected_state) # Adding an on-going but closed course run should not change the result self.create_run_ongoing_closed(course) with self.assertNumQueries(2): state = course.state self.assertEqual(state, expected_state)
def test_admin_course_list_view(self): """ The admin list view of courses should display the title of the related page. """ user = UserFactory(is_staff=True, is_superuser=True) self.client.login(username=user.username, password="******") # Create a course linked to a page course = CourseFactory() # Get the admin list view url = reverse("admin:courses_course_changelist") response = self.client.get(url, follow=True) # Check that the page includes the title field self.assertContains(response, course.extended_object.get_title(), status_code=200)
def test_api_course_run_sync_create_partial_not_required(self, mock_signal): """ If the submitted data is not related to an existing course run and some optional fields are missing, it should create the course run. """ course = CourseFactory(code="DemoX") Title.objects.update(publisher_state=PUBLISHER_STATE_DEFAULT) data = { "resource_link": "http://example.edx:8073/courses/course-v1:edX+DemoX+01/course/", "enrollment_end": "2020-12-24T09:31:59.417972Z", "languages": ["en", "fr"], } self.assertEqual( course.extended_object.title_set.first().publisher_state, PUBLISHER_STATE_DEFAULT, ) mock_signal.reset_mock() response = self.client.post( "/api/v1.0/course-runs-sync", data, content_type="application/json", HTTP_AUTHORIZATION=( "SIG-HMAC-SHA256 313cefea7a14f26ed7dc249719bc5a86bce36b0c63a9d27b2e30e3a616e108d6" ), ) self.assertEqual(response.status_code, 200) self.assertEqual(json.loads(response.content), {"success": True}) self.assertEqual(CourseRun.objects.count(), 1) # Check the new draft course run draft_course_run = CourseRun.objects.get(direct_course=course) draft_serializer = SyncCourseRunSerializer(instance=draft_course_run) data.update({"start": None, "end": None, "enrollment_start": None}) self.assertEqual(draft_serializer.data, data) # The page is not marked dirty because the course run is to be scheduled self.assertEqual( course.extended_object.title_set.first().publisher_state, PUBLISHER_STATE_DEFAULT, ) self.assertFalse(mock_signal.called)
def test_templates_course_detail_placeholder(self): """ Draft editing course page should contain all key placeholders when empty. """ user = UserFactory(is_staff=True, is_superuser=True) self.client.login(username=user.username, password="******") course = CourseFactory(page_title="Very interesting course") page = course.extended_object url = "{:s}?edit".format(page.get_absolute_url(language="en")) response = self.client.get(url) pattern = ( r'<div class="course-detail__content__row course-detail__content__introduction">' r'<div class="cms-placeholder') self.assertIsNotNone(re.search(pattern, str(response.content))) pattern = ( r'<div class="course-detail__content__row course-detail__content__categories">' r'<div class="cms-placeholder') self.assertIsNotNone(re.search(pattern, str(response.content))) pattern = ( r'<div class="course-detail__content__row course-detail__content__teaser">' r'<div class="cms-placeholder') self.assertIsNotNone(re.search(pattern, str(response.content))) pattern = ( r'<h2 class="course-detail__content__row__title">About the course</h2>' r'<div class="cms-placeholder') self.assertIsNotNone(re.search(pattern, str(response.content))) pattern = ( r'<div class="section__items course-detail__content__organizations__items">' r'<div class="cms-placeholder') self.assertIsNotNone(re.search(pattern, str(response.content))) pattern = ( r'<div class="section__items course-detail__content__team__items">' r'<div class="cms-placeholder') self.assertIsNotNone(re.search(pattern, str(response.content))) pattern = ( r'<div class="course-detail__content__row course-detail__content__information">' r'<div class="cms-placeholder') self.assertIsNotNone(re.search(pattern, str(response.content))) pattern = ( r'<h3 class="course-detail__content__license__item__title">' r'License for the course content</h3><div class="cms-placeholder') self.assertIsNotNone(re.search(pattern, str(response.content)))
def test_api_course_run_sync_create_sync_to_draft(self, mock_signal): """ If the submitted data is not related to an existing course run, a new course run should be created. In "sync_to_draft" mode, the synchronization should be limited to the draft course run, even if the related course is published. """ course = CourseFactory(code="DemoX", should_publish=True) data = { "resource_link": "http://example.edx:8073/courses/course-v1:edX+DemoX+01/course/", "start": "2020-12-09T09:31:59.417817Z", "end": "2021-03-14T09:31:59.417895Z", "enrollment_start": "2020-11-09T09:31:59.417936Z", "enrollment_end": "2020-12-24T09:31:59.417972Z", "languages": ["en", "fr"], } self.assertEqual( course.extended_object.title_set.first().publisher_state, PUBLISHER_STATE_DEFAULT, ) mock_signal.reset_mock() response = self.client.post( "/api/v1.0/course-runs-sync", data, content_type="application/json", HTTP_AUTHORIZATION=( "SIG-HMAC-SHA256 338f7c262254e8220fea54467526f8f1f4562ee3adf1e3a71abaf23a20b739e4" ), ) self.assertEqual(response.status_code, 200) self.assertEqual(json.loads(response.content), {"success": True}) self.assertEqual(CourseRun.objects.count(), 1) # Check the new draft course run draft_course_run = CourseRun.objects.get(direct_course=course) draft_serializer = SyncCourseRunSerializer(instance=draft_course_run) self.assertEqual(draft_serializer.data, data) self.assertEqual( course.extended_object.title_set.first().publisher_state, PUBLISHER_STATE_DIRTY, ) self.assertFalse(mock_signal.called)
def test_cms_wizards_course_run_submit_form_success(self, mock_snapshot): """ A user with the required permissions submitting a valid CourseRunWizardForm should be able to create a course run and its related page. """ course = CourseFactory() # Create a user with just the required permissions user = UserFactory( is_staff=True, permissions=[ "courses.add_courserun", "cms.add_page", "cms.change_page" ], ) # Submit a valid form form = CourseRunWizardForm( data={"title": "My title"}, wizard_language="en", wizard_user=user, wizard_page=course.extended_object, ) self.assertTrue(form.is_valid()) page = form.save() course_run = page.courserun # The course run and its related page should have been created as draft Page.objects.drafts().get(id=page.id) CourseRun.objects.get(id=course_run.id, extended_object=page) self.assertEqual(page.get_title(), "My title") # The slug should have been automatically set self.assertEqual(page.get_slug(), "my-title") # The course run should be a child of the course page self.assertEqual(course_run.extended_object.parent_page, course.extended_object) # The languages field should have been set self.assertEqual(course_run.languages, ["en"]) # Snapshot was not request and should not have been triggered self.assertFalse(mock_snapshot.called)
def test_models_course_copy_relations_publish(self): """ When publishing a draft course, the draft course run should be copied to a newly created course run with its parler translations. In a second part of the test, we check that when publishing a course that was already published, the draft course run should be copied to the existing public course run with its parler translations. """ # 1- Publishing a draft course course = CourseFactory(page_title="my course title") TitleFactory(page=course.extended_object, language="fr", title="mon titre de cours") course_run = CourseRunFactory(direct_course=course, title="my run") CourseRunTranslation.objects.create(master=course_run, language_code="fr", title="ma session") self.assertEqual(Course.objects.count(), 1) self.assertEqual(CourseRun.objects.count(), 1) self.assertEqual(CourseRunTranslation.objects.count(), 2) self.assertEqual(Title.objects.count(), 2) self.assertTrue(course.extended_object.publish("fr")) self.assertEqual(Course.objects.count(), 2) self.assertEqual(CourseRun.objects.count(), 2) self.assertEqual(CourseRunTranslation.objects.count(), 3) self.assertEqual(Title.objects.count(), 3) # 2- Publishing a course that was already published self.assertTrue(course.extended_object.publish("en")) self.assertEqual(CourseRunTranslation.objects.count(), 4) self.assertEqual(Title.objects.count(), 4) course_run.refresh_from_db() public_course_run = course_run.public_course_run self.assertEqual(public_course_run.title, "my run") with switch_language(public_course_run, "fr"): self.assertEqual(public_course_run.title, "ma session")
def test_templates_course_detail_teaser_empty_cover_empty_edit(self): """ Without video in `course_teaser` placeholder and no image in `course_cover` placeholder, no component should be present on the `course_teaser` placeholder. """ user = UserFactory(is_staff=True, is_superuser=True) self.client.login(username=user.username, password="******") course = CourseFactory() response = self.client.get(course.extended_object.get_absolute_url()) self.assertEqual(response.status_code, 200) pattern = (r'<div class="subheader__teaser">' r'<p class="empty">' r"Add a teaser video or add a cover image below" r" and it will be used as teaser image as well." r"</p>" r"</div>") self.assertIsNotNone(re.search(pattern, str(response.content)))
def test_templates_course_detail_placeholder(self): """ Draft editing course page should contain all key placeholders when empty. """ user = UserFactory(is_staff=True, is_superuser=True) self.client.login(username=user.username, password="******") course = CourseFactory(page_title="Very interesting course") page = course.extended_object url = "{:s}?edit".format(page.get_absolute_url(language="en")) response = self.client.get(url) pattern = ( r'<div class="course-detail__row course-detail__description">' r'<h2 class="course-detail__title">Description</h2>' r'<div class="cms-placeholder') self.assertIsNotNone(re.search(pattern, str(response.content))) pattern = (r'<div class="category-badge-list__container">' r'<div class="cms-placeholder') self.assertIsNotNone(re.search(pattern, str(response.content))) pattern = r'<div class="subheader__teaser"><div class="cms-placeholder' self.assertIsNotNone(re.search(pattern, str(response.content))) pattern = (r'<div class="subheader__content">' r'<div class="characteristics">' r"<ul.*</ul>" r"</div>" r'<div class="cms-placeholder') self.assertIsNotNone(re.search(pattern, str(response.content))) pattern = ( r'<div class="section__items section__items--organizations">' r'<div class="cms-placeholder') self.assertIsNotNone(re.search(pattern, str(response.content))) pattern = (r'<div class="section__items section__items--team">' r'<div class="cms-placeholder') self.assertIsNotNone(re.search(pattern, str(response.content))) pattern = ( r'<div class="course-detail__row course-detail__information">' r'<div class="cms-placeholder') self.assertIsNotNone(re.search(pattern, str(response.content))) pattern = ( r'<h3 class="course-detail__label">' r'License for the course content</h3><div class="cms-placeholder') self.assertIsNotNone(re.search(pattern, str(response.content)))
def test_models_course_subjects_copied_when_publishing(self): """ When publishing a course, the links to draft subjects on the draft version of the course should be copied (clear then add) to set equivalent links between the corresponding published course and published subjects. """ # Create subjects: 2 published and 1 draft subject1, subject2 = SubjectFactory.create_batch(2, should_publish=True) subject3 = SubjectFactory() # Create a draft course draft_course = CourseFactory( with_subjects=[subject1, subject2, subject3]) # The draft course should see all subjects self.assertEqual(set(draft_course.subjects.all()), {subject1, subject2, subject3}) # Publish the course and check that the subjects are copied draft_course.extended_object.publish("en") published_course = Course.objects.get( extended_object__publisher_is_draft=False) self.assertEqual( set(published_course.subjects.all()), {subject1.public_extension, subject2.public_extension}, ) # A published subject should see the published course self.assertEqual(subject1.public_extension.courses.first(), published_course) # The subjects that are removed from the draft course should only be cleared from the # published course upon publishing draft_course.subjects.remove(subject1) self.assertEqual( set(published_course.subjects.all()), {subject1.public_extension, subject2.public_extension}, ) draft_course.extended_object.publish("en") self.assertEqual(set(published_course.subjects.all()), {subject2.public_extension}) # The published subject that was removed should not see the published course any more self.assertIsNone(subject1.public_extension.courses.first())
def test_templates_person_detail_related_courses(self): """ The courses to which a person has participated should appear on this person's detail page. """ user = UserFactory(is_staff=True, is_superuser=True) self.client.login(username=user.username, password="******") person = PersonFactory() course = CourseFactory(fill_team=[person]) url = person.extended_object.get_absolute_url() response = self.client.get(url) # The course should be present on the page self.assertContains( response, f'<p class="course-glimpse__title">{course.extended_object.get_title():s}</p>', html=True, )
def test_open_graph_course_no_cover_image(self): """ Test if a course without a cover image has the default logo as og:image meta """ course = CourseFactory( page_title="Introduction to Programming", code="IntroProg", page_languages=["en", "fr"], ) course_page = course.extended_object course_page.publish("en") url = course_page.get_absolute_url(language="en") response = self.client.get(url) response_content = response.content.decode("UTF-8") match = re.search("<meta[^>]+og:image[^>]+>", response_content) html_meta_og_image = match.group(0) self.assertIn("richie/images/logo.png", html_meta_og_image)
def test_models_course_copy_relations_publish_recursive_loop(self): """ In a previous version of the the CourseRun method "copy_translations" in which we used instances instead of update queries, this test was generating an infinite recursive loop. """ course = CourseFactory(page_title="my course title") TitleFactory(page=course.extended_object, language="fr", title="mon titre de cours") course_run = CourseRunFactory(direct_course=course, title="my run") course_run_translation_fr = CourseRunTranslation.objects.create( master=course_run, language_code="fr", title="ma session") self.assertTrue(course.extended_object.publish("fr")) course_run_translation_fr.title = "ma session modifiée" course_run_translation_fr.save() self.assertTrue(course.extended_object.publish("fr"))
def test_templates_course_detail_runs_future_open(self): """ Priority 1: an upcoming open course run should show in a separate section. """ course = CourseFactory(page_title="my course", should_publish=True) self.create_run_future_open(course) url = course.extended_object.get_absolute_url() response = self.client.get(url) self.assertEqual(response.status_code, 200) self.assertNotContains(response, "No open course runs") self.assertContains( response, '<a href="/en/my-course/my-course-run/">Enroll now</a>', html=True, ) self.assertNotContains( response, '<h3 class="course-detail__aside__runs__block__title">')
def test_admin_form_course_run_staff_all_permissions(self): """A staff user with all permissions can submit a form.""" course = CourseFactory() user = UserFactory(is_staff=True) self.add_permission(user, "change_page") PagePermission.objects.create( page=course.extended_object, user=user, can_add=False, can_change=True, can_delete=False, can_publish=False, can_move_page=False, ) form = self._get_admin_form(course, user) self.assertTrue(form.is_valid())
def test_course_organization_main_always_included_in_organizations(self): """ The main organization should always be in the organizations linked via many-to-many """ organization1, organization2 = OrganizationFactory.create_batch(2) course = CourseFactory(organization_main=organization1) self.assertEqual(list(course.organizations.all()), [organization1]) # Now set the second organization as the main course.organization_main = organization2 course.save() self.assertEqual(course.organization_main, organization2) self.assertEqual(list(course.organizations.all()), [organization1, organization2]) # Setting an organization that is already included as many-to-many should not fail course.organization_main = organization1 course.save() self.assertEqual(course.organization_main, organization1) self.assertEqual(list(course.organizations.all()), [organization1, organization2])
def test_views_redirect_edx_courses_success_with_old_course_uri(self): """Old OpenEdX course urls are redirected to the corresponding page in richie.""" course = CourseFactory(code="abc", page_title="Physique 101", should_publish=True) TitleFactory(page=course.extended_object, language="en", title="Physics 101") course.extended_object.publish("en") response = self.client.get("/courses/sorbonne/abc/001/about/") self.assertRedirects( response, "/fr/physique-101/", status_code=301, target_status_code=200, fetch_redirect_response=True, )
def test_context_processors_queries_are_cached(self): """ Once the page is cached, no db queries should be made again """ organizations = OrganizationFactory.create_batch(2, should_publish=True, page_languages=["fr"]) course = CourseFactory(should_publish=True, fill_organizations=organizations, page_languages=["fr"]) page = course.extended_object # Get the page a first time to cache it self.client.get(page.get_public_url()) # Check that db queries are well cached # The one remaining is related to django-cms with self.assertNumQueries(1): self.client.get(page.get_public_url())
def test_models_category_get_courses(self): """ It should be possible to retrieve the list of related courses on the category instance. The number of queries should be minimal. """ category = CategoryFactory(should_publish=True) courses = CourseFactory.create_batch( 3, page_title="my title", fill_categories=[category], should_publish=True ) retrieved_courses = category.get_courses() with self.assertNumQueries(2): self.assertEqual(set(retrieved_courses), set(courses)) with self.assertNumQueries(0): for course in retrieved_courses: self.assertEqual( course.extended_object.prefetched_titles[0].title, "my title" )
def test_indexers_courses_get_es_document_for_course_not_published(self): """ A course indexed with no published title shoud not be listed. """ course = CourseFactory( page_title={"en": "a course", "fr": "un cours"}, should_publish=True ) course_document = CoursesIndexer.get_es_document_for_course(course) self.assertTrue(course_document["is_listed"]) # Only after unpublishing all languages, the course stops being listed course.extended_object.unpublish("en") course_document = CoursesIndexer.get_es_document_for_course(course) self.assertTrue(course_document["is_listed"]) course.extended_object.unpublish("fr") course_document = CoursesIndexer.get_es_document_for_course(course) self.assertFalse(course_document["is_listed"])
def test_organization_courses_copied_when_publishing(self): """ When publishing a organization, the links to draft courses on the draft version of the organization should be copied (clear then add) to the published version. Links to published courses should not be copied as they are redundant and not up-to-date. """ # Create draft courses course1, course2 = CourseFactory.create_batch(2) # Create a draft organization draft_organization = OrganizationFactory( with_courses=[course1, course2]) # Publish course1 course1.extended_object.publish("en") course1.refresh_from_db() # The draft organization should see all courses and propose a custom filter to easily # access the draft versions self.assertEqual( set(draft_organization.courses.all()), {course1, course1.public_extension, course2}, ) self.assertEqual(set(draft_organization.courses.drafts()), {course1, course2}) # Publish the organization and check that the courses are copied draft_organization.extended_object.publish("en") published_organization = Organization.objects.get( extended_object__publisher_is_draft=False) self.assertEqual(set(published_organization.courses.all()), {course1, course2}) # When publishing, the courses that are obsolete should be cleared draft_organization.courses.remove(course2) self.assertEqual(set(published_organization.courses.all()), {course1, course2}) # courses on the published organization are only cleared after publishing the draft page draft_organization.extended_object.publish("en") self.assertEqual(set(published_organization.courses.all()), {course1})
def test_indexers_courses_get_es_document_no_image_icon_picture(self): """ ES document is created without errors when a icon image for the course is actually a Picture instance without an image on it. """ # Create the example course to index and get hold of its course_icons placeholder course = CourseFactory(should_publish=True) course_icons_placeholder = course.extended_object.placeholders.filter( slot="course_icons").first() # Create a category and add it to the course on the icons placeholder category = CategoryFactory(should_publish=True, color="#654321") add_plugin(course_icons_placeholder, CategoryPlugin, "en", **{"page": category.extended_object}) course.extended_object.publish("en") # Make sure we associate an image-less picture with the category through # the icon placeholder category_icon_placeholder = category.extended_object.placeholders.filter( slot="icon").first() add_plugin(category_icon_placeholder, SimplePicturePlugin, "en", picture=None) category.extended_object.publish("en") indexed_courses = list( CoursesIndexer.get_es_documents(index="some_index", action="some_action")) self.assertEqual(len(indexed_courses), 1) self.assertEqual( indexed_courses[0]["_id"], str(course.extended_object.get_public_object().id), ) self.assertEqual( indexed_courses[0]["icon"], { "en": { "color": "#654321", "title": category.extended_object.get_title() } }, )
def test_templates_course_detail_cms_draft_content_no_code(self): """ Validate that the code is replaced by "..." on the draft page when the "code" field is not set. """ user = UserFactory(is_staff=True, is_superuser=True) self.client.login(username=user.username, password="******") course = CourseFactory(page_title="Very interesting course", code=None) # The page should be visible as draft to the staff user url = course.extended_object.get_absolute_url() response = self.client.get(url) self.assertEqual(response.status_code, 200) self.assertContains( response, '<div class="subheader__code">Ref. ...</div>', html=True, )
def test_models_course_run_delete_published_cascade(self): """ Deleting a draft course run that is published should delete its public counterpart and all its translations by cascade. """ course = CourseFactory(page_languages=["en", "fr"]) course_run = CourseRunFactory(direct_course=course) CourseRunTranslation.objects.create(master=course_run, language_code="fr", title="mon titre") self.assertTrue(course_run.direct_course.extended_object.publish("en")) self.assertTrue(course_run.direct_course.extended_object.publish("fr")) self.assertEqual(CourseRun.objects.count(), 2) self.assertEqual(CourseRunTranslation.objects.count(), 4) course_run.delete() self.assertFalse(CourseRun.objects.exists()) self.assertFalse(CourseRunTranslation.objects.exists())
def prepare_to_test_state(self, state): """ Not a test. Create objects and mock to help testing the impact of the state on template rendering. """ course = CourseFactory(page_title="my course", should_publish=True) CourseRunFactory( page_parent=course.extended_object, page_title="my course run", should_publish=True, ) url = course.extended_object.get_absolute_url() with mock.patch.object( CourseRun, "state", new_callable=mock.PropertyMock, return_value=state ): response = self.client.get(url) self.assertEqual(response.status_code, 200) return response