diff --git a/.github/workflows/continous-integration.yml b/.github/workflows/continous-integration.yml index 37503c2a9..b8ac0278c 100644 --- a/.github/workflows/continous-integration.yml +++ b/.github/workflows/continous-integration.yml @@ -54,7 +54,7 @@ jobs: DJANGO_SECRET_KEY: secret DOMAIN_REDIRECT_MIDDLEWARE_ENABLED: False GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - NETWORK_SITE_URL: https://foundation.mozilla.org + NETWORK_SITE_URL: http://localhost:8000 PIPENV_VERBOSITY: -1 PULSE_API_DOMAIN: https://network-pulse-api-production.herokuapp.com PULSE_DOMAIN: https://www.mozillapulse.org @@ -127,6 +127,7 @@ jobs: options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5 env: ALLOWED_HOSTS: 127.0.0.1,localhost,mozfest.localhost,default-site.com,secondary-site.com + BASKET_URL: https://basket-dev.allizom.org CONTENT_TYPE_NO_SNIFF: True CORS_ALLOWED_ORIGINS: "*" DATABASE_URL: postgres://postgres:postgres@localhost:5432/network @@ -134,7 +135,7 @@ jobs: DJANGO_SECRET_KEY: secret DOMAIN_REDIRECT_MIDDLEWARE_ENABLED: False GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - NETWORK_SITE_URL: https://foundation.mozilla.org + NETWORK_SITE_URL: http://localhost:8000 PIPENV_VERBOSITY: -1 PULSE_API_DOMAIN: https://network-pulse-api-production.herokuapp.com PULSE_DOMAIN: https://www.mozillapulse.org diff --git a/.github/workflows/visual-regression-testing.yml b/.github/workflows/visual-regression-testing.yml index 77f6bb92c..7da666441 100644 --- a/.github/workflows/visual-regression-testing.yml +++ b/.github/workflows/visual-regression-testing.yml @@ -7,7 +7,7 @@ on: types: [submitted] push: branches: - - 'main' + - "main" jobs: visual_regression_tests: name: Percy CI @@ -32,7 +32,7 @@ jobs: DJANGO_SECRET_KEY: secret DOMAIN_REDIRECT_MIDDLEWARE_ENABLED: False GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - NETWORK_SITE_URL: https://foundation.mozilla.org + NETWORK_SITE_URL: http://localhost:8000 PIPENV_VERBOSITY: -1 PULSE_API_DOMAIN: https://network-pulse-api-production.herokuapp.com PULSE_DOMAIN: https://www.mozillapulse.org diff --git a/network-api/networkapi/utility/faker/streamfield_provider.py b/network-api/networkapi/utility/faker/streamfield_provider.py index e73a2ba1c..84a923ff8 100644 --- a/network-api/networkapi/utility/faker/streamfield_provider.py +++ b/network-api/networkapi/utility/faker/streamfield_provider.py @@ -450,6 +450,18 @@ def generate_blog_index_callout_box_field(): ) +def generate_blog_newsletter_signup_field(): + from networkapi.wagtailpages.factory.customblocks.newsletter_signup_block import ( + BlogNewsletterSignupBlockFactory, + ) + from networkapi.wagtailpages.pagemodels.customblocks.newsletter_signup_block import ( + BlogNewsletterSignupBlock, + ) + + block = BlogNewsletterSignupBlockFactory.create() + return generate_field("newsletter_signup", BlogNewsletterSignupBlock().get_api_representation(block)) + + class StreamfieldProvider(BaseProvider): """ A custom Faker Provider for relative image urls, for use with factory_boy @@ -498,6 +510,7 @@ class StreamfieldProvider(BaseProvider): "banner_video": generate_banner_video_field, "current_events_slider": generate_current_events_slider_field, "callout_box": generate_blog_index_callout_box_field, + "blog_newsletter_signup": generate_blog_newsletter_signup_field, } streamfield_data = [] diff --git a/network-api/networkapi/wagtailcustomization/factories/__init__.py b/network-api/networkapi/wagtailcustomization/factories/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/network-api/networkapi/wagtailcustomization/factories/snippets.py b/network-api/networkapi/wagtailcustomization/factories/snippets.py new file mode 100644 index 000000000..4d4557723 --- /dev/null +++ b/network-api/networkapi/wagtailcustomization/factories/snippets.py @@ -0,0 +1,27 @@ +from wagtail_factories.blocks import ChooserBlockFactory + + +class SnippetChooserBlockFactory(ChooserBlockFactory): + """Abstract factory for snippet chooser blocks. + + Concrete implementations should define the `model` Meta attribute and a `snippet` field + with a subfactory pointing to the snippet factory: + ``` + class SignupChooserBlockFactory(SnippetChooserBlockFactory): + snippet = factory.SubFactory("networkapi.wagtailpages.factory.Signup") + + class Meta: + model = Signup + ``` + """ + + class Meta: + abstract = True + + @classmethod + def _build(cls, model_class, snippet): + return snippet + + @classmethod + def _create(cls, model_class, snippet): + return snippet diff --git a/network-api/networkapi/wagtailpages/factory/blog.py b/network-api/networkapi/wagtailpages/factory/blog.py index 21eba3180..12915648d 100644 --- a/network-api/networkapi/wagtailpages/factory/blog.py +++ b/network-api/networkapi/wagtailpages/factory/blog.py @@ -23,6 +23,8 @@ from .tagging import add_tags RANDOM_SEED = settings.RANDOM_SEED TESTING = settings.TESTING blog_body_streamfield_fields = [ + "paragraph", + "blog_newsletter_signup", "paragraph", "image", "image_text", diff --git a/network-api/networkapi/wagtailpages/factory/customblocks/__init__.py b/network-api/networkapi/wagtailpages/factory/customblocks/__init__.py index 51f724472..1da825405 100644 --- a/network-api/networkapi/wagtailpages/factory/customblocks/__init__.py +++ b/network-api/networkapi/wagtailpages/factory/customblocks/__init__.py @@ -4,3 +4,9 @@ from .blog_cta_card_with_text_block import BlogCTACardWithTextBlockFactory from .cta_aside_block import CTAAsideBlockFactory from .image_block import ImageBlockFactory from .link_button_block import LinkButtonBlockFactory +from .newsletter_signup_block import ( + BlogNewsletterSignupBlockFactory, + BlogSignupChooserBlockFactory, + NewsletterSignupBlockFactory, + SignupChooserBlockFactory, +) diff --git a/network-api/networkapi/wagtailpages/factory/customblocks/newsletter_signup_block.py b/network-api/networkapi/wagtailpages/factory/customblocks/newsletter_signup_block.py new file mode 100644 index 000000000..6e8ccd863 --- /dev/null +++ b/network-api/networkapi/wagtailpages/factory/customblocks/newsletter_signup_block.py @@ -0,0 +1,36 @@ +import factory +import wagtail_factories + +from networkapi.wagtailcustomization.factories.snippets import ( + SnippetChooserBlockFactory, +) +from networkapi.wagtailpages.models import BlogSignup, Signup +from networkapi.wagtailpages.pagemodels import customblocks + + +class SignupChooserBlockFactory(SnippetChooserBlockFactory): + snippet = factory.SubFactory("networkapi.wagtailpages.factory.signup.SignupFactory") + + class Meta: + model = Signup + + +class NewsletterSignupBlockFactory(wagtail_factories.StructBlockFactory): + class Meta: + model = customblocks.NewsletterSignupBlock + + signup = factory.SubFactory(SignupChooserBlockFactory) + + +class BlogSignupChooserBlockFactory(SnippetChooserBlockFactory): + snippet = factory.SubFactory("networkapi.wagtailpages.factory.signup.BlogSignupFactory") + + class Meta: + model = BlogSignup + + +class BlogNewsletterSignupBlockFactory(wagtail_factories.StructBlockFactory): + class Meta: + model = customblocks.BlogNewsletterSignupBlock + + signup = factory.SubFactory(BlogSignupChooserBlockFactory) diff --git a/network-api/networkapi/wagtailpages/factory/signup.py b/network-api/networkapi/wagtailpages/factory/signup.py index 067fc1f91..03fcf707d 100644 --- a/network-api/networkapi/wagtailpages/factory/signup.py +++ b/network-api/networkapi/wagtailpages/factory/signup.py @@ -1,4 +1,4 @@ -from networkapi.wagtailpages.models import Signup +from networkapi.wagtailpages.models import BlogSignup, Signup from .abstract import CTAFactory @@ -6,3 +6,8 @@ from .abstract import CTAFactory class SignupFactory(CTAFactory): class Meta: model = Signup + + +class BlogSignupFactory(CTAFactory): + class Meta: + model = BlogSignup diff --git a/network-api/networkapi/wagtailpages/migrations/0114_add_blog_newsletter_signup.py b/network-api/networkapi/wagtailpages/migrations/0114_add_blog_newsletter_signup.py new file mode 100644 index 000000000..09ac27d76 --- /dev/null +++ b/network-api/networkapi/wagtailpages/migrations/0114_add_blog_newsletter_signup.py @@ -0,0 +1,991 @@ +# Generated by Django 3.2.21 on 2023-11-14 15:18 + +import uuid + +import django.db.models.deletion +import wagtail.blocks +import wagtail.blocks.static_block +import wagtail.documents.blocks +import wagtail.embeds.blocks +import wagtail.fields +import wagtail.images.blocks +import wagtail.snippets.blocks +import wagtailmedia.blocks +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("wagtailcore", "0078_referenceindex"), + ("wagtailpages", "0113_alter_blogpage_body"), + ] + + operations = [ + migrations.AlterField( + model_name="blogpage", + name="body", + field=wagtail.fields.StreamField( + [ + ( + "accordion", + wagtail.blocks.StructBlock( + [ + ( + "accordion_items", + wagtail.blocks.ListBlock( + wagtail.blocks.StructBlock( + [ + ( + "title", + wagtail.blocks.CharBlock( + help_text="Heading for the Accordion Item" + ), + ), + ( + "content", + wagtail.blocks.StreamBlock( + [ + ( + "rich_text", + wagtail.blocks.RichTextBlock( + blank=True, + features=[ + "bold", + "italic", + "link", + "ul", + "ol", + "document-link", + ], + ), + ), + ( + "datawrapper", + wagtail.embeds.blocks.EmbedBlock( + help_text='Enter the "visualization only" link of the Datawrapper chart. It looks something like this: https://datawrapper.dwcdn.net/KwSKp/1/', + icon="image", + template="wagtailpages/blocks/datawrapper_block.html", + ), + ), + ( + "image", + wagtail.blocks.StructBlock( + [ + ( + "image", + wagtail.images.blocks.ImageChooserBlock(), + ), + ( + "altText", + wagtail.blocks.CharBlock( + help_text="Image description (for screen readers).", + required=True, + ), + ), + ] + ), + ), + ( + "video", + wagtail.blocks.StructBlock( + [ + ( + "url", + wagtail.blocks.CharBlock( + help_text="For YouTube: go to your YouTube video and click “Share,” then “Embed,” and then copy and paste the provided URL only. For example: https://www.youtube.com/embed/3FIVXBawyQw For Vimeo: follow similar steps to grab the embed URL. For example: https://player.vimeo.com/video/9004979", + label="Embed URL", + ), + ), + ( + "caption", + wagtail.blocks.CharBlock(required=False), + ), + ( + "captionURL", + wagtail.blocks.CharBlock( + help_text="Optional URL for caption to link to.", + required=False, + ), + ), + ( + "video_width", + wagtail.blocks.ChoiceBlock( + choices=[ + ("normal", "Normal"), + ("wide", "Wide"), + ("full_width", "Full Width"), + ], + help_text="Wide videos are col-12, Full-Width videos reach both ends of the screen.", + ), + ), + ] + ), + ), + ] + ), + ), + ] + ) + ), + ) + ] + ), + ), + ( + "paragraph", + wagtail.blocks.RichTextBlock( + features=[ + "bold", + "italic", + "link", + "h2", + "h3", + "h4", + "h5", + "ol", + "ul", + "hr", + "document-link", + ], + template="wagtailpages/blocks/rich_text_block.html", + ), + ), + ( + "card_grid", + wagtail.blocks.StructBlock( + [ + ( + "cards", + wagtail.blocks.ListBlock( + wagtail.blocks.StructBlock( + [ + ("image", wagtail.images.blocks.ImageChooserBlock()), + ( + "alt_text", + wagtail.blocks.CharBlock( + help_text="Alt text for card's image.", required=False + ), + ), + ("title", wagtail.blocks.CharBlock(help_text="Heading for the card.")), + ("body", wagtail.blocks.TextBlock(help_text="Body text of the card.")), + ( + "link_url", + wagtail.blocks.CharBlock( + help_text="Optional URL that this card should link out to. (Note: If left blank, link will not render.) ", + required=False, + ), + ), + ( + "link_label", + wagtail.blocks.CharBlock( + help_text="Optional Label for the URL link above. (Note: If left blank, link will not render.) ", + required=False, + ), + ), + ] + ), + help_text="Please use a minimum of 2 cards.", + ), + ) + ] + ), + ), + ( + "CTA_card", + wagtail.blocks.StructBlock( + [ + ( + "style", + wagtail.blocks.ChoiceBlock( + choices=[("pop", "Pop"), ("outline", "Outline"), ("filled", "Filled")] + ), + ), + ( + "title", + wagtail.blocks.CharBlock( + help_text="Optional title for the card.", max_length=100, required=False + ), + ), + ( + "body", + wagtail.blocks.RichTextBlock( + features=["bold", "italic", "link", "hr", "h4", "h5", "ul", "ol"], + help_text="Body text of the card.", + ), + ), + ( + "image", + wagtail.blocks.ListBlock( + wagtail.blocks.StructBlock( + [ + ("image", wagtail.images.blocks.ImageChooserBlock()), + ( + "altText", + wagtail.blocks.CharBlock( + help_text="Image description (for screen readers).", + required=True, + ), + ), + ] + ), + default=[], + max_num=1, + min_num=0, + ), + ), + ( + "button", + wagtail.blocks.ListBlock( + wagtail.blocks.StructBlock( + [ + ("label", wagtail.blocks.CharBlock()), + ("URL", wagtail.blocks.CharBlock()), + ( + "styling", + wagtail.blocks.ChoiceBlock( + choices=[ + ("btn-primary", "Primary button"), + ("btn-secondary", "Secondary button"), + ] + ), + ), + ] + ), + default=[], + max_num=1, + min_num=0, + ), + ), + ] + ), + ), + ( + "CTA_card_with_text", + wagtail.blocks.StructBlock( + [ + ( + "alignment", + wagtail.blocks.ChoiceBlock( + choices=[("right", "Right"), ("left", "Left")], + help_text="For full-width cards, please use a regular Blog CTA Card block with a separate paragraph.", + ), + ), + ( + "card", + wagtail.blocks.StructBlock( + [ + ( + "style", + wagtail.blocks.ChoiceBlock( + choices=[ + ("pop", "Pop"), + ("outline", "Outline"), + ("filled", "Filled"), + ] + ), + ), + ( + "title", + wagtail.blocks.CharBlock( + help_text="Optional title for the card.", + max_length=100, + required=False, + ), + ), + ( + "body", + wagtail.blocks.RichTextBlock( + features=["bold", "italic", "link", "hr", "h4", "h5", "ul", "ol"], + help_text="Body text of the card.", + ), + ), + ( + "image", + wagtail.blocks.ListBlock( + wagtail.blocks.StructBlock( + [ + ("image", wagtail.images.blocks.ImageChooserBlock()), + ( + "altText", + wagtail.blocks.CharBlock( + help_text="Image description (for screen readers).", + required=True, + ), + ), + ] + ), + default=[], + max_num=1, + min_num=0, + ), + ), + ( + "button", + wagtail.blocks.ListBlock( + wagtail.blocks.StructBlock( + [ + ("label", wagtail.blocks.CharBlock()), + ("URL", wagtail.blocks.CharBlock()), + ( + "styling", + wagtail.blocks.ChoiceBlock( + choices=[ + ("btn-primary", "Primary button"), + ("btn-secondary", "Secondary button"), + ] + ), + ), + ] + ), + default=[], + max_num=1, + min_num=0, + ), + ), + ], + required=True, + template="wagtailpages/blocks/blog_cta_card_block_regular.html", + ), + ), + ( + "paragraph", + wagtail.blocks.RichTextBlock( + features=[ + "bold", + "italic", + "link", + "h2", + "h3", + "h4", + "h5", + "ol", + "ul", + "hr", + "document-link", + ], + help_text="Text to be displayed next to the card.", + template="wagtailpages/blocks/rich_text_block.html", + ), + ), + ] + ), + ), + ( + "image_grid", + wagtail.blocks.StructBlock( + [ + ( + "grid_items", + wagtail.blocks.ListBlock( + wagtail.blocks.StructBlock( + [ + ("image", wagtail.images.blocks.ImageChooserBlock()), + ( + "alt_text", + wagtail.blocks.CharBlock( + help_text="Alt text for this image.", required=False + ), + ), + ( + "caption", + wagtail.blocks.CharBlock( + help_text="Please remember to properly attribute any images we use.", + required=False, + ), + ), + ( + "url", + wagtail.blocks.CharBlock( + help_text="Optional URL that this figure should link out to.", + required=False, + ), + ), + ( + "square_image", + wagtail.blocks.BooleanBlock( + default=True, + help_text="If left checked, the image will be cropped to be square.", + required=False, + ), + ), + ] + ) + ), + ) + ] + ), + ), + ( + "iframe", + wagtail.blocks.StructBlock( + [ + ( + "url", + wagtail.blocks.CharBlock( + help_text="Please note that only URLs from allow-listed domains will work." + ), + ), + ( + "height", + wagtail.blocks.IntegerBlock( + help_text="Optional integer pixel value for custom iFrame height", + required=False, + ), + ), + ("caption", wagtail.blocks.CharBlock(required=False)), + ( + "captionURL", + wagtail.blocks.CharBlock( + help_text="Optional URL that this caption should link out to.", required=False + ), + ), + ( + "iframe_width", + wagtail.blocks.ChoiceBlock( + choices=[("normal", "Normal"), ("wide", "Wide"), ("full_width", "Full Width")], + help_text="Wide iframes are col-12, Full-Width iframes reach both ends of the screen", + ), + ), + ( + "disable_scroll", + wagtail.blocks.BooleanBlock( + default=False, + help_text='Checking this will add "scrolling=no" to the iframe. Use this if your iframe is rendering an unnecessary scroll bar or whitespace below it.', + required=False, + ), + ), + ] + ), + ), + ( + "image", + wagtail.blocks.StructBlock( + [ + ("image", wagtail.images.blocks.ImageChooserBlock()), + ( + "altText", + wagtail.blocks.CharBlock( + help_text="Image description (for screen readers).", required=True + ), + ), + ("caption", wagtail.blocks.CharBlock(required=False)), + ( + "captionURL", + wagtail.blocks.CharBlock( + help_text="Optional URL that this caption should link out to.", required=False + ), + ), + ( + "image_width", + wagtail.blocks.ChoiceBlock( + choices=[("normal", "Normal"), ("wide", "Wide"), ("full_width", "Full Width")], + help_text="Wide images are col-12, Full-Width Images reach both ends of the screen (16:6 images recommended for full width)", + ), + ), + ] + ), + ), + ( + "audio", + wagtail.blocks.StructBlock( + [ + ("audio", wagtailmedia.blocks.AudioChooserBlock()), + ("caption", wagtail.blocks.CharBlock(required=False)), + ] + ), + ), + ( + "image_text", + wagtail.blocks.StructBlock( + [ + ("image", wagtail.images.blocks.ImageChooserBlock()), + ( + "altText", + wagtail.blocks.CharBlock( + help_text="Image description (for screen readers).", required=True + ), + ), + ( + "text", + wagtail.blocks.RichTextBlock( + features=[ + "bold", + "italic", + "link", + "h2", + "h3", + "h4", + "h5", + "ol", + "ul", + "hr", + "document-link", + ] + ), + ), + ( + "url", + wagtail.blocks.CharBlock( + help_text="Optional URL that this image should link out to.", required=False + ), + ), + ( + "top_divider", + wagtail.blocks.BooleanBlock( + help_text="Optional divider above content block.", required=False + ), + ), + ( + "bottom_divider", + wagtail.blocks.BooleanBlock( + help_text="Optional divider below content block.", required=False + ), + ), + ] + ), + ), + ( + "image_text_mini", + wagtail.blocks.StructBlock( + [ + ("image", wagtail.images.blocks.ImageChooserBlock()), + ( + "altText", + wagtail.blocks.CharBlock( + help_text="Image description (for screen readers).", required=True + ), + ), + ("text", wagtail.blocks.RichTextBlock(features=["bold", "italic", "link"])), + ] + ), + ), + ( + "video", + wagtail.blocks.StructBlock( + [ + ( + "url", + wagtail.blocks.CharBlock( + help_text="For YouTube: go to your YouTube video and click “Share,” then “Embed,” and then copy and paste the provided URL only. For example: https://www.youtube.com/embed/3FIVXBawyQw For Vimeo: follow similar steps to grab the embed URL. For example: https://player.vimeo.com/video/9004979", + label="Embed URL", + ), + ), + ("caption", wagtail.blocks.CharBlock(required=False)), + ( + "captionURL", + wagtail.blocks.CharBlock( + help_text="Optional URL for caption to link to.", required=False + ), + ), + ( + "video_width", + wagtail.blocks.ChoiceBlock( + choices=[("normal", "Normal"), ("wide", "Wide"), ("full_width", "Full Width")], + help_text="Wide videos are col-12, Full-Width videos reach both ends of the screen.", + ), + ), + ] + ), + ), + ( + "linkbutton", + wagtail.blocks.StructBlock( + [ + ("label", wagtail.blocks.CharBlock()), + ("URL", wagtail.blocks.CharBlock()), + ( + "styling", + wagtail.blocks.ChoiceBlock( + choices=[ + ("btn-primary", "Primary button"), + ("btn-secondary", "Secondary button"), + ] + ), + ), + ] + ), + ), + ( + "looping_video", + wagtail.blocks.StructBlock( + [ + ( + "video_url", + wagtail.blocks.CharBlock( + help_text='Log into Vimeo using 1Password and upload the desired video. Then select the video and click "Advanced", "Distribution", and "Video File Links". Copy and paste the link here.', + label="Embed URL", + ), + ) + ] + ), + ), + ( + "pulse_listing", + wagtail.blocks.StructBlock( + [ + ( + "search_terms", + wagtail.blocks.CharBlock( + help_text="Test your search at mozillapulse.org/search", + label="Search", + required=False, + ), + ), + ( + "max_number_of_results", + wagtail.blocks.IntegerBlock( + default=6, + help_text="Choose 1-12. If you want visitors to see more, link to a search or tag on Pulse.", + max_value=12, + min_value=0, + required=True, + ), + ), + ( + "only_featured_entries", + wagtail.blocks.BooleanBlock( + default=False, + help_text="Featured items are selected by Pulse moderators.", + label="Display only featured entries", + required=False, + ), + ), + ( + "newest_first", + wagtail.blocks.ChoiceBlock( + choices=[ + ("True", "Show newer entries first"), + ("False", "Show older entries first"), + ], + label="Sort", + ), + ), + ( + "advanced_filter_header", + wagtail.blocks.static_block.StaticBlock( + admin_text="-------- ADVANCED FILTERS: OPTIONS TO DISPLAY FEWER, MORE TARGETED RESULTS. --------", + label=" ", + ), + ), + ( + "issues", + wagtail.blocks.ChoiceBlock( + choices=[ + ("all", "All"), + ("Decentralization", "Decentralization"), + ("Digital Inclusion", "Digital Inclusion"), + ("Online Privacy & Security", "Online Privacy & Security"), + ("Open Innovation", "Open Innovation"), + ("Web Literacy", "Web Literacy"), + ] + ), + ), + ( + "help", + wagtail.blocks.ChoiceBlock( + choices=[ + ("all", "All"), + ("Attend", "Attend"), + ("Create content", "Create content"), + ("Code", "Code"), + ("Design", "Design"), + ("Fundraise", "Fundraise"), + ("Join community", "Join community"), + ("Localize & translate", "Localize & translate"), + ("Mentor", "Mentor"), + ("Plan & organize", "Plan & organize"), + ("Promote", "Promote"), + ("Take action", "Take action"), + ("Test & feedback", "Test & feedback"), + ("Write documentation", "Write documentation"), + ], + label="Type of help needed", + ), + ), + ( + "direct_link", + wagtail.blocks.BooleanBlock( + default=False, + help_text="Checked: user goes to project link. Unchecked: user goes to pulse entry", + label="Direct link", + required=False, + ), + ), + ] + ), + ), + ( + "single_quote", + wagtail.blocks.StructBlock( + [ + ("quote", wagtail.blocks.RichTextBlock(features=["bold"])), + ("attribution", wagtail.blocks.CharBlock(required=False)), + ( + "attribution_info", + wagtail.blocks.RichTextBlock(features=["bold", "link", "large"], required=False), + ), + ] + ), + ), + ( + "slider", + wagtail.blocks.StructBlock( + [ + ( + "title", + wagtail.blocks.CharBlock(help_text="Heading for the slider.", required=False), + ), + ( + "slides", + wagtail.blocks.StreamBlock( + [ + ( + "slide", + wagtail.blocks.StructBlock( + [ + ( + "title", + wagtail.blocks.CharBlock( + help_text="Heading of the card.", required=False + ), + ), + ( + "image", + wagtail.images.blocks.ImageChooserBlock( + help_text="The image associated with this event." + ), + ), + ( + "caption", + wagtail.blocks.TextBlock( + help_text="Caption for slider image", required=False + ), + ), + ( + "body", + wagtail.blocks.RichTextBlock( + blank=True, + features=["bold", "italic", "link", "large"], + required=False, + ), + ), + ( + "buttons", + wagtail.blocks.StreamBlock( + [ + ( + "internal", + wagtail.blocks.StructBlock( + [ + ( + "label", + wagtail.blocks.CharBlock( + help_text="Label for this link." + ), + ), + ( + "link", + wagtail.blocks.PageChooserBlock( + help_text="Page that this should link out to." + ), + ), + ] + ), + ), + ( + "external", + wagtail.blocks.StructBlock( + [ + ( + "label", + wagtail.blocks.CharBlock( + help_text="Label for this link." + ), + ), + ( + "link", + wagtail.blocks.URLBlock( + help_text="URL that this should link out to." + ), + ), + ] + ), + ), + ( + "document", + wagtail.blocks.StructBlock( + [ + ( + "label", + wagtail.blocks.CharBlock( + help_text="Label for this link." + ), + ), + ( + "document", + wagtail.documents.blocks.DocumentChooserBlock( + help_text="Document that this should link out to." + ), + ), + ], + help_text='An iCal document can be attached here for an "Add to Calendar" button.', + ), + ), + ], + help_text="A list of buttons that will appear at the bottom of the card.", + max_num=2, + required=False, + ), + ), + ] + ), + ) + ], + help_text="A list of slides.", + ), + ), + ] + ), + ), + ( + "spacer", + wagtail.blocks.StructBlock( + [ + ( + "size", + wagtail.blocks.ChoiceBlock( + choices=[ + ("1", "quarter spacing"), + ("2", "half spacing"), + ("3", "single spacing"), + ("4", "one and a half spacing"), + ("5", "triple spacing"), + ] + ), + ) + ] + ), + ), + ( + "airtable", + wagtail.blocks.StructBlock( + [ + ( + "url", + wagtail.blocks.URLBlock( + help_text="Copied from the Airtable embed code. The word 'embed' will be in the url" + ), + ), + ( + "height", + wagtail.blocks.IntegerBlock( + default=533, + help_text="The pixel height on desktop view, usually copied from the Airtable embed code", + ), + ), + ] + ), + ), + ( + "datawrapper", + wagtail.embeds.blocks.EmbedBlock( + help_text='Enter the "visualization only" link of the Datawrapper chart. It looks something like this: https://datawrapper.dwcdn.net/KwSKp/1/', + icon="image", + template="wagtailpages/blocks/datawrapper_block.html", + ), + ), + ( + "typeform", + wagtail.blocks.StructBlock( + [ + ( + "embed_id", + wagtail.blocks.CharBlock( + help_text="The embed id of your Typeform page (e.g. if the form is on admin.typeform.com/form/e8zScc6t, the id will be: e8zScc6t)", + required=True, + ), + ), + ( + "button_type", + wagtail.blocks.ChoiceBlock( + choices=[ + ("btn-primary", "Primary button"), + ("btn-secondary", "Secondary button"), + ] + ), + ), + ( + "button_text", + wagtail.blocks.CharBlock( + help_text="This is a text prompt for users to open the typeform content", + required=True, + ), + ), + ] + ), + ), + ( + "newsletter_signup", + wagtail.blocks.StructBlock( + [("signup", wagtail.snippets.blocks.SnippetChooserBlock("wagtailpages.BlogSignup"))] + ), + ), + ], + use_json_field=True, + ), + ), + migrations.AlterField( + model_name="cta", + name="newsletter", + field=models.CharField( + default="mozilla-foundation", help_text="The (pre-existing) newsletter to sign up for", max_length=100 + ), + ), + migrations.CreateModel( + name="BlogSignup", + fields=[ + ("id", models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), + ("translation_key", models.UUIDField(default=uuid.uuid4, editable=False)), + ( + "name", + models.CharField( + default="", help_text="Identify this component for other editors", max_length=100 + ), + ), + ( + "header", + models.CharField( + blank=True, help_text="Heading that will display on page for this component", max_length=500 + ), + ), + ( + "newsletter", + models.CharField( + default="mozilla-foundation", + help_text="The (pre-existing) newsletter to sign up for", + max_length=100, + ), + ), + ( + "description", + wagtail.fields.RichTextField(blank=True, help_text="Signup's body (richtext)", max_length=300), + ), + ( + "locale", + models.ForeignKey( + editable=False, + on_delete=django.db.models.deletion.PROTECT, + related_name="+", + to="wagtailcore.locale", + ), + ), + ], + options={ + "verbose_name": "Blog Signup", + "verbose_name_plural": "Blog Signups", + "ordering": ["name"], + "abstract": False, + "unique_together": {("translation_key", "locale")}, + }, + ), + ] diff --git a/network-api/networkapi/wagtailpages/models.py b/network-api/networkapi/wagtailpages/models.py index 4bb3d2df6..f701aa535 100644 --- a/network-api/networkapi/wagtailpages/models.py +++ b/network-api/networkapi/wagtailpages/models.py @@ -58,6 +58,7 @@ from .pagemodels.campaign_index import CampaignIndexPage from .pagemodels.campaigns import ( CTA, BanneredCampaignPage, + BlogSignup, CampaignPage, OpportunityPage, Petition, diff --git a/network-api/networkapi/wagtailpages/pagemodels/blog/blog.py b/network-api/networkapi/wagtailpages/pagemodels/blog/blog.py index 3ddd11b24..3d0dcd438 100644 --- a/network-api/networkapi/wagtailpages/pagemodels/blog/blog.py +++ b/network-api/networkapi/wagtailpages/pagemodels/blog/blog.py @@ -58,6 +58,7 @@ base_fields = [ ("airtable", customblocks.AirTableBlock()), ("datawrapper", customblocks.DatawrapperBlock()), ("typeform", customblocks.TypeformBlock()), + ("newsletter_signup", customblocks.BlogNewsletterSignupBlock()), ] @@ -112,7 +113,11 @@ class BlogPage(BasePage): # Custom base form for additional validation base_form_class = BlogPageForm - body = StreamField(base_fields, block_counts={"typeform": {"max_num": 1}}, use_json_field=True) + body = StreamField( + base_fields, + block_counts={"typeform": {"max_num": 1}, "newsletter_signup": {"max_num": 1}}, + use_json_field=True, + ) topics = ParentalManyToManyField( BlogPageTopic, diff --git a/network-api/networkapi/wagtailpages/pagemodels/campaigns.py b/network-api/networkapi/wagtailpages/pagemodels/campaigns.py index d2d7842d4..2bf6cecf6 100644 --- a/network-api/networkapi/wagtailpages/pagemodels/campaigns.py +++ b/network-api/networkapi/wagtailpages/pagemodels/campaigns.py @@ -16,8 +16,7 @@ from .mixin.foundation_metadata import FoundationMetadataPageMixin from .modular import MiniSiteNameSpace -@register_snippet -class CTA(models.Model): +class CTABase(models.Model): name = models.CharField( default="", max_length=100, @@ -34,7 +33,7 @@ class CTA(models.Model): newsletter = models.CharField( max_length=100, - help_text="The (pre-existing) SalesForce newsletter to sign up for", + help_text="The (pre-existing) newsletter to sign up for", default="mozilla-foundation", ) @@ -48,6 +47,12 @@ class CTA(models.Model): def __str__(self): return self.name + class Meta: + abstract = True + + +@register_snippet +class CTA(CTABase): class Meta: ordering = ["-id"] verbose_name_plural = "CTA" @@ -135,6 +140,18 @@ class Signup(TranslatableMixin, CTA): verbose_name = "Signup" +@register_snippet +class BlogSignup(TranslatableMixin, CTABase): + description = RichTextField( + help_text="Signup's body (richtext)", features=["bold", "italic"], max_length=300, blank=True + ) + + class Meta(TranslatableMixin.Meta): + ordering = ["name"] + verbose_name = "Blog Signup" + verbose_name_plural = "Blog Signups" + + class OpportunityPage(MiniSiteNameSpace): content_panels = Page.content_panels + [ FieldPanel("header"), diff --git a/network-api/networkapi/wagtailpages/pagemodels/customblocks/__init__.py b/network-api/networkapi/wagtailpages/pagemodels/customblocks/__init__.py index 58be26b73..a2058a940 100644 --- a/network-api/networkapi/wagtailpages/pagemodels/customblocks/__init__.py +++ b/network-api/networkapi/wagtailpages/pagemodels/customblocks/__init__.py @@ -39,7 +39,7 @@ from .latest_profile_list import LatestProfileList from .link_button_block import LinkButtonBlock from .listing import ListingBlock from .looping_video_block import LoopingVideoBlock -from .newsletter_signup_block import NewsletterSignupBlock +from .newsletter_signup_block import BlogNewsletterSignupBlock, NewsletterSignupBlock from .profile import ProfileBlock from .profile_by_id import ProfileById from .profile_directory import ProfileDirectory, TabbedProfileDirectory diff --git a/network-api/networkapi/wagtailpages/pagemodels/customblocks/newsletter_signup_block.py b/network-api/networkapi/wagtailpages/pagemodels/customblocks/newsletter_signup_block.py index 72939289e..ecaf55c85 100644 --- a/network-api/networkapi/wagtailpages/pagemodels/customblocks/newsletter_signup_block.py +++ b/network-api/networkapi/wagtailpages/pagemodels/customblocks/newsletter_signup_block.py @@ -8,3 +8,11 @@ class NewsletterSignupBlock(blocks.StructBlock): class Meta: icon = "placeholder" template = "wagtailpages/blocks/newsletter_signup_block.html" + + +class BlogNewsletterSignupBlock(blocks.StructBlock): + signup = SnippetChooserBlock("wagtailpages.BlogSignup") + + class Meta: + icon = "placeholder" + template = "wagtailpages/blocks/blog_newsletter_signup_block.html" diff --git a/network-api/networkapi/wagtailpages/templates/wagtailpages/blocks/blog_newsletter_signup_block.html b/network-api/networkapi/wagtailpages/templates/wagtailpages/blocks/blog_newsletter_signup_block.html new file mode 100644 index 000000000..203ce49a0 --- /dev/null +++ b/network-api/networkapi/wagtailpages/templates/wagtailpages/blocks/blog_newsletter_signup_block.html @@ -0,0 +1,15 @@ +{% extends "./base_streamfield_block.html" %} +{% load wagtailcore_tags wagtailimages_tags static i18n l10n %} + +{% block block_container_classes %}tw-my-18{% endblock block_container_classes %} + +{% block block_content %} +
+{% endblock block_content %} diff --git a/network-api/networkapi/wagtailpages/tests/customblocks/test_newsletter_signup_blocks.py b/network-api/networkapi/wagtailpages/tests/customblocks/test_newsletter_signup_blocks.py new file mode 100644 index 000000000..658a5102f --- /dev/null +++ b/network-api/networkapi/wagtailpages/tests/customblocks/test_newsletter_signup_blocks.py @@ -0,0 +1,22 @@ +from django.test import TestCase + +from networkapi.wagtailpages.factory import customblocks as customblock_factories +from networkapi.wagtailpages.models import BlogSignup, Signup + + +class TestNewsletterSignupBlock(TestCase): + def test_newsletter_signup_block(self): + Signup.objects.all().delete() + self.assertEqual(Signup.objects.count(), 0) + signup_block = customblock_factories.NewsletterSignupBlockFactory() + self.assertTrue(isinstance(signup_block["signup"], Signup)) + self.assertEqual(Signup.objects.count(), 1) + + +class TestBlogNewsletterSignupBlock(TestCase): + def test_blog_newsletter_signup_block(self): + BlogSignup.objects.all().delete() + self.assertEqual(BlogSignup.objects.count(), 0) + signup_block = customblock_factories.BlogNewsletterSignupBlockFactory() + self.assertTrue(isinstance(signup_block["signup"], BlogSignup)) + self.assertEqual(BlogSignup.objects.count(), 1) diff --git a/source/images/glyphs/letter.svg b/source/images/glyphs/letter.svg new file mode 100644 index 000000000..476675b89 --- /dev/null +++ b/source/images/glyphs/letter.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/source/js/common/inject-react/index.js b/source/js/common/inject-react/index.js index 15346e0b6..d96a36371 100644 --- a/source/js/common/inject-react/index.js +++ b/source/js/common/inject-react/index.js @@ -1,4 +1,5 @@ import injectJoinUs from "./join-us.js"; +import injectNewsletterSignup from "./newsletter-signup-module.js"; import injectPetitionThankYou from "./petition-thank-you.js"; /** @@ -8,6 +9,7 @@ import injectPetitionThankYou from "./petition-thank-you.js"; */ export const injectCommonReactComponents = (apps, siteUrl) => { injectJoinUs(apps, siteUrl); + injectNewsletterSignup(apps, siteUrl); // FormAssembly petition thank you screen injectPetitionThankYou(apps); }; diff --git a/source/js/common/inject-react/newsletter-signup-module.js b/source/js/common/inject-react/newsletter-signup-module.js new file mode 100644 index 000000000..f822a9932 --- /dev/null +++ b/source/js/common/inject-react/newsletter-signup-module.js @@ -0,0 +1,29 @@ +import React from "react"; +import { createRoot } from "react-dom/client"; +import BlogBodySignupForm from "../../components/newsletter-signup/organisms/blog-body-signup.jsx"; + +/** + * Inject newsletter signup forms + * @param {Array} apps The existing array we are using to to track all React client rendering calls + * @param {String} siteUrl Foundation site base URL + */ +export default (apps, siteUrl) => { + document + .querySelectorAll(`.newsletter-signup-module[data-module-type="blog-body"]`) + .forEach((element) => { + const props = element.dataset; + const sid = props.signupId || 0; + + props.apiUrl = `${siteUrl}/api/campaign/signups/${sid}/`; + props.isHidden = false; + + apps.push( + new Promise((resolve) => { + const root = createRoot(element); + root.render( ++ {errorMessage} +
+ > + ); + } + + return ( +{message}
; +}; + +export default ValidationError; diff --git a/source/js/components/newsletter-signup/data/country-options.js b/source/js/components/newsletter-signup/data/country-options.js new file mode 100644 index 000000000..a7abe03c3 --- /dev/null +++ b/source/js/components/newsletter-signup/data/country-options.js @@ -0,0 +1,13 @@ +import SALESFORCE_COUNTRY_LIST from "../../petition/salesforce-country-list.js"; +import { getText } from "../../petition/locales"; + +let countryDefault = { value: "", label: getText(`Your country`) }; +let countryOptions = Object.keys(SALESFORCE_COUNTRY_LIST).map((code) => { + return { + value: code, + label: SALESFORCE_COUNTRY_LIST[code], + }; +}); +countryOptions.unshift(countryDefault); + +export const COUNTRY_OPTIONS = countryOptions; diff --git a/source/js/components/newsletter-signup/data/language-options.js b/source/js/components/newsletter-signup/data/language-options.js new file mode 100644 index 000000000..81ff1e20c --- /dev/null +++ b/source/js/components/newsletter-signup/data/language-options.js @@ -0,0 +1,17 @@ +const LANGUAGES = { + en: `English`, + de: `Deutsch`, + es: `Español`, + fr: `Français`, + pl: `Polski`, + "pt-BR": `Português`, +}; + +let languageOptions = Object.keys(LANGUAGES).map((code) => { + return { + value: code, + label: LANGUAGES[code], + }; +}); + +export const LANGUAGE_OPTIONS = languageOptions; diff --git a/source/js/components/newsletter-signup/molecules/input-checkbox-with-label.jsx b/source/js/components/newsletter-signup/molecules/input-checkbox-with-label.jsx new file mode 100644 index 000000000..bc8faa75d --- /dev/null +++ b/source/js/components/newsletter-signup/molecules/input-checkbox-with-label.jsx @@ -0,0 +1,41 @@ +import React from "react"; +import PropTypes from "prop-types"; +import Label from "../atoms/label.jsx"; +import InputCheckbox from "../atoms/input-checkbox.jsx"; + +const InputCheckboxField = ({ id, label, errorMessage, ...otherProps }) => { + return ( ++ {errorMessage} +
+ )} +