def test_api_course_run_sync_existing_published_sync_to_draft( self, mock_signal): """ If a course run exists in "sync_to_draft" mode (draft and public versions), only the draft version should be udpated and the related course page should be marked dirty. """ link = "http://example.edx:8073/courses/course-v1:edX+DemoX+01/course/" course = CourseFactory(code="DemoX") course_run = CourseRunFactory(direct_course=course, resource_link=link, sync_mode="sync_to_draft") course.extended_object.publish("en") course.refresh_from_db() self.assertEqual( course.extended_object.title_set.first().publisher_state, PUBLISHER_STATE_DEFAULT, ) mock_signal.reset_mock() origin_data = SyncCourseRunSerializer(instance=course_run).data data = { "resource_link": link, "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"], } 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(), 2) # Check that the draft course run was updated draft_course_run = CourseRun.objects.get(direct_course=course) draft_serializer = SyncCourseRunSerializer(instance=draft_course_run) self.assertEqual(draft_serializer.data, data) # Check that the public course run was NOT updated public_course_run = CourseRun.objects.get( direct_course=course.public_extension) public_serializer = SyncCourseRunSerializer(instance=public_course_run) self.assertEqual(public_serializer.data, origin_data) self.assertEqual( course.extended_object.title_set.first().publisher_state, PUBLISHER_STATE_DIRTY, ) self.assertFalse(mock_signal.called)
def test_models_course_run_fields_course_runs(self): """ The "course_runs" field should always return the course runs linked to the draft version of the course. """ # Create a draft course with course runs course = CourseFactory() course_runs = set(CourseRunFactory.create_batch(2, course=course)) # The course runs should be accessible from the course self.assertEqual(set(course.course_runs.all()), course_runs) # The published course should point to the same course runs course.extended_object.publish("en") course.refresh_from_db() self.assertEqual(set(course.public_extension.course_runs.all()), course_runs)
def test_signals_courses_unpublish(self, mock_bulk, *_): """ Unpublishing a course in a language should update its document in the Elasticsearch courses index or delete it if there is no language published anymore. """ course = CourseFactory(page_languages=["en", "fr"], should_publish=True) self.run_commit_hooks() mock_bulk.reset_mock() # - Unpublish the first language self.assertTrue(course.extended_object.unpublish("en")) course.refresh_from_db() # Elasticsearch should not be called before the db transaction is successful self.assertFalse(mock_bulk.called) self.run_commit_hooks() self.assertEqual(mock_bulk.call_count, 1) self.assertEqual(len(mock_bulk.call_args[1]["actions"]), 1) action = mock_bulk.call_args[1]["actions"][0] self.assertEqual(action["_id"], course.get_es_id()) self.assertEqual(action["_op_type"], "index") self.assertEqual(action["_index"], "test_courses") mock_bulk.reset_mock() # - Unpublish the second language self.assertTrue(course.extended_object.unpublish("fr")) course.refresh_from_db() # Elasticsearch should not be called before the db transaction is successful self.assertFalse(mock_bulk.called) self.run_commit_hooks() self.assertEqual(mock_bulk.call_count, 1) self.assertEqual(len(mock_bulk.call_args[1]["actions"]), 1) action = mock_bulk.call_args[1]["actions"][0] self.assertEqual(action["_id"], course.get_es_id()) self.assertEqual(action["_op_type"], "delete") self.assertEqual(action["_index"], "test_courses")
def test_course_change_view_post(self): """ Validate that the course can be updated via the admin. In particular, make sure that when a course is updated from the admin, the main organization is automatically added to the many-to-many field "organizations". See http://stackoverflow.com/a/1925784/469575 for details on the issue. """ user = UserFactory(is_staff=True, is_superuser=True) self.client.login(username=user.username, password="******") # Create a course, some organizations and some subjects organization1, organization2, organization3 = OrganizationFactory.create_batch( 3 ) subject1, subject2 = SubjectFactory.create_batch(2) course = CourseFactory( with_organizations=[organization1], with_subjects=[subject1] ) self.assertEqual( set(course.organizations.all()), {organization1, course.organization_main} ) self.assertEqual(set(course.subjects.all()), {subject1}) # Get the admin change view url = reverse("admin:courses_course_change", args=[course.id]) data = { "active_session": "xyz", "organization_main": organization2.id, "organizations": [organization3.id], "subjects": [subject2.id], } response = self.client.post(url, data) self.assertEqual(response.status_code, 302) # Check that the course was updated as expected course.refresh_from_db() self.assertEqual(course.active_session, "xyz") self.assertEqual(course.organization_main, organization2) self.assertEqual(set(course.subjects.all()), {subject2}) # Check that the main organization was added and the old organization cleared self.assertEqual( set(course.organizations.all()), {organization2, organization3} )
def test_models_course_run_get_course_direct_child(self): """ We should be able to retrieve the course from a course run that is its direct child. """ course = CourseFactory() course_run = CourseRunFactory(direct_course=course) self.assertTrue(course.extended_object.publish("en")) course.refresh_from_db() course_run.refresh_from_db() # Add a sibling course to make sure it is not returned CourseFactory(should_publish=True) # Add a snapshot to make sure it does not interfere CourseFactory(page_parent=course.extended_object, should_publish=True) self.assertEqual(course_run.get_course(), course) self.assertEqual(course_run.public_course_run.get_course(), course.public_extension)
def test_models_course_run_get_course_direct_child_with_parent(self): """ We should be able to retrieve the course from a course run that is its direct child when the course is below a root page (this is creating a difficulty because the query we build in `get_course` can create duplicates if we don't add the right clauses). """ page = create_i18n_page("A page", published=True) course = CourseFactory(page_parent=page) course_run = CourseRunFactory(direct_course=course) self.assertTrue(course.extended_object.publish("en")) course.refresh_from_db() course_run.refresh_from_db() # Add a sibling course to make sure it is not returned CourseFactory(should_publish=True) # Add a snapshot to make sure it does not interfere CourseFactory(page_parent=course.extended_object, should_publish=True) self.assertEqual(course_run.get_course(), course) self.assertEqual(course_run.public_course_run.get_course(), course.public_extension)
def test_signals_courses(self, mock_bulk, *_): """ Publishing a course should update its document in the Elasticsearch courses index. """ course = CourseFactory() self.run_commit_hooks() # Elasticsearch should not be called until the course is published self.assertFalse(mock_bulk.called) self.assertTrue(course.extended_object.publish("en")) course.refresh_from_db() # Elasticsearch should not be called before the db transaction is successful self.assertFalse(mock_bulk.called) self.run_commit_hooks() self.assertEqual(mock_bulk.call_count, 1) self.assertEqual(len(mock_bulk.call_args[1]["actions"]), 1) action = mock_bulk.call_args[1]["actions"][0] self.assertEqual(action["_id"], str(course.public_extension.extended_object_id)) self.assertEqual(action["_type"], "course")
def test_indexers_courses_get_es_documents_from_models(self, _mock_picture): """ Happy path: the data is retrieved from the models properly formatted """ # Create a course with a page in both english and french published_categories = [ CategoryFactory( # L-0001 fill_icon=True, page_title={"en": "Title L-0001", "fr": "Titre L-0001"}, should_publish=True, ), CategoryFactory( # L-0002 fill_icon=True, page_title={"en": "Title L-0002", "fr": "Titre L-0002"}, should_publish=True, ), ] draft_category = CategoryFactory(fill_icon=True) # L-0003 main_organization = OrganizationFactory( # L-0004 page_title={ "en": "english main organization title", "fr": "titre organisation principale français", }, should_publish=True, ) other_draft_organization = OrganizationFactory( # L-0005 page_title={ "en": "english other organization title", "fr": "titre autre organisation français", } ) other_published_organization = OrganizationFactory( # L-0006 page_title={ "en": "english other organization title", "fr": "titre autre organisation français", }, should_publish=True, ) person1 = PersonFactory( page_title={"en": "Eugène Delacroix", "fr": "Eugène Delacroix"}, should_publish=True, ) person2 = PersonFactory( page_title={"en": "Comte de Saint-Germain", "fr": "Earl of Saint-Germain"}, should_publish=True, ) person_draft = PersonFactory( page_title={"en": "Jules de Polignac", "fr": "Jules de Polignac"} ) course = CourseFactory( duration=[3, WEEK], effort=[2, HOUR, WEEK], fill_categories=published_categories + [draft_category], fill_cover=True, fill_icons=published_categories + [draft_category], fill_organizations=[ main_organization, other_draft_organization, other_published_organization, ], fill_team=[person1, person_draft, person2], page_title={ "en": "an english course title", "fr": "un titre cours français", }, ) CourseRunFactory.create_batch(2, direct_course=course) course.extended_object.publish("en") course.extended_object.publish("fr") course.refresh_from_db() # Add a description in several languages placeholder = course.public_extension.extended_object.placeholders.get( slot="course_description" ) plugin_params = {"placeholder": placeholder, "plugin_type": "CKEditorPlugin"} add_plugin(body="english description line 1.", language="en", **plugin_params) add_plugin(body="english description line 2.", language="en", **plugin_params) add_plugin(body="a propos français ligne 1.", language="fr", **plugin_params) add_plugin(body="a propos français ligne 2.", language="fr", **plugin_params) # The results were properly formatted and passed to the consumer expected_course = { "_id": str(course.public_extension.extended_object_id), "_index": "some_index", "_op_type": "some_action", "_type": "course", "absolute_url": { "en": "/en/an-english-course-title/", "fr": "/fr/un-titre-cours-francais/", }, "categories": ["L-0001", "L-0002"], "categories_names": { "en": ["Title L-0001", "Title L-0002"], "fr": ["Titre L-0001", "Titre L-0002"], }, "complete": { "en": [ "an english course title", "english course title", "course title", "title", ], "fr": [ "un titre cours français", "titre cours français", "cours français", "français", ], }, "course_runs": [ { "start": course_run.public_course_run.start, "end": course_run.public_course_run.end, "enrollment_start": course_run.public_course_run.enrollment_start, "enrollment_end": course_run.public_course_run.enrollment_end, "languages": course_run.public_course_run.languages, } for course_run in course.get_course_runs().order_by("-end") ], "cover_image": { "en": {"info": "picture info"}, "fr": {"info": "picture info"}, }, "description": { "en": "english description line 1. english description line 2.", "fr": "a propos français ligne 1. a propos français ligne 2.", }, "duration": {"en": "3 weeks", "fr": "3 semaines"}, "effort": {"en": "2 hours/week", "fr": "2 heures/semaine"}, "icon": { "en": { "color": published_categories[0].color, "info": "picture info", "title": "Title L-0001", }, "fr": { "color": published_categories[0].color, "info": "picture info", "title": "Titre L-0001", }, }, "is_new": False, "is_listed": True, "organization_highlighted": { "en": "english main organization title", "fr": "titre organisation principale français", }, "organizations": ["L-0004", "L-0006"], "organizations_names": { "en": [ "english main organization title", "english other organization title", ], "fr": [ "titre organisation principale français", "titre autre organisation français", ], }, "persons": [ str(person1.public_extension.extended_object_id), str(person2.public_extension.extended_object_id), ], "persons_names": { "en": ["Eugène Delacroix", "Comte de Saint-Germain"], "fr": ["Eugène Delacroix", "Earl of Saint-Germain"], }, "title": {"fr": "un titre cours français", "en": "an english course title"}, } indexed_courses = list( CoursesIndexer.get_es_documents(index="some_index", action="some_action") ) self.assertEqual(len(indexed_courses), 1) self.assertEqual(indexed_courses[0], expected_course)
def test_api_course_run_sync_existing_published_sync_to_public( self, mock_signal): """ If a course run exists in "sync_to_public" mode (draft and public versions), it should be updated, draft and public versions. """ link = "http://example.edx:8073/courses/course-v1:edX+DemoX+01/course/" course = CourseFactory(code="DemoX") CourseRunFactory(direct_course=course, resource_link=link, sync_mode="sync_to_public") course.extended_object.publish("en") course.refresh_from_db() self.assertEqual( course.extended_object.title_set.first().publisher_state, PUBLISHER_STATE_DEFAULT, ) mock_signal.reset_mock() data = { "resource_link": link, "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"], "enrollment_count": 15682, "catalog_visibility": "course_and_search", } response = self.client.post( "/api/v1.0/course-runs-sync", data, content_type="application/json", HTTP_AUTHORIZATION= ("SIG-HMAC-SHA256 25de22f3674a207a2bd3923dcc5e302a21c9aac8eee7c835f084349da69d0472" ), ) self.assertEqual(response.status_code, 200) self.assertEqual(json.loads(response.content), {"success": True}) self.assertEqual(CourseRun.objects.count(), 2) # Check that the existing draft course run was updated draft_course_run = CourseRun.objects.get(direct_course=course) draft_serializer = SyncCourseRunSerializer(instance=draft_course_run) self.assertEqual(draft_serializer.data, data) # Check that the existing public course run was updated public_course_run = CourseRun.objects.get( direct_course=course.public_extension) public_serializer = SyncCourseRunSerializer(instance=public_course_run) self.assertEqual(public_serializer.data, data) self.assertEqual( course.extended_object.title_set.first().publisher_state, PUBLISHER_STATE_DEFAULT, ) mock_signal.assert_called_once_with(sender=Page, instance=course.extended_object, language=None)
def test_models_course_get_course_runs(self): """ The `get_course_runs` method should return all descendants ranked by start date, not only direct children. """ course = CourseFactory(page_languages=["en", "fr"]) # Create draft and published course runs for this course course_run = CourseRunFactory(direct_course=course) self.assertTrue(course.extended_object.publish("en")) self.assertTrue(course.extended_object.publish("fr")) course_run_draft = CourseRunFactory(direct_course=course) # Create a child course with draft and published course runs (what results from # snapshotting a course) child_course = CourseFactory(page_languages=["en", "fr"], page_parent=course.extended_object) child_course_run = CourseRunFactory(direct_course=child_course) self.assertTrue(child_course.extended_object.publish("en")) self.assertTrue(child_course.extended_object.publish("fr")) child_course_run_draft = CourseRunFactory(direct_course=child_course) # Create another course, not related to the first one, with draft and published course runs other_course = CourseFactory(page_languages=["en", "fr"]) CourseRunFactory(direct_course=other_course) self.assertTrue(other_course.extended_object.publish("en")) self.assertTrue(other_course.extended_object.publish("fr")) CourseRunFactory(direct_course=other_course) # Check that the draft course retrieves all its descendant course runs # 3 draft course runs and 2 published course runs per course self.assertEqual(CourseRun.objects.count(), 3 * 3) sorted_runs = sorted( [ course_run, course_run_draft, child_course_run, child_course_run_draft ], key=lambda o: o.start, reverse=True, ) for run in sorted_runs: run.refresh_from_db() with self.assertNumQueries(2): self.assertEqual(list(course.get_course_runs()), sorted_runs) # Check that the published course retrieves only the published descendant course runs course.refresh_from_db() public_course = course.public_extension with self.assertNumQueries(3): result = list(public_course.get_course_runs()) expected_public_course_runs = sorted( [course_run.public_course_run, child_course_run.public_course_run], key=lambda o: o.start, reverse=True, ) self.assertEqual(result, expected_public_course_runs)