diff --git a/app.json b/app.json index 4ad97eddf..2f4318520 100644 --- a/app.json +++ b/app.json @@ -250,6 +250,10 @@ "description": "Form ID for Hubspot Forms API", "required": false }, + "HUBSPOT_ENTERPRISE_PAGE_FORM_ID": { + "description": "Form ID for Hubspot for Enterprise Page", + "required": false + }, "HUBSPOT_FOOTER_FORM_GUID": { "description": "Form guid over hub spot for footer block.", "required": false diff --git a/cms/api.py b/cms/api.py index a2a0b3029..606b02ade 100644 --- a/cms/api.py +++ b/cms/api.py @@ -7,7 +7,7 @@ from django.contrib.contenttypes.models import ContentType from wagtail.models import Page, Site from cms import models as cms_models -from cms.constants import CERTIFICATE_INDEX_SLUG +from cms.constants import CERTIFICATE_INDEX_SLUG, ENTERPRISE_PAGE_SLUG log = logging.getLogger(__name__) @@ -201,6 +201,37 @@ def ensure_index_pages(): # pylint: disable=too-many-branches home_page.add_child(instance=blog_index) +def ensure_enterprise_page(): + """ + Ensures that an enterprise page with the correct slug exists. + """ + enterprise_page = cms_models.EnterprisePage.objects.first() + + if enterprise_page and enterprise_page.slug == ENTERPRISE_PAGE_SLUG: + return + + enterprise_page_data = { + "title": "Enterprise Page", + "slug": ENTERPRISE_PAGE_SLUG, + "description": "Deepen your team’s career knowledge and expand their abilities with MIT xPRO’s online " + "courses for professionals.", + "action_title": "Find out what MIT xPRO can do for your team.", + "headings": [ + { + "type": "heading", + "value": { + "upper_head": "THE BEST COMPANIES", + "middle_head": "CONNECT WITH", + "bottom_head": "THE BEST MINDS AT MIT", + }, + }, + ], + } + enterprise_page = cms_models.EnterprisePage(**enterprise_page_data) + home_page = get_home_page() + home_page.add_child(instance=enterprise_page) + + def configure_wagtail(): """ Ensures that all appropriate changes have been made to Wagtail that will @@ -209,3 +240,4 @@ def configure_wagtail(): ensure_home_page_and_site() ensure_catalog_page() ensure_index_pages() + ensure_enterprise_page() diff --git a/cms/blocks.py b/cms/blocks.py index ce0070f99..bd26451f1 100644 --- a/cms/blocks.py +++ b/cms/blocks.py @@ -116,6 +116,48 @@ class CourseRunCertificateOverrides(blocks.StructBlock): ) +class BannerHeadingBlock(blocks.StructBlock): + """ + A custom block designed for creating banner headings on an enterprise page. + """ + + upper_head = blocks.CharBlock(max_length=25, help_text="The main heading.") + middle_head = blocks.CharBlock(max_length=25, help_text="Secondary heading.") + bottom_head = blocks.CharBlock(max_length=25, help_text="Lower heading.") + + class Meta: + icon = "title" + label = "Banner Headings" + + +class SuccessStoriesBlock(blocks.StructBlock): + """ + A custom block designed to represent an individual success story. + """ + + title = blocks.CharBlock( + max_length=255, help_text="Enter the title of the success story." + ) + image = ImageChooserBlock( + help_text="Select an image to accompany the success story.", + ) + content = blocks.TextBlock( + help_text="Provide the detailed content or description of the success story." + ) + call_to_action = blocks.CharBlock( + max_length=100, + default="Read More", + help_text="Enter the text for the call-to-action button (e.g., 'Read More').", + ) + action_url = blocks.URLBlock( + help_text="Provide the URL that the call-to-action button should link to.", + ) + + class Meta: + icon = "tick-inverse" + label = "Success Story" + + def validate_unique_readable_ids(value): """ Validates that all of the course run override blocks in this stream field have diff --git a/cms/constants.py b/cms/constants.py index ad7166927..99441f460 100644 --- a/cms/constants.py +++ b/cms/constants.py @@ -6,6 +6,7 @@ CERTIFICATE_INDEX_SLUG = "certificate" WEBINAR_INDEX_SLUG = "webinars" BLOG_INDEX_SLUG = "blog" +ENTERPRISE_PAGE_SLUG = "enterprise" ALL_TOPICS = "All Topics" ALL_TAB = "all-tab" diff --git a/cms/factories.py b/cms/factories.py index 1f11f093a..e82beb248 100644 --- a/cms/factories.py +++ b/cms/factories.py @@ -11,6 +11,7 @@ FacultyBlock, LearningTechniqueBlock, ResourceBlock, + SuccessStoriesBlock, UserTestimonialBlock, ) from cms.constants import UPCOMING_WEBINAR @@ -18,9 +19,11 @@ BlogIndexPage, CatalogPage, CertificatePage, + CompaniesLogoCarouselSection, CourseIndexPage, CoursePage, CoursesInProgramPage, + EnterprisePage, ExternalCoursePage, ExternalProgramPage, FacultyMembersPage, @@ -29,7 +32,9 @@ FrequentlyAskedQuestionPage, HomePage, ImageCarouselPage, + LearningJourneySection, LearningOutcomesPage, + LearningStrategyFormSection, LearningTechniquesPage, NewsAndEventsBlock, NewsAndEventsPage, @@ -38,6 +43,7 @@ ResourcePage, SignatoryPage, SiteNotification, + SuccessStoriesSection, TextSection, TextVideoSection, UserTestimonialsPage, @@ -500,3 +506,73 @@ class BlogIndexPageFactory(wagtail_factories.PageFactory): class Meta: model = BlogIndexPage + + +class EnterprisePageFactory(wagtail_factories.PageFactory): + """EnterprisePage factory""" + + class Meta: + model = EnterprisePage + + +class CompaniesLogoCarouselPageFactory(wagtail_factories.PageFactory): + """CompaniesLogoCarouselPage factory class""" + + heading = factory.fuzzy.FuzzyText(prefix="heading") + images = wagtail_factories.StreamFieldFactory( + {"image": factory.SubFactory(wagtail_factories.ImageChooserBlockFactory)} + ) + + class Meta: + model = CompaniesLogoCarouselSection + + +class LearningJourneyPageFactory(wagtail_factories.PageFactory): + """LearningJourneyPage factory class""" + + heading = factory.fuzzy.FuzzyText(prefix="heading ") + description = factory.fuzzy.FuzzyText() + journey_image = factory.SubFactory(wagtail_factories.ImageFactory) + journey_items = factory.SubFactory(wagtail_factories.StreamFieldFactory) + call_to_action = factory.fuzzy.FuzzyText(prefix="call_to_action ") + action_url = factory.Faker("uri") + pdf_file = factory.SubFactory(wagtail_factories.DocumentFactory) + + class Meta: + model = LearningJourneySection + + +class SuccessStoriesBlockFactory(wagtail_factories.StructBlockFactory): + """SuccessStoriesBlock factory class""" + + title = factory.fuzzy.FuzzyText(prefix="title ") + image = factory.SubFactory(wagtail_factories.ImageChooserBlockFactory) + content = factory.fuzzy.FuzzyText(prefix="content ") + call_to_action = factory.fuzzy.FuzzyText(prefix="call_to_action ") + action_url = factory.Faker("uri") + + class Meta: + model = SuccessStoriesBlock + + +class SuccessStoriesPageFactory(wagtail_factories.PageFactory): + """SuccessStoriesPage factory class""" + + heading = factory.fuzzy.FuzzyText(prefix="heading ") + subhead = factory.fuzzy.FuzzyText(prefix="Subhead ") + success_stories = wagtail_factories.StreamFieldFactory( + {"success_story": factory.SubFactory(SuccessStoriesBlockFactory)} + ) + + class Meta: + model = SuccessStoriesSection + + +class LearningStrategyFormPageFactory(wagtail_factories.PageFactory): + """LearningStrategyForm factory class""" + + heading = factory.fuzzy.FuzzyText(prefix="heading ") + subhead = factory.fuzzy.FuzzyText(prefix="Subhead ") + + class Meta: + model = LearningStrategyFormSection diff --git a/cms/migrations/0068_enterprisepage.py b/cms/migrations/0068_enterprisepage.py new file mode 100644 index 000000000..08c3e4cde --- /dev/null +++ b/cms/migrations/0068_enterprisepage.py @@ -0,0 +1,351 @@ +# Generated by Django 3.2.23 on 2024-01-11 10:03 + +import cms.models +from django.db import migrations, models +import django.db.models.deletion +import wagtail.blocks +import wagtail.fields +import wagtail.images.blocks + + +class Migration(migrations.Migration): + + dependencies = [ + ("wagtailcore", "0089_log_entry_data_json_null_to_object"), + ("wagtailimages", "0025_alter_image_file_alter_rendition_file"), + ("wagtaildocs", "0012_uploadeddocument"), + ("cms", "0067_wagtail_5_upgrade"), + ] + + operations = [ + migrations.CreateModel( + name="CompaniesLogoCarouselSection", + fields=[ + ( + "page_ptr", + models.OneToOneField( + auto_created=True, + on_delete=django.db.models.deletion.CASCADE, + parent_link=True, + primary_key=True, + serialize=False, + to="wagtailcore.page", + ), + ), + ( + "images", + wagtail.fields.StreamField( + [ + ( + "image", + wagtail.images.blocks.ImageChooserBlock( + help_text="Choose an image to upload." + ), + ) + ], + help_text="Add images for this section.", + use_json_field=True, + ), + ), + ( + "heading", + wagtail.fields.RichTextField( + help_text="The main heading of the Companies Logo Carousel section." + ), + ), + ], + options={ + "verbose_name": "Companies Logo Carousel", + }, + bases=("wagtailcore.page",), + ), + migrations.CreateModel( + name="LearningStrategyFormSection", + fields=[ + ( + "page_ptr", + models.OneToOneField( + auto_created=True, + on_delete=django.db.models.deletion.CASCADE, + parent_link=True, + primary_key=True, + serialize=False, + to="wagtailcore.page", + ), + ), + ( + "heading", + wagtail.fields.RichTextField( + help_text="Enter the main heading for the learning strategy form section." + ), + ), + ( + "subhead", + wagtail.fields.RichTextField( + help_text="A subheading to provide additional context or information." + ), + ), + ( + "consent", + wagtail.fields.RichTextField( + help_text="Enter the consent message to be displayed when users submit the form." + ), + ), + ], + options={ + "verbose_name": "Learning Strategy Form", + }, + bases=("wagtailcore.page",), + ), + migrations.CreateModel( + name="SuccessStoriesSection", + fields=[ + ( + "page_ptr", + models.OneToOneField( + auto_created=True, + on_delete=django.db.models.deletion.CASCADE, + parent_link=True, + primary_key=True, + serialize=False, + to="wagtailcore.page", + ), + ), + ( + "heading", + wagtail.fields.RichTextField( + help_text="The main heading for the success stories section." + ), + ), + ( + "subhead", + wagtail.fields.RichTextField( + help_text="A subheading to provide additional context or information." + ), + ), + ( + "success_stories", + wagtail.fields.StreamField( + [ + ( + "success_story", + wagtail.blocks.StructBlock( + [ + ( + "title", + wagtail.blocks.CharBlock( + help_text="Enter the title of the success story.", + max_length=255, + ), + ), + ( + "image", + wagtail.images.blocks.ImageChooserBlock( + help_text="Select an image to accompany the success story." + ), + ), + ( + "content", + wagtail.blocks.TextBlock( + help_text="Provide the detailed content or description of the success story." + ), + ), + ( + "call_to_action", + wagtail.blocks.CharBlock( + default="Read More", + help_text="Enter the text for the call-to-action button (e.g., 'Read More').", + max_length=100, + ), + ), + ( + "action_url", + wagtail.blocks.URLBlock( + help_text="Provide the URL that the call-to-action button should link to." + ), + ), + ] + ), + ) + ], + help_text="Manage the individual success stories. Each story is a separate block.", + use_json_field=True, + ), + ), + ], + options={ + "verbose_name": "Success Stories", + }, + bases=("wagtailcore.page",), + ), + migrations.CreateModel( + name="LearningJourneySection", + fields=[ + ( + "page_ptr", + models.OneToOneField( + auto_created=True, + on_delete=django.db.models.deletion.CASCADE, + parent_link=True, + primary_key=True, + serialize=False, + to="wagtailcore.page", + ), + ), + ( + "heading", + wagtail.fields.RichTextField( + help_text="The main heading of the learning journey section." + ), + ), + ( + "description", + wagtail.fields.RichTextField( + help_text="A detailed description of the learning journey section." + ), + ), + ( + "journey_items", + wagtail.fields.StreamField( + [("journey", wagtail.blocks.TextBlock(icon="plus"))], + help_text="Enter the text for this learning journey item.", + use_json_field=True, + ), + ), + ( + "call_to_action", + models.CharField( + default="View Full Diagram", + help_text="Text for the call-to-action button.", + max_length=30, + ), + ), + ( + "action_url", + models.URLField( + blank=True, + help_text="URL for the call-to-action button, used if no PDF is linked.", + null=True, + ), + ), + ( + "journey_image", + models.ForeignKey( + blank=True, + help_text="Optional image to visually represent the learning journey at least 560x618 pixels.", + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="+", + to="wagtailimages.image", + ), + ), + ( + "pdf_file", + models.ForeignKey( + blank=True, + help_text="PDF document linked to the call-to-action button, prioritized over the URL.", + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="+", + to="wagtaildocs.document", + ), + ), + ], + options={ + "verbose_name": "Learning Journey", + }, + bases=("wagtailcore.page",), + ), + migrations.CreateModel( + name="EnterprisePage", + fields=[ + ( + "page_ptr", + models.OneToOneField( + auto_created=True, + on_delete=django.db.models.deletion.CASCADE, + parent_link=True, + primary_key=True, + serialize=False, + to="wagtailcore.page", + ), + ), + ( + "headings", + wagtail.fields.StreamField( + [ + ( + "heading", + wagtail.blocks.StructBlock( + [ + ( + "upper_head", + wagtail.blocks.CharBlock( + help_text="The main heading.", + max_length=25, + ), + ), + ( + "middle_head", + wagtail.blocks.CharBlock( + help_text="Secondary heading.", + max_length=25, + ), + ), + ( + "bottom_head", + wagtail.blocks.CharBlock( + help_text="Lower heading.", + max_length=25, + ), + ), + ] + ), + ) + ], + help_text="Add banner headings for this page.", + use_json_field=True, + ), + ), + ( + "description", + wagtail.fields.RichTextField( + help_text="Enter a description for the call-to-action section under banner." + ), + ), + ( + "action_title", + models.CharField( + help_text="The text to show on the call to action button", + max_length=100, + ), + ), + ( + "background_image", + models.ForeignKey( + blank=True, + help_text="Background image size must be at least 1440x613 pixels.", + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="+", + to="wagtailimages.image", + ), + ), + ( + "overlay_image", + models.ForeignKey( + blank=True, + help_text="Select an overlay image for the banner section at leasr 544x444 pixels.", + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="+", + to="wagtailimages.image", + ), + ), + ], + options={ + "verbose_name": "Enterprise", + }, + bases=(cms.models.WagtailCachedPageMixin, "wagtailcore.page"), + ), + ] diff --git a/cms/models.py b/cms/models.py index 5db378618..8205f1ea2 100644 --- a/cms/models.py +++ b/cms/models.py @@ -19,7 +19,12 @@ from django.utils.functional import cached_property from django.utils.text import slugify from modelcluster.fields import ParentalKey, ParentalManyToManyField -from wagtail.admin.panels import FieldPanel, InlinePanel, TitleFieldPanel +from wagtail.admin.panels import ( + FieldPanel, + InlinePanel, + MultiFieldPanel, + TitleFieldPanel, +) from wagtail.blocks import ( CharBlock, PageChooserBlock, @@ -30,6 +35,7 @@ ) from wagtail.contrib.routable_page.models import RoutablePageMixin, route from wagtail.coreutils import WAGTAIL_APPEND_SLASH +from wagtail.documents.models import Document from wagtail.fields import RichTextField, StreamField from wagtail.images.blocks import ImageChooserBlock from wagtail.images.models import Image @@ -40,11 +46,13 @@ from blog.api import fetch_blog from cms.api import filter_and_sort_catalog_pages from cms.blocks import ( + BannerHeadingBlock, CourseRunCertificateOverrides, FacultyBlock, LearningTechniqueBlock, NewsAndEventsBlock, ResourceBlock, + SuccessStoriesBlock, UserTestimonialBlock, validate_unique_readable_ids, ) @@ -54,6 +62,7 @@ BLOG_INDEX_SLUG, CERTIFICATE_INDEX_SLUG, COURSE_INDEX_SLUG, + ENTERPRISE_PAGE_SLUG, FORMAT_ONLINE, FORMAT_OTHER, ON_DEMAND_WEBINAR, @@ -765,6 +774,7 @@ class HomePage(RoutablePageMixin, MetadataPageMixin, WagtailCachedPageMixin, Pag "SignatoryIndexPage", "WebinarIndexPage", "BlogIndexPage", + "EnterprisePage", ] @property @@ -1867,9 +1877,9 @@ class FacultyMembersPage(CourseProgramChildPage): ] -class ImageCarouselPage(CourseProgramChildPage): +class AbstractImageCarousel(Page): """ - Page that holds image carousel. + Abstract class that holds image carousel. """ images = StreamField( @@ -1881,6 +1891,15 @@ class ImageCarouselPage(CourseProgramChildPage): content_panels = [FieldPanel("title"), FieldPanel("images")] + class Meta: + abstract = True + + +class ImageCarouselPage(CourseProgramChildPage, AbstractImageCarousel): + """ + Page that holds image carousel. + """ + class Meta: verbose_name = "Image Carousel" @@ -2219,3 +2238,321 @@ class SiteNotification(models.Model): def __str__(self): return str(self.message) + + +class EnterpriseChildPage(Page): + """ + Abstract base class for pages that are children of an Enterprise Page. + + This model is not intended to be used directly but as a base for other specific page types. + It provides basic functionalities like auto-generating slugs and limiting page creation. + """ + + class Meta: + abstract = True + + parent_page_types = ["EnterprisePage"] + promote_panels = [] + subpage_types = [] + + @classmethod + def can_create_at(cls, parent): + """ + Ensures that only one instance of this page type can be created + under each parent. + """ + return ( + super().can_create_at(parent) + and not parent.get_children().type(cls).exists() + ) + + def save(self, clean=True, user=None, log_action=False, **kwargs): + """ + Auto-generates a slug for this page if it doesn't already have one. + + The slug is generated from the page title and its ID to ensure uniqueness. + """ + if not self.title: + self.title = self.__class__._meta.verbose_name.title() + + if not self.slug: + self.slug = slugify(f"{self.title}-{self.id}") + + super().save(clean=clean, user=user, log_action=log_action, **kwargs) + + def serve(self, request, *args, **kwargs): + """ + Prevents direct access to this page type by raising a 404 error. + + These pages are not intended to be standalone and should not be accessible by URL. + """ + raise Http404 + + +class CompaniesLogoCarouselSection(EnterpriseChildPage, AbstractImageCarousel): + """ + A custom page model for displaying a carousel of company trust logos. + """ + + heading = RichTextField( + help_text="The main heading of the Companies Logo Carousel section." + ) + + content_panels = [FieldPanel("heading"), FieldPanel("images")] + + class Meta: + verbose_name = "Companies Logo Carousel" + + +class LearningJourneySection(EnterpriseChildPage): + """ + A page model representing a section of a learning journey. + + This model includes a heading, a descriptive text, an optional image, and + a call-to-action button. The call-to-action button can be linked to either + a URL or a PDF document. The section also contains a list of learning + journey items. + """ + + heading = RichTextField( + help_text="The main heading of the learning journey section." + ) + description = RichTextField( + help_text="A detailed description of the learning journey section.", + ) + journey_image = models.ForeignKey( + Image, + null=True, + blank=True, + on_delete=models.SET_NULL, + related_name="+", + help_text="Optional image to visually represent the learning journey at least 560x618 pixels.", + ) + journey_items = StreamField( + [("journey", TextBlock(icon="plus"))], + blank=False, + help_text="Enter the text for this learning journey item.", + use_json_field=True, + ) + call_to_action = models.CharField( + max_length=30, + default="View Full Diagram", + help_text="Text for the call-to-action button.", + ) + action_url = models.URLField( + null=True, + blank=True, + help_text="URL for the call-to-action button, used if no PDF is linked.", + ) + pdf_file = models.ForeignKey( + Document, + null=True, + blank=True, + on_delete=models.SET_NULL, + related_name="+", + help_text="PDF document linked to the call-to-action button, prioritized over the URL.", + ) + + content_panels = [ + FieldPanel("heading"), + FieldPanel("journey_image"), + FieldPanel("journey_items"), + FieldPanel("description"), + MultiFieldPanel( + [ + FieldPanel("call_to_action"), + FieldPanel("action_url"), + FieldPanel("pdf_file"), + ], + heading="Button Settings", + ), + ] + + @property + def button_url(self): + """ + Determines the URL for the call-to-action button. + + The method gives priority to the linked PDF file's URL, + if no PDF is linked, it falls back to the action_url. + """ + return self.pdf_file.url if self.pdf_file else self.action_url + + def clean(self): + """Validates that either action_url or pdf_file must be added.""" + super().clean() + if not self.action_url and not self.pdf_file: + raise ValidationError( + "Please enter an Action URL or select a PDF document." + ) + + class Meta: + verbose_name = "Learning Journey" + + +class SuccessStoriesSection(EnterpriseChildPage): + """ + A page model for showcasing success stories related to an enterprise. + + This page includes a primary heading, an optional subheading, and a collection of + success stories. + """ + + heading = RichTextField( + help_text="The main heading for the success stories section." + ) + subhead = RichTextField( + help_text="A subheading to provide additional context or information.", + ) + success_stories = StreamField( + [("success_story", SuccessStoriesBlock())], + blank=False, + help_text="Manage the individual success stories. Each story is a separate block.", + use_json_field=True, + ) + + content_panels = [ + FieldPanel("heading"), + FieldPanel("subhead"), + FieldPanel("success_stories"), + ] + + class Meta: + verbose_name = "Success Stories" + + +class LearningStrategyFormSection(EnterpriseChildPage): + """ + A page model for a section dedicated to a learning strategy form. + + This section includes a main heading and an optional subheading. + The actual form is added by Hubspot in template. + """ + + heading = RichTextField( + help_text="Enter the main heading for the learning strategy form section.", + ) + subhead = RichTextField( + help_text="A subheading to provide additional context or information.", + ) + consent = RichTextField( + help_text="Enter the consent message to be displayed when users submit the form." + ) + + content_panels = [ + FieldPanel("heading"), + FieldPanel("subhead"), + FieldPanel("consent"), + ] + + class Meta: + verbose_name = "Learning Strategy Form" + + +class EnterprisePage(WagtailCachedPageMixin, Page): + """ + Represents an enterprise page in the CMS. + """ + + slug = ENTERPRISE_PAGE_SLUG + template = "enterprise_page.html" + parent_page_types = ["HomePage"] + subpage_types = [ + "CompaniesLogoCarouselSection", + "LearningJourneySection", + "SuccessStoriesSection", + "LearningStrategyFormSection", + ] + + headings = StreamField( + [("heading", BannerHeadingBlock())], + help_text="Add banner headings for this page.", + use_json_field=True, + ) + background_image = models.ForeignKey( + Image, + null=True, + blank=True, + on_delete=models.SET_NULL, + related_name="+", + help_text="Background image size must be at least 1440x613 pixels.", + ) + overlay_image = models.ForeignKey( + Image, + null=True, + blank=True, + on_delete=models.SET_NULL, + related_name="+", + help_text="Select an overlay image for the banner section at leasr 544x444 pixels.", + ) + description = RichTextField( + help_text="Enter a description for the call-to-action section under banner." + ) + action_title = models.CharField( + max_length=100, + help_text="The text to show on the call to action button", + ) + + content_panels = Page.content_panels + [ + FieldPanel("headings"), + FieldPanel("background_image"), + FieldPanel("overlay_image"), + FieldPanel("description"), + FieldPanel("action_title"), + ] + + class Meta: + verbose_name = "Enterprise" + + def serve(self, request, *args, **kwargs): + """ + Serves the enterprise page. + + This method is overridden to handle specific rendering needs for + the enterprise template, especially during previews. + """ + return Page.serve(self, request, *args, **kwargs) + + @property + def companies_logo_carousel(self): + """ + Gets the "Companies Logo Carousel" section subpage + """ + return self._get_child_page_of_type(CompaniesLogoCarouselSection) + + @property + def learning_journey(self): + """ + Gets the "Learning Journey" section subpage + """ + return self._get_child_page_of_type(LearningJourneySection) + + @property + def success_stories_carousel(self): + """ + Gets the "Success Stories Carousel" section subpage + """ + return self._get_child_page_of_type(SuccessStoriesSection) + + @property + def learning_strategy_form(self): + """ + Gets the "Learning Strategy Form" section subpage + """ + return self._get_child_page_of_type(LearningStrategyFormSection) + + def get_context(self, request, *args, **kwargs): + """ + Builds the context for rendering the enterprise page. + """ + return { + **super().get_context(request, *args, **kwargs), + **get_base_context(request), + "companies_logo_carousel": self.companies_logo_carousel, + "learning_journey": self.learning_journey, + "success_stories_carousel": self.success_stories_carousel, + "learning_strategy_form": self.learning_strategy_form, + "hubspot_enterprise_page_form_id": settings.HUBSPOT_CONFIG.get( + "HUBSPOT_ENTERPRISE_PAGE_FORM_ID" + ), + } diff --git a/cms/models_test.py b/cms/models_test.py index b733cb1e5..6e77bf0a5 100644 --- a/cms/models_test.py +++ b/cms/models_test.py @@ -11,18 +11,20 @@ from wagtail.coreutils import WAGTAIL_APPEND_SLASH from cms.constants import ( + FORMAT_ONLINE, + FORMAT_OTHER, ON_DEMAND_WEBINAR, ON_DEMAND_WEBINAR_BUTTON_TITLE, UPCOMING_WEBINAR, UPCOMING_WEBINAR_BUTTON_TITLE, WEBINAR_HEADER_BANNER, - FORMAT_ONLINE, - FORMAT_OTHER, ) from cms.factories import ( CertificatePageFactory, + CompaniesLogoCarouselPageFactory, CoursePageFactory, CoursesInProgramPageFactory, + EnterprisePageFactory, ExternalCoursePageFactory, ExternalProgramPageFactory, FacultyMembersPageFactory, @@ -31,7 +33,9 @@ FrequentlyAskedQuestionPageFactory, HomePageFactory, ImageCarouselPageFactory, + LearningJourneyPageFactory, LearningOutcomesPageFactory, + LearningStrategyFormPageFactory, LearningTechniquesPageFactory, NewsAndEventsPageFactory, ProgramFactory, @@ -39,6 +43,7 @@ ResourcePageFactory, SignatoryPageFactory, SiteNotificationFactory, + SuccessStoriesPageFactory, TextSectionFactory, TextVideoSectionFactory, UserTestimonialsPageFactory, @@ -51,6 +56,7 @@ CoursesInProgramPage, ForTeamsPage, FrequentlyAskedQuestionPage, + LearningJourneySection, LearningOutcomesPage, LearningTechniquesPage, SignatoryPage, @@ -1602,3 +1608,133 @@ def _assert_news_and_events_values(news_and_events_page): assert news_and_events.value.get("content") == f"content-{count}" assert news_and_events.value.get("call_to_action") == f"call_to_action-{count}" assert news_and_events.value.get("action_url") == f"action_url-{count}" + + +def test_enterprise_page_companies_logo_carousel(): + """ + companies_logo_carousel property should return expected values. + """ + + enterprise_page = EnterprisePageFactory.create( + action_title="title", description="description" + ) + assert not enterprise_page.companies_logo_carousel + + del enterprise_page.child_pages + + companies_logo_carousel = CompaniesLogoCarouselPageFactory.create( + parent=enterprise_page, + heading="heading", + images__0__image__image__title="image-title-0", + images__1__image__image__title="image-title-1", + images__2__image__image__title="image-title-2", + images__3__image__image__title="image-title-3", + ) + + assert enterprise_page.companies_logo_carousel == companies_logo_carousel + assert companies_logo_carousel.heading == "heading" + + for index, image in enumerate(companies_logo_carousel.images): + assert image.value.title == "image-title-{}".format(index) + + +def test_enterprise_page_learning_journey(): + """ + LearningJourneyPage should return expected values if it exists + """ + + enterprise_page = EnterprisePageFactory.create( + action_title="title", description="description" + ) + + assert not enterprise_page.learning_journey + assert LearningJourneySection.can_create_at(enterprise_page) + + learning_journey = LearningJourneyPageFactory( + parent=enterprise_page, + heading="heading", + description="description", + journey_items=json.dumps([{"type": "journey", "value": "value"}]), + journey_image__title="background-image", + ) + + assert learning_journey.get_parent() == enterprise_page + assert learning_journey.heading == "heading" + assert learning_journey.description == "description" + + for block in learning_journey.journey_items: # pylint: disable=not-an-iterable + assert block.block_type == "journey" + assert block.value == "value" + + assert learning_journey.action_url + assert learning_journey.pdf_file + + del enterprise_page.child_pages + + assert enterprise_page.learning_journey == learning_journey + assert not LearningOutcomesPage.can_create_at(enterprise_page) + + +def test_enterprise_page_success_stories(): + """ + SuccessStories subpage should provide expected values + """ + + enterprise_page = EnterprisePageFactory.create( + action_title="title", description="description" + ) + + assert not enterprise_page.success_stories_carousel + del enterprise_page.child_pages + + success_stories_carousel = SuccessStoriesPageFactory.create( + parent=enterprise_page, + heading="heading", + subhead="subhead", + success_stories__0__success_story__title="title", + success_stories__0__success_story__image__image__title="image", + success_stories__0__success_story__content="content", + success_stories__0__success_story__call_to_action="call_to_action", + success_stories__0__success_story__action_url="action_url", + success_stories__1__success_story__title="title", + success_stories__1__success_story__image__image__title="image", + success_stories__1__success_story__content="content", + success_stories__1__success_story__call_to_action="call_to_action", + success_stories__1__success_story__action_url="action_url", + ) + + assert enterprise_page.success_stories_carousel == success_stories_carousel + assert success_stories_carousel.heading == "heading" + assert success_stories_carousel.subhead == "subhead" + + for success_stories in success_stories_carousel.success_stories: + assert success_stories.value.get("title") == "title" + assert success_stories.value.get("image").title == "image" + assert success_stories.value.get("content") == "content" + assert success_stories.value.get("call_to_action") == "call_to_action" + assert success_stories.value.get("action_url") == "action_url" + + +def test_enterprise_page_learning_strategy_form(): + """ + LearningStrategyForm subpage should provide expected values + """ + + enterprise_page = EnterprisePageFactory.create( + action_title="title", description="description" + ) + + assert not enterprise_page.learning_strategy_form + del enterprise_page.child_pages + + learning_strategy_form = LearningStrategyFormPageFactory.create( + parent=enterprise_page, + heading="heading", + subhead="subhead", + consent="consent", + ) + + assert enterprise_page.learning_strategy_form == learning_strategy_form + assert learning_strategy_form.heading == "heading" + assert learning_strategy_form.subhead == "subhead" + assert learning_strategy_form.consent == "consent" diff --git a/cms/templates/enterprise_page.html b/cms/templates/enterprise_page.html new file mode 100644 index 000000000..4f3c88512 --- /dev/null +++ b/cms/templates/enterprise_page.html @@ -0,0 +1,40 @@ +{% extends "base.html" %} + +{% load static render_bundle image_version_url %} + +{% block title %}{{ page.title }}{% endblock %} + +{% block headercontent %} +
+{% render_bundle 'header' %} +{% endblock %} + + +{% block content %} + +