예제 #1
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)
    def test_templates_course_detail_cms_published_content(self):
        """
        Validate that the important elements are displayed on a published course page
        """
        categories = CategoryFactory.create_batch(4)
        organizations = OrganizationFactory.create_batch(4)

        course = CourseFactory(
            page_title="Very interesting course",
            fill_organizations=organizations,
            fill_categories=categories,
        )
        page = course.extended_object
        course_run1, _course_run2 = CourseRunFactory.create_batch(
            2, page_parent=course.extended_object, languages=["en", "fr"]
        )
        self.assertFalse(course_run1.extended_object.publish("en"))

        # Publish only 2 out of 4 categories and 2 out of 4 organizations
        categories[0].extended_object.publish("en")
        categories[1].extended_object.publish("en")
        organizations[0].extended_object.publish("en")
        organizations[1].extended_object.publish("en")

        # The unpublished objects may have been published and unpublished which puts them in a
        # status different from objects that have never been published.
        # We want to test both cases.
        categories[2].extended_object.publish("en")
        categories[2].extended_object.unpublish("en")
        organizations[2].extended_object.publish("en")
        organizations[2].extended_object.unpublish("en")

        # The page should not be visible before it is published
        url = page.get_absolute_url()
        response = self.client.get(url)
        self.assertEqual(response.status_code, 404)

        # Publish and ensure content is correct
        page.publish("en")

        # Now we can publish children course runs: publish only 1 of the 2
        course_run1.extended_object.parent_page.refresh_from_db()
        self.assertTrue(course_run1.extended_object.publish("en"))

        response = self.client.get(url)
        self.assertEqual(response.status_code, 200)

        self.assertContains(
            response, "<title>Very interesting course</title>", html=True
        )
        self.assertContains(
            response,
            '<h1 class="course-detail__content__title">Very interesting course</h1>',
            html=True,
        )

        # Only published categories should be present on the page
        for category in categories[:2]:
            self.assertContains(
                response,
                '<a class="category-plugin-tag" href="{:s}">{:s}</a>'.format(
                    category.extended_object.get_absolute_url(),
                    category.extended_object.get_title(),
                ),
                html=True,
            )
        for category in categories[-2:]:
            self.assertNotContains(response, category.extended_object.get_title())

        # Public organizations should be in response content
        for organization in organizations[:2]:
            self.assertContains(
                response,
                '<div class="organization-plugin__title">{title:s}</div>'.format(
                    title=organization.extended_object.get_title()
                ),
                html=True,
            )

        # Draft organizations should not be in response content
        for organization in organizations[-2:]:
            self.assertNotContains(
                response, organization.extended_object.get_title(), html=True
            )

        # Only the published course run should be in response content
        self.assertContains(response, "<dd>English and french</dd>", html=True, count=1)
    def test_templates_course_detail_cms_published_content(self):
        """
        Validate that the important elements are displayed on a published course page
        """
        categories = CategoryFactory.create_batch(4)
        icons = CategoryFactory.create_batch(4, fill_icon=True)
        organizations = OrganizationFactory.create_batch(4)

        course = CourseFactory(
            page_title="Very interesting course",
            fill_organizations=organizations,
            fill_categories=categories,
            fill_icons=icons,
        )
        page = course.extended_object
        # Create 2 ongoing open course runs
        now = timezone.now()
        course_run1, _course_run2 = CourseRunFactory.create_batch(
            2,
            page_parent=course.extended_object,
            start=now - timedelta(hours=1),
            end=now + timedelta(hours=2),
            enrollment_end=now + timedelta(hours=1),
            languages=["en", "fr"],
        )
        self.assertFalse(course_run1.extended_object.publish("en"))

        # Publish only 2 out of 4 categories, icons and organizations
        categories[0].extended_object.publish("en")
        categories[1].extended_object.publish("en")
        icons[0].extended_object.publish("en")
        icons[1].extended_object.publish("en")
        organizations[0].extended_object.publish("en")
        organizations[1].extended_object.publish("en")

        # The unpublished objects may have been published and unpublished which puts them in a
        # status different from objects that have never been published.
        # We want to test both cases.
        categories[2].extended_object.publish("en")
        categories[2].extended_object.unpublish("en")
        icons[2].extended_object.publish("en")
        icons[2].extended_object.unpublish("en")
        organizations[2].extended_object.publish("en")
        organizations[2].extended_object.unpublish("en")

        # The page should not be visible before it is published
        url = page.get_absolute_url()
        response = self.client.get(url)
        self.assertEqual(response.status_code, 404)

        # Publish and ensure content is correct
        page.publish("en")

        # Now we can publish children course runs: publish only 1 of the 2
        course_run1.extended_object.parent_page.refresh_from_db()
        self.assertTrue(course_run1.extended_object.publish("en"))

        response = self.client.get(url)
        self.assertEqual(response.status_code, 200)

        self.assertContains(response,
                            "<title>Very interesting course</title>",
                            html=True)
        self.assertContains(
            response,
            '<h1 class="course-detail__content__title">Very interesting course</h1>',
            html=True,
        )

        # Only published categories should be present on the page
        for category in categories[:2]:
            self.assertContains(
                response,
                ('<a class="category-plugin-tag" href="{:s}">'
                 '<div class="category-plugin-tag__title">{:s}</div></a>'
                 ).format(
                     category.extended_object.get_absolute_url(),
                     category.extended_object.get_title(),
                 ),
                html=True,
            )
        for category in categories[-2:]:
            self.assertNotContains(response,
                                   category.extended_object.get_title())

        # Only published icons should be present on the page
        pattern = (
            r'<a.*class="category-plugin-tag".*href="{link:s}".*>'
            r'<figure class="category-plugin-tag__figure">'
            r'<figcaption.*class="category-plugin-tag__figure__caption".*>'
            r".*{title:s}.*</figcaption>"
            r'<img src="/media/filer_public_thumbnails/filer_public/.*icon\.jpg.*alt="">'
        )

        for icon in icons[:2]:
            self.assertIsNotNone(
                re.search(
                    pattern.format(
                        link=icon.extended_object.get_absolute_url(),
                        title=icon.extended_object.get_title(),
                    ),
                    str(response.content),
                ))
        for icon in icons[-2:]:
            self.assertNotContains(response, icon.extended_object.get_title())

        # Public organizations should be in response content
        for organization in organizations[:2]:
            self.assertContains(
                response,
                '<div class="organization-glimpse__title">{title:s}</div>'.
                format(title=organization.extended_object.get_title()),
                html=True,
            )

        # Draft organizations should not be in response content
        for organization in organizations[-2:]:
            self.assertNotContains(response,
                                   organization.extended_object.get_title(),
                                   html=True)

        # Only the published course run should be in response content
        self.assertContains(response,
                            "<dd>English and french</dd>",
                            html=True,
                            count=1)
예제 #4
0
 def test_models_course_field_duration_null(self):
     """The duration field can be null."""
     course = CourseFactory(duration=None)
     self.assertIsNone(course.duration)
     self.assertEqual(course.get_duration_display(), "")
예제 #5
0
 def test_models_course_field_duration_display_singular(self):
     """Validate that a value of 1 time unit is displayed as expected."""
     course = CourseFactory(duration=[1, "day"])
     self.assertEqual(course.get_duration_display(), "1 day")
예제 #6
0
 def test_models_course_field_effort_null(self):
     """The effort field can be null."""
     course = CourseFactory(effort=None)
     self.assertIsNone(course.effort)
     self.assertEqual(course.get_effort_display(), "")
