From 2493394caf212ffbe9c18f403b388f1781854f51 Mon Sep 17 00:00:00 2001 From: Tiago-Salles Date: Wed, 16 Oct 2024 14:39:06 +0000 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8(frontend)=20add=20faqs=20to=20a=20cat?= =?UTF-8?q?egory=20and=20get=20from=20a=20course?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - add course_faq placeholder to category detail page - get the course_faq content from the category detail page --- CHANGELOG.md | 2 + src/richie/apps/courses/settings/__init__.py | 19 + .../courses/cms/category_detail.html | 13 + .../templates/courses/cms/course_detail.html | 17 + .../apps/courses/templatetags/extra_tags.py | 33 ++ .../courses/test_templates_category_detail.py | 161 ++++++++ ...categories_pages_additional_information.py | 386 ++++++++++++++++++ 7 files changed, 631 insertions(+) create mode 100644 tests/apps/courses/test_templatetags_extra_tags_get_categories_pages_additional_information.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 1e57d9a8ee..3932c1ab1f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,6 +32,8 @@ Versioning](https://semver.org/spec/v2.0.0.html). in the learner dashboard - Add `PaymentScheduleHelper` utils - Display installment information on certificate product blocks +- Add Additional Information section for a category and + use them in a course page ### Changed diff --git a/src/richie/apps/courses/settings/__init__.py b/src/richie/apps/courses/settings/__init__.py index 3e54800989..9034b19942 100644 --- a/src/richie/apps/courses/settings/__init__.py +++ b/src/richie/apps/courses/settings/__init__.py @@ -304,6 +304,25 @@ def richie_placeholder_conf(name): "plugins": ["CKEditorPlugin"], "limits": {"CKEditorPlugin": 1}, }, + "courses/cms/category_detail.html additional_information": { + "name": _("Additional Information"), + "plugins": ["SectionPlugin"], + "parent_classes": { + "CKEditorPlugin": ["SectionPlugin"], + "SimplePicturePlugin": ["SectionPlugin"], + "GlimpsePlugin": ["SectionPlugin"], + "NestedItemPlugin": ["SectionPlugin"], + }, + "child_classes": { + "SectionPlugin": [ + "CKEditorPlugin", + "SimplePicturePlugin", + "GlimpsePlugin", + "NestedItemPlugin", + ], + "NestedItemPlugin": ["NestedItemPlugin"], + }, + }, # Person detail "courses/cms/person_detail.html categories": { "name": _("Categories"), diff --git a/src/richie/apps/courses/templates/courses/cms/category_detail.html b/src/richie/apps/courses/templates/courses/cms/category_detail.html index 2746f8a67a..c60317d924 100644 --- a/src/richie/apps/courses/templates/courses/cms/category_detail.html +++ b/src/richie/apps/courses/templates/courses/cms/category_detail.html @@ -190,6 +190,19 @@

{% trans "Related blogposts" %}

{% endif %} {% endwith %} +
+
+ {% if not current_page|is_empty_placeholder:"additional_information" and not current_page.reverse_id %} +

{% trans 'Configure this page id to show this additional information on all related course pages' %}

+ {% endif %} + {% placeholder "additional_information" or %} + {% if request.toolbar.edit_mode_active %} +

{% trans 'Enter additional information for this category' %}

+ {% endif %} + {% endplaceholder %} +
+
+ {% with persons=category.get_persons %} {% if persons %} {% autopaginate persons GLIMPSE_PAGINATION_PERSONS %} diff --git a/src/richie/apps/courses/templates/courses/cms/course_detail.html b/src/richie/apps/courses/templates/courses/cms/course_detail.html index 9077a76bfd..82e9f0f7a3 100644 --- a/src/richie/apps/courses/templates/courses/cms/course_detail.html +++ b/src/richie/apps/courses/templates/courses/cms/course_detail.html @@ -472,6 +472,23 @@

{% endif %} {% endblock information %} + {% block category_additional_information %} + + + {% get_categories_pages_additional_information current_page.course as pages %} + {% if pages %} +
+ {% with is_syllabus_property=True %} + {% for page in pages %} + {% with reverse_id=page.reverse_id %} + {% show_placeholder "additional_information" reverse_id %} + {% endwith %} + {% endfor %} + {% endwith %} +
+ {% endif %} + {% endblock category_additional_information %} + {% block licenses %} {% if current_page.publisher_is_draft or not current_page|is_empty_placeholder:"course_license_content" or not current_page|is_empty_placeholder:"course_license_participation" %}
diff --git a/src/richie/apps/courses/templatetags/extra_tags.py b/src/richie/apps/courses/templatetags/extra_tags.py index 550952729c..cd0cff4e28 100644 --- a/src/richie/apps/courses/templatetags/extra_tags.py +++ b/src/richie/apps/courses/templatetags/extra_tags.py @@ -13,6 +13,7 @@ from classytags.arguments import Argument, MultiValueArgument from classytags.core import Options, Tag from classytags.utils import flatten_context +from cms.api import Page from cms.templatetags.cms_tags import ( Placeholder, PlaceholderOptions, @@ -23,6 +24,8 @@ from cms.utils.plugins import get_plugins from richie.apps.courses.defaults import RICHIE_MAX_ARCHIVED_COURSE_RUNS +from richie.apps.courses.models.category import Category +from richie.apps.courses.models.course import Course from ..lms import LMSHandler from ..models import CourseRunCatalogVisibility @@ -271,6 +274,36 @@ def joanie_product_widget_props(context): return json.dumps({"productId": product_id, "courseCode": course_code}) +@register.simple_tag() +def get_categories_pages_additional_information(course: Course) -> list[Page]: + """ + Return categories pages have additional information and also have a page id. + + usage: `{% get_categories_pages_additional_information current_page.course as pages %}` + """ + + categories_pages: list[Category] = course.get_categories() + categories_pages = categories_pages.filter( + extended_object__reverse_id__isnull=False + ) + + categories_with_information: list[Category] = [] + for category in categories_pages: + additional_information = category.extended_object.get_placeholders().get( + slot="additional_information" + ) + plugins = additional_information.get_plugins() + + if len(plugins) > 0: + categories_with_information.append(category) + + pages_have_additional_information: list[Page] = [ + category.extended_object for category in categories_with_information + ] + + return pages_have_additional_information + + @register.simple_tag(takes_context=True) def course_runs_list_widget_props(context): """ diff --git a/tests/apps/courses/test_templates_category_detail.py b/tests/apps/courses/test_templates_category_detail.py index e67b927194..8a1243f5c7 100644 --- a/tests/apps/courses/test_templates_category_detail.py +++ b/tests/apps/courses/test_templates_category_detail.py @@ -21,6 +21,7 @@ OrganizationFactory, PersonFactory, ) +from richie.plugins.nesteditem.defaults import ACCORDION class CategoryCMSTestCase(CMSTestCase): @@ -553,3 +554,163 @@ def test_template_category_detail_meta_description_empty(self): response, ' CategoryFactory: + """ + This method adds additional information to a category + """ + + placeholder = component.extended_object.placeholders.get( + slot="additional_information" + ) + + section = add_plugin( + language="en", + placeholder=placeholder, + plugin_type="SectionPlugin", + title="Additional Information", + ) + + container = add_plugin( + language="en", + placeholder=placeholder, + plugin_type="NestedItemPlugin", + variant=ACCORDION, + target=section, + ) + + for question in range(1, 3): + question_container = add_plugin( + language="en", + placeholder=placeholder, + plugin_type="NestedItemPlugin", + target=container, + content=f"{question}. question?", + variant=ACCORDION, + ) + + add_plugin( + language="en", + placeholder=placeholder, + plugin_type="NestedItemPlugin", + target=question_container, + content=f"Answer of question {question}.", + variant=ACCORDION, + ) + + return component + + @transaction.atomic + def test_get_categories_pages_additional_information_filled_list_with_lookup(self): + """ + This test validates when a course has categories with additional information and + the page_lookup `reverse_id` set the custom tag + `get_categories_pages_additional_information` must return a list + with the corresponding categories pages + """ + + category1 = CategoryFactory.create(page_title="Accessible", should_publish=True) + category2 = CategoryFactory.create( + page_title="Earth and universe sciences", should_publish=True + ) + icon1 = CategoryFactory.create( + page_title="Available on edX.org", should_publish=True + ) + icon2 = CategoryFactory.create( + page_title="Payment promotion", should_publish=True + ) + + for component in [category1, category2, icon1, icon2]: + reverse_id = component.extended_object.get_title().lower().replace(" ", "-") + component.extended_object.reverse_id = reverse_id + component.extended_object.save() + + for component in [category1, icon1]: + component = self._add_info(component) + + course: Course = CourseFactory.create( + fill_categories=[category1, category2], + fill_icons=[icon1, icon2], + ) + + all_categories = course.get_categories() + categories_pages_have_info = get_categories_pages_additional_information(course) + + self.assertTrue(len(all_categories) == 4) + self.assertTrue(isinstance(categories_pages_have_info, list)) + self.assertTrue(len(categories_pages_have_info) == 2) + + for page in categories_pages_have_info: + self.assertTrue(page.get_title() in ["Accessible", "Available on edX.org"]) + plugins = ( + page.get_placeholders().get(slot="additional_information").get_plugins() + ) + self.assertTrue(len(plugins) > 0) + + self.assertTrue(plugins[0].plugin_type == "SectionPlugin") + + for plugin in plugins[1:]: + self.assertTrue(plugin.plugin_type == "NestedItemPlugin") + + @transaction.atomic + def test_get_categories_pages_additional_information_empty_list_with_lookup(self): + """ + This test validates when a course does not have categories with additional information + but with the page_lookup `reverse_id` set the custom tag + `get_categories_pages_additional_information` must return an empty + """ + + category1 = CategoryFactory.create(page_title="Accessible", should_publish=True) + category2 = CategoryFactory.create( + page_title="Earth and universe sciences", should_publish=True + ) + icon1 = CategoryFactory.create( + page_title="Available on edX.org", should_publish=True + ) + icon2 = CategoryFactory.create( + page_title="Payment promotion", should_publish=True + ) + + for component in [category1, category2, icon1, icon2]: + reverse_id = component.extended_object.get_title().lower().replace(" ", "-") + component.extended_object.reverse_id = reverse_id + component.extended_object.save() + + course: Course = CourseFactory.create( + fill_categories=[category1, category2], + fill_icons=[icon1, icon2], + ) + + all_categories = course.get_categories() + categories_pages_have_info = get_categories_pages_additional_information(course) + + self.assertTrue(len(all_categories) == 4) + self.assertTrue(isinstance(categories_pages_have_info, list)) + self.assertTrue(len(categories_pages_have_info) == 0) + + for category in all_categories: + page = category.extended_object + plugins = ( + page.get_placeholders().get(slot="additional_information").get_plugins() + ) + self.assertTrue(len(plugins) == 0) + + @transaction.atomic + def test_get_categories_pages_additional_information_filled_list_no_lookup(self): + """ + This test validates when a course has categories with additional information but + without page_lookup `reverse_id` the custom tag + `get_categories_pages_additional_information` must return an empty list + """ + + category1 = CategoryFactory.create(page_title="Accessible", should_publish=True) + category2 = CategoryFactory.create( + page_title="Earth and universe sciences", should_publish=True + ) + icon1 = CategoryFactory.create( + page_title="Available on edX.org", should_publish=True + ) + icon2 = CategoryFactory.create( + page_title="Payment promotion", should_publish=True + ) + + for component in [category1, icon1]: + component = self._add_info(component) + + course: Course = CourseFactory.create( + fill_categories=[category1, category2], + fill_icons=[icon1, icon2], + ) + + all_categories = course.get_categories() + categories_pages_have_info = get_categories_pages_additional_information(course) + + self.assertTrue(len(all_categories) == 4) + + for category in all_categories: + page = category.extended_object + self.assertTrue(page.reverse_id is None) + + categories_have_info = [ + category + for category in all_categories + if len( + category.extended_object.get_placeholders() + .get(slot="additional_information") + .get_plugins() + ) + > 0 + ] + self.assertTrue(len(categories_have_info) == 2) + + for category in categories_have_info: + page = category.extended_object + self.assertTrue(page.get_title() in ["Accessible", "Available on edX.org"]) + plugins = ( + page.get_placeholders().get(slot="additional_information").get_plugins() + ) + + self.assertTrue(plugins[0].plugin_type == "SectionPlugin") + + for plugin in plugins[1:]: + self.assertTrue(plugin.plugin_type == "NestedItemPlugin") + + self.assertTrue(isinstance(categories_pages_have_info, list)) + self.assertTrue(len(categories_pages_have_info) == 0) + + @transaction.atomic + def test_get_categories_pages_additional_information_filled_list_content(self): + """ + This test validates when a course has categories with additional information it + will insert to the content page their information + """ + + category1 = CategoryFactory.create(page_title="Accessible", should_publish=True) + category2 = CategoryFactory.create( + page_title="Earth and universe sciences", should_publish=True + ) + icon1 = CategoryFactory.create( + page_title="Available on edX.org", should_publish=True + ) + icon2 = CategoryFactory.create( + page_title="Payment promotion", should_publish=True + ) + + for component in [category1, category2, icon1, icon2]: + reverse_id = component.extended_object.get_title().lower().replace(" ", "-") + component.extended_object.reverse_id = reverse_id + component.extended_object.save() + + for component in [category1, icon1]: + component = self._add_info(component) + component.get_page().publish("en") + + course: Course = CourseFactory.create( + fill_categories=[category1, category2], + fill_icons=[icon1, icon2], + ) + + all_categories = course.get_categories() + categories_pages_have_info = get_categories_pages_additional_information(course) + + self.assertTrue(len(all_categories) == 4) + self.assertTrue(isinstance(categories_pages_have_info, list)) + self.assertTrue(len(categories_pages_have_info) == 2) + + page = course.get_page() + page.publish("en") + url = page.get_absolute_url() + response = self.client.get(url) + + self.assertEqual(response.status_code, 200) + self.assertContains( + response, + '
', + ) + + for question in range(1, 3): + self.assertContains( + response, f'' + ) + self.assertContains( + response, + f'', + ) + + @transaction.atomic + def test_get_categories_pages_additional_information_content_no_lookup(self): + """ + This test validates when a course has categories with additional information but + without `reverse_id` it will not insert the section title nor the + section block + """ + + category1 = CategoryFactory.create(page_title="Accessible", should_publish=True) + category2 = CategoryFactory.create( + page_title="Earth and universe sciences", should_publish=True + ) + icon1 = CategoryFactory.create( + page_title="Available on edX.org", should_publish=True + ) + icon2 = CategoryFactory.create( + page_title="Payment promotion", should_publish=True + ) + + for component in [category1, icon1]: + component = self._add_info(component) + component.get_page().publish("en") + + course: Course = CourseFactory.create( + fill_categories=[category1, category2], + fill_icons=[icon1, icon2], + ) + + page = course.get_page() + page.publish("en") + url = page.get_absolute_url() + response = self.client.get(url) + + self.assertEqual(response.status_code, 200) + self.assertNotContains( + response, + '
', + ) + + for question in range(1, 3): + self.assertNotContains( + response, f'' + ) + self.assertNotContains( + response, + f'', + ) + + @transaction.atomic + def test_get_categories_pages_additional_information_no_content_with_lookup(self): + """ + This test validates when a course does not have categories with additional information but + with `reverse_id` it will not insert the section title nor the section block + """ + + category1 = CategoryFactory.create(page_title="Accessible", should_publish=True) + category2 = CategoryFactory.create( + page_title="Earth and universe sciences", should_publish=True + ) + icon1 = CategoryFactory.create( + page_title="Available on edX.org", should_publish=True + ) + icon2 = CategoryFactory.create( + page_title="Payment promotion", should_publish=True + ) + + for component in [category1, category2, icon1, icon2]: + reverse_id = component.extended_object.get_title().lower().replace(" ", "-") + component.extended_object.reverse_id = reverse_id + component.extended_object.save() + component.get_page().publish("en") + + course: Course = CourseFactory.create( + fill_categories=[category1, category2], + fill_icons=[icon1, icon2], + ) + + page = course.get_page() + page.publish("en") + url = page.get_absolute_url() + response = self.client.get(url) + + self.assertEqual(response.status_code, 200) + self.assertNotContains( + response, + '
', + ) + + for question in range(1, 3): + self.assertNotContains( + response, f'' + ) + self.assertNotContains( + response, + f'', + )