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)
예제 #2
0
    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)
예제 #3
0
    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")
예제 #4
0
    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}
        )
예제 #5
0
    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)
예제 #6
0
    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)
예제 #7
0
    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")
예제 #8
0
    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)
예제 #10
0
    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)