예제 #7
0
 def test_models_course_field_effort_display_singular(self):
     """Validate that a value of 1 time unit is displayed as expected."""
     course = CourseFactory(effort=[1, "day", "week"])
     self.assertEqual(course.get_effort_display(), "1 day/week")
    def test_templates_course_detail_cms_draft_content(self):
        """
        A staff user should see a draft course including its draft elements with
        an annotation
        """
        user = UserFactory(is_staff=True, is_superuser=True)
        self.client.login(username=user.username, password="******")

        subjects = SubjectFactory.create_batch(4)
        organizations = OrganizationFactory.create_batch(4)

        course = CourseFactory(
            organization_main=organizations[0],
            title="Very interesting course",
            with_organizations=organizations,
            with_subjects=subjects,
        )
        page = course.extended_object
        course_runs = CourseRunFactory.create_batch(2, course=course)

        # Publish only 2 out of 4 subjects and 2 out of 4 organizations
        subjects[0].extended_object.publish("en")
        subjects[1].extended_object.publish("en")
        organizations[0].extended_object.publish("en")
        organizations[1].extended_object.publish("en")

        # The unpublished objects may have been published and unpublished which puts them in a
        # status different from objects that have never been published.
        # We want to test both cases.
        subjects[2].extended_object.publish("en")
        subjects[2].extended_object.unpublish("en")
        organizations[2].extended_object.publish("en")
        organizations[2].extended_object.unpublish("en")

        # The page should be visible as draft to the staff user
        url = page.get_absolute_url()
        response = self.client.get(url)

        self.assertEqual(response.status_code, 200)

        self.assertContains(response,
                            "<title>Very interesting course en</title>",
                            html=True)
        self.assertContains(
            response,
            '<h1 class="course-detail__title">Very interesting course en</h1>',
            html=True,
        )

        # organization 1 is marked as main and not duplicated
        self.assertContains(
            response,
            '<li class="{element:s} {element:s}--main">{title:s}</li>'.format(
                element="course-detail__content__organizations__item",
                title=organizations[0].extended_object.get_title(),
            ),
            html=True,
        )
        self.assertNotContains(
            response,
            ('<li class="course-detail__content__organizations__item">{:s}</li>'
             ).format(organizations[0].extended_object.get_title()),
            html=True,
        )
        # organization 2 is not marked as a draft since it has been published
        self.assertContains(
            response,
            '<li class="course-detail__content__organizations__item">{:s}</li>'
            .format(organizations[1].extended_object.get_title()),
            html=True,
        )
        # Draft organizations should be present on the page with an annotation for styling
        for organization in organizations[:2]:
            self.assertNotContains(
                response,
                '<li class="{element:s} {element:s}--draft">{title:s}</li>'.
                format(
                    element="course-detail__content__organizations__item",
                    title=organization.extended_object.get_title(),
                ),
                html=True,
            )

        # The published subjects should be present on the page
        for subject in subjects[:2]:
            self.assertContains(
                response,
                '<li class="course-detail__content__subjects__item">{:s}</li>'.
                format(subject.extended_object.get_title()),
                html=True,
            )
        # Draft subjects should also be present on the page with an annotation for styling
        for subject in subjects[-2:]:
            self.assertContains(
                response,
                '<li class="{element:s} {element:s}--draft">{title:s}</li>'.
                format(
                    element="course-detail__content__subjects__item",
                    title=subject.extended_object.get_title(),
                ),
                html=True,
            )

        # Course runs should be in the page
        for course_run in course_runs:
            self.assertContains(
                response,
                '<a class="course-run__aside__link" href="{:s}">'.format(
                    course_run.resource_link),
            )
    def test_templates_course_detail_cms_published_content(self):
        """
        Validate that the important elements are displayed on a published course page
        """
        subjects = SubjectFactory.create_batch(4)
        organizations = OrganizationFactory.create_batch(4)

        course = CourseFactory(
            organization_main=organizations[0],
            title="Very interesting course",
            with_organizations=organizations,
            with_subjects=subjects,
        )
        page = course.extended_object
        course_runs = CourseRunFactory.create_batch(2, course=course)

        # Publish only 2 out of 4 subjects and 2 out of 4 organizations
        subjects[0].extended_object.publish("en")
        subjects[1].extended_object.publish("en")
        organizations[0].extended_object.publish("en")
        organizations[1].extended_object.publish("en")

        # The unpublished objects may have been published and unpublished which puts them in a
        # status different from objects that have never been published.
        # We want to test both cases.
        subjects[2].extended_object.publish("en")
        subjects[2].extended_object.unpublish("en")
        organizations[2].extended_object.publish("en")
        organizations[2].extended_object.unpublish("en")

        # The page should not be visible before it is published
        url = page.get_absolute_url()
        response = self.client.get(url)

        # Publish and ensure content is correct
        page.publish("en")
        response = self.client.get(url)

        self.assertEqual(response.status_code, 200)

        self.assertContains(response,
                            "<title>Very interesting course en</title>",
                            html=True)
        self.assertContains(
            response,
            '<h1 class="course-detail__title">Very interesting course en</h1>',
            html=True,
        )

        # Only published subjects should be present on the page
        for subject in subjects[:2]:
            self.assertContains(
                response,
                '<li class="course-detail__content__subjects__item">{:s}</li>'.
                format(subject.extended_object.get_title()),
                html=True,
            )
        for subject in subjects[-2:]:
            self.assertNotContains(response,
                                   subject.extended_object.get_title())

        # organization 1 is marked as main organization
        self.assertContains(
            response,
            '<li class="{element:s} {element:s}--main">{title:s}</li>'.format(
                element="course-detail__content__organizations__item",
                title=organizations[0].extended_object.get_title(),
            ),
            html=True,
        )

        # organization 2 is the only "common" org in listing
        self.assertContains(
            response,
            '<li class="course-detail__content__organizations__item">{:s}</li>'
            .format(organizations[1].extended_object.get_title()),
            html=True,
        )

        # Draft organization should not be in response content
        for organization in organizations[-2:]:
            self.assertNotContains(response,
                                   organization.extended_object.get_title(),
                                   html=True)

        # Course runs should be in the page
        for course_run in course_runs:
            self.assertContains(
                response,
                '<a class="course-run__aside__link" href="{:s}">'.format(
                    course_run.resource_link),
            )
    def test_templates_course_detail_rdfa(self):
        """
        Extract RDFa tags from the HTML markup and check that it is complete as expected.
        """
        # Create organizations
        main_organization = OrganizationFactory(page_title="Main org",
                                                fill_logo=True,
                                                should_publish=True)
        other_organization = OrganizationFactory(page_title="Other org",
                                                 fill_logo=True,
                                                 should_publish=True)

        # Create persons
        author1 = PersonFactory(page_title="François", fill_portrait=True)
        placeholder = author1.extended_object.placeholders.get(slot="bio")
        add_plugin(
            language="en",
            placeholder=placeholder,
            plugin_type="PlainTextPlugin",
            body="La bio de François",
        )
        author2 = PersonFactory(page_title="Jeanne",
                                fill_portrait=True,
                                should_publish=True)

        # Create a course with cover image, team and organizations
        licence_content, licence_participation = LicenceFactory.create_batch(2)
        course = CourseFactory(
            code="abcde",
            effort=[3, "hour"],
            page_title="Very interesting course",
            fill_cover=True,
            fill_organizations=[main_organization, other_organization],
            fill_team=[author1, author2],
            fill_licences=[
                ("course_license_content", licence_content),
                ("course_license_participation", licence_participation),
            ],
        )

        # Add an introduction to the course
        placeholder = course.extended_object.placeholders.get(
            slot="course_introduction")
        add_plugin(
            language="en",
            placeholder=placeholder,
            plugin_type="PlainTextPlugin",
            body="Introduction to interesting course",
        )

        # Create an ongoing open course run that will be published (created before
        # publishing the page)
        now = datetime(2030, 6, 15, tzinfo=timezone.utc)
        CourseRunFactory(
            direct_course=course,
            start=datetime(2030, 6, 30, tzinfo=timezone.utc),
            end=datetime(2030, 8, 1, tzinfo=timezone.utc),
            enrollment_start=datetime(2030, 6, 14, tzinfo=timezone.utc),
            enrollment_end=datetime(2030, 6, 16, tzinfo=timezone.utc),
            languages=["en", "fr"],
        )
        CourseRunFactory(
            direct_course=course,
            start=datetime(2030, 6, 1, tzinfo=timezone.utc),
            end=datetime(2030, 7, 10, tzinfo=timezone.utc),
            enrollment_start=datetime(2030, 6, 13, tzinfo=timezone.utc),
            enrollment_end=datetime(2030, 6, 20, tzinfo=timezone.utc),
            languages=["de"],
        )

        author1.extended_object.publish("en")
        course.extended_object.publish("en")

        url = course.extended_object.get_absolute_url()
        with mock.patch.object(timezone, "now", return_value=now):
            response = self.client.get(url)
        self.assertEqual(response.status_code, 200)

        processor = pyRdfa()
        content = str(response.content)
        parser = html5lib.HTMLParser(
            tree=html5lib.treebuilders.getTreeBuilder("dom"))
        dom = parser.parse(io.StringIO(content))
        graph = processor.graph_from_DOM(dom)

        # Retrieve the course top node (body)
        (subject, ) = graph.subjects(
            URIRef("http://www.w3.org/1999/02/22-rdf-syntax-ns#type"),
            URIRef("https://schema.org/Course"),
        )
        self.assertEqual(len(list(graph.triples((subject, None, None)))), 38)

        # Opengraph
        self.assertTrue((
            subject,
            URIRef("http://ogp.me/ns#url"),
            Literal("http://example.com/en/very-interesting-course/"),
        ) in graph)
        self.assertTrue((subject, URIRef("http://ogp.me/ns#site_name"),
                         Literal("example.com")) in graph)
        self.assertTrue((subject, URIRef("http://ogp.me/ns#type"),
                         Literal("website")) in graph)
        self.assertTrue((subject, URIRef("http://ogp.me/ns#locale"),
                         Literal("en")) in graph)
        self.assertTrue((subject, URIRef("http://ogp.me/ns#determiner"),
                         Literal("")) in graph)
        self.assertTrue((
            subject,
            URIRef("http://ogp.me/ns#title"),
            Literal("Very interesting course"),
        ) in graph)
        self.assertTrue((
            subject,
            URIRef("http://ogp.me/ns#description"),
            Literal("Introduction to interesting course"),
        ) in graph)

        (image_value, ) = graph.objects(subject,
                                        URIRef("http://ogp.me/ns#image"))
        pattern = (
            r"/media/filer_public_thumbnails/filer_public/.*cover\.jpg__"
            r"1200x630_q85_crop_replace_alpha-%23FFFFFF_subject_location")
        self.assertIsNotNone(re.search(pattern, str(image_value)))

        # Schema.org
        # - Course
        self.assertTrue((
            subject,
            URIRef("http://www.w3.org/1999/02/22-rdf-syntax-ns#type"),
            URIRef("https://schema.org/Course"),
        ) in graph)
        self.assertTrue((
            subject,
            URIRef("https://schema.org/name"),
            Literal("Very interesting course"),
        ) in graph)
        self.assertTrue((
            subject,
            URIRef("https://schema.org/description"),
            Literal("Introduction to interesting course"),
        ) in graph)
        self.assertTrue((subject, URIRef("https://schema.org/courseCode"),
                         Literal("ABCDE")) in graph)
        self.assertTrue((subject,
                         URIRef("https://schema.org/isAccessibleForFree"),
                         Literal("true")) in graph)
        self.assertTrue((subject, URIRef("https://schema.org/timeRequired"),
                         Literal("PT3H")) in graph)
        self.assertTrue((
            subject,
            URIRef("https://schema.org/stylesheet"),
            URIRef("/static/richie/css/main.css"),
        ) in graph)
        self.assertTrue((
            subject,
            URIRef("https://schema.org/shortcut"),
            URIRef("/static/richie/favicon/favicon.ico"),
        ) in graph)
        self.assertTrue((
            subject,
            URIRef("https://schema.org/icon"),
            URIRef("/static/richie/favicon/favicon.ico"),
        ) in graph)
        self.assertTrue((
            subject,
            URIRef("https://schema.org/icon"),
            URIRef("/static/richie/favicon/favicon-16x16.png"),
        ) in graph)
        self.assertTrue((
            subject,
            URIRef("https://schema.org/icon"),
            URIRef("/static/richie/favicon/favicon-32x32.png"),
        ) in graph)
        self.assertTrue((
            subject,
            URIRef("https://schema.org/apple-touch-icon"),
            URIRef("/static/richie/favicon/apple-touch-icon.png"),
        ) in graph)
        self.assertTrue((
            subject,
            URIRef("https://schema.org/mask-icon"),
            URIRef("/static/richie/favicon/safari-pinned-tab.svg"),
        ) in graph)
        self.assertTrue((
            subject,
            URIRef("https://schema.org/manifest"),
            URIRef("/static/richie/favicon/site.webmanifest"),
        ) in graph)
        self.assertTrue((
            subject,
            URIRef("https://schema.org/noreferrer"),
            URIRef("https://www.facebook.com/example"),
        ) in graph)
        self.assertTrue((
            subject,
            URIRef("https://schema.org/noopener"),
            URIRef("https://www.facebook.com/example"),
        ) in graph)
        self.assertTrue((
            subject,
            URIRef("https://schema.org/alternate"),
            URIRef("http://example.com/en/very-interesting-course/"),
        ) in graph)
        self.assertTrue((
            subject,
            URIRef("https://schema.org/alternate"),
            URIRef("http://example.com/fr/very-interesting-course/"),
        ) in graph)

        (image_value, ) = graph.objects(subject,
                                        URIRef("https://schema.org/image"))
        pattern = (
            r"/media/filer_public_thumbnails/filer_public/.*cover\.jpg__"
            r"300x170_q85_crop_replace_alpha-%23FFFFFF_subject_location")
        self.assertIsNotNone(re.search(pattern, str(image_value)))

        self.assertTrue((subject, URIRef("https://schema.org/license"),
                         URIRef(licence_content.url)) in graph)
        self.assertTrue((
            None,
            URIRef("https://schema.org/license"),
            URIRef(licence_participation.url),
        ) not in graph)
        # - Main organization (Provider)
        self.assertTrue((subject, URIRef("https://schema.org/provider"),
                         URIRef("/en/main-org/")) in graph)
        self.assertTrue((
            URIRef("/en/main-org/"),
            URIRef("https://schema.org/name"),
            Literal("Main org"),
        ) in graph)
        self.assertTrue((
            URIRef("/en/main-org/"),
            URIRef("https://schema.org/url"),
            Literal("http://example.com/en/main-org/"),
        ) in graph)

        (logo_value, ) = graph.objects(URIRef("/en/main-org/"),
                                       URIRef("https://schema.org/logo"))
        pattern = (r"/media/filer_public_thumbnails/filer_public/.*logo.jpg__"
                   r"200x113_q85_replace_alpha-%23FFFFFF_subject_location")
        self.assertIsNotNone(re.search(pattern, str(logo_value)))

        # - Organizations (Contributor)
        contributor_subjects = list(
            graph.objects(subject, URIRef("https://schema.org/contributor")))
        self.assertEqual(len(contributor_subjects), 2)

        self.assertTrue((
            contributor_subjects[0],
            URIRef("http://www.w3.org/1999/02/22-rdf-syntax-ns#type"),
            URIRef("https://schema.org/CollegeOrUniversity"),
        ) in graph)
        self.assertTrue((
            contributor_subjects[1],
            URIRef("http://www.w3.org/1999/02/22-rdf-syntax-ns#type"),
            URIRef("https://schema.org/CollegeOrUniversity"),
        ) in graph)

        self.assertTrue((
            URIRef("/en/main-org/"),
            URIRef("https://schema.org/name"),
            Literal("Main org"),
        ) in graph)

        self.assertTrue((
            URIRef("/en/other-org/"),
            URIRef("https://schema.org/name"),
            Literal("Other org"),
        ) in graph)

        self.assertTrue((
            URIRef("/en/main-org/"),
            URIRef("https://schema.org/url"),
            Literal("http://example.com/en/main-org/"),
        ) in graph)

        self.assertTrue((
            URIRef("/en/other-org/"),
            URIRef("https://schema.org/url"),
            Literal("http://example.com/en/other-org/"),
        ) in graph)

        pattern = (r"/media/filer_public_thumbnails/filer_public/.*logo.jpg__"
                   r"200x113_q85_replace_alpha-%23FFFFFF_subject_location")
        (logo_value, ) = graph.objects(URIRef("/en/main-org/"),
                                       URIRef("https://schema.org/logo"))
        self.assertIsNotNone(re.search(pattern, str(logo_value)))

        (logo_value, ) = graph.objects(URIRef("/en/other-org/"),
                                       URIRef("https://schema.org/logo"))
        self.assertIsNotNone(re.search(pattern, str(logo_value)))

        # - Team (Person)
        author_subjects = list(
            graph.objects(subject, URIRef("https://schema.org/author")))
        self.assertEqual(len(author_subjects), 2)

        self.assertTrue((
            author_subjects[0],
            URIRef("http://www.w3.org/1999/02/22-rdf-syntax-ns#type"),
            URIRef("https://schema.org/Person"),
        ) in graph)
        self.assertTrue((
            author_subjects[1],
            URIRef("http://www.w3.org/1999/02/22-rdf-syntax-ns#type"),
            URIRef("https://schema.org/Person"),
        ) in graph)

        for name in ["Fran\\xc3\\xa7ois", "Jeanne"]:
            (author_subject, ) = graph.subjects(
                URIRef("https://schema.org/name"), Literal(name))
            self.assertTrue(author_subject in author_subjects)

        (author_subject, ) = graph.subjects(
            URIRef("https://schema.org/description"),
            Literal("La bio de Fran\\xc3\\xa7ois"),
        )
        self.assertTrue(author_subject in author_subjects)

        for url in [
                "http://example.com/en/francois/",
                "http://example.com/en/jeanne/"
        ]:
            (author_subject, ) = graph.subjects(
                URIRef("https://schema.org/url"), Literal(url))
            self.assertTrue(author_subject in author_subjects)

        pattern = (
            r"/media/filer_public_thumbnails/filer_public/.*portrait.jpg__"
            r"200x200_q85_crop_replace_alpha-%23FFFFFF_subject_location")
        for author_subject in author_subjects:
            (portrait_value, ) = graph.objects(
                author_subject, URIRef("https://schema.org/image"))
            self.assertIsNotNone(re.search(pattern, str(portrait_value)))

        # - Course runs (CourseInstance)
        course_run_subjects = list(
            graph.objects(subject,
                          URIRef("https://schema.org/hasCourseInstance")))
        self.assertEqual(len(course_run_subjects), 2)

        self.assertTrue((
            course_run_subjects[0],
            URIRef("http://www.w3.org/1999/02/22-rdf-syntax-ns#type"),
            URIRef("https://schema.org/CourseInstance"),
        ) in graph)
        self.assertTrue((
            course_run_subjects[0],
            URIRef("https://schema.org/courseMode"),
            Literal("online"),
        ) in graph)
        self.assertTrue((
            course_run_subjects[1],
            URIRef("http://www.w3.org/1999/02/22-rdf-syntax-ns#type"),
            URIRef("https://schema.org/CourseInstance"),
        ) in graph)
        self.assertTrue((
            course_run_subjects[1],
            URIRef("https://schema.org/courseMode"),
            Literal("online"),
        ) in graph)

        for start_date in ["2030-06-01", "2030-06-30"]:
            (subject, ) = graph.subjects(
                URIRef("https://schema.org/startDate"), Literal(start_date))
            self.assertTrue(subject in course_run_subjects)

        for end_date in ["2030-07-10", "2030-08-01"]:
            (subject, ) = graph.subjects(URIRef("https://schema.org/endDate"),
                                         Literal(end_date))
            self.assertTrue(subject in course_run_subjects)
