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)
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(), "")
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")
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(), "")
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)
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)
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())
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")
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(), )
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)
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)
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 ], )
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")
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.")
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()), [])
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")
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())
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.")
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, )
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")
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())
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(), }, ], }, )
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)
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")