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 %} + +
+ + {% include "partials/enterprise/banner.html" %} + + {% if companies_logo_carousel %} + {% include "partials/enterprise/companies_logo_carousel.html" with page=companies_logo_carousel %} + {% endif %} + + {% if learning_journey %} +
+ {% include "partials/enterprise/learning_journey.html" with page=learning_journey %} + {% endif %} + + {% if success_stories_carousel %} + {% include "partials/enterprise/success_stories.html" with page=success_stories_carousel %} + {% endif %} + + {% if hubspot_portal_id and hubspot_enterprise_page_form_id %} +
+ {% if learning_strategy_form %} + {% include "partials/enterprise/learning_strategy_form.html" with page=learning_strategy_form %} + {% endif %} + {% endif %} +
+ +{% endblock %} diff --git a/cms/templates/partials/enterprise/banner.html b/cms/templates/partials/enterprise/banner.html new file mode 100644 index 000000000..c7084242c --- /dev/null +++ b/cms/templates/partials/enterprise/banner.html @@ -0,0 +1,52 @@ +{% load static wagtailembeds_tags image_version_url wagtailcore_tags %} + +{% block extrahead %} + +{% endblock %} + +
+
+
+
+ {% for block in page.headings %} +

{{ block.value.upper_head|upper }}

+

{{ block.value.middle_head|upper }}

+

{{ block.value.bottom_head|upper }}

+ {% endfor %} + Inquire Now +
+ {% if page.overlay_image %} + Banner Hero Image + {% else %} + Banner Hero Image + {% endif %} +
+
+
+
+
+
+
{{ page.description|richtext }}
+ {{ page.action_title }} +
+
+
diff --git a/cms/templates/partials/enterprise/companies_logo_carousel.html b/cms/templates/partials/enterprise/companies_logo_carousel.html new file mode 100644 index 000000000..d7209b12f --- /dev/null +++ b/cms/templates/partials/enterprise/companies_logo_carousel.html @@ -0,0 +1,16 @@ +{% load image_version_url wagtailcore_tags %} + +
+
+
{{ companies_logo_carousel.heading|richtext }}
+ +
+
diff --git a/cms/templates/partials/enterprise/learning_journey.html b/cms/templates/partials/enterprise/learning_journey.html new file mode 100644 index 000000000..dbfcc85c8 --- /dev/null +++ b/cms/templates/partials/enterprise/learning_journey.html @@ -0,0 +1,23 @@ +{% load static image_version_url wagtailcore_tags %} + +
+
+
{{ page.heading|richtext }}
+
+
+
    + {% for journey_item in page.journey_items %} +
  • {{ journey_item }}
  • + {% endfor %} +
+
{{ page.description|richtext }}
+ {{ page.call_to_action|upper }} +
+ {% if page.journey_image %} + Learning Journey + {% else %} + Learning Journey + {% endif %} +
+
+
diff --git a/cms/templates/partials/enterprise/learning_strategy_form.html b/cms/templates/partials/enterprise/learning_strategy_form.html new file mode 100644 index 000000000..ac2a9c9e1 --- /dev/null +++ b/cms/templates/partials/enterprise/learning_strategy_form.html @@ -0,0 +1,27 @@ +{% load wagtailcore_tags %} + +
+
+
{{ page.heading|richtext }}
+
{{ page.subhead|richtext }}
+
+ + + + +
+
+
diff --git a/cms/templates/partials/enterprise/success_stories.html b/cms/templates/partials/enterprise/success_stories.html new file mode 100644 index 000000000..bb16f5df3 --- /dev/null +++ b/cms/templates/partials/enterprise/success_stories.html @@ -0,0 +1,24 @@ +{% load image_version_url wagtailcore_tags %} + +
+
+
+
{{ page.heading|richtext }}
+
{{ page.subhead|richtext }}
+
+
+ {% for block in page.success_stories %} +
+
+ {{ block.value.title }} +
+

{{ block.value.title }}

+

{{ block.value.content }}