예제 #11
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",
            },
            should_publish=True,
        )
        CourseRunFactory.create_batch(
            2, page_parent=course.extended_object, should_publish=True
        )

        # 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_extension.start,
                    "end": course_run.public_extension.end,
                    "enrollment_start": course_run.public_extension.enrollment_start,
                    "enrollment_end": course_run.public_extension.enrollment_end,
                    "languages": course_run.public_extension.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,
            "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)
예제 #12
0
    def test_cms_plugins_course_render_on_public_page(self):
        """
        Test that a CoursePlugin correctly renders course's page specific information
        """
        icon_category_main = CategoryFactory(
            page_title={
                "en": "icon title",
                "fr": "titre icone"
            },
            fill_icon=True,
            should_publish=True,
        )
        icon_category_secondary = CategoryFactory(fill_icon=True,
                                                  should_publish=True)

        # Create a course with a page in both english and french
        organization = OrganizationFactory(page_title="public title",
                                           should_publish=True)

        course = CourseFactory(
            page_title={
                "en": "public title",
                "fr": "titre public"
            },
            fill_organizations=[organization],
            fill_icons=[icon_category_main, icon_category_secondary],
            fill_cover={
                "original_filename": "cover.jpg",
                "default_alt_text": "my cover",
            },
            should_publish=True,
        )
        course_page = course.extended_object

        # Create a page to add the plugin to
        page = create_i18n_page({"en": "A page", "fr": "Une page"})
        placeholder = page.placeholders.get(slot="maincontent")
        add_plugin(placeholder, CoursePlugin, "en", **{"page": course_page})
        add_plugin(placeholder, CoursePlugin, "fr", **{"page": course_page})

        page.publish("en")
        page.publish("fr")

        # Check the page content in English
        url = page.get_absolute_url(language="en")
        response = self.client.get(url)
        self.assertContains(response, "public title")
        self.assertNotContains(response, "titre public")

        # The course's url should be present
        self.assertContains(
            response,
            '<a href="{url}" class="course-glimpse course-glimpse--link'.
            format(url=course_page.get_absolute_url()),
            status_code=200,
        )

        # The course's name should be present
        self.assertContains(
            response,
            '<p class="course-glimpse__content__title">{title}</p>'.format(
                title=course_page.get_title()),
            status_code=200,
        )
        # The course's main organization should be present
        self.assertContains(
            response,
            "<p>{title}</p>".format(
                title=organization.extended_object.get_title()),
            status_code=200,
        )

        # The course's cover should be present
        pattern = (
            r'<div class="course-glimpse__media">'
            r'<img src="/media/filer_public_thumbnails/filer_public/.*cover\.jpg__300x170'
            r'.*alt=""')
        self.assertIsNotNone(re.search(pattern, str(response.content)))

        # The course's icon should be present
        pattern = (
            r'<div class="course-glimpse__icon">'
            r'.*<img src="/media/filer_public_thumbnails/filer_public/.*icon\.jpg__60x60'
            r'.*alt="icon title"')
        self.assertIsNotNone(re.search(pattern, str(response.content)))

        # The draft course plugin should not be present
        # Check if draft is shown after unpublish
        course_page.unpublish("en")
        page.publish("en")
        response = self.client.get(url)
        self.assertNotContains(response, "public title")
        self.assertNotContains(response, course_page.get_absolute_url())

        # Check the page content in french
        url = page.get_absolute_url(language="fr")
        response = self.client.get(url)
        self.assertContains(response, "titre public")

        # The course's url should be present
        self.assertContains(
            response,
            '<a href="{url}" class="course-glimpse course-glimpse--link'.
            format(url=course_page.get_absolute_url()),
            status_code=200,
        )

        # The course's name should be present
        self.assertContains(
            response,
            '<p class="course-glimpse__content__title">{title}</p>'.format(
                title=course_page.get_title()),
            status_code=200,
        )

        # The course's main organization should be present
        self.assertContains(
            response,
            "<p>{title}</p>".format(
                title=organization.extended_object.get_title()),
            status_code=200,
        )

        # The course's cover should be present
        pattern = (
            r'<div class="course-glimpse__media">'
            r'<img src="/media/filer_public_thumbnails/filer_public/.*cover\.jpg__300x170'
            r'.*alt=""')
        self.assertIsNotNone(re.search(pattern, str(response.content)))

        # The course's icon should be present
        pattern = (
            r'<div class="course-glimpse__icon">'
            r'.*<img src="/media/filer_public_thumbnails/filer_public/.*icon\.jpg__60x60'
            r'.*alt="titre icone"')
        self.assertIsNotNone(re.search(pattern, str(response.content)))

        # The draft course plugin should not be present
        # Check if draft is shown after unpublish
        course_page.unpublish("fr")
        page.publish("fr")
        response = self.client.get(url)
        self.assertNotContains(response, "titre public")
        self.assertNotContains(response, course_page.get_absolute_url())
예제 #13
0
    def test_templates_organization_detail_cms_published_content(self):
        """
        Validate that the important elements are displayed on a published organization page
        """
        # Categories
        published_category = CategoryFactory(should_publish=True)
        extra_published_category = CategoryFactory(should_publish=True)
        unpublished_category = CategoryFactory(should_publish=True)
        unpublished_category.extended_object.unpublish("en")
        not_published_category = CategoryFactory()

        # Organizations
        organization = OrganizationFactory(
            page_title="La Sorbonne",
            fill_categories=[
                published_category,
                not_published_category,
                unpublished_category,
            ],
        )

        # Courses
        published_course = CourseFactory(fill_organizations=[organization],
                                         should_publish=True)
        extra_published_course = CourseFactory(should_publish=True)
        unpublished_course = CourseFactory(fill_organizations=[organization])
        not_published_course = CourseFactory(fill_organizations=[organization])

        # Republish courses to take into account adding the organization
        published_course.extended_object.publish("en")
        unpublished_course.extended_object.publish("en")
        unpublished_course.extended_object.unpublish("en")

        page = organization.extended_object

        # The page should not be visible before it is published
        url = page.get_absolute_url()
        response = self.client.get(url)
        self.assertEqual(response.status_code, 404)

        # Publish the organization
        page.publish("en")

        # Modify the draft version of the published category
        title_obj = published_category.extended_object.title_set.get(
            language="en")
        title_obj.title = "modified category"
        title_obj.save()

        # Modify the draft version of the published course
        title_obj = published_course.extended_object.title_set.get(
            language="en")
        title_obj.title = "modified course"
        title_obj.save()

        # Add a new category to the draft organization page but don't publish the modification
        add_plugin(
            page.placeholders.get(slot="categories"),
            CategoryPlugin,
            "en",
            page=extra_published_category.extended_object,
        )

        # Add a new organization to the draft course page but don't publish the modification
        add_plugin(
            extra_published_course.extended_object.placeholders.get(
                slot="course_organizations"),
            OrganizationPlugin,
            "en",
            page=page,
        )

        # Ensure the published page content is correct
        response = self.client.get(url)
        self.assertContains(response,
                            "<title>La Sorbonne</title>",
                            html=True,
                            status_code=200)
        self.assertContains(
            response,
            '<h1 class="organization-detail__title">La Sorbonne</h1>',
            html=True,
        )

        # Published category should not be on the page since we only display
        # them in draft mode
        self.assertNotContains(
            response,
            ('<a class="category-tag" href="{:s}">'
             '<span class="category-tag__title">{:s}</span></a>').format(
                 published_category.public_extension.extended_object.
                 get_absolute_url(),
                 published_category.public_extension.extended_object.get_title(
                 ),
             ),
            html=True,
        )
        # The other categories should not be leaked:
        # - new_category linked only on the draft organization page
        self.assertNotContains(
            response, extra_published_category.extended_object.get_title())
        # - not published category
        self.assertNotContains(
            response, not_published_category.extended_object.get_title())
        # - unpublished category
        self.assertNotContains(
            response, unpublished_category.extended_object.get_title())

        # The published courses should be on the page in its published version
        self.assertContains(
            response,
            '<p class="course-glimpse__title">{:s}</p>'.format(
                published_course.public_extension.extended_object.get_title()),
            html=True,
        )
        # The other courses should not be leaked:
        # - new course linked only on the draft organization page
        self.assertNotContains(
            response, extra_published_course.extended_object.get_title())
        # - not published course
        self.assertNotContains(
            response, not_published_course.extended_object.get_title())
        # - unpublished course
        self.assertNotContains(response,
                               unpublished_course.extended_object.get_title())

        # Modified draft category and course should not be leaked
        self.assertNotContains(response, "modified")
예제 #14
0
    def test_templates_organization_detail_cms_draft_content(self):
        """
        A staff user should see a draft organization including only the related objects that
        are published.
        """
        user = UserFactory(is_staff=True, is_superuser=True)
        self.client.login(username=user.username, password="******")

        published_category = CategoryFactory(should_publish=True)
        not_published_category = CategoryFactory()

        organization = OrganizationFactory(
            page_title="La Sorbonne",
            fill_categories=[published_category, not_published_category],
        )

        published_course = CourseFactory(fill_organizations=[organization],
                                         should_publish=True)
        not_published_course = CourseFactory(fill_organizations=[organization])

        # Republish courses to take into account adding the organization
        published_course.extended_object.publish("en")

        # Modify the draft version of the published category
        title_obj = published_category.extended_object.title_set.get(
            language="en")
        title_obj.title = "modified category"
        title_obj.save()

        # Modify the draft version of the published course
        title_obj = published_course.extended_object.title_set.get(
            language="en")
        title_obj.title = "modified course"
        title_obj.save()

        page = organization.extended_object

        # The page should be visible as draft to the staff user
        url = page.get_absolute_url()
        response = self.client.get(url)
        self.assertContains(response,
                            "<title>La Sorbonne</title>",
                            html=True,
                            status_code=200)
        self.assertContains(
            response,
            '<h1 class="organization-detail__title">La Sorbonne</h1>',
            html=True,
        )

        # The published category should be on the page in its published version
        self.assertContains(
            response,
            ('<a class="category-tag" href="{:s}">'
             '<span class="category-tag__title">{:s}</span></a>').format(
                 published_category.public_extension.extended_object.
                 get_absolute_url(),
                 published_category.public_extension.extended_object.get_title(
                 ),
             ),
            html=True,
        )
        # The not published category should not be on the page
        self.assertNotContains(
            response,
            not_published_category.extended_object.get_title(),
        )
        # The modified draft category should not be leaked
        self.assertNotContains(response, "modified category")

        # The published course should be on the page in its draft version
        self.assertContains(
            response,
            '<p class="course-glimpse__title">modified course</p>',
            html=True,
        )

        # The not published course should not be on the page
        self.assertNotContains(
            response,
            not_published_course.extended_object.get_title(),
        )
예제 #15
0
    def test_models_course_create_page_role(self, *_):
        """
        If the CMS_PERMISSIONS settings is True, a page role should be created when calling
        `create_page_role` on a course.
        Calling the method several times should not duplicate permissions.
        """
        def get_random_role_dict():
            return {
                "django_permissions": ["cms.change_page"],
                "course_page_permissions": {
                    "can_change": random.choice([True, False]),
                    "can_add": random.choice([True, False]),
                    "can_delete": random.choice([True, False]),
                    "can_change_advanced_settings":
                    random.choice([True, False]),
                    "can_publish": random.choice([True, False]),
                    "can_change_permissions": random.choice([True, False]),
                    "can_move_page": random.choice([True, False]),
                    "can_view":
                    False,  # can_view = True would make it a view restriction...
                    "grant_on": random.randint(1, 5),
                },
                "course_folder_permissions": {
                    "can_read": random.choice([True, False]),
                    "can_edit": random.choice([True, False]),
                    "can_add_children": random.choice([True, False]),
                    "type": random.randint(0, 2),
                },
            }

        page = PageFactory(title__title="My title")
        course = CourseFactory(extended_object=page)
        self.assertFalse(page.roles.exists())

        role_dict = get_random_role_dict()
        with mock.patch.dict(defaults.COURSE_ADMIN_ROLE, role_dict):
            course.create_page_role()

        # Call the method another time with different permissions to check it has no effect
        with mock.patch.dict(defaults.COURSE_ADMIN_ROLE,
                             get_random_role_dict()):
            course.create_page_role()

        # A page role should have been created
        self.assertEqual(page.roles.count(), 1)
        role = page.roles.get(role="ADMIN")
        self.assertEqual(role.group.name, "Admin | My title")
        self.assertEqual(role.group.permissions.count(), 1)
        self.assertEqual(role.folder.name, "Admin | My title")

        # All expected permissions should have been assigned to the group:
        # - Django permissions
        self.assertEqual(role.group.permissions.first().codename,
                         "change_page")
        # - DjangoCMS page permissions
        self.assertEqual(
            PagePermission.objects.filter(group=role.group).count(), 1)
        page_permission = PagePermission.objects.get(group=role.group)
        for key, value in role_dict["course_page_permissions"].items():
            self.assertEqual(getattr(page_permission, key), value)
        # The Django Filer folder permissions
        self.assertEqual(
            FolderPermission.objects.filter(group_id=role.group_id).count(), 1)
        folder_permission = FolderPermission.objects.get(
            group_id=role.group_id)
        for key, value in role_dict["course_folder_permissions"].items():
            self.assertEqual(getattr(folder_permission, key), value)