+ {{ block.value.call_to_action }} +
+
+
+ {% endfor %} +
+
+
diff --git a/cms/views_test.py b/cms/views_test.py index 99e97635e..22afeb63b 100644 --- a/cms/views_test.py +++ b/cms/views_test.py @@ -4,6 +4,7 @@ import factory import pytest +from django.conf import settings from django.core.cache import cache from django.core.exceptions import ValidationError from django.urls import reverse @@ -17,6 +18,7 @@ WEBINAR_DEFAULT_IMAGES, ) from cms.factories import ( + EnterprisePageFactory, BlogIndexPageFactory, CatalogPageFactory, CourseIndexPageFactory, @@ -580,3 +582,28 @@ def test_blog_page_context(client, wagtail_basics): context = resp.context_data assert "posts" in context + + +def test_enterprise_page_context(client, wagtail_basics): + """ + Test that enterprise page show correctly + """ + enterprise_page = EnterprisePageFactory.create( + parent=wagtail_basics.root, action_title="Read More", description="description" + ) + enterprise_page.save_revision().publish() + + resp = client.get(enterprise_page.get_url()) + context = resp.context_data + + assert resp.status_code == status.HTTP_200_OK + assert context["page"] == enterprise_page + + assert "companies_logo_carousel" in context + assert "learning_journey" in context + assert "success_stories_carousel" in context + assert "learning_strategy_form" in context + + assert context["hubspot_enterprise_page_form_id"] == settings.HUBSPOT_CONFIG.get( + "HUBSPOT_ENTERPRISE_PAGE_FORM_ID" + ) diff --git a/mitxpro/settings.py b/mitxpro/settings.py index 13ba4eda7..914218c88 100644 --- a/mitxpro/settings.py +++ b/mitxpro/settings.py @@ -1261,6 +1261,11 @@ default=None, description="Form ID for Hubspot Forms API", ), + "HUBSPOT_ENTERPRISE_PAGE_FORM_ID": get_string( + name="HUBSPOT_ENTERPRISE_PAGE_FORM_ID", + default=None, + description="Form ID for Hubspot for Enterprise Page", + ), } # Sheets settings diff --git a/static/images/enterprise/ctrl-left.png b/static/images/enterprise/ctrl-left.png new file mode 100644 index 000000000..283a2786e Binary files /dev/null and b/static/images/enterprise/ctrl-left.png differ diff --git a/static/images/enterprise/ctrl-right.png b/static/images/enterprise/ctrl-right.png new file mode 100644 index 000000000..aced1f5a7 Binary files /dev/null and b/static/images/enterprise/ctrl-right.png differ diff --git a/static/images/enterprise/enterprise-page-banner-bg.png b/static/images/enterprise/enterprise-page-banner-bg.png new file mode 100644 index 000000000..7db16716a Binary files /dev/null and b/static/images/enterprise/enterprise-page-banner-bg.png differ diff --git a/static/images/enterprise/enterprise-page-banner-hero.png b/static/images/enterprise/enterprise-page-banner-hero.png new file mode 100644 index 000000000..b88f6e815 Binary files /dev/null and b/static/images/enterprise/enterprise-page-banner-hero.png differ diff --git a/static/images/enterprise/enterprise-page-dots-bg.png b/static/images/enterprise/enterprise-page-dots-bg.png new file mode 100644 index 000000000..977936eb2 Binary files /dev/null and b/static/images/enterprise/enterprise-page-dots-bg.png differ diff --git a/static/images/enterprise/enterprise-page-learning-path.png b/static/images/enterprise/enterprise-page-learning-path.png new file mode 100644 index 000000000..1436d70e4 Binary files /dev/null and b/static/images/enterprise/enterprise-page-learning-path.png differ diff --git a/static/js/companies_logo_carousel.js b/static/js/companies_logo_carousel.js new file mode 100644 index 000000000..d907c6bee --- /dev/null +++ b/static/js/companies_logo_carousel.js @@ -0,0 +1,40 @@ +/*eslint-env jquery*/ +/*eslint semi: ["error", "always"]*/ + +const numLogoSlides = $(".companies-logo-carousel .slide").length; + +export default function companiesLogoCarousel() { + $(".companies-logo-carousel").slick({ + rows: 0, + slidesToShow: 6, + slidesToScroll: 3, + infinite: false, + autoplay: false, + dots: numLogoSlides > 6, + autoplaySpeed: 2000, + responsive: [ + { + breakpoint: 1200, + settings: { + slidesToShow: 4, + dots: numLogoSlides > 4 + } + }, + { + breakpoint: 992, + settings: { + slidesToShow: 3, + dots: numLogoSlides > 3 + } + }, + { + breakpoint: 767, + settings: { + slidesToShow: 2, + slidesToScroll: 2, + dots: numLogoSlides > 2 + } + } + ] + }); +} diff --git a/static/js/entry/django.js b/static/js/entry/django.js index 098933574..c2a3c63b5 100644 --- a/static/js/entry/django.js +++ b/static/js/entry/django.js @@ -19,6 +19,8 @@ import facultyCarousel from "../faculty_carousel.js" import productDetails from "../product_detail.js" import topicsCarousel from "../catalog-topics-carousel.js" import blogPostsCarousel from "../blog_posts_carousel" +import companiesLogoCarousel from "../companies_logo_carousel.js" +import successStoriesCarousel from "../success_stories_carousel.js" document.addEventListener("DOMContentLoaded", function() { notifications() @@ -33,4 +35,6 @@ document.addEventListener("DOMContentLoaded", function() { imageCarousel() facultyCarousel() productDetails() + companiesLogoCarousel() + successStoriesCarousel() }) diff --git a/static/js/success_stories_carousel.js b/static/js/success_stories_carousel.js new file mode 100644 index 000000000..b2cae144a --- /dev/null +++ b/static/js/success_stories_carousel.js @@ -0,0 +1,41 @@ +/*eslint-env jquery*/ +/*eslint semi: ["error", "always"]*/ + +export default function successStoriesCarousel() { + const numSuccessStoriesSlides = $(".success-stories-slider .slide").length; + + $(".success-stories-slider").slick({ + rows: 0, + slidesToShow: 1, + slidesToScroll: 1, + dots: numSuccessStoriesSlides > 1, + infinite: false, + autoplay: false, + responsive: [ + { + breakpoint: 1024, + settings: { + slidesToShow: 1, + slidesToScroll: 1, + dots: numSuccessStoriesSlides > 1 + } + }, + { + breakpoint: 992, + settings: { + slidesToShow: 1, + slidesToScroll: 1, + dots: numSuccessStoriesSlides > 1 + } + }, + { + breakpoint: 767, + settings: { + slidesToShow: 1, + slidesToScroll: 1, + dots: numSuccessStoriesSlides > 1 + } + } + ] + }); +} diff --git a/static/scss/detail/enterprise/companies-logo-carousel.scss b/static/scss/detail/enterprise/companies-logo-carousel.scss new file mode 100644 index 000000000..7ecabec7d --- /dev/null +++ b/static/scss/detail/enterprise/companies-logo-carousel.scss @@ -0,0 +1,110 @@ +// sass-lint:disable mixins-before-declarations + +.companies-logo-block { + background: rgba(255, 255, 255, 0.05); + padding: 0px 40px 50px 40px; + position: relative; + z-index: 1; + + @include media-breakpoint-down (sm) { + width: 100%; + padding: 15px 5px; + } + + @include media-breakpoint-only(md) { + padding: 0px 40px 50px 40px; + } + + .companies-logo-heading { + color: $black; + font-size: 24px; + font-style: normal; + font-weight: 500; + text-align: center; + margin-bottom: 0px; + + p { + margin-bottom: 0px; + } + + @include media-breakpoint-down (sm) { + text-align: initial; + line-height: normal; + } + } +} + +.companies-logo-carousel { + + .slick-list { + margin: 30px 0px; + + @include media-breakpoint-down (sm) { + margin: 20px 0px; + } + } + + .img-holder { + height: 100%; + text-align: center; + padding: 0 15px; + letter-spacing: -.32em; + + @include media-breakpoint-down (sm) { + padding: 0px 10px; + } + + &:after { + display: inline-block; + vertical-align: middle; + content: ''; + width: 2px; + margin: -2px; + height: inherit; + } + + img { + display: inline-block; + max-width: 100%; + height: auto; + vertical-align: middle; + } + } + + .slick-prev, .slick-next { + display: none !important; + } + + .slick-dots { + padding: 0px; + + @include media-breakpoint-down (sm) { + padding: 0px; + } + } + + .slick-dots li { + + @include media-breakpoint-down (sm) { + margin: 0 20px 10px; + } + + &.slick-active { + + button { + background: $primary; + } + } + + button { + width: 60px; + border-radius: 5px; + height: 8px; + background: $events-background; + + &:hover { + background: $primary; + } + } + } +} diff --git a/static/scss/detail/enterprise/learning-journey.scss b/static/scss/detail/enterprise/learning-journey.scss new file mode 100644 index 000000000..27227e5f7 --- /dev/null +++ b/static/scss/detail/enterprise/learning-journey.scss @@ -0,0 +1,170 @@ +// sass-lint:disable mixins-before-declarations + +.learning-journey-block { + padding-top: 70px; + background: url('#{$static-path}/images/enterprise-page-dots-bg.png') repeat-x 0% 100% $alice-blue; + position: relative; + + @include media-breakpoint-down (sm) { + background: none; + padding: 30px 5px 0px 5px; + } + + .learning-journey-heading { + color: $black; + text-align: center; + font-size: 32px; + font-style: normal; + font-weight: 500; + line-height: normal; + margin-bottom: 50px; + + @include media-breakpoint-down (sm) { + text-align: initial; + font-size: 24px; + font-weight: 400; + margin-bottom: 25px; + } + + p { + margin-bottom: 0px; + } + } +} + +.learning-journey-container { + display: flex; + justify-content: space-between; + + @include media-breakpoint-down (sm) { + display: block; + } + + @include media-breakpoint-only(lg) { + justify-content: space-around; + } + + .learning-journey-content { + display: flex; + flex-direction: column; + width: 400px; + + @include media-breakpoint-down (md) { + width: 100%; + } + + @include media-breakpoint-only(md) { + width: 285px; + } + + @include media-breakpoint-only(lg) { + width: 300px; + } + + ul { + padding: 0px; + margin-bottom: 15px; + } + + li { + list-style-type: none; + border-radius: 5px; + background: $white; + padding: 12px 18px; + box-shadow: 0px 2px 5px 0px rgba(0, 0, 0, 0.05); + color: $black; + font-size: 20px; + font-weight: 500; + margin-bottom: 15px; + + @include media-breakpoint-only(lg) { + font-size: 17px; + } + } + + .learning-journey-description { + color: $black; + font-size: 18px; + font-style: normal; + font-weight: 500; + line-height: 27px; + width: 449px; + margin-bottom: 0px; + -webkit-line-clamp: 3; // sass-lint:disable-line no-vendor-prefixes + + @include media-breakpoint-down (md) { + width: 100%; + font-size: 16px; + line-height: 25px; + } + + @include media-breakpoint-only(lg) { + line-height: 25px; + width: 300px; + } + + p { + margin-bottom: 0px; + } + } + + a { + border-radius: 5px; + background: $primary; + color: $white; + font-size: 20px; + font-style: normal; + font-weight: 500; + text-transform: uppercase; + margin-top: 40px; + width: 80%; + display: inline-flex; + padding: 12px 20px; + flex-direction: column; + align-items: center; + z-index: 1; + + @include media-breakpoint-down (sm) { + width: 100%; + margin: 35px 0px 30px; + } + + @include media-breakpoint-only(md) { + padding: 8px 12px; + } + + @include media-breakpoint-only(lg) { + padding: 10px 15px; + } + + &:hover { + background: $dark-blue; + color: $white; + text-decoration: none; + transition: all .2s ease-in; + } + } + } + + img { + width: 560px; + height: 618px; + position: relative; + z-index: 1; + max-width: 100%; + margin-bottom: -1px; + + @include media-breakpoint-down (sm) { + height: 386px; + } + + @include media-breakpoint-only(md) { + width: 350px; + height: 570px; + } + + @include media-breakpoint-only(lg) { + width: 450px; + } + } +} diff --git a/static/scss/detail/enterprise/learning-strategy-form.scss b/static/scss/detail/enterprise/learning-strategy-form.scss new file mode 100644 index 000000000..9172e3ee8 --- /dev/null +++ b/static/scss/detail/enterprise/learning-strategy-form.scss @@ -0,0 +1,237 @@ +// sass-lint:disable mixins-before-declarations placeholder-in-extend + +.learning-strategy-block { + width: 100%; + padding: 75px 0 0; + background: url('#{$static-path}/images/enterprise-page-dots-bg.png') repeat-x 0% 60% $alice-blue; + background-size: contain; + + @include media-breakpoint-down (sm) { + padding: 30px 5px 40px 5px; + background: $alice-blue; + } + + .learning-strategy-heading { + color: $black; + text-align: center; + font-size: 32px; + font-style: normal; + font-weight: 500; + line-height: normal; + margin-bottom: 24px; + + p { + margin-bottom: 0px; + } + + @include media-breakpoint-down (sm) { + font-size: 24px; + text-align: initial; + margin-bottom: 15px; + } + } + + .learning-strategy-subhead { + color: $black; + text-align: center; + font-size: 20px; + font-style: normal; + font-weight: 400; + margin-bottom: 50px; + + p { + margin-bottom: 0px; + } + + @include media-breakpoint-down (sm) { + font-size: 18px; + text-align: initial; + font-weight: 500; + line-height: 27px; + margin-bottom: 0px; + } + } +} + +.learning-strategy-form { + border-radius: 5px 5px 0px 0px; + background: $white; + margin: 0px 45px; + padding: 40px 35px; + + @include media-breakpoint-down (sm) { + background: none; + margin: 0px; + padding: 0px; + width: 100%; + } + + .consent { + display: flex; + align-items: center; + justify-content: center; + border-radius: 5px; + background: $alice-blue; + margin: 10px 15px 0px; + + @include media-breakpoint-down (sm) { + margin: 20px 0px 0px; + background: $white; + } + + .consent-msg { + color: $black; + font-size: 16px; + font-style: normal; + font-weight: 500; + line-height: normal; + text-align: center; + margin: 0px; + padding: 15px; + + @include media-breakpoint-down (sm) { + margin: 0px; + padding: 20px; + text-align: initial; + } + + p { + margin-bottom: 0px; + } + } + } +} + +.hbspt-form { + + form { + + .hs-form-field:first-child { + width: 100%; + } + + .hs-form-field { + display: inline-flex; + flex-direction: column; + align-items: initial; + padding: 0px 15px 30px 15px; + width: 50%; + + @include media-breakpoint-down (sm) { + display: block; + padding: 0px; + width: 100%; + margin-bottom: 0px; + } + + label { + color: rgba(0, 0, 0, 0.60); + font-size: 18px; + font-style: normal; + font-weight: 500; + line-height: normal; + + @include media-breakpoint-down (sm) { + padding-top: 20px; + } + } + + input[type="text"], + input[type="email"], + input[type="tel"], + select { + height: 50px; + width: 100%; + align-self: stretch; + border-radius: 5px; + border: 1px solid #dbe3eb; + background: $white; + box-shadow: 0px 2px 5px 0px rgba(0, 0, 0, 0.05); + padding: 10px 15px; + color: $black; + font-size: 18px; + font-style: normal; + font-weight: 500; + line-height: normal; + } + + .hs-error-msgs { + padding: 0; + margin: 0; + list-style: none; + + @include media-breakpoint-down(sm) { + left: 0; + right: 0; + } + + label { + color: $primary; + display: block; + } + } + } + + .hs-form-field:nth-child(n+2) { + box-sizing: border-box; + } + + .hs-submit { + padding: 20px 155px; + + @include media-breakpoint-down(md) { + padding: 0px; + margin: 20px 0px 0px; + } + + input[type="submit"] { + @extend .btn; + @extend .btn-primary; + border-radius: 5px; + background: $primary; + width: 500px; + height: 55px; + margin: 0px auto 0px; + display: block; + position: relative; + padding: 5px 10px; + font-family: Rajdhani; + font-size: 20px; + font-style: normal; + font-weight: 700; + line-height: normal; + text-transform: uppercase; + + @include media-breakpoint-down(sm) { + width: 100%; + height: auto; + padding: 15px; + } + + &:hover { + background: $dark-blue; + text-decoration: none; + transition: all .2s ease-in; + } + } + } + + .hs-recaptcha { + align-items: center; + width: 100%; + padding: 10px 0px; + + @include media-breakpoint-down(sm) { + display: flex; + align-items: center; + justify-content: center; + padding: 0px; + margin: 20px 0px 0px; + } + } + + .hs_error_rollup { + display: none; + } + } +} diff --git a/static/scss/detail/enterprise/success-stories.scss b/static/scss/detail/enterprise/success-stories.scss new file mode 100644 index 000000000..984c19d1b --- /dev/null +++ b/static/scss/detail/enterprise/success-stories.scss @@ -0,0 +1,246 @@ +// sass-lint:disable mixins-before-declarations + +.success-stories-block { + background: $dark-blue; + padding: 75px 100px; + overflow: hidden; + position: relative; + background-size: cover; + + @include media-breakpoint-down (sm) { + padding: 40px 5px 50px 5px; + } + + @include media-breakpoint-only(md) { + padding: 50px 75px; + } + + .success-stories-headings { + text-align: center; + line-height: normal; + font-style: normal; + + @include media-breakpoint-down (sm) { + text-align: initial; + } + + .success-stories-heading { + color: #eeb84a; + font-size: 32px; + font-style: normal; + font-weight: 500; + margin-bottom: 25px; + + p { + margin-bottom: 0px; + } + } + + .success-stories-subhead { + color: $white; + overflow: hidden; + font-size: 20px; + font-weight: 400; + margin-bottom: 40px; + + @include media-breakpoint-down (sm) { + font-size: 18px; + font-weight: 500; + line-height: 27px; + margin-bottom: 25px; + } + + @include media-breakpoint-only(md) { + font-size: 18px; + } + + p { + margin-bottom: 0px; + } + } + } +} + +.success-stories-slider { + position: relative; + margin: 0 -15px; + + @include media-breakpoint-down (sm) { + display: block; + } + + &:after { + display: block; + clear: both; + content: ""; + } + + .slide { + float: left; + width: 100%; + padding: 0 15px; + opacity: 0.3; + + &.slick-active { + opacity: 1; + } + } + + .slick-prev, .slick-next { + margin: -6% 0 0 !important; + background: transparent; + color: transparent; + } + + .slick-prev { + background: url('#{$static-path}/images/enterprise/ctrl-left.png') no-repeat center center; + width: 64px; + height: 64px; + border: none; + left: 10px; + right: auto; + + @include media-breakpoint-down (sm) { + background: none; + } + + @include media-breakpoint-only(md) { + left: 0px; + } + } + + .slick-next { + background: url('#{$static-path}/images/enterprise/ctrl-right.png') no-repeat center center; + width: 64px; + height: 64px; + border: none; + left: auto; + right: 10px; + + @include media-breakpoint-down (sm) { + background: none; + } + + @include media-breakpoint-only(md) { + right: 0px; + } + } + + .slick-dots { + padding: 20px 0 0; + } + + .slick-dots li { + + &.slick-active { + button { + background: $primary; + } + } + + button { + width: 60px; + border-radius: 5px; + height: 8px; + background: rgba(255, 255, 255, 0.15); + + &:hover { + background: $primary; + } + } + } + + .slick-list { + overflow: hidden; + } + + .slide-holder { + display: flex; + align-items: center; + padding: 0px 80px; + font-style: normal; + color: $white; + + @include media-breakpoint-down (sm) { + display: block; + align-items: initial; + margin-top: 2px; + padding: 0px; + + img { + width: 350px; + height: 120px; + } + } + + @include media-breakpoint-only(md) { + padding: 0px 20px; + } + + h2 { + font-size: 25px; + font-weight: 700; + line-height: normal; + + @include media-breakpoint-down (sm) { + font-size: 20px; + margin-bottom: 0px; + } + + @include media-breakpoint-only(md) { + font-size: 18px; + } + } + + p { + overflow: hidden; + text-overflow: ellipsis; + font-size: 16px; + font-weight: 500; + line-height: 27px; + margin: 18px 0px; + // sass-lint:disable no-vendor-prefixes + -webkit-line-clamp: 3; + display: -webkit-box; + -webkit-box-orient: vertical; + align-self: stretch; + + @include media-breakpoint-down (sm) { + font-size: 18px; + // sass-lint:disable no-vendor-prefixes + -webkit-line-clamp: unset; + margin: 30px 0px; + } + } + + a { + display: flex; + border-radius: 3px; + border: 1px solid rgba(0, 0, 0, 0.10); + background: $white; + width: 113px; + height: 40px; + padding: 15px 20px; + align-items: center; + color: $primary; + font-size: 16px; + font-weight: 500; + margin-top: 10px; + + &:hover { + background: $primary; + color: $white; + text-decoration: none; + transition: all .2s ease-in; + } + } + + .slide-content { + padding: 30px; + + @include media-breakpoint-down (sm) { + padding: 30px 5px 30px 5px; + } + } + } +} diff --git a/static/scss/enterprise.scss b/static/scss/enterprise.scss new file mode 100644 index 000000000..a5d18c838 --- /dev/null +++ b/static/scss/enterprise.scss @@ -0,0 +1,237 @@ +// sass-lint:disable mixins-before-declarations + +.enterprise-page { + font-family: Rajdhani; + + .nav-line { + background: linear-gradient(270deg, #f0ba46 0%, #95bb83 51.02%, #3dbdbf 100%); + height: 2px; + max-width: 100%; + } + + .section-line { + background: linear-gradient(270deg, #f0ba46 0%, #95bb83 51.02%, #3dbdbf 100%); + height: 5px; + max-width: 100%; + } +} + +.enterprise-banner-block { + + @include media-breakpoint-down (sm) { + padding-bottom: 30px; + } + + .enterprise-banner { + display: flex; + align-items: center; + justify-content: space-between; + padding: 50px 0px; + + @include media-breakpoint-down (sm) { + display: block; + padding: 16px 5px; + } + + img { + width: 544px; + height: 444px; + + @include media-breakpoint-down (sm) { + width: 100%; + height: auto; + padding-top: 13px; + } + + @include media-breakpoint-only(md) { + width: 350px; + height: 270px; + } + + @include media-breakpoint-only(lg) { + width: 500px; + height: 400px; + } + } + + .enterprise-banner-content { + text-align: left; + + @include media-breakpoint-down (sm) { + width: fit-content; + padding: 16px 0px; + } + + h3 { + color: $primary; + font-size: 35px; + font-style: normal; + font-weight: 700; + + @include media-breakpoint-down (sm) { + font-size: 25px; + padding: 0px; + margin: 0px; + } + + @include media-breakpoint-only(md) { + font-size: 25px; + line-height: 10px; + } + + @include media-breakpoint-only(lg) { + font-size: 25px; + line-height: 20px; + } + } + + h1 { + color: $primary; + font-size: 71px; + font-style: normal; + font-weight: 700; + margin: 10px 0px; + line-height: 50px; + + @include media-breakpoint-down (sm) { + font-size: 45px; + padding: 0px; + margin: 0px; + line-height: 55px; + } + + @include media-breakpoint-only(md) { + font-size: 45px; + line-height: 40px; + } + + @include media-breakpoint-only(lg) { + font-size: 55px; + line-height: 35px; + } + } + + h2 { + color: $black; + font-size: 45px; + font-style: normal; + font-weight: 700; + line-height: 50px; + + @include media-breakpoint-down (sm) { + font-size: 30px; + padding: 0px; + margin: 0px; + line-height: 25px; + } + + @include media-breakpoint-only(md) { + font-size: 30px; + line-height: 15px; + } + + @include media-breakpoint-only(lg) { + font-size: 35px; + line-height: 30px; + } + } + + a { + border-radius: 5px; + background: $primary; + color: $white; + padding: 12px 20px; + display: flex; + justify-content: center; + text-align: center; + font-size: 20px; + font-style: normal; + font-weight: 700; + text-transform: uppercase; + margin-top: 30px; + width: 63%; + + &:hover { + background: $dark-blue; + text-decoration: none; + transition: all .2s ease-in; + } + + @include media-breakpoint-down (sm) { + margin-bottom: 2px; + margin-top: 25px; + width: 95%; + } + } + } + } +} + +.enterprise-careers-companies-block { + display: inline-flex; + width: 100%; + height: 100%; + background: linear-gradient(180deg, #f0f5f7 0%, rgba(240, 245, 247, 0) 100%); + border-top: 1px rgba(0, 0, 0, 0.07) solid; + padding: 50px 40px 50px 40px; + text-align: center; + + @include media-breakpoint-down (sm) { + padding: 20px 5px 20px 5px; + text-align: unset; + } + + .careers-companies-content { + flex-direction: column; + justify-content: flex-start; + align-items: center; + display: inline-flex; + font-size: 18px; + font-weight: 500; + line-height: 27px; + word-wrap: break-word; + + @include media-breakpoint-down (sm) { + font-size: 16px; + align-self: stretch; + } + } + + .careers-companies-description { + align-self: stretch; + color: black; + margin-bottom: 40px; + text-align: start; + + @include media-breakpoint-down (sm) { + margin-bottom: 30px; + } + + p { + margin-bottom: 0px; + } + } + + a { + padding: 10px 20px; + background: white; + box-shadow: 0px 2px 5px rgba(0, 0, 0, 0.05); + border-radius: 5px; + border: 1px rgba(0, 0, 0, 0.07) solid; + color: $primary; + text-align: center; + + &:hover { + background: $primary; + color: $white; + text-decoration: none; + transition: all .2s ease-in; + } + + @include media-breakpoint-down (sm) { + font-weight: 600; + padding: 10px 15px; + width: 100%; + } + } +} diff --git a/static/scss/layout.scss b/static/scss/layout.scss index ec3554264..3335df609 100644 --- a/static/scss/layout.scss +++ b/static/scss/layout.scss @@ -43,6 +43,12 @@ @import "certificates/certificate"; @import "expandable"; @import "receipt"; +@import "enterprise"; +@import "detail/enterprise/companies-logo-carousel"; +@import "detail/enterprise/learning-journey"; +@import "detail/enterprise/success-stories"; +@import "detail/enterprise/learning-strategy-form"; + body { font-family: Rajdhani, "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif; diff --git a/static/scss/variables.scss b/static/scss/variables.scss index c70acb2d5..55647b1c3 100644 --- a/static/scss/variables.scss +++ b/static/scss/variables.scss @@ -33,3 +33,7 @@ $footer-bg: #262622; $footer-border: #686461; $dc-background: #f2f2f2; + +$black: #000; +$white: #fff; +$alice-blue: #f4f6f7;