예제 #16
0
def create_demo_site():
    """
    Create a simple site tree structure for developpers to work in realistic environment.

    We create multilingual pages, add organizations under the related page and add
    plugins to each page.
    """
    site = Site.objects.get(id=1)

    # Create pages as described in PAGES_INFOS
    pages_created = recursive_page_creation(site, PAGES_INFO)

    # Create some licences
    licences = LicenceFactory.create_batch(
        NB_OBJECTS["licences"], logo__file__from_path=pick_image("licence")())

    # Generate each category tree and return a list of the leaf categories
    icons = list(
        create_categories(
            **ICONS_INFO,
            fill_banner=pick_image("banner"),
            fill_logo=pick_image("logo"),
            page_parent=pages_created["categories"],
        ))
    levels = list(
        create_categories(
            **LEVELS_INFO,
            fill_banner=pick_image("banner"),
            fill_logo=pick_image("logo"),
            page_parent=pages_created["categories"],
        ))
    subjects = list(
        create_categories(
            **SUBJECTS_INFO,
            fill_banner=pick_image("banner"),
            fill_logo=pick_image("logo"),
            page_parent=pages_created["categories"],
        ))
    partnerships = list(
        create_categories(
            **PARTNERSHIPS_INFO,
            fill_banner=pick_image("banner"),
            fill_logo=pick_image("logo"),
            page_parent=pages_created["categories"],
        ))

    # Create organizations under the `Organizations` page
    organizations = []
    for i in range(NB_OBJECTS["organizations"]):
        # Randomly assign each organization to a partnership level category
        organizations.append(
            OrganizationFactory(
                page_in_navigation=True,
                page_languages=["en", "fr"],
                page_parent=pages_created["organizations"],
                fill_banner=pick_image("banner"),
                fill_categories=[random.choice(partnerships)]  # nosec
                if (i % 2 == 0) else [],
                fill_description=True,
                fill_logo=pick_image("logo"),
                should_publish=True,
                with_permissions=True,
            ))

    # Create persons under the `persons` page
    persons = []
    persons_for_organization = defaultdict(list)
    for _ in range(NB_OBJECTS["persons"]):
        # Randomly assign each person to a set of organizations
        person_organizations = random.sample(
            organizations,
            random.randint(1, NB_OBJECTS["person_organizations"]),  # nosec
        )
        person = PersonFactory(
            page_in_navigation=True,
            page_languages=["en", "fr"],
            page_parent=pages_created["persons"],
            fill_categories=random.sample(
                subjects,
                random.randint(1, NB_OBJECTS["person_subjects"])  # nosec
            ),
            fill_organizations=person_organizations,
            fill_portrait=pick_image("portrait"),
            fill_bio=True,
            should_publish=True,
        )
        persons.append(person)
        for organization in person_organizations:
            persons_for_organization[organization.id].append(person)

    # Assign each person randomly to an organization so that our course are tagged realistically
    # If organizations and persons are tagged randomly on courses, each organizations will
    # in the end be related to most persons... not what we want.

    # Create courses under the `Course` page with categories and organizations
    # relations
    courses = []
    for _ in range(NB_OBJECTS["courses"]):
        video_sample = random.choice(VIDEO_SAMPLE_LINKS)  # nosec

        # Randomly assign each course to a set of organizations
        course_organizations = random.sample(
            organizations, NB_OBJECTS["course_organizations"])

        # Only the persons members of these organizations are eligible to be part
        # of the course team
        eligible_persons = set(person for o in course_organizations
                               for person in persons_for_organization[o.id])

        course = CourseFactory(
            page_in_navigation=True,
            page_languages=["en", "fr"],
            page_parent=pages_created["courses"],
            fill_licences=[
                ("course_license_content", random.choice(licences)),  # nosec
                ("course_license_participation",
                 random.choice(licences)),  # nosec
            ],
            fill_team=random.sample(
                eligible_persons,
                min(
                    random.randint(1, NB_OBJECTS["course_persons"]),  # nosec
                    len(eligible_persons),
                ),
            ),
            fill_teaser=video_sample,
            fill_cover=pick_image("cover")(video_sample.image),
            fill_categories=[
                *random.sample(
                    subjects,
                    random.randint(1, NB_OBJECTS["course_subjects"])  # nosec
                ),
                random.choice(levels),  # nosec
            ],
            fill_icons=random.sample(icons, get_number_of_icons()),
            fill_organizations=course_organizations,
            fill_texts=[
                "course_description",
                "course_format",
                "course_prerequisites",
                "course_plan",
                # "course_license_content",
                # "course_license_participation",
            ],
            should_publish=True,
        )
        course.create_permissions_for_organization(course_organizations[0])
        courses.append(course)

        # Add a random number of course runs to the course
        nb_course_runs = get_number_of_course_runs()
        # pick a subset of languages for this course (otherwise all courses will have more or
        # less all the languages across their course runs!)
        languages_subset = random.sample(
            ["de", "en", "es", "fr", "it", "nl"],
            random.randint(1, 4)  # nosec
        )
        for i in range(nb_course_runs):
            CourseRunFactory(
                __sequence=i,
                languages=random.sample(
                    languages_subset,
                    random.randint(1, len(languages_subset))  # nosec
                ),
                page_in_navigation=False,
                page_languages=["en", "fr"],
                page_parent=course.extended_object,
                should_publish=True,
            )

    # Create blog posts under the `News` page
    blogposts = []
    for _ in range(NB_OBJECTS["blogposts"]):
        post = BlogPostFactory.create(
            page_in_navigation=True,
            page_languages=["en", "fr"],
            page_parent=pages_created["blogposts"],
            fill_cover=pick_image("cover"),
            fill_excerpt=True,
            fill_body=True,
            fill_categories=[
                *random.sample(subjects, NB_OBJECTS["blogpost_categories"]),
                random.choice(levels),  # nosec
            ],
            fill_author=random.sample(persons, 1),
            should_publish=True,
        )
        blogposts.append(post)

    # Create programs under the `Programs` page
    programs = []
    for _ in range(NB_OBJECTS["programs"]):
        program = ProgramFactory.create(
            page_in_navigation=True,
            page_languages=["en", "fr"],
            page_parent=pages_created["programs"],
            fill_cover=pick_image("cover"),
            fill_excerpt=True,
            fill_body=True,
            fill_courses=[
                *random.sample(courses, NB_OBJECTS["programs_courses"])
            ],
            should_publish=True,
        )
        programs.append(program)

    # Once everything has been created, use some content to create a homepage
    placeholder = pages_created["home"].placeholders.get(slot="maincontent")

    # - Get a banner image
    banner = image_getter(pick_image("banner")())

    # - Get a logo image
    logo = image_getter(pick_image("logo")())

    # - Create the home page in each language
    for language, content in HOMEPAGE_CONTENT.items():
        # Add a banner
        add_plugin(
            language=language,
            placeholder=placeholder,
            plugin_type="LargeBannerPlugin",
            title=content["banner_title"],
            background_image=banner,
            logo=logo,
            logo_alt_text="logo",
            content=content["banner_content"],
            template=content["banner_template"],
        )
        # Add highlighted courses with a button
        courses_section = add_plugin(
            language=language,
            placeholder=placeholder,
            plugin_type="SectionPlugin",
            title=content["courses_title"],
            template=content["section_template"],
        )
        for course in random.sample(courses, NB_OBJECTS["home_courses"]):
            add_plugin(
                language=language,
                placeholder=placeholder,
                plugin_type="CoursePlugin",
                target=courses_section,
                page=course.extended_object,
            )
        add_plugin(
            language=language,
            placeholder=placeholder,
            plugin_type="LinkPlugin",
            target=courses_section,
            name=content["courses_button_title"],
            template=content["button_template_name"],
            internal_link=pages_created["courses"],
        )

        # Add highlighted blogposts
        blogposts_section = add_plugin(
            language=language,
            placeholder=placeholder,
            plugin_type="SectionPlugin",
            title=content["blogposts_title"],
            template=content["section_template"],
        )
        for blogpost in random.sample(blogposts, NB_OBJECTS["home_blogposts"]):
            add_plugin(
                language=language,
                placeholder=placeholder,
                plugin_type="BlogPostPlugin",
                target=blogposts_section,
                page=blogpost.extended_object,
            )
        add_plugin(
            language=language,
            placeholder=placeholder,
            plugin_type="LinkPlugin",
            target=blogposts_section,
            name=content["blogposts_button_title"],
            template=content["button_template_name"],
            internal_link=pages_created["blogposts"],
        )

        # Add highlighted programs
        programs_section = add_plugin(
            language=language,
            placeholder=placeholder,
            plugin_type="SectionPlugin",
            title=content["programs_title"],
            template=content["section_template"],
        )
        for program in random.sample(programs, NB_OBJECTS["home_programs"]):
            add_plugin(
                language=language,
                placeholder=placeholder,
                plugin_type="ProgramPlugin",
                target=programs_section,
                page=program.extended_object,
            )
        add_plugin(
            language=language,
            placeholder=placeholder,
            plugin_type="LinkPlugin",
            target=programs_section,
            name=content["programs_button_title"],
            template=content["button_template_name"],
            internal_link=pages_created["programs"],
        )

        # Add highlighted organizations
        organizations_section = add_plugin(
            language=language,
            placeholder=placeholder,
            plugin_type="SectionPlugin",
            title=content["organizations_title"],
            template=content["section_template"],
        )
        for organization in random.sample(organizations,
                                          NB_OBJECTS["home_organizations"]):
            add_plugin(
                language=language,
                placeholder=placeholder,
                plugin_type="OrganizationPlugin",
                target=organizations_section,
                page=organization.extended_object,
            )
        add_plugin(
            language=language,
            placeholder=placeholder,
            plugin_type="LinkPlugin",
            target=organizations_section,
            name=content["organizations_button_title"],
            template=content["button_template_name"],
            internal_link=pages_created["organizations"],
        )

        # Add highlighted subjects
        subjects_section = add_plugin(
            language=language,
            placeholder=placeholder,
            plugin_type="SectionPlugin",
            title=content["subjects_title"],
            template=content["section_template"],
        )
        for subject in random.sample(subjects, NB_OBJECTS["home_subjects"]):
            add_plugin(
                language=language,
                placeholder=placeholder,
                plugin_type="CategoryPlugin",
                target=subjects_section,
                page=subject.extended_object,
            )
        add_plugin(
            language=language,
            placeholder=placeholder,
            plugin_type="LinkPlugin",
            target=subjects_section,
            name=content["subjects_button_title"],
            template=content["button_template_name"],
            internal_link=pages_created["categories"],
        )

        # Add highlighted persons
        persons_section = add_plugin(
            language=language,
            placeholder=placeholder,
            plugin_type="SectionPlugin",
            title=content["persons_title"],
            template=content["section_template"],
        )
        for person in random.sample(persons, NB_OBJECTS["home_persons"]):
            add_plugin(
                language=language,
                placeholder=placeholder,
                plugin_type="PersonPlugin",
                target=persons_section,
                page=person.extended_object,
            )
        add_plugin(
            language=language,
            placeholder=placeholder,
            plugin_type="LinkPlugin",
            target=persons_section,
            name=content["persons_button_title"],
            template=content["button_template_name"],
            internal_link=pages_created["persons"],
        )

        # Once content has been added we must publish again homepage
        pages_created["home"].publish(language)

    # Fill the single column sample page
    placeholder = pages_created["annex__about"].placeholders.get(
        slot="maincontent")

    # - Get a banner image
    banner = image_getter(pick_image("banner")())

    # - Get a logo image
    logo = image_getter(pick_image("logo")())

    # - Get a video
    video_sample = random.choice(VIDEO_SAMPLE_LINKS)  # nosec

    # - Create sample page in each language
    for language, content in SINGLECOLUMN_CONTENT.items():
        # Add a banner
        add_plugin(
            language=language,
            placeholder=placeholder,
            plugin_type="LargeBannerPlugin",
            title=content["banner_title"],
            background_image=banner,
            content=content["banner_content"],
            template=content["banner_template"],
        )
        # HTML paragraphs
        create_text_plugin(
            pages_created["annex__about"],
            placeholder,
            nb_paragraphs=random.randint(3, 4),  # nosec
            languages=[language],
            plugin_type="TextPlugin",
        )
        # A large video sample
        add_plugin(
            language=language,
            placeholder=placeholder,
            plugin_type="VideoPlayerPlugin",
            label=video_sample.label,
            embed_link=video_sample.url,
            template="full-width",
        )
        # Section with some various plugins
        sample_section = add_plugin(
            language=language,
            placeholder=placeholder,
            plugin_type="SectionPlugin",
            title=content["section_sample_title"],
            template=content["section_sample_template"],
        )
        add_plugin(
            language=language,
            placeholder=placeholder,
            plugin_type="OrganizationPlugin",
            target=sample_section,
            page=random.choice(organizations).extended_object,  # nosec
        )
        add_plugin(
            language=language,
            placeholder=placeholder,
            plugin_type="CoursePlugin",
            target=sample_section,
            page=random.choice(courses).extended_object,  # nosec
        )
        add_plugin(
            language=language,
            placeholder=placeholder,
            plugin_type="OrganizationPlugin",
            target=sample_section,
            page=random.choice(organizations).extended_object,  # nosec
        )
        add_plugin(
            language=language,
            placeholder=placeholder,
            plugin_type="BlogPostPlugin",
            target=sample_section,
            page=random.choice(blogposts).extended_object,  # nosec
        )
        add_plugin(
            language=language,
            placeholder=placeholder,
            plugin_type="LinkPlugin",
            target=sample_section,
            name=content["section_sample_button_title"],
            template=content["button_template_name"],
            internal_link=pages_created["home"],
        )
        # Add a licence
        add_plugin(
            language=language,
            placeholder=placeholder,
            plugin_type="LicencePlugin",
            licence=random.choice(licences),  # nosec
        )
        # Add a simple picture entry
        add_plugin(
            language=language,
            placeholder=placeholder,
            plugin_type="SimplePicturePlugin",
            picture=logo,
        )
        # Add a plain text
        text = factory.Faker(
            "text",
            max_nb_chars=random.randint(150, 250),
            locale=language  # nosec
        ).generate({})
        add_plugin(
            language=language,
            placeholder=placeholder,
            plugin_type="PlainTextPlugin",
            body=text,
        )

        # Once content has been added we must publish again the about page
        pages_created["annex__about"].publish(language)

    # Create a sitemap page
    placeholder = pages_created["annex__sitemap"].placeholders.get(
        slot="maincontent")

    for language in pages_created["annex__sitemap"].get_languages():
        parent_instance = add_plugin(language=language,
                                     placeholder=placeholder,
                                     plugin_type="HTMLSitemapPlugin")
        for name, params in SITEMAP_PAGE_PARAMS.items():
            add_plugin(
                language=language,
                placeholder=placeholder,
                plugin_type="HTMLSitemapPagePlugin",
                target=parent_instance,
                root_page=pages_created[name],
                **params,
            )

        # Once content has been added we must publish again the sitemap
        pages_created["annex__sitemap"].publish(language)
예제 #17
0
    def test_models_course_get_course_runs(self):
        """
        The `get_course_runs` and `get_course_runs_for_language` methods should return all
        descendants ranked by path, not only children and should respect publication status.
        """
        course = CourseFactory(page_languages=["en", "fr"],
                               should_publish=True)

        # Create draft and published course runs for this course
        # We want to test 4 situations:
        # - a draft course run
        # - a course run published in the current language
        # - a course run published in another language
        # - a course run published in the current language that was then unpublished
        course_runs = CourseRunFactory.create_batch(
            3, page_parent=course.extended_object, page_languages=["en"])
        self.assertTrue(course_runs[0].extended_object.publish("en"))
        self.assertTrue(course_runs[1].extended_object.publish("en"))
        self.assertTrue(course_runs[1].extended_object.unpublish("en"))

        course_run_fr = CourseRunFactory(
            page_parent=course.extended_object,
            page_languages=["fr"],
            should_publish=True,
        )

        # 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,
            should_publish=True,
        )
        child_course_runs = CourseRunFactory.create_batch(
            3, page_parent=child_course.extended_object, page_languages=["en"])
        self.assertTrue(child_course_runs[0].extended_object.publish("en"))
        self.assertTrue(child_course_runs[1].extended_object.publish("en"))
        self.assertTrue(child_course_runs[1].extended_object.unpublish("en"))

        child_course_run_fr = CourseRunFactory(
            page_parent=child_course.extended_object,
            page_languages=["fr"],
            should_publish=True,
        )

        # Create another course, not related to the first one, with draft and published course runs
        other_course = CourseFactory(page_languages=["en", "fr"],
                                     should_publish=True)
        other_course_runs = CourseRunFactory.create_batch(
            3, page_parent=other_course.extended_object, page_languages=["en"])
        self.assertTrue(other_course_runs[0].extended_object.publish("en"))
        self.assertTrue(other_course_runs[1].extended_object.publish("en"))
        self.assertTrue(other_course_runs[1].extended_object.unpublish("en"))

        CourseRunFactory(
            page_parent=other_course.extended_object,
            page_languages=["fr"],
            should_publish=True,
        )

        # 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 * (4 + 3))

        with self.assertNumQueries(2):
            self.assertEqual(
                list(course.get_course_runs()),
                course_runs + [course_run_fr] + child_course_runs +
                [child_course_run_fr],
            )

        with self.assertNumQueries(1):
            self.assertEqual(
                list(course.get_course_runs_for_language(language="en")),
                course_runs + child_course_runs,
            )

        # Check that the published course retrieves only the published descendant course runs
        course_runs[0].refresh_from_db()
        child_course_runs[0].refresh_from_db()
        public_course = course.public_extension

        with self.assertNumQueries(3):
            result = list(public_course.get_course_runs())
        self.assertEqual(
            result,
            [
                course_runs[0].public_extension,
                course_run_fr.public_extension,
                child_course_runs[0].public_extension,
                child_course_run_fr.public_extension,
            ],
        )

        with self.assertNumQueries(1):
            result = list(
                public_course.get_course_runs_for_language(language="en"))
        self.assertEqual(
            result,
            [
                course_runs[0].public_extension,
                child_course_runs[0].public_extension
            ],
        )
예제 #18
0
def create_demo_site():
    """
    Create a simple site tree structure for developpers to work in realistic environment.

    We create multilingual pages, add organizations under the related page and add
    plugins to each page.
    """
    site = Site.objects.get(id=1)

    # Create pages as described in PAGES_INFOS
    pages_created = recursive_page_creation(site, PAGE_INFOS)

    # Create some licences
    licences = LicenceFactory.create_batch(NB_LICENCES)

    # Create organizations under the `Organizations` page
    organizations = OrganizationFactory.create_batch(
        NB_ORGANIZATIONS,
        page_in_navigation=True,
        page_languages=["en", "fr"],
        page_parent=pages_created["organizations"],
        fill_banner=True,
        fill_description=True,
        fill_logo=True,
        should_publish=True,
    )

    # Generate each category tree and return a list of the leaf categories
    levels = list(create_categories(LEVELS_INFO, pages_created["categories"]))
    subjects = list(
        create_categories(SUBJECTS_INFO, pages_created["categories"]))

    title = PersonTitleFactory(translation=None)
    PersonTitleTranslationFactory(master=title,
                                  language_code="en",
                                  title="Doctor",
                                  abbreviation="Dr.")
    PersonTitleTranslationFactory(master=title,
                                  language_code="fr",
                                  title="Docteur",
                                  abbreviation="Dr.")

    # Create persons under the `persons` page
    persons = PersonFactory.create_batch(
        NB_PERSONS,
        page_in_navigation=True,
        page_languages=["en", "fr"],
        page_parent=pages_created["persons"],
        person_title=random.choice([title, None]),
        fill_portrait=True,
        fill_resume=True,
        should_publish=True,
    )

    # Create courses under the `Course` page with categories and organizations
    # relations
    courses = []
    for _ in range(NB_COURSES):
        video_sample = random.choice(VIDEO_SAMPLE_LINKS)

        course = CourseFactory(
            page_in_navigation=True,
            page_languages=["en", "fr"],
            page_parent=pages_created["courses"],
            fill_licences=[
                ("course_license_content", random.choice(licences)),
                ("course_license_participation", random.choice(licences)),
            ],
            fill_team=random.sample(persons, NB_COURSES_PERSONS_PLUGINS),
            fill_teaser=video_sample,
            fill_cover=video_sample.image,
            fill_categories=[
                *random.sample(
                    subjects, random.randint(1, NB_COURSES_SUBJECT_RELATIONS)),
                random.choice(levels),
            ],
            fill_organizations=random.sample(
                organizations, NB_COURSES_ORGANIZATION_RELATIONS),
            fill_texts=[
                "course_syllabus",
                "course_format",
                "course_prerequisites",
                "course_plan",
                # "course_license_content",
                # "course_license_participation",
            ],
            should_publish=True,
        )
        courses.append(course)

        # Add a random number of course runs to the course
        nb_course_runs = get_number_of_course_runs()

        # 1) Make sure we have one course run open for enrollment
        now = timezone.now()
        CourseRunFactory(
            __sequence=nb_course_runs,
            page_in_navigation=False,
            page_parent=course.extended_object,
            start=now + timedelta(days=1),
            enrollment_start=now - timedelta(days=5),
            enrollment_end=now + timedelta(days=5),
            should_publish=True,
        )

        # 2) Add more random course runs
        for i in range(nb_course_runs - 1, 0, -1):
            CourseRunFactory(
                __sequence=i,
                page_in_navigation=False,
                page_languages=["en", "fr"],
                page_parent=course.extended_object,
                should_publish=True,
            )

    # Once everything has been created, use some content to create a homepage
    placeholder = pages_created["home"].placeholders.get(slot="maincontent")

    # - Get a banner image
    banner_file = file_getter("banner")()
    wrapped_banner = File(banner_file, banner_file.name)
    banner = Image.objects.create(file=wrapped_banner)

    # - Get a logo image
    logo_file = file_getter("logo")()
    wrapped_logo = File(logo_file, logo_file.name)
    logo = Image.objects.create(file=wrapped_logo)

    # - Create the home page in each language
    for language, content in HOMEPAGE_CONTENT.items():
        # Add a banner
        add_plugin(
            language=language,
            placeholder=placeholder,
            plugin_type="LargeBannerPlugin",
            title=content["banner_title"],
            background_image=banner,
            logo=logo,
            logo_alt_text="logo",
            content=content["banner_content"],
            template=content["banner_template"],
        )
        # Add highlighted courses
        courses_section = add_plugin(
            language=language,
            placeholder=placeholder,
            plugin_type="SectionPlugin",
            title=content["courses_title"],
            template=content["section_template"],
        )
        for course in random.sample(courses, NB_HOME_HIGHLIGHTED_COURSES):
            add_plugin(
                language=language,
                placeholder=placeholder,
                plugin_type="CoursePlugin",
                target=courses_section,
                page=course.extended_object,
            )

        # Add highlighted organizations
        organizations_section = add_plugin(
            language=language,
            placeholder=placeholder,
            plugin_type="SectionPlugin",
            title=content["organizations_title"],
            template=content["section_template"],
        )
        for organization in random.sample(organizations,
                                          NB_HOME_HIGHLIGHTED_ORGANIZATIONS):
            add_plugin(
                language=language,
                placeholder=placeholder,
                plugin_type="OrganizationPlugin",
                target=organizations_section,
                page=organization.extended_object,
            )

        # Add highlighted subjects
        subjects_section = add_plugin(
            language=language,
            placeholder=placeholder,
            plugin_type="SectionPlugin",
            title=content["subjects_title"],
            template=content["section_template"],
        )
        for subject in random.sample(subjects, NB_HOME_HIGHLIGHTED_SUBJECTS):
            add_plugin(
                language=language,
                placeholder=placeholder,
                plugin_type="CategoryPlugin",
                target=subjects_section,
                page=subject.extended_object,
            )

        # Add highlighted persons
        persons_section = add_plugin(
            language=language,
            placeholder=placeholder,
            plugin_type="SectionPlugin",
            title=content["persons_title"],
            template=content["section_template"],
        )
        for person in random.sample(persons, NB_HOME_HIGHLIGHTED_PERSONS):
            add_plugin(
                language=language,
                placeholder=placeholder,
                plugin_type="PersonPlugin",
                target=persons_section,
                page=person.extended_object,
            )

        # Once content has been added we must publish again homepage in every
        # edited Languages
        pages_created["home"].publish("en")
        pages_created["home"].publish("fr")
예제 #19
0
 def test_models_course_field_effort_positive(self):
     """The first value should be a positive integer."""
     with self.assertRaises(ValidationError) as context:
         CourseFactory(effort=[-1, "day", "month"])
     self.assertEqual(context.exception.messages[0],
                      "An effort should be positive.")
예제 #20
0
    def test_models_organization_get_courses_language_fallback_draft(self):
        """
        Validate that the reverse courses lookup works as expected with language fallback
        on a draft page.
        """
        organization1, organization2, organization3 = OrganizationFactory.create_batch(
            3, should_publish=True)
        course = CourseFactory(should_publish=True)
        placeholder = course.extended_object.placeholders.get(
            slot="course_organizations")
        cms_languages = {
            "default": {
                "public": True,
                "hide_untranslated": False,
                "redirect_on_fallback": False,
                "fallbacks": ["en", "fr", "de"],
            }
        }

        # Reverse plugin lookups should fallback up to the second priority language
        add_plugin(
            language="de",
            placeholder=placeholder,
            plugin_type="OrganizationPlugin",
            **{"page": organization1.extended_object},
        )
        with override_settings(CMS_LANGUAGES=cms_languages):
            with translation.override("en"):
                self.assertEqual(list(organization1.get_courses()), [course])
                self.assertEqual(list(organization2.get_courses()), [])
                self.assertEqual(list(organization3.get_courses()), [])

            with translation.override("fr"):
                self.assertEqual(list(organization1.get_courses()), [course])
                self.assertEqual(list(organization2.get_courses()), [])
                self.assertEqual(list(organization3.get_courses()), [])

            with translation.override("de"):
                self.assertEqual(list(organization1.get_courses()), [course])
                self.assertEqual(list(organization2.get_courses()), [])
                self.assertEqual(list(organization3.get_courses()), [])

        # Reverse plugin lookups should fallback to the first priority language if available
        # and ignore the second priority language unless it is the current language
        add_plugin(
            language="fr",
            placeholder=placeholder,
            plugin_type="OrganizationPlugin",
            **{"page": organization2.extended_object},
        )
        with override_settings(CMS_LANGUAGES=cms_languages):
            with translation.override("en"):
                self.assertEqual(list(organization1.get_courses()), [])
                self.assertEqual(list(organization2.get_courses()), [course])
                self.assertEqual(list(organization3.get_courses()), [])

            with translation.override("fr"):
                self.assertEqual(list(organization1.get_courses()), [])
                self.assertEqual(list(organization2.get_courses()), [course])
                self.assertEqual(list(organization3.get_courses()), [])

            with translation.override("de"):
                self.assertEqual(list(organization1.get_courses()), [course])
                self.assertEqual(list(organization2.get_courses()), [])
                self.assertEqual(list(organization3.get_courses()), [])

        # Reverse plugin lookups should stick to the current language if available and
        # ignore plugins on fallback languages
        add_plugin(
            language="en",
            placeholder=placeholder,
            plugin_type="OrganizationPlugin",
            **{"page": organization3.extended_object},
        )
        with override_settings(CMS_LANGUAGES=cms_languages):
            with translation.override("en"):
                self.assertEqual(list(organization1.get_courses()), [])
                self.assertEqual(list(organization2.get_courses()), [])
                self.assertEqual(list(organization3.get_courses()), [course])

            with translation.override("fr"):
                self.assertEqual(list(organization1.get_courses()), [])
                self.assertEqual(list(organization2.get_courses()), [course])
                self.assertEqual(list(organization3.get_courses()), [])

            with translation.override("de"):
                self.assertEqual(list(organization1.get_courses()), [course])
                self.assertEqual(list(organization2.get_courses()), [])
                self.assertEqual(list(organization3.get_courses()), [])
예제 #21
0
 def test_models_course_field_effort_display_plural(self):
     """Validate that a plural number of time units is displayed as expected."""
     course = CourseFactory(effort=[2, "day", "week"])
     self.assertEqual(course.get_effort_display(), "2 days/week")
예제 #22
0
    def test_cms_plugins_course_render_on_public_page(self):
        """
        Test that an CoursePlugin correctly renders course's page specific information
        """
        # Create a course with a page in both english and french
        organization = OrganizationFactory(
            page_title="public title", should_publish=True
        )

        course = CourseFactory(
            page_title={"en": "public title", "fr": "titre public"},
            fill_organizations=[organization],
        )
        course_page = course.extended_object

        course_page.publish("en")
        course_page.publish("fr")

        # Create a page to add the plugin to
        page = create_i18n_page({"en": "A page", "fr": "Une page"})
        placeholder = page.placeholders.get(slot="maincontent")
        add_plugin(placeholder, CoursePlugin, "en", **{"page": course_page})
        add_plugin(placeholder, CoursePlugin, "fr", **{"page": course_page})

        page.publish("en")
        page.publish("fr")

        # Check the page content in English
        url = page.get_absolute_url(language="en")
        response = self.client.get(url)
        self.assertContains(response, "public title")
        self.assertNotContains(response, "titre public")

        # The course's url should be present
        self.assertContains(
            response,
            '<a class="course-plugin" href="{url}"'.format(
                url=course_page.get_absolute_url()
            ),
            status_code=200,
        )

        # The course's name should be present
        self.assertContains(
            response,
            '<p class="course-glimpse__content__title">{title}</p>'.format(
                title=course_page.get_title()
            ),
            status_code=200,
        )
        # The course's main organization should be present
        self.assertContains(
            response,
            "<p>{title}</p>".format(title=organization.extended_object.get_title()),
            status_code=200,
        )

        # The draft course plugin should not be present
        # Check if draft is shown after unpublish
        course_page.unpublish("en")
        page.publish("en")
        response = self.client.get(url)
        self.assertNotContains(response, "public title")
        self.assertNotContains(response, course_page.get_absolute_url())

        # Check the page content in french
        url = page.get_absolute_url(language="fr")
        response = self.client.get(url)
        self.assertContains(response, "titre public")

        # The course's url should be present
        self.assertContains(
            response,
            '<a class="course-plugin" href="{url}"'.format(
                url=course_page.get_absolute_url()
            ),
            status_code=200,
        )

        # The course's name should be present
        self.assertContains(
            response,
            '<p class="course-glimpse__content__title">{title}</p>'.format(
                title=course_page.get_title()
            ),
            status_code=200,
        )

        # The course's main organization should be present
        self.assertContains(
            response,
            "<p>{title}</p>".format(title=organization.extended_object.get_title()),
            status_code=200,
        )

        # The draft course plugin should not be present
        # Check if draft is shown after unpublish
        course_page.unpublish("fr")
        page.publish("fr")
        response = self.client.get(url)
        self.assertNotContains(response, "titre public")
        self.assertNotContains(response, course_page.get_absolute_url())
예제 #23
0
 def test_models_course_field_duration_positive(self):
     """The first value should be a positive integer."""
     with self.assertRaises(ValidationError) as context:
         CourseFactory(duration=[-1, "day"])
     self.assertEqual(context.exception.messages[0],
                      "A composite duration should be positive.")
예제 #24
0
    def test_templates_course_detail_cms_draft_content(self):
        """
        A staff user should see a draft course including only the related objects that
        are published.
        """
        user = UserFactory(is_staff=True, is_superuser=True)
        self.client.login(username=user.username, password="******")

        categories = CategoryFactory.create_batch(4)
        organizations = OrganizationFactory.create_batch(2)
        published_organizations = OrganizationFactory.create_batch(2)

        course = CourseFactory(
            page_title="Very interesting course",
            fill_organizations=organizations + published_organizations,
            fill_categories=categories,
        )
        page = course.extended_object
        now = timezone.now()
        CourseRunFactory(
            direct_course=course,
            start=now - timedelta(hours=1),
            end=now + timedelta(hours=2),
            enrollment_end=now + timedelta(hours=1),
            languages=["en", "fr"],
        )

        program_published, program_unpublished = ProgramFactory.create_batch(
            2, fill_courses=[course], should_publish=True)
        program_unpublished.extended_object.unpublish("en")

        # Publish only 2 out of 4 categories and 2 out of 4 organizations
        self.assertTrue(categories[0].extended_object.publish("en"))
        self.assertTrue(categories[1].extended_object.publish("en"))
        self.assertTrue(
            published_organizations[0].extended_object.publish("en"))
        self.assertTrue(
            published_organizations[1].extended_object.publish("en"))

        # The unpublished objects may have been published and unpublished which puts them in a
        # status different from objects that have never been published.
        # We want to test both cases.
        self.assertTrue(categories[2].extended_object.publish("en"))
        self.assertTrue(categories[2].extended_object.unpublish("en"))
        self.assertTrue(organizations[0].extended_object.publish("en"))
        self.assertTrue(organizations[0].extended_object.unpublish("en"))

        # The page should be visible as draft to the staff user
        url = page.get_absolute_url()
        response = self.client.get(url)

        self.assertEqual(response.status_code, 200)

        self.assertContains(response,
                            "<title>Very interesting course</title>",
                            html=True)
        self.assertContains(
            response,
            '<h1 class="subheader__title">Very interesting course</h1>',
            html=True,
        )
        self.assertContains(
            response,
            f'<div class="subheader__code">Ref. {course.code:s}</div>',
            html=True,
        )

        # only public organizations should be present on the page
        for organization in organizations:
            self.assertNotContains(response,
                                   organization.extended_object.get_title())
        for organization in published_organizations:
            self.assertContains(
                response,
                '<div class="organization-glimpse__title">{title:s}</div>'.
                format(title=organization.extended_object.get_title()),
                html=True,
            )

        # The published categories should be present on the page
        for category in categories[:2]:
            self.assertContains(
                response,
                ('<a class="category-badge" href="{:s}">'
                 '<span class="category-badge__title">{:s}</span></a>').format(
                     category.extended_object.get_absolute_url(),
                     category.extended_object.get_title(),
                 ),
                html=True,
            )
        # Draft categories should not be present on the page
        for category in categories[-2:]:
            self.assertNotContains(
                response,
                category.extended_object.get_title(),
                html=True,
            )
        # The course run should be in the page
        self.assertContains(response,
                            "<dd>English and french</dd>",
                            html=True,
                            count=1)

        # Both programs should be in response content
        self.assertContains(response, "course-detail__programs")
        self.assertContains(response, "This course is part of programs")
        self.assertContains(response,
                            program_published.extended_object.get_title(),
                            html=True,
                            count=1)
        self.assertContains(
            response,
            program_unpublished.extended_object.get_title(),
            html=True,
            count=1,
        )
예제 #25
0
 def test_models_course_field_duration_display_plural(self):
     """Validate that a plural number of time units is displayed as expected."""
     course = CourseFactory(duration=[2, "day"])
     self.assertEqual(course.get_duration_display(), "2 days")
예제 #26
0
    def test_templates_course_detail_cms_published_content(self):
        """
        Validate that the important elements are displayed on a published course page
        """
        categories = CategoryFactory.create_batch(4)
        icons = CategoryFactory.create_batch(4, fill_icon=True)
        organizations = OrganizationFactory.create_batch(4)

        course = CourseFactory(
            page_title="Very interesting course",
            fill_organizations=organizations,
            fill_categories=categories,
            fill_icons=icons,
        )
        page = course.extended_object
        # Create an ongoing open course run that will be published (created before
        # publishing the page)
        now = timezone.now()
        CourseRunFactory(
            direct_course=course,
            start=now - timedelta(hours=1),
            end=now + timedelta(hours=2),
            enrollment_end=now + timedelta(hours=1),
            languages=["en", "fr"],
        )

        program_published, program_unpublished = ProgramFactory.create_batch(
            2, fill_courses=[course], should_publish=True)
        program_unpublished.extended_object.unpublish("en")

        # Publish only 2 out of 4 categories, icons and organizations
        self.assertTrue(categories[0].extended_object.publish("en"))
        self.assertTrue(categories[1].extended_object.publish("en"))
        self.assertTrue(icons[0].extended_object.publish("en"))
        self.assertTrue(icons[1].extended_object.publish("en"))
        self.assertTrue(organizations[0].extended_object.publish("en"))
        self.assertTrue(organizations[1].extended_object.publish("en"))

        # The unpublished objects may have been published and unpublished which puts them in a
        # status different from objects that have never been published.
        # We want to test both cases.
        self.assertTrue(categories[2].extended_object.publish("en"))
        self.assertTrue(categories[2].extended_object.unpublish("en"))
        self.assertTrue(icons[2].extended_object.publish("en"))
        self.assertTrue(icons[2].extended_object.unpublish("en"))
        self.assertTrue(organizations[2].extended_object.publish("en"))
        self.assertTrue(organizations[2].extended_object.unpublish("en"))

        # The page should not be visible before it is published
        url = page.get_absolute_url()
        response = self.client.get(url)
        self.assertEqual(response.status_code, 404)

        # Publish and ensure content is correct
        self.assertTrue(page.publish("en"))

        # Create an unpublished ongoing open course run (created after
        # publishing the page)
        CourseRunFactory(
            direct_course=course,
            start=now - timedelta(hours=1),
            end=now + timedelta(hours=2),
            enrollment_end=now + timedelta(hours=1),
            languages=["en", "fr"],
        )

        response = self.client.get(url)
        self.assertEqual(response.status_code, 200)

        self.assertContains(response,
                            "<title>Very interesting course</title>",
                            html=True)
        self.assertContains(
            response,
            f'<div class="subheader__code">Ref. {course.code:s}</div>',
            html=True,
        )
        self.assertContains(
            response,
            '<h1 class="subheader__title">Very interesting course</h1>',
            html=True,
        )

        # Only published categories should be present on the page
        for category in categories[:2]:
            self.assertContains(
                response,
                ('<a class="category-badge" href="{:s}">'
                 '<span class="category-badge__title">{:s}</span></a>').format(
                     category.extended_object.get_absolute_url(),
                     category.extended_object.get_title(),
                 ),
                html=True,
            )
        for category in categories[-2:]:
            self.assertNotContains(response,
                                   category.extended_object.get_title())

        # Only published icons should be present on the page
        pattern = (
            r'<a.*class="category-badge".*href="{link:s}".*>'
            r'<img src="/media/filer_public_thumbnails/filer_public/.*icon\.jpg.*alt="{title:s}">'
            r'<span class="category-badge__title">'
            r".*{title:s}.*</span>")

        for icon in icons[:2]:
            self.assertIsNotNone(
                re.search(
                    pattern.format(
                        link=icon.extended_object.get_absolute_url(),
                        title=icon.extended_object.get_title(),
                    ),
                    str(response.content),
                ))
        for icon in icons[-2:]:
            self.assertNotContains(response, icon.extended_object.get_title())

        # Public organizations should be in response content
        for organization in organizations[:2]:
            self.assertContains(
                response,
                '<div class="organization-glimpse__title">{title:s}</div>'.
                format(title=organization.extended_object.get_title()),
                html=True,
            )

        # Draft organizations should not be in response content
        for organization in organizations[-2:]:
            self.assertNotContains(response,
                                   organization.extended_object.get_title(),
                                   html=True)

        # Only the published course run should be in response content
        self.assertEqual(CourseRun.objects.count(), 3)
        self.assertContains(response,
                            "<dd>English and french</dd>",
                            html=True,
                            count=1)

        # Only the published program should be in response content
        self.assertContains(response, "course-detail__programs")
        self.assertContains(response, "This course is part of a program")
        self.assertContains(response,
                            program_published.extended_object.get_title(),
                            html=True,
                            count=1)
        self.assertNotContains(response,
                               program_unpublished.extended_object.get_title())
예제 #27
0
    def test_indexable_filters_internationalization(self):
        """
        Indexable filters (such as categories and organizations by default) should have
        their names localized in the filter definitions in course search responses.
        """
        # Create the meta categories, each with a child category that should appear in facets
        subjects_meta = CategoryFactory(page_reverse_id="subjects",
                                        should_publish=True)
        subject = CategoryFactory(page_parent=subjects_meta.extended_object,
                                  should_publish=True)
        levels_meta = CategoryFactory(page_reverse_id="levels",
                                      should_publish=True)
        level = CategoryFactory(page_parent=levels_meta.extended_object,
                                should_publish=True)
        # Create 2 organizations that should appear in facets
        org_meta = OrganizationFactory(page_reverse_id="organizations",
                                       should_publish=True)
        org_1 = OrganizationFactory(
            page_parent=org_meta.extended_object,
            page_title="First organization",
            should_publish=True,
        )
        org_2 = OrganizationFactory(
            page_parent=org_meta.extended_object,
            page_title="Second organization",
            should_publish=True,
        )
        # Create a course linked to our categories and organizations
        CourseFactory(
            fill_categories=[subject, level],
            fill_organizations=[org_1, org_2],
            should_publish=True,
        )
        # Index our objects into ES
        bulk_compat(
            actions=[
                *ES_INDICES.categories.get_es_documents(),
                *ES_INDICES.organizations.get_es_documents(),
                *ES_INDICES.courses.get_es_documents(),
            ],
            chunk_size=500,
            client=ES_CLIENT,
        )
        ES_INDICES_CLIENT.refresh()

        response = self.client.get("/api/v1.0/courses/?scope=filters")
        self.assertEqual(response.status_code, 200)

        self.assertEqual(
            response.json()["filters"]["subjects"],
            {
                "base_path":
                "0001",
                "human_name":
                "Subjects",
                "is_autocompletable":
                True,
                "is_drilldown":
                False,
                "is_searchable":
                True,
                "name":
                "subjects",
                "position":
                2,
                "has_more_values":
                False,
                "values": [{
                    "count": 1,
                    "human_name": subject.extended_object.get_title(),
                    "key": subject.get_es_id(),
                }],
            },
        )
        self.assertEqual(
            response.json()["filters"]["levels"],
            {
                "base_path":
                "0002",
                "human_name":
                "Levels",
                "is_autocompletable":
                True,
                "is_drilldown":
                False,
                "is_searchable":
                True,
                "name":
                "levels",
                "position":
                3,
                "has_more_values":
                False,
                "values": [{
                    "count": 1,
                    "human_name": level.extended_object.get_title(),
                    "key": level.get_es_id(),
                }],
            },
        )
        self.assertEqual(
            response.json()["filters"]["organizations"],
            {
                "base_path":
                "0003",
                "human_name":
                "Organizations",
                "is_autocompletable":
                True,
                "is_drilldown":
                False,
                "is_searchable":
                True,
                "name":
                "organizations",
                "position":
                4,
                "has_more_values":
                False,
                "values": [
                    {
                        "count": 1,
                        "human_name": org_1.extended_object.get_title(),
                        "key": org_1.get_es_id(),
                    },
                    {
                        "count": 1,
                        "human_name": org_2.extended_object.get_title(),
                        "key": org_2.get_es_id(),
                    },
                ],
            },
        )
예제 #28
0
    def test_models_course_create_permissions_for_organization(self, *_):
        """
        If the CMS_PERMISSIONS settings is True, a page and folder permission should be created
        for the course when calling the `create_permissions_for_organization` method.
        Calling the method several times should not duplicate permissions.
        """
        def get_random_role_dict():
            return {
                "courses_page_permissions": {
                    "can_change": random.choice([True, False]),
                    "can_add": random.choice([True, False]),
                    "can_delete": random.choice([True, False]),
                    "can_change_advanced_settings":
                    random.choice([True, False]),
                    "can_publish": random.choice([True, False]),
                    "can_change_permissions": random.choice([True, False]),
                    "can_move_page": random.choice([True, False]),
                    "can_view":
                    False,  # can_view = True would make it a view restriction...
                    "grant_on": random.randint(1, 5),
                },
                "courses_folder_permissions": {
                    "can_read": random.choice([True, False]),
                    "can_edit": random.choice([True, False]),
                    "can_add_children": random.choice([True, False]),
                    "type": random.randint(0, 2),
                },
            }

        course = CourseFactory()
        PageRoleFactory(page=course.extended_object, role="ADMIN")

        organization = OrganizationFactory()
        organization_role = PageRoleFactory(page=organization.extended_object,
                                            role="ADMIN")

        role_dict = get_random_role_dict()
        with mock.patch.dict(defaults.ORGANIZATION_ADMIN_ROLE, role_dict):
            course.create_permissions_for_organization(organization)

        # Call the method another time with different permissions to check it has no effect
        with mock.patch.dict(defaults.ORGANIZATION_ADMIN_ROLE,
                             get_random_role_dict()):
            course.create_permissions_for_organization(organization)

        # All expected permissions should have been assigned to the group:
        # - DjangoCMS page permissions
        self.assertEqual(
            PagePermission.objects.filter(
                group=organization_role.group).count(), 1)
        page_permission = PagePermission.objects.get(
            group=organization_role.group)
        for key, value in role_dict["courses_page_permissions"].items():
            self.assertEqual(getattr(page_permission, key), value)
        # The Django Filer folder permissions
        self.assertEqual(
            FolderPermission.objects.filter(
                group_id=organization_role.group_id).count(),
            1,
        )
        folder_permission = FolderPermission.objects.get(
            group_id=organization_role.group_id)
        for key, value in role_dict["courses_folder_permissions"].items():
            self.assertEqual(getattr(folder_permission, key), value)
    def test_templates_course_detail_cms_draft_content(self):
        """
        A staff user should see a draft course including its draft elements with
        an annotation
        """
        user = UserFactory(is_staff=True, is_superuser=True)
        self.client.login(username=user.username, password="******")

        categories = CategoryFactory.create_batch(4)
        organizations = OrganizationFactory.create_batch(4)

        course = CourseFactory(
            page_title="Very interesting course",
            fill_organizations=organizations,
            fill_categories=categories,
        )
        page = course.extended_object
        course_run1, _course_run2 = CourseRunFactory.create_batch(
            2, page_parent=course.extended_object, languages=["en", "fr"]
        )

        # Publish only 1 of the course runs
        course_run1.extended_object.publish("en")

        # Publish only 2 out of 4 categories and 2 out of 4 organizations
        categories[0].extended_object.publish("en")
        categories[1].extended_object.publish("en")
        organizations[0].extended_object.publish("en")
        organizations[1].extended_object.publish("en")

        # The unpublished objects may have been published and unpublished which puts them in a
        # status different from objects that have never been published.
        # We want to test both cases.
        categories[2].extended_object.publish("en")
        categories[2].extended_object.unpublish("en")
        organizations[2].extended_object.publish("en")
        organizations[2].extended_object.unpublish("en")

        # The page should be visible as draft to the staff user
        url = page.get_absolute_url()
        response = self.client.get(url)

        self.assertEqual(response.status_code, 200)

        self.assertContains(
            response, "<title>Very interesting course</title>", html=True
        )
        self.assertContains(
            response,
            '<h1 class="course-detail__content__title">Very interesting course</h1>',
            html=True,
        )

        # Draft and public organizations should all be present on the page
        for organization in organizations:
            self.assertContains(
                response,
                '<div class="organization-glimpse__title">{title:s}</div>'.format(
                    title=organization.extended_object.get_title()
                ),
                html=True,
            )

        # Draft organizations should be annotated for styling
        self.assertContains(response, "organization-glimpse--draft", count=2)

        # The published categories should be present on the page
        for category in categories[:2]:
            self.assertContains(
                response,
                (
                    '<a class="category-plugin-tag" href="{:s}">'
                    '<div class="category-plugin-tag__title">{:s}</div></a>'
                ).format(
                    category.extended_object.get_absolute_url(),
                    category.extended_object.get_title(),
                ),
                html=True,
            )
        # Draft categories should also be present on the page with an annotation for styling
        for category in categories[-2:]:
            self.assertContains(
                response,
                (
                    '<a class="{element:s} {element:s}--draft" href="{url:s}">'
                    '<div class="category-plugin-tag__title">{title:s}</div></a>'
                ).format(
                    url=category.extended_object.get_absolute_url(),
                    element="category-plugin-tag",
                    title=category.extended_object.get_title(),
                ),
                html=True,
            )
        # The draft and the published course runs should both be in the page
        self.assertContains(response, "<dd>English and french</dd>", html=True, count=2)
예제 #30
0
 def test_models_course_unique_code_draft(self):
     """The code field should be unique among all draft courses."""
     CourseFactory(code="123")
     with self.assertRaises(ValidationError):
         CourseFactory(code="123")