From cd1178f83fe7c575e6265540d03c0cc92183abed Mon Sep 17 00:00:00 2001 From: Michael Volo Date: Wed, 24 Jul 2024 20:48:03 -0500 Subject: [PATCH] New RootPage and FlexPage types to support new homepage (and more) (#1567) * spike a customizable page * Update tests.py * structure for root page and child pages * add aria and promote image * update link and cta structure * remove some validations on page block * allow a root page to live under the home page (for now) * block rearranging * flatten flex page content * config nesting * hero block restructure * make rich text FE ready * hero fixes * add lookup for layout snippet * squash migrations * remove squashed migrations * fussing with page fields * working on rootpage schema * fix docker environments holy cow * return a url for link values * make heroes more flexible * allow svg image uploads to images * add padding option to heroes * remove card cta, we can just use text links * allow internal links in rtf * add some more configs and fix hex validation * Remove max count on hero config * display value for LinkBlocks * add cta block to hero * add cta to cards (not hero) * ctalinkblock to cards * limit 1 list block for cta_block on cards * relabel * squashing migrations * delete squashed migrations * update failing test * refactor page test imports for maintainability * formatting test files for readability * allow blogs to be linked to impact page * update help text on book detail page * general page cache clear * remove background image from page layout * remove background image from layout api rep * update middleware for serving new home page to bots * refactor how layouts are configured (#1570) --------- Co-authored-by: tom --- allies/models.py | 2 + api/models.py | 2 + api/serializers.py | 2 + .../0157_alter_book_print_isbn_13_and_more.py | 27 + books/models.py | 4 +- docker/entrypoint | 5 +- docker/start | 5 +- global_settings/signals.py | 1 + openstax/middleware.py | 6 +- openstax/settings/base.py | 41 + pages/custom_blocks.py | 124 ++- ...epage_squashed_0110_alter_rootpage_body.py | 259 ++++++ ..._body_squashed_0130_alter_rootpage_body.py | 555 +++++++++++++ .../0131_alter_impact_making_a_difference.py | 52 ++ .../migrations/0132_remove_rootpage_layout.py | 17 + pages/migrations/0133_rootpage_layout.py | 68 ++ .../migrations/0134_alter_rootpage_layout.py | 68 ++ .../migrations/0135_alter_rootpage_layout.py | 69 ++ .../migrations/0136_alter_rootpage_layout.py | 70 ++ pages/models.py | 204 ++++- .../wagtail_hooks/css/page_wagtail_hooks.css | 20 - pages/templates/wagtailadmin/admin_base.html | 12 + pages/tests.py | 758 +++++++++--------- pages/wagtail_hooks.py | 24 - snippets/migrations/0037_pagelayout.py | 55 ++ .../0038_alter_pagelayout_layout.py | 18 + .../0039_alter_pagelayout_layout.py | 18 + ...0040_remove_pagelayout_background_image.py | 17 + snippets/migrations/0041_delete_pagelayout.py | 16 + snippets/tests.py | 101 +-- 30 files changed, 2131 insertions(+), 489 deletions(-) create mode 100644 books/migrations/0157_alter_book_print_isbn_13_and_more.py create mode 100644 pages/migrations/0084_customizablepage_squashed_0110_alter_rootpage_body.py create mode 100644 pages/migrations/0111_alter_rootpage_body_squashed_0130_alter_rootpage_body.py create mode 100644 pages/migrations/0131_alter_impact_making_a_difference.py create mode 100644 pages/migrations/0132_remove_rootpage_layout.py create mode 100644 pages/migrations/0133_rootpage_layout.py create mode 100644 pages/migrations/0134_alter_rootpage_layout.py create mode 100644 pages/migrations/0135_alter_rootpage_layout.py create mode 100644 pages/migrations/0136_alter_rootpage_layout.py delete mode 100644 pages/static/wagtail_hooks/css/page_wagtail_hooks.css create mode 100644 pages/templates/wagtailadmin/admin_base.html delete mode 100644 pages/wagtail_hooks.py create mode 100644 snippets/migrations/0037_pagelayout.py create mode 100644 snippets/migrations/0038_alter_pagelayout_layout.py create mode 100644 snippets/migrations/0039_alter_pagelayout_layout.py create mode 100644 snippets/migrations/0040_remove_pagelayout_background_image.py create mode 100644 snippets/migrations/0041_delete_pagelayout.py diff --git a/allies/models.py b/allies/models.py index eda56683e..8fbf7116f 100644 --- a/allies/models.py +++ b/allies/models.py @@ -88,3 +88,5 @@ def ally_subject_list(self): FieldPanel('short_description'), FieldPanel('long_description'), ] + + parent_page_types = ['pages.HomePage'] diff --git a/api/models.py b/api/models.py index db2f3d722..cdff1299c 100644 --- a/api/models.py +++ b/api/models.py @@ -19,6 +19,7 @@ class CustomizationRequest(models.Model): complete = models.BooleanField(default=False) created = models.DateTimeField(auto_now_add=True) + class FeatureFlag(models.Model): name = models.CharField(max_length=255, unique=True, help_text='Create flag names with underscores to be more machine friendly. Eg. awesome_feature') description = models.TextField(blank=True, default='') @@ -29,6 +30,7 @@ class FeatureFlag(models.Model): def __str__(self): return self.name + class WebviewSettings(models.Model): name = models.CharField(max_length=255, unique=True) value = models.CharField(max_length=255) diff --git a/api/serializers.py b/api/serializers.py index 247c013a2..f7a25346d 100644 --- a/api/serializers.py +++ b/api/serializers.py @@ -4,6 +4,7 @@ from wagtail.documents.models import Document from api.models import ProgressTracker, CustomizationRequest + class AdopterSerializer(serializers.HyperlinkedModelSerializer): class Meta: @@ -72,6 +73,7 @@ class ModuleListingField(serializers.StringRelatedField): def to_internal_value(self, value): return value + class CustomizationRequestSerializer(serializers.ModelSerializer): modules = ModuleListingField(many=True) diff --git a/books/migrations/0157_alter_book_print_isbn_13_and_more.py b/books/migrations/0157_alter_book_print_isbn_13_and_more.py new file mode 100644 index 000000000..b754c4967 --- /dev/null +++ b/books/migrations/0157_alter_book_print_isbn_13_and_more.py @@ -0,0 +1,27 @@ +# Generated by Django 5.0.7 on 2024-07-22 21:27 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("books", "0156_book_content_warning"), + ] + + operations = [ + migrations.AlterField( + model_name="book", + name="print_isbn_13", + field=models.CharField( + blank=True, help_text="ISBN 13 for print version (color).", max_length=255, null=True + ), + ), + migrations.AlterField( + model_name="book", + name="print_softcover_isbn_13", + field=models.CharField( + blank=True, help_text="ISBN 13 for print version (black and white).", max_length=255, null=True + ), + ), + ] diff --git a/books/models.py b/books/models.py index 03e3f5086..16d36f28d 100644 --- a/books/models.py +++ b/books/models.py @@ -599,9 +599,9 @@ def get_title_image_url(self): ], null=True, use_json_field=True) print_isbn_13 = models.CharField(max_length=255, blank=True, null=True, - help_text='ISBN 13 for print version (hardcover).') + help_text='ISBN 13 for print version (color).') print_softcover_isbn_13 = models.CharField(max_length=255, blank=True, null=True, - help_text='ISBN 13 for print version (softcover).') + help_text='ISBN 13 for print version (black and white).') digital_isbn_13 = models.CharField(max_length=255, blank=True, null=True, help_text='ISBN 13 for digital version.') ibook_isbn_13 = models.CharField(max_length=255, blank=True, null=True, help_text='ISBN 13 for iBook version.') ibook_volume_2_isbn_13 = models.CharField(max_length=255, blank=True, null=True, diff --git a/docker/entrypoint b/docker/entrypoint index 1462a654a..2a2810807 100755 --- a/docker/entrypoint +++ b/docker/entrypoint @@ -1,5 +1,8 @@ #!/usr/bin/env bash -set -e + +set -o errexit +set -o pipefail +set -o nounset # uncomment this to load docker secrets into environment variables # export $(egrep -v '^#' /run/secrets/* | xargs) diff --git a/docker/start b/docker/start index 9058960fc..668c2a069 100755 --- a/docker/start +++ b/docker/start @@ -1,7 +1,10 @@ #!/usr/bin/env bash +set -o errexit +set -o pipefail +set -o nounset + cd /code/ || (echo "application code must be mounted at /code/"; exit 1); DJANGO_SETTINGS_MODULE=openstax.settings.docker python3 manage.py migrate -DJANGO_SETTINGS_MODULE=openstax.settings.docker python3 manage.py createsuperuser DJANGO_SETTINGS_MODULE=openstax.settings.docker python3 manage.py runserver 0.0.0.0:8000 diff --git a/global_settings/signals.py b/global_settings/signals.py index 191dbaadd..c629b0c77 100644 --- a/global_settings/signals.py +++ b/global_settings/signals.py @@ -11,6 +11,7 @@ def clear_cloudfront_on_page_publish(sender, **kwargs): invalidate_cloudfront_caches('v2/pages') # clear general pages invalidate_cloudfront_caches('spike') + invalidate_cloudfront_caches('general') # clear resources invalidate_cloudfront_caches('books/resources') diff --git a/openstax/middleware.py b/openstax/middleware.py index e534b17ef..7fafca860 100644 --- a/openstax/middleware.py +++ b/openstax/middleware.py @@ -11,7 +11,7 @@ from books.models import Book, BookIndex from openstax.functions import build_image_url from news.models import NewsArticle -from pages.models import HomePage, Supporters, PrivacyPolicy, K12Subject, Subject, Subjects +from pages.models import HomePage, Supporters, PrivacyPolicy, K12Subject, Subject, Subjects, RootPage class HttpSmartRedirectResponse(HttpResponsePermanentRedirect): @@ -108,7 +108,7 @@ def __call__(self, request, *args, **kwargs): # index of last / to find slug, except when there isn't a last / if url_path == '': - page_slug = "openstax-homepage" + page_slug = "home" else: index = url_path.rindex('/') page_slug = url_path[index+1:] @@ -195,6 +195,8 @@ def page_by_slug(self, page_slug): return Supporters.objects.all() if page_slug == 'openstax-homepage': return HomePage.objects.filter(locale=1) + if page_slug == 'home': + return RootPage.objects.filter(locale=1) diff --git a/openstax/settings/base.py b/openstax/settings/base.py index d580a0456..679805762 100644 --- a/openstax/settings/base.py +++ b/openstax/settings/base.py @@ -406,6 +406,10 @@ WAGTAIL_USAGE_COUNT_ENABLED = False WAGTAIL_USER_CUSTOM_FIELDS = ['is_staff'] WAGTAIL_GRAVATAR_PROVIDER_URL = '//www.gravatar.com/avatar' +# serve wagtail documents direct for use with remote (s3) storage +WAGTAILADMIN_EXTERNAL_LINK_CONVERSION = 'exact' +WAGTAIL_REDIRECTS_FILE_STORAGE = 'cache' +WAGTAILFORMS_HELP_TEXT_ALLOW_HTML = True WAGTAILSEARCH_BACKENDS = { 'default': { @@ -417,6 +421,7 @@ ImageFile.LOAD_TRUNCATED_IMAGES = True +WAGTAILIMAGES_EXTENSIONS = ["gif", "jpg", "jpeg", "png", "webp", "svg"] WAGTAILIMAGES_FORMAT_CONVERSIONS = { 'webp': 'webp', 'jpeg': 'webp', @@ -426,6 +431,42 @@ WAGTAILIMAGES_MAX_UPLOAD_SIZE = 2 * 1024 * 1024 # 2MB DATA_UPLOAD_MAX_NUMBER_FIELDS = 10240 +WAGTAILADMIN_RICH_TEXT_EDITORS = { + 'default': { + 'WIDGET': 'wagtail.admin.rich_text.DraftailRichTextArea', + 'OPTIONS': { + 'features': ['h1', + 'h2', + 'h3', + 'h4', + 'h5', + 'h6', + 'bold', + 'italic', + 'ol', + 'ul', + 'hr', + 'link', + 'document-link', + 'image', + 'embed', + 'code', + 'blockquote', + 'superscript', + 'subscript', + 'strikethrough'] + } + }, +} + +from wagtail.embeds.oembed_providers import youtube, vimeo +WAGTAILEMBEDS_FINDERS = [ + { + 'class': 'wagtail.embeds.finders.oembed', + 'providers': [youtube, vimeo] + } +] + ########## # Sentry # ########## diff --git a/pages/custom_blocks.py b/pages/custom_blocks.py index 80bbe7638..462da9ee4 100644 --- a/pages/custom_blocks.py +++ b/pages/custom_blocks.py @@ -7,11 +7,117 @@ from api.serializers import ImageSerializer from openstax.functions import build_image_url, build_document_url +from wagtail.rich_text import expand_db_html + + +class APIRichTextBlock(blocks.RichTextBlock): + def get_api_representation(self, value, context=None): + representation = super().get_api_representation(value, context) + return expand_db_html(representation) + + class Meta: + icon = 'doc-full' + + +class LinkBlock(blocks.StreamBlock): + external = blocks.URLBlock(required=False) + internal = blocks.PageChooserBlock(required=False) + document = DocumentChooserBlock(required=False) + + class Meta: + icon = 'link' + max_num = 1 + + def get_api_representation(self, value, context=None): + for child in value: + if child.block_type == 'document': + return { + 'value': child.value.url, + 'type': child.block_type, + 'metadata': child.value.content_type, + } + elif child.block_type == 'external': + return { + 'value': child.block.get_prep_value(child.value), + 'type': child.block_type, + } + elif child.block_type == 'internal': + return { + 'value': child.value.url_path, + 'type': child.block_type, + } + else: + return None + + +class CTALinkBlock(blocks.StructBlock): + text = blocks.CharBlock(required=True) + aria_label = blocks.CharBlock(required=False) + target = LinkBlock(required=True) + + class Meta: + icon = 'placeholder' + label = "Call to Action" + + +class CTAButtonBarBlock(blocks.StructBlock): + actions = blocks.ListBlock(CTALinkBlock(required=False, label="Button"), + default=[], + max_num=2, + label='Actions' + ) + + class Meta: + icon = 'placeholder' + label = "Calls to Action" class ImageFormatChoiceBlock(FieldBlock): - field = forms.ChoiceField(choices=( - ('left', 'Wrap left'), ('right', 'Wrap right'), ('mid', 'Mid width'), ('full', 'Full width'),)) + field = forms.ChoiceField(required=False, choices=( + ('left', 'Wrap left'), + ('right', 'Wrap right'), + ('mid', 'Mid width'), + ('full', 'Full width'), + )) + + +class APIImageChooserBlock(ImageChooserBlock): + def get_api_representation(self, value, context=None): + try: + return ImageSerializer(context=context).to_representation(value) + except AttributeError: + return None + + +class DividerBlock(StructBlock): + image = APIImageChooserBlock() + config = blocks.StreamBlock([ + ('alignment', blocks.ChoiceBlock(choices=[ + ('center', 'Center'), + ('content_left', 'Left side of content.'), + ('content_right', 'Right side of content.'), + ('body_left', 'Left side of window.'), + ('body_right', 'Right side of window.'), + ], default='center')), + ('width', blocks.RegexBlock(regex=r'^[0-9]+(px|%|rem)$', required=False, error_messages={ + 'invalid': "must be valid css measurement. eg: 30px, 50%, 10rem" + })), + ('height', blocks.RegexBlock(regex=r'^[0-9]+(px|%|rem)$', required=False, error_messages={ + 'invalid': "must be valid css measurement. eg: 30px, 50%, 10rem" + })), + ('offset_vertical', blocks.RegexBlock(regex=r'^\-?[0-9]+(px|%|rem)$', required=False, error_messages={ + 'invalid': "must be valid css measurement. eg: 30px, 50%, 10rem" + })), + ('offset_horizontal', blocks.RegexBlock(regex=r'^\-?[0-9]+(px|%|rem)$', required=False, error_messages={ + 'invalid': "must be valid css measurement. eg: 30px, 50%, 10rem" + })) + ], block_counts={ + 'alignment': {'max_num': 1}, + 'width': {'max_num': 1}, + 'height': {'max_num': 1}, + 'offset_vertical': {'max_num': 1}, + 'offset_horizontal': {'max_num': 1}, + }, required=False) class ImageBlock(StructBlock): @@ -22,13 +128,6 @@ class ImageBlock(StructBlock): identifier = blocks.CharBlock(required=False, help_text="Used by the frontend for Google Analytics.") -class APIImageChooserBlock(ImageChooserBlock): # Use this block to return the path in the page API, does not support alt_text and alignment - def get_api_representation(self, value, context=None): - try: - return ImageSerializer(context=context).to_representation(value) - except AttributeError: - return None - class ColumnBlock(blocks.StructBlock): heading = blocks.CharBlock(required=False) content = blocks.RichTextBlock(required=False) @@ -48,7 +147,7 @@ class FAQBlock(blocks.StructBlock): document = DocumentChooserBlock(required=False) class Meta: - icon = 'placeholder' + icon = 'bars' class BookProviderBlock(blocks.StructBlock): @@ -90,10 +189,11 @@ class CardImageBlock(blocks.StructBlock): class Meta: icon = 'image' + class StoryBlock(blocks.StructBlock): image = APIImageChooserBlock(required=False) story_text = blocks.TextBlock(required=False) - linked_story = blocks.PageChooserBlock(target_model='pages.ImpactStory') + linked_story = blocks.PageChooserBlock(page_type=['pages.ImpactStory', 'news.NewsArticle']) embedded_video = blocks.RawHTMLBlock(required=False) class Meta: @@ -138,6 +238,7 @@ class TestimonialBlock(blocks.StructBlock): author_name = blocks.CharBlock(required=True) author_title = blocks.CharBlock(required=True) testimonial = blocks.RichTextBlock(required=True) + class Meta: author_icon = 'image' max_num = 4 @@ -163,4 +264,3 @@ def get_api_representation(self, value, context=None): 'cover': build_document_url(value['cover'].url), 'title': value['title'], } - diff --git a/pages/migrations/0084_customizablepage_squashed_0110_alter_rootpage_body.py b/pages/migrations/0084_customizablepage_squashed_0110_alter_rootpage_body.py new file mode 100644 index 000000000..28edca803 --- /dev/null +++ b/pages/migrations/0084_customizablepage_squashed_0110_alter_rootpage_body.py @@ -0,0 +1,259 @@ +# Generated by Django 5.0.7 on 2024-07-16 00:11 + +import django.db.models.deletion +import pages.custom_blocks +import wagtail.blocks +import wagtail.documents.blocks +import wagtail.fields +from django.db import migrations, models + + +class Migration(migrations.Migration): + + replaces = [ + ("pages", "0084_customizablepage"), + ("pages", "0085_customizablepage_page_layout"), + ("pages", "0086_rootpage_flexiblepage_delete_customizablepage"), + ("pages", "0087_alter_rootpage_body"), + ("pages", "0088_alter_rootpage_body"), + ("pages", "0089_alter_rootpage_body"), + ("pages", "0090_rename_page_layout_rootpage_layout_and_more"), + ("pages", "0091_alter_rootpage_body"), + ("pages", "0092_rename_flexiblepage_flexpage_alter_rootpage_body"), + ("pages", "0093_alter_rootpage_body"), + ("pages", "0094_alter_rootpage_body"), + ("pages", "0095_alter_rootpage_body"), + ("pages", "0096_alter_rootpage_body"), + ("pages", "0097_alter_rootpage_body"), + ("pages", "0098_alter_rootpage_body"), + ("pages", "0099_alter_rootpage_body"), + ("pages", "0100_alter_rootpage_body"), + ("pages", "0101_alter_rootpage_body"), + ("pages", "0102_alter_rootpage_body"), + ("pages", "0103_alter_rootpage_body"), + ("pages", "0104_alter_rootpage_body"), + ("pages", "0105_alter_rootpage_body"), + ("pages", "0106_alter_rootpage_body"), + ("pages", "0107_alter_rootpage_body"), + ("pages", "0108_alter_rootpage_body"), + ("pages", "0109_alter_rootpage_body"), + ("pages", "0110_alter_rootpage_body"), + ] + + dependencies = [ + ("pages", "0083_alter_k12mainpage_testimonials"), + ("snippets", "0037_pagelayout"), + ("wagtailcore", "0089_log_entry_data_json_null_to_object"), + ("wagtailimages", "0025_alter_image_file_alter_rendition_file"), + ] + + operations = [ + migrations.CreateModel( + name="RootPage", + 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", + ), + ), + ( + "body", + wagtail.fields.StreamField( + [ + ( + "hero", + wagtail.blocks.StructBlock( + [ + ("text", pages.custom_blocks.APIRichTextBlock()), + ("image", pages.custom_blocks.APIImageChooserBlock(required=False)), + ( + "cta", + wagtail.blocks.ListBlock( + wagtail.blocks.StructBlock( + [ + ("text", wagtail.blocks.CharBlock(required=False)), + ("aria_label", wagtail.blocks.CharBlock(required=False)), + ( + "target", + wagtail.blocks.StreamBlock( + [ + ( + "external", + wagtail.blocks.URLBlock(required=False), + ), + ( + "internal", + wagtail.blocks.PageChooserBlock(required=False), + ), + ( + "document", + wagtail.documents.blocks.DocumentChooserBlock( + required=False + ), + ), + ], + required=False, + ), + ), + ], + required=False, + ), + label="Calls to Action", + max_num=2, + ), + ), + ( + "config", + wagtail.blocks.StreamBlock( + [ + ( + "image_alignment", + wagtail.blocks.ChoiceBlock( + choices=[ + ("left", "Left"), + ("right", "Right"), + ("topLeft", "Top Left"), + ("topRight", "Top Right"), + ("bottomLeft", "Bottom Left"), + ("bottomRight", "Bottom Right"), + ] + ), + ), + ( + "image_size", + wagtail.blocks.ChoiceBlock( + choices=[ + ("auto", "Auto"), + ("contain", "Contain"), + ("cover", "Cover"), + ] + ), + ), + ], + max_num=2, + required=False, + ), + ), + ] + ), + ), + ( + "section", + wagtail.blocks.StreamBlock( + [ + ( + "cards", + wagtail.blocks.ListBlock( + wagtail.blocks.StructBlock( + [ + ("text", pages.custom_blocks.APIRichTextBlock()), + ( + "cta", + wagtail.blocks.StructBlock( + [ + ("text", wagtail.blocks.CharBlock(required=False)), + ( + "aria_label", + wagtail.blocks.CharBlock(required=False), + ), + ( + "target", + wagtail.blocks.StreamBlock( + [ + ( + "external", + wagtail.blocks.URLBlock( + required=False + ), + ), + ( + "internal", + wagtail.blocks.PageChooserBlock( + required=False + ), + ), + ( + "document", + wagtail.documents.blocks.DocumentChooserBlock( + required=False + ), + ), + ], + required=False, + ), + ), + ], + required=False, + ), + ), + ] + ) + ), + ), + ( + "config", + wagtail.blocks.StreamBlock( + [ + ( + "corner_style", + wagtail.blocks.ChoiceBlock( + choices=[("rounded", "Rounded"), ("square", "Square")] + ), + ) + ], + max_num=1, + ), + ), + ("text", pages.custom_blocks.APIRichTextBlock()), + ("html", wagtail.blocks.RawHTMLBlock()), + ] + ), + ), + ("html", wagtail.blocks.RawHTMLBlock()), + ], + use_json_field=True, + ), + ), + ("layout", models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to="snippets.pagelayout")), + ( + "promote_image", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="+", + to="wagtailimages.image", + ), + ), + ], + options={ + "abstract": False, + }, + bases=("wagtailcore.page",), + ), + migrations.CreateModel( + name="FlexPage", + fields=[ + ( + "rootpage_ptr", + models.OneToOneField( + auto_created=True, + on_delete=django.db.models.deletion.CASCADE, + parent_link=True, + primary_key=True, + serialize=False, + to="pages.rootpage", + ), + ), + ], + options={ + "abstract": False, + }, + bases=("pages.rootpage",), + ), + ] diff --git a/pages/migrations/0111_alter_rootpage_body_squashed_0130_alter_rootpage_body.py b/pages/migrations/0111_alter_rootpage_body_squashed_0130_alter_rootpage_body.py new file mode 100644 index 000000000..29aeb966d --- /dev/null +++ b/pages/migrations/0111_alter_rootpage_body_squashed_0130_alter_rootpage_body.py @@ -0,0 +1,555 @@ +# Generated by Django 5.0.7 on 2024-07-22 20:17 + +import pages.custom_blocks +import wagtail.blocks +import wagtail.documents.blocks +import wagtail.fields +from django.db import migrations + + +class Migration(migrations.Migration): + + replaces = [ + ("pages", "0111_alter_rootpage_body"), + ("pages", "0112_alter_rootpage_body"), + ("pages", "0113_alter_rootpage_body"), + ("pages", "0114_alter_rootpage_body"), + ("pages", "0115_alter_rootpage_body"), + ("pages", "0116_alter_rootpage_body"), + ("pages", "0117_alter_rootpage_body"), + ("pages", "0118_alter_rootpage_body"), + ("pages", "0119_alter_rootpage_body"), + ("pages", "0120_alter_rootpage_body"), + ("pages", "0121_alter_rootpage_body"), + ("pages", "0122_alter_rootpage_body"), + ("pages", "0123_alter_rootpage_body"), + ("pages", "0124_alter_rootpage_body"), + ("pages", "0125_alter_rootpage_body"), + ("pages", "0126_alter_rootpage_body"), + ("pages", "0127_alter_rootpage_body"), + ("pages", "0128_alter_rootpage_body"), + ("pages", "0129_alter_rootpage_body"), + ("pages", "0130_alter_rootpage_body"), + ] + + dependencies = [ + ("pages", "0084_customizablepage_squashed_0110_alter_rootpage_body"), + ] + + operations = [ + migrations.AlterField( + model_name="rootpage", + name="body", + field=wagtail.fields.StreamField( + [ + ( + "hero", + wagtail.blocks.StructBlock( + [ + ( + "content", + wagtail.blocks.StreamBlock( + [ + ( + "cards_block", + wagtail.blocks.StructBlock( + [ + ( + "cards", + wagtail.blocks.ListBlock( + wagtail.blocks.StructBlock( + [ + ( + "text", + pages.custom_blocks.APIRichTextBlock(), + ), + ( + "cta_block", + wagtail.blocks.ListBlock( + wagtail.blocks.StructBlock( + [ + ( + "text", + wagtail.blocks.CharBlock( + required=True + ), + ), + ( + "aria_label", + wagtail.blocks.CharBlock( + required=False + ), + ), + ( + "target", + wagtail.blocks.StreamBlock( + [ + ( + "external", + wagtail.blocks.URLBlock( + required=False + ), + ), + ( + "internal", + wagtail.blocks.PageChooserBlock( + required=False + ), + ), + ( + "document", + wagtail.documents.blocks.DocumentChooserBlock( + required=False + ), + ), + ], + required=True, + ), + ), + ], + label="Link", + required=False, + ), + default=[], + label="Call To Action", + max_num=1, + ), + ), + ] + ) + ), + ), + ( + "config", + wagtail.blocks.StreamBlock( + [ + ( + "card_size", + wagtail.blocks.IntegerBlock( + help_text="Width multiplier. default 27.", + min_value=0, + ), + ), + ( + "card_style", + wagtail.blocks.ChoiceBlock( + choices=[ + ("rounded", "Rounded"), + ("square", "Square"), + ] + ), + ), + ], + block_counts={ + "card_size": {"max_num": 1}, + "card_style": {"max_num": 1}, + }, + required=False, + ), + ), + ], + label="Cards Block", + ), + ), + ("text", pages.custom_blocks.APIRichTextBlock()), + ("html", wagtail.blocks.RawHTMLBlock()), + ( + "cta_block", + wagtail.blocks.StructBlock( + [ + ( + "actions", + wagtail.blocks.ListBlock( + wagtail.blocks.StructBlock( + [ + ( + "text", + wagtail.blocks.CharBlock(required=True), + ), + ( + "aria_label", + wagtail.blocks.CharBlock(required=False), + ), + ( + "target", + wagtail.blocks.StreamBlock( + [ + ( + "external", + wagtail.blocks.URLBlock( + required=False + ), + ), + ( + "internal", + wagtail.blocks.PageChooserBlock( + required=False + ), + ), + ( + "document", + wagtail.documents.blocks.DocumentChooserBlock( + required=False + ), + ), + ], + required=True, + ), + ), + ], + label="Button", + required=False, + ), + default=[], + label="Actions", + max_num=2, + ), + ) + ] + ), + ), + ] + ), + ), + ("image", pages.custom_blocks.APIImageChooserBlock(required=False)), + ("image_alt", wagtail.blocks.CharBlock(required=False)), + ( + "config", + wagtail.blocks.StreamBlock( + [ + ( + "image_alignment", + wagtail.blocks.ChoiceBlock( + choices=[ + ("left", "Left"), + ("right", "Right"), + ("topLeft", "Top Left"), + ("topRight", "Top Right"), + ("bottomLeft", "Bottom Left"), + ("bottomRight", "Bottom Right"), + ] + ), + ), + ( + "image_size", + wagtail.blocks.ChoiceBlock( + choices=[ + ("auto", "Auto"), + ("contain", "Contain"), + ("cover", "Cover"), + ] + ), + ), + ( + "padding", + wagtail.blocks.IntegerBlock( + help_text="Padding multiplier. default 0.", min_value=0 + ), + ), + ( + "background_color", + wagtail.blocks.RegexBlock( + error_mssages={"invalid": "not a valid hex color."}, + help_text="eg: #ff0000", + regex="#[a-zA-Z0-9]{6}", + ), + ), + ], + block_counts={ + "background_color": {"max_num": 1}, + "image_alignment": {"max_num": 1}, + "image_size": {"max_num": 1}, + "padding": {"max_num": 1}, + }, + required=False, + ), + ), + ] + ), + ), + ( + "section", + wagtail.blocks.StructBlock( + [ + ( + "content", + wagtail.blocks.StreamBlock( + [ + ( + "cards_block", + wagtail.blocks.StructBlock( + [ + ( + "cards", + wagtail.blocks.ListBlock( + wagtail.blocks.StructBlock( + [ + ( + "text", + pages.custom_blocks.APIRichTextBlock(), + ), + ( + "cta_block", + wagtail.blocks.ListBlock( + wagtail.blocks.StructBlock( + [ + ( + "text", + wagtail.blocks.CharBlock( + required=True + ), + ), + ( + "aria_label", + wagtail.blocks.CharBlock( + required=False + ), + ), + ( + "target", + wagtail.blocks.StreamBlock( + [ + ( + "external", + wagtail.blocks.URLBlock( + required=False + ), + ), + ( + "internal", + wagtail.blocks.PageChooserBlock( + required=False + ), + ), + ( + "document", + wagtail.documents.blocks.DocumentChooserBlock( + required=False + ), + ), + ], + required=True, + ), + ), + ], + label="Link", + required=False, + ), + default=[], + label="Call To Action", + max_num=1, + ), + ), + ] + ) + ), + ), + ( + "config", + wagtail.blocks.StreamBlock( + [ + ( + "card_size", + wagtail.blocks.IntegerBlock( + help_text="Width multiplier. default 27.", + min_value=0, + ), + ), + ( + "card_style", + wagtail.blocks.ChoiceBlock( + choices=[ + ("rounded", "Rounded"), + ("square", "Square"), + ] + ), + ), + ], + block_counts={ + "card_size": {"max_num": 1}, + "card_style": {"max_num": 1}, + }, + required=False, + ), + ), + ], + label="Cards Block", + ), + ), + ("text", pages.custom_blocks.APIRichTextBlock()), + ("html", wagtail.blocks.RawHTMLBlock()), + ( + "cta_block", + wagtail.blocks.StructBlock( + [ + ( + "actions", + wagtail.blocks.ListBlock( + wagtail.blocks.StructBlock( + [ + ( + "text", + wagtail.blocks.CharBlock(required=True), + ), + ( + "aria_label", + wagtail.blocks.CharBlock(required=False), + ), + ( + "target", + wagtail.blocks.StreamBlock( + [ + ( + "external", + wagtail.blocks.URLBlock( + required=False + ), + ), + ( + "internal", + wagtail.blocks.PageChooserBlock( + required=False + ), + ), + ( + "document", + wagtail.documents.blocks.DocumentChooserBlock( + required=False + ), + ), + ], + required=True, + ), + ), + ], + label="Button", + required=False, + ), + default=[], + label="Actions", + max_num=2, + ), + ) + ] + ), + ), + ] + ), + ), + ( + "config", + wagtail.blocks.StreamBlock( + [ + ( + "background_color", + wagtail.blocks.RegexBlock( + error_mssages={"invalid": "not a valid hex color."}, + help_text="eg: #ff0000", + regex="#[a-zA-Z0-9]{6}", + ), + ), + ( + "padding", + wagtail.blocks.IntegerBlock( + help_text="Padding multiplier. default 0.", min_value=0 + ), + ), + ( + "text_alignment", + wagtail.blocks.ChoiceBlock( + choices=[("center", "Center"), ("left", "Left"), ("right", "Right")] + ), + ), + ], + block_counts={ + "background_color": {"max_num": 1}, + "padding": {"max_num": 1}, + "text_alignment": {"max_num": 1}, + }, + required=False, + ), + ), + ] + ), + ), + ( + "divider", + wagtail.blocks.StructBlock( + [ + ("image", pages.custom_blocks.APIImageChooserBlock()), + ( + "config", + wagtail.blocks.StreamBlock( + [ + ( + "alignment", + wagtail.blocks.ChoiceBlock( + choices=[ + ("center", "Center"), + ("content_left", "Left side of content."), + ("content_right", "Right side of content."), + ("body_left", "Left side of window."), + ("body_right", "Right side of window."), + ] + ), + ), + ( + "width", + wagtail.blocks.RegexBlock( + error_messages={ + "invalid": "must be valid css measurement. eg: 30px, 50%, 10rem" + }, + regex="^[0-9]+(px|%|rem)$", + required=False, + ), + ), + ( + "height", + wagtail.blocks.RegexBlock( + error_messages={ + "invalid": "must be valid css measurement. eg: 30px, 50%, 10rem" + }, + regex="^[0-9]+(px|%|rem)$", + required=False, + ), + ), + ( + "offset_vertical", + wagtail.blocks.RegexBlock( + error_messages={ + "invalid": "must be valid css measurement. eg: 30px, 50%, 10rem" + }, + regex="^\\-?[0-9]+(px|%|rem)$", + required=False, + ), + ), + ( + "offset_horizontal", + wagtail.blocks.RegexBlock( + error_messages={ + "invalid": "must be valid css measurement. eg: 30px, 50%, 10rem" + }, + regex="^\\-?[0-9]+(px|%|rem)$", + required=False, + ), + ), + ], + block_counts={ + "alignment": {"max_num": 1}, + "height": {"max_num": 1}, + "offset_horizontal": {"max_num": 1}, + "offset_vertical": {"max_num": 1}, + "width": {"max_num": 1}, + }, + required=False, + ), + ), + ] + ), + ), + ("html", wagtail.blocks.RawHTMLBlock()), + ], + use_json_field=True, + ), + ), + ] diff --git a/pages/migrations/0131_alter_impact_making_a_difference.py b/pages/migrations/0131_alter_impact_making_a_difference.py new file mode 100644 index 000000000..805b1fcc6 --- /dev/null +++ b/pages/migrations/0131_alter_impact_making_a_difference.py @@ -0,0 +1,52 @@ +# Generated by Django 5.0.7 on 2024-07-22 21:25 + +import pages.custom_blocks +import wagtail.blocks +import wagtail.fields +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ("pages", "0111_alter_rootpage_body_squashed_0130_alter_rootpage_body"), + ] + + operations = [ + migrations.AlterField( + model_name="impact", + name="making_a_difference", + field=wagtail.fields.StreamField( + [ + ( + "content", + wagtail.blocks.StructBlock( + [ + ("heading", wagtail.blocks.CharBlock()), + ("description", wagtail.blocks.RichTextBlock()), + ( + "stories", + wagtail.blocks.ListBlock( + wagtail.blocks.StructBlock( + [ + ("image", pages.custom_blocks.APIImageChooserBlock(required=False)), + ("story_text", wagtail.blocks.TextBlock(required=False)), + ( + "linked_story", + wagtail.blocks.PageChooserBlock( + page_type=["pages.ImpactStory", "news.NewsArticle"] + ), + ), + ("embedded_video", wagtail.blocks.RawHTMLBlock(required=False)), + ] + ) + ), + ), + ] + ), + ) + ], + use_json_field=True, + ), + ), + ] diff --git a/pages/migrations/0132_remove_rootpage_layout.py b/pages/migrations/0132_remove_rootpage_layout.py new file mode 100644 index 000000000..7c1e8a163 --- /dev/null +++ b/pages/migrations/0132_remove_rootpage_layout.py @@ -0,0 +1,17 @@ +# Generated by Django 5.0.7 on 2024-07-24 23:45 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ("pages", "0131_alter_impact_making_a_difference"), + ] + + operations = [ + migrations.RemoveField( + model_name="rootpage", + name="layout", + ), + ] diff --git a/pages/migrations/0133_rootpage_layout.py b/pages/migrations/0133_rootpage_layout.py new file mode 100644 index 000000000..da6ebfabd --- /dev/null +++ b/pages/migrations/0133_rootpage_layout.py @@ -0,0 +1,68 @@ +# Generated by Django 5.0.7 on 2024-07-24 23:47 + +import wagtail.blocks +import wagtail.documents.blocks +import wagtail.fields +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ("pages", "0132_remove_rootpage_layout"), + ] + + operations = [ + migrations.AddField( + model_name="rootpage", + name="layout", + field=wagtail.fields.StreamField( + [ + ( + "landing", + wagtail.blocks.StructBlock( + [ + ( + "nav_links", + wagtail.blocks.ListBlock( + wagtail.blocks.StructBlock( + [ + ("text", wagtail.blocks.CharBlock(required=True)), + ("aria_label", wagtail.blocks.CharBlock(required=False)), + ( + "target", + wagtail.blocks.StreamBlock( + [ + ("external", wagtail.blocks.URLBlock(required=False)), + ( + "internal", + wagtail.blocks.PageChooserBlock(required=False), + ), + ( + "document", + wagtail.documents.blocks.DocumentChooserBlock( + required=False + ), + ), + ], + required=True, + ), + ), + ], + label="Link", + required=False, + ), + default=[], + label="Nav Links", + max_num=1, + ), + ) + ] + ), + ) + ], + default=[], + use_json_field=True, + ), + ), + ] diff --git a/pages/migrations/0134_alter_rootpage_layout.py b/pages/migrations/0134_alter_rootpage_layout.py new file mode 100644 index 000000000..7e640c53b --- /dev/null +++ b/pages/migrations/0134_alter_rootpage_layout.py @@ -0,0 +1,68 @@ +# Generated by Django 5.0.7 on 2024-07-24 23:50 + +import wagtail.blocks +import wagtail.documents.blocks +import wagtail.fields +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ("pages", "0133_rootpage_layout"), + ] + + operations = [ + migrations.AlterField( + model_name="rootpage", + name="layout", + field=wagtail.fields.StreamField( + [ + ("default", wagtail.blocks.StructBlock([])), + ( + "landing", + wagtail.blocks.StructBlock( + [ + ( + "nav_links", + wagtail.blocks.ListBlock( + wagtail.blocks.StructBlock( + [ + ("text", wagtail.blocks.CharBlock(required=True)), + ("aria_label", wagtail.blocks.CharBlock(required=False)), + ( + "target", + wagtail.blocks.StreamBlock( + [ + ("external", wagtail.blocks.URLBlock(required=False)), + ( + "internal", + wagtail.blocks.PageChooserBlock(required=False), + ), + ( + "document", + wagtail.documents.blocks.DocumentChooserBlock( + required=False + ), + ), + ], + required=True, + ), + ), + ], + label="Link", + required=False, + ), + default=[], + label="Nav Links", + ), + ) + ] + ), + ), + ], + default=[], + use_json_field=True, + ), + ), + ] diff --git a/pages/migrations/0135_alter_rootpage_layout.py b/pages/migrations/0135_alter_rootpage_layout.py new file mode 100644 index 000000000..80b1202ad --- /dev/null +++ b/pages/migrations/0135_alter_rootpage_layout.py @@ -0,0 +1,69 @@ +# Generated by Django 5.0.7 on 2024-07-24 23:56 + +import wagtail.blocks +import wagtail.documents.blocks +import wagtail.fields +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ("pages", "0134_alter_rootpage_layout"), + ] + + operations = [ + migrations.AlterField( + model_name="rootpage", + name="layout", + field=wagtail.fields.StreamField( + [ + ("default", wagtail.blocks.StructBlock([])), + ( + "landing", + wagtail.blocks.StructBlock( + [ + ( + "nav_links", + wagtail.blocks.ListBlock( + wagtail.blocks.StructBlock( + [ + ("text", wagtail.blocks.CharBlock(required=True)), + ("aria_label", wagtail.blocks.CharBlock(required=False)), + ( + "target", + wagtail.blocks.StreamBlock( + [ + ("external", wagtail.blocks.URLBlock(required=False)), + ( + "internal", + wagtail.blocks.PageChooserBlock(required=False), + ), + ( + "document", + wagtail.documents.blocks.DocumentChooserBlock( + required=False + ), + ), + ], + required=True, + ), + ), + ], + label="Link", + required=False, + ), + default=[], + label="Nav Links", + ), + ) + ] + ), + ), + ], + blank=True, + default=[], + use_json_field=True, + ), + ), + ] diff --git a/pages/migrations/0136_alter_rootpage_layout.py b/pages/migrations/0136_alter_rootpage_layout.py new file mode 100644 index 000000000..d0fe81b6f --- /dev/null +++ b/pages/migrations/0136_alter_rootpage_layout.py @@ -0,0 +1,70 @@ +# Generated by Django 5.0.7 on 2024-07-24 23:58 + +import wagtail.blocks +import wagtail.documents.blocks +import wagtail.fields +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ("pages", "0135_alter_rootpage_layout"), + ] + + operations = [ + migrations.AlterField( + model_name="rootpage", + name="layout", + field=wagtail.fields.StreamField( + [ + ("default", wagtail.blocks.StructBlock([])), + ( + "landing", + wagtail.blocks.StructBlock( + [ + ( + "nav_links", + wagtail.blocks.ListBlock( + wagtail.blocks.StructBlock( + [ + ("text", wagtail.blocks.CharBlock(required=True)), + ("aria_label", wagtail.blocks.CharBlock(required=False)), + ( + "target", + wagtail.blocks.StreamBlock( + [ + ("external", wagtail.blocks.URLBlock(required=False)), + ( + "internal", + wagtail.blocks.PageChooserBlock(required=False), + ), + ( + "document", + wagtail.documents.blocks.DocumentChooserBlock( + required=False + ), + ), + ], + required=True, + ), + ), + ], + label="Link", + required=False, + ), + default=[], + label="Nav Links", + ), + ) + ], + label="Landing Page", + ), + ), + ], + blank=True, + default=[], + use_json_field=True, + ), + ), + ] diff --git a/pages/models.py b/pages/models.py index bb1d48242..573f10c88 100644 --- a/pages/models.py +++ b/pages/models.py @@ -9,6 +9,7 @@ from wagtail.models import Orderable, Page from wagtail.api import APIField from wagtail.models import Site +from rest_framework.fields import Field from api.models import FeatureFlag from openstax.functions import build_image_url, build_document_url @@ -30,12 +31,206 @@ InfoBoxBlock, \ TestimonialBlock, \ AllyLogoBlock, \ - AssignableBookBlock + AssignableBookBlock, \ + DividerBlock, \ + APIRichTextBlock, \ + CTAButtonBarBlock, \ + CTALinkBlock -from .custom_fields import \ - Group +from .custom_fields import Group import snippets.models as snippets +# Constants for styling options on Root/Flex pages +# consider moving to a constants.py file +CARDS_STYLE_CHOICES = [ + ('rounded', 'Rounded'), + ('square', 'Square'), +] +HERO_IMAGE_ALIGNMENT_CHOICES = [ + ('left', 'Left'), + ('right', 'Right'), + ('topLeft', 'Top Left'), + ('topRight', 'Top Right'), + ('bottomLeft', 'Bottom Left'), + ('bottomRight', 'Bottom Right'), +] +HERO_IMAGE_SIZE_CHOICES = [ + ('auto', 'Auto'), + ('contain', 'Contain'), + ('cover', 'Cover'), +] +SECTION_CONTENT_BLOCKS = [ + ('cards_block', blocks.StructBlock([ + ('cards', blocks.ListBlock( + blocks.StructBlock([ + ('text', APIRichTextBlock()), + ('cta_block', blocks.ListBlock(CTALinkBlock(required=False, label="Link"), + default=[], + max_num=1, + label='Call To Action' + )), + ]), + )), + ('config', blocks.StreamBlock([ + ('card_size', blocks.IntegerBlock(min_value=0, help_text='Width multiplier. default 27.')), + ('card_style', blocks.ChoiceBlock(choices=CARDS_STYLE_CHOICES)), + ], block_counts={ + 'card_size': {'max_num': 1}, + 'card_style': {'max_num': 1}, + }, required=False)), + ], label="Cards Block")), + ('text', APIRichTextBlock()), + ('html', blocks.RawHTMLBlock()), + ('cta_block', CTAButtonBarBlock()), +] + +# we have one RootPage, which is the parent of all other pages +# this is the only page that should be created at the top level of the page tree +# this should be the homepage +class RootPage(Page): + layout = StreamField([ + ('default', blocks.StructBlock([ + ])), + ('landing', blocks.StructBlock([ + ('nav_links', blocks.ListBlock(CTALinkBlock(required=False, label="Link"), + default=[], + label='Nav Links' + )), + ], label='Landing Page')), + ], max_num=1, blank=True, collapsed=True, use_json_field=True, default=[]) + + body = StreamField([ + ('hero', blocks.StructBlock([ + ('content', blocks.StreamBlock(SECTION_CONTENT_BLOCKS)), + ('image', APIImageChooserBlock(required=False)), + ('image_alt', blocks.CharBlock(required=False)), + ('config', blocks.StreamBlock([ + ('image_alignment', blocks.ChoiceBlock(choices=HERO_IMAGE_ALIGNMENT_CHOICES)), + ('image_size', blocks.ChoiceBlock(choices=HERO_IMAGE_SIZE_CHOICES)), + ('padding', blocks.IntegerBlock(min_value=0, help_text='Padding multiplier. default 0.')), + ('background_color', blocks.RegexBlock( + regex=r'#[a-zA-Z0-9]{6}', + help_text='eg: #ff0000', + error_mssages={'invalid': 'not a valid hex color.'} + )), + ], block_counts={ + 'image_alignment': {'max_num': 1}, + 'image_size': {'max_num': 1}, + 'padding': {'max_num': 1}, + 'background_color': {'max_num': 1}, + }, required=False)) + ])), + ('section', blocks.StructBlock([ + ('content', blocks.StreamBlock(SECTION_CONTENT_BLOCKS)), + ('config', blocks.StreamBlock([ + ('background_color', blocks.RegexBlock( + regex=r'#[a-zA-Z0-9]{6}', + help_text='eg: #ff0000', + error_mssages={'invalid': 'not a valid hex color.'} + )), + ('padding', blocks.IntegerBlock(min_value=0, help_text='Padding multiplier. default 0.')), + ('text_alignment', blocks.ChoiceBlock(choices=[ + ('center', 'Center'), + ('left', 'Left'), + ('right', 'Right'), + ], default='left')), + ], block_counts={ + 'background_color': {'max_num': 1}, + 'padding': {'max_num': 1}, + 'text_alignment': {'max_num': 1}, + }, required=False)) + ])), + ('divider', DividerBlock()), + ('html', blocks.RawHTMLBlock()), + ], use_json_field=True) + + promote_image = models.ForeignKey( + 'wagtailimages.Image', + null=True, + blank=False, + on_delete=models.SET_NULL, + related_name='+' + ) + + api_fields = [ + APIField('layout'), + APIField('body'), + APIField('slug'), + APIField('seo_title'), + APIField('search_description'), + ] + + content_panels = [ + TitleFieldPanel('title', help_text="For CMS use only. Use 'Promote' tab above to edit SEO information."), + FieldPanel('layout'), + FieldPanel('body'), + ] + + promote_panels = [ + FieldPanel('slug', widget=SlugInput), + FieldPanel('seo_title'), + FieldPanel('search_description'), + FieldPanel('promote_image') + ] + + template = 'page.html' + preview_modes = [] + max_count = 1 + # TODO: we are allowing this to be built as a child of the homepage. Not ideal. + # Once the home page is released, use something to migrate homepage children to root page and remove this parent type. + parent_page_types = ['wagtailcore.Page', 'pages.HomePage'] + subpage_types = ['pages.FlexPage'] # which might also require allowing all pages to be children. + + def __str__(self): + return self.path + + def get_url_parts(self, *args, **kwargs): + url_parts = super(RootPage, self).get_url_parts(*args, **kwargs) + + if url_parts is None: + # in this case, the page doesn't have a well-defined URL in the first place - + # for example, it's been created at the top level of the page tree + # and hasn't been associated with a site record + return None + + site_id, root_url, page_path = url_parts + + # return '/' in place of the real page path for the root page + return site_id, root_url, '/' + + def get_sitemap_urls(self, request=None): + return [ + { + 'location': '{}/'.format(Site.find_for_request(request).root_url), + 'lastmod': (self.last_published_at or self.latest_revision_created_at), + } + ] + +# subclass of RootPage with a few overrides for subpages +class FlexPage(RootPage): + parent_page_types = ['pages.RootPage'] + template = 'page.html' + + def get_url_parts(self, *args, **kwargs): + url_parts = super(FlexPage, self).get_url_parts(*args, **kwargs) + + if url_parts is None: + return None + + site_id, root_url, page_path = url_parts + + return site_id, root_url, page_path + + def get_sitemap_urls(self, request=None): + return [ + { + 'location': '{}/{}'.format(Site.find_for_request(request).root_url, self.slug), + 'lastmod': (self.last_published_at or self.latest_revision_created_at), + } + ] + + +#TODO: start removing these pages as we move to the above structure for all pages. class AboutUsPage(Page): who_heading = models.CharField(max_length=255) @@ -480,10 +675,9 @@ class Meta: 'books.BookIndex', 'news.NewsIndex', 'news.PressIndex', + 'pages.RootPage', ] - max_count = 1 - def __str__(self): return self.path diff --git a/pages/static/wagtail_hooks/css/page_wagtail_hooks.css b/pages/static/wagtail_hooks/css/page_wagtail_hooks.css deleted file mode 100644 index 26d3acf09..000000000 --- a/pages/static/wagtail_hooks/css/page_wagtail_hooks.css +++ /dev/null @@ -1,20 +0,0 @@ -.modal-content .link-types :first-child { - /* hide the 'internal' link option from the page chooser */ - display: none; -} - -.modal-content .link-types { - /* ensure the 'before' element can be positioned absolute */ - position: relative; -} - -.modal-content .link-types::before { - /* hide the left '|' bar */ - background: white; - bottom: 0; - content: ''; - left: 0; - position: absolute; - top: 0; - width: 5px; -} \ No newline at end of file diff --git a/pages/templates/wagtailadmin/admin_base.html b/pages/templates/wagtailadmin/admin_base.html new file mode 100644 index 000000000..910434ae2 --- /dev/null +++ b/pages/templates/wagtailadmin/admin_base.html @@ -0,0 +1,12 @@ +{% extends "wagtailadmin/admin_base.html" %} +{% load static %} + +{% block branding_title %}OpenStax CMS{% endblock %} + +{% block branding_logo %} + OpenStax CMS +{% endblock %} + +{% block branding_favicon %} + +{% endblock %} diff --git a/pages/tests.py b/pages/tests.py index 7ddb1f753..e11ced0d3 100644 --- a/pages/tests.py +++ b/pages/tests.py @@ -2,56 +2,21 @@ import json from django.test import TestCase, Client -from django.core.management import call_command from wagtail.test.utils import WagtailTestUtils, WagtailPageTestCase from wagtail.models import Page -from pages.models import (HomePage, - ContactUs, - AboutUsPage, - GeneralPage, - Supporters, - MapPage, - Give, - TermsOfService, - FAQ, - GiveForm, - Accessibility, - Licensing, - Technology, - ErrataList, - PrivacyPolicy, - PrintOrder, - LearningResearchPage, - TeamPage, - Careers, - Impact, - InstitutionalPartnership, - InstitutionalPartnerProgramPage, - CreatorFestPage, - PartnersPage, - WebinarPage, - MathQuizPage, - LLPHPage, - TutorMarketing, - Subjects, - Subject, - FormHeadings, - AllyLogos, - K12MainPage, - Assignable, ImpactStory) - -from news.models import NewsIndex, PressIndex -from books.models import BookIndex +from pages import models as page_models + from shared.test_utilities import assertPathDoesNotRedirectToTrailingSlash, mock_user_login from http import cookies + class HomePageTests(WagtailPageTestCase): def setUp(self): mock_user_login() def test_cant_create_homepage_under_homepage(self): - self.assertCanNotCreateAt(HomePage, HomePage) + self.assertCanNotCreateAt(page_models.HomePage, page_models.HomePage) def test_homepage_return_correct_page(self): response = self.client.get('/') @@ -59,88 +24,34 @@ def test_homepage_return_correct_page(self): def test_can_create_homepage(self): root_page = Page.objects.get(title="Root") - homepage = HomePage(title="Hello World", - slug="hello-world", - ) + homepage = page_models.HomePage(title="Hello World", + slug="hello-world", + ) root_page.add_child(instance=homepage) retrieved_page = Page.objects.get(id=homepage.id) self.assertEqual(retrieved_page.title, "Hello World") - def test_allowed_subpages(self): - self.assertAllowedSubpageTypes(HomePage, { - ContactUs, - AboutUsPage, - GeneralPage, - NewsIndex, - PressIndex, - BookIndex, - Supporters, - MapPage, - Give, - TermsOfService, - FAQ, - GiveForm, - Accessibility, - Licensing, - Technology, - ErrataList, - PrivacyPolicy, - PrintOrder, - LearningResearchPage, - TeamPage, - Careers, - Impact, - InstitutionalPartnership, - InstitutionalPartnerProgramPage, - CreatorFestPage, - PartnersPage, - WebinarPage, - MathQuizPage, - LLPHPage, - TutorMarketing, - Subjects, - FormHeadings, - AllyLogos, - K12MainPage, - Assignable, - }) class PageTests(WagtailPageTestCase): def setUp(self): mock_user_login() root_page = Page.objects.get(title="Root") - self.homepage = HomePage(title="Hello World", - slug="hello-world", - ) + self.homepage = page_models.HomePage(title="Hello World", + slug="hello-world", + ) root_page.add_child(instance=self.homepage) def test_can_create_ipp_page(self): - self.assertCanCreateAt(HomePage, InstitutionalPartnerProgramPage) - - def test_can_create_llph_page(self): - llph_page = LLPHPage(title="LLPH", - heading="Heading", - subheading="Subheading", - signup_link_href="http://rice.edu", - signup_link_text="Click me", - info_link_slug="/llph-slug", - info_link_text="Click me", - book_heading="Book heading", - book_description="I should accept HTML.") - self.homepage.add_child(instance=llph_page) - self.assertCanCreateAt(HomePage, LLPHPage) - - retrieved_page = Page.objects.get(id=llph_page.id) - self.assertEqual(retrieved_page.title, "LLPH") + self.assertCanCreateAt(page_models.HomePage, page_models.InstitutionalPartnerProgramPage) def test_can_create_team_page(self): - team_page = TeamPage(title="Team Page", - header="Heading", - subheader="Subheading", - team_header="Our Team") + team_page = page_models.TeamPage(title="Team Page", + header="Heading", + subheader="Subheading", + team_header="Our Team") self.homepage.add_child(instance=team_page) - self.assertCanCreateAt(HomePage, TeamPage) + self.assertCanCreateAt(page_models.HomePage, page_models.TeamPage) revision = team_page.save_revision() revision.publish() team_page.save() @@ -150,348 +61,442 @@ def test_can_create_team_page(self): self.assertEqual(retrieved_page.title, "Team Page") def test_can_create_about_us_page(self): - about_us = AboutUsPage(title='About Us', - who_heading='About Us', - who_paragraph='Who paragraph', - what_heading='what heading', - what_paragraph='what paragraph', - where_heading='where heading', - where_paragraph='where paragraph', - ) + about_us = page_models.AboutUsPage(title='About Us', + who_heading='About Us', + who_paragraph='Who paragraph', + what_heading='what heading', + what_paragraph='what paragraph', + where_heading='where heading', + where_paragraph='where paragraph', + ) self.homepage.add_child(instance=about_us) - self.assertCanCreateAt(HomePage, AboutUsPage) + self.assertCanCreateAt(page_models.HomePage, page_models.AboutUsPage) retrieved_page = Page.objects.get(id=about_us.id) self.assertEqual(retrieved_page.title, "About Us") def test_can_create_k12_main_page(self): - k12_page = K12MainPage(title='K12 Main Page', - banner_headline='banner heading', - banner_description='banner description', - subject_list_default='subject list default', - highlights_header='highlights header', - subject_library_header='subjects library header', - subject_library_description='subjects library description', - ) + k12_page = page_models.K12MainPage(title='K12 Main Page', + banner_headline='banner heading', + banner_description='banner description', + subject_list_default='subject list default', + highlights_header='highlights header', + subject_library_header='subjects library header', + subject_library_description='subjects library description', + ) self.homepage.add_child(instance=k12_page) - self.assertCanCreateAt(HomePage, K12MainPage) + self.assertCanCreateAt(page_models.HomePage, page_models.K12MainPage) retrieved_page = Page.objects.get(id=k12_page.id) self.assertEqual(retrieved_page.title, "K12 Main Page") def test_can_create_contact_us_page(self): - contact_us_page = ContactUs(title='Contact Us', - tagline='this is a tagline', - mailing_header='Mailing header', - mailing_address='123 Street, East SomeTown, Tx', - customer_service='How can I help you?', - ) + contact_us_page = page_models.ContactUs(title='Contact Us', + tagline='this is a tagline', + mailing_header='Mailing header', + mailing_address='123 Street, East SomeTown, Tx', + customer_service='How can I help you?', + ) self.homepage.add_child(instance=contact_us_page) - self.assertCanCreateAt(HomePage, ContactUs) + self.assertCanCreateAt(page_models.HomePage, page_models.ContactUs) retrieved_page = Page.objects.get(id=contact_us_page.id) self.assertEqual(retrieved_page.title, "Contact Us") def test_can_create_general_page(self): - general_page = GeneralPage(title='General Page', - body=json.dumps( - [{"id": "ae6f048b-6eb5-42e7-844f-cfcd459f81b5", "type": "heading", - "value": "General Page"}, - {"id": "a21bcbd4-fec4-432e-bf06-966d739c6de9", "type": "paragraph", - "value": "

This is a test of a general page.

Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

"}, - {"id": "4d339739-131c-4547-954b-0787afdc4914", "type": "tagline", - "value": "This is a test"}] - ), - ) + general_page = page_models.GeneralPage(title='General Page', + body=json.dumps( + [{"id": "ae6f048b-6eb5-42e7-844f-cfcd459f81b5", "type": "heading", + "value": "General Page"}, + {"id": "a21bcbd4-fec4-432e-bf06-966d739c6de9", "type": "paragraph", + "value": "

This is a test of a " + "general page.

Lorem " + "ipsum dolor sit amet, consectetur adipiscing elit, " + "sed do eiusmod tempor incididunt ut labore et dolore " + "magna aliqua. Ut enim ad minim veniam, quis nostrud " + "exercitation ullamco laboris nisi ut aliquip ex ea " + "commodo consequat. Duis aute irure dolor in " + "reprehenderit in voluptate velit esse cillum dolore eu " + "fugiat nulla pariatur. Excepteur sint occaecat " + "cupidatat non proident, sunt in culpa qui officia " + "deserunt mollit anim id est laborum.

"}, + {"id": "4d339739-131c-4547-954b-0787afdc4914", "type": "tagline", + "value": "This is a test"}] + ), + ) self.homepage.add_child(instance=general_page) - self.assertCanCreateAt(HomePage, GeneralPage) + self.assertCanCreateAt(page_models.HomePage, page_models.GeneralPage) retrieved_page = Page.objects.get(id=general_page.id) self.assertEqual(retrieved_page.title, "General Page") def test_can_create_supporters_page(self): - supporters_page = Supporters(title='Supporters Page', - banner_heading='Banner heading', - banner_description='Banner description', - funder_groups=json.dumps( - [{"id": "5cf47334-37f6-433c-b695-80936bc7d236", "type": "content", "value": - {"image": None, "funders": [{"id": "647ce8be-eabb-40a2-abb9-136c2bb00e53", "type": "item", "value": {"url": "https://openstax.org", "funder_name": "Musser Foundation"}}, - {"id": "c31a9a7a-f6e2-4b74-929b-5bc39f96568d", "type": "item", "value": {"url": "https://openstax.org", "funder_name": "Mike and Patricia Foundation"}} - ]}}] - ), - disclaimer='This field cannot be left blank', - ) + supporters_page = page_models.Supporters(title='Supporters Page', + banner_heading='Banner heading', + banner_description='Banner description', + funder_groups=json.dumps( + [{"id": "5cf47334-37f6-433c-b695-80936bc7d236", "type": "content", + "value": + {"image": None, "funders": [ + {"id": "647ce8be-eabb-40a2-abb9-136c2bb00e53", + "type": "item", "value": {"url": "https://openstax.org", + "funder_name": "Musser " + "Foundation"}}, + {"id": "c31a9a7a-f6e2-4b74-929b-5bc39f96568d", + "type": "item", "value": {"url": "https://openstax.org", + "funder_name": "Mike and " + "Patricia " + "Foundation"}} + ]}}] + ), + disclaimer='This field cannot be left blank', + ) self.homepage.add_child(instance=supporters_page) - self.assertCanCreateAt(HomePage, Supporters) + self.assertCanCreateAt(page_models.HomePage, page_models.Supporters) retrieved_page = Page.objects.get(id=supporters_page.id) self.assertEqual(retrieved_page.title, "Supporters Page") def test_can_create_tos_page(self): - tos_page = TermsOfService(title='Terms of Service Page', - intro_heading='Intro heading', - terms_of_service_content='This is the terms of service', - ) + tos_page = page_models.TermsOfService(title='Terms of Service Page', + intro_heading='Intro heading', + terms_of_service_content='This is the terms of service', + ) self.homepage.add_child(instance=tos_page) - self.assertCanCreateAt(HomePage, TermsOfService) + self.assertCanCreateAt(page_models.HomePage, page_models.TermsOfService) retrieved_page = Page.objects.get(id=tos_page.id) self.assertEqual(retrieved_page.title, "Terms of Service Page") def test_can_create_faq_page(self): - faq_page = FAQ(title='FAQ Page', - intro_heading='Intro heading', - intro_description='This is the FAQ page', - questions=json.dumps( - [{"id": "bc328439-9ad5-4fe7-9adc-1dba59389330", "type": "question", - "value": {"slug": "how-does-openstax-work", - "answer": "

Using OpenStax is simple! Review the textbook online, and if you decide to use it in your class, let us know. To access faculty-only materials, you can create an OpenStax account and request faculty access. Once we manually verify that you’re an instructor, you will have access to all faculty content. Include the textbook URL in your course materials, and from there, students can choose how they want to view the book.

If you’re a student, simply access the web view, download a PDF, or purchase a hard copy via Amazon or your campus. Even students whose professors have not adopted OpenStax are welcome to use OpenStax textbooks.

", - "document": None, "question": "

How does OpenStax work?

"}}, - {"id": "c985605f-cc41-4758-84dc-a5e42098814c", "type": "question", - "value": {"slug": "why-use-openstax-textbooks", - "answer": "

The costs of textbooks are rising, and students have difficulty keeping up with the high price of required materials. A large percentage of students show up for the first day of class without the course textbook. Imagine if every student had immediate, unlimited access to the text. How would that help you meet your goals?

Open resources also allow you to use the text in a way that’s best for you and your students. You aren’t bound by copyright or digital rights restrictions, and you can adapt the book as you see fit.

", - "document": None, - "question": "

Why should instructors use OpenStax textbooks?

"}} - ] - ) - ) + faq_page = page_models.FAQ(title='FAQ Page', + intro_heading='Intro heading', + intro_description='This is the FAQ page', + questions=json.dumps( + [{"id": "bc328439-9ad5-4fe7-9adc-1dba59389330", "type": "question", + "value": {"slug": "how-does-openstax-work", + "answer": "

Using OpenStax is simple! Review the textbook " + "online, and if you decide to use it in your class, " + "let us " + "know. To access faculty-only materials, " + "you can create an OpenStax account and request faculty " + "access. Once we manually verify that you’re an " + "instructor, you will have access to all faculty " + "content. Include the textbook URL in your course " + "materials, and from there, students can choose how they " + "want to view the book.

If you’re a student, " + "simply access the web view, download a PDF, or purchase " + "a hard copy via Amazon or your campus. Even students " + "whose professors have not adopted OpenStax are welcome " + "to use OpenStax textbooks.

", + "document": None, "question": "

How does OpenStax work?

"}}, + {"id": "c985605f-cc41-4758-84dc-a5e42098814c", "type": "question", + "value": {"slug": "why-use-openstax-textbooks", + "answer": "

The costs of textbooks are rising, and students have " + "difficulty keeping up with the high price of required " + "materials. A large percentage of students show up for " + "the first day of class without the course textbook. " + "Imagine if every student had immediate, " + "unlimited access to the text. How would that help you " + "meet your goals?

Open resources also allow you to " + "use the text in a way that’s best for you and your " + "students. You aren’t bound by copyright or digital " + "rights restrictions, and you can adapt the book as you " + "see fit.

", + "document": None, + "question": "

Why should instructors use OpenStax " + "textbooks?

"}} + ] + ) + ) self.homepage.add_child(instance=faq_page) - self.assertCanCreateAt(HomePage, FAQ) + self.assertCanCreateAt(page_models.HomePage, page_models.FAQ) retrieved_page = Page.objects.get(id=faq_page.id) self.assertEqual(retrieved_page.title, "FAQ Page") def test_can_create_accessibility_page(self): - accessibility_page = Accessibility(title='Accessibility Page', - intro_heading='Intro heading', - accessibility_content='This is about accessibility', - ) + accessibility_page = page_models.Accessibility(title='Accessibility Page', + intro_heading='Intro heading', + accessibility_content='This is about accessibility', + ) self.homepage.add_child(instance=accessibility_page) - self.assertCanCreateAt(HomePage, Accessibility) + self.assertCanCreateAt(page_models.HomePage, page_models.Accessibility) retrieved_page = Page.objects.get(id=accessibility_page.id) self.assertEqual(retrieved_page.title, "Accessibility Page") def test_can_create_licensing_page(self): - licensing_page = Licensing(title='Licensing Page', - intro_heading='Intro heading', - licensing_content='This is about licensing', - ) + licensing_page = page_models.Licensing(title='Licensing Page', + intro_heading='Intro heading', + licensing_content='This is about licensing', + ) self.homepage.add_child(instance=licensing_page) - self.assertCanCreateAt(HomePage, Licensing) + self.assertCanCreateAt(page_models.HomePage, page_models.Licensing) retrieved_page = Page.objects.get(id=licensing_page.id) self.assertEqual(retrieved_page.title, "Licensing Page") def test_can_create_technology_page(self): - technology_page = Technology(title='Technology Page', - intro_heading='Intro heading', - intro_description='intro description', - banner_cta='CTA!, CTA!', - select_tech_heading='select tech heading', - select_tech_step_1='Step 1', - select_tech_step_2='Step 2', - select_tech_step_3='Step 3', - new_frontier_heading='new frontier heading', - new_frontier_subheading='subheading', - new_frontier_description='new frontier description', - new_frontier_cta_1='CTA 1', - new_frontier_cta_2='CTA 2', - ) + technology_page = page_models.Technology(title='Technology Page', + intro_heading='Intro heading', + intro_description='intro description', + banner_cta='CTA!, CTA!', + select_tech_heading='select tech heading', + select_tech_step_1='Step 1', + select_tech_step_2='Step 2', + select_tech_step_3='Step 3', + new_frontier_heading='new frontier heading', + new_frontier_subheading='subheading', + new_frontier_description='new frontier description', + new_frontier_cta_1='CTA 1', + new_frontier_cta_2='CTA 2', + ) self.homepage.add_child(instance=technology_page) - self.assertCanCreateAt(HomePage, Technology) + self.assertCanCreateAt(page_models.HomePage, page_models.Technology) retrieved_page = Page.objects.get(id=technology_page.id) self.assertEqual(retrieved_page.title, "Technology Page") def test_can_create_careers_page(self): - careers_page = Careers(title='Careers Page', - intro_heading='Intro heading', - careers_content='This is about careers', - ) + careers_page = page_models.Careers(title='Careers Page', + intro_heading='Intro heading', + careers_content='This is about careers', + ) self.homepage.add_child(instance=careers_page) - self.assertCanCreateAt(HomePage, Careers) + self.assertCanCreateAt(page_models.HomePage, page_models.Careers) retrieved_page = Page.objects.get(id=careers_page.id) self.assertEqual(retrieved_page.title, "Careers Page") def test_can_create_privacy_policy_page(self): - privacy_page = PrivacyPolicy(title='Privacy Policy Page', - intro_heading='Intro heading', - privacy_content='This is about privacy', - ) + privacy_page = page_models.PrivacyPolicy(title='Privacy Policy Page', + intro_heading='Intro heading', + privacy_content='This is about privacy', + ) self.homepage.add_child(instance=privacy_page) - self.assertCanCreateAt(HomePage, PrivacyPolicy) + self.assertCanCreateAt(page_models.HomePage, page_models.PrivacyPolicy) retrieved_page = Page.objects.get(id=privacy_page.id) self.assertEqual(retrieved_page.title, "Privacy Policy Page") - def test_can_create_print_order_page(self): - print_page = PrintOrder(title='Print Order Page', - intro_heading='Intro heading', - intro_description='Intro description', - featured_provider_intro_blurb='Blurb, blurb, blurb', - other_providers_intro_blurb='Another blurb', - providers=json.dumps( - [{"id": "18ff0a7a-4f63-4c51-bd01-c6f8daf47b77", "type": "provider", - "value": {"cta": "Order from UnknownEdu", - "url": "http://info.unknownedu.com/openstax", "icon": None, - "name": "UnknownEdu", - "blurb": "UnknownEdu handles the fulfillment and distribution of all formats of OpenStax textbooks to college bookstore and K12 schools.", - "canadian": False}}] - ) - - ) - self.homepage.add_child(instance=print_page) - self.assertCanCreateAt(HomePage, PrintOrder) - - retrieved_page = Page.objects.get(id=print_page.id) - self.assertEqual(retrieved_page.title, "Print Order Page") - def test_can_create_impact_page(self): - impact_page = Impact(title='Impact Page', - improving_access=json.dumps( - [{"id": "b42f66a2-a4b2-4c84-a581-3535a0fbc20b", "type": "content", "value": {"image": {"link": "", "image": None}, - "heading": "Why Open Education Matters", - "button_href": "https://riceconnect.rice.edu/donation/support-openstax-impact", - "button_text": "Give today", - "description": "

OpenStax believes that learning is a public good and that every learner has the fundamental right to pursue their education in areas that inspire them most. Our goal is to remain 100% free and easily accessible, while improving learning proficiency for every learner. Open education can open doorways to new careers, intellectual pursuits, and the betterment of society.

What might you achieve if you had the right resources at your fingertips? Join us today and help millions of learners experience an affordable and engaging education.

"}}] - ), - reach=json.dumps( - [{"id": "0b1d5fe8-f77a-4eb6-ae6a-1dc58cb08c10", "type": "content", "value": {"cards": [ - {"id": "ce31a4fb-b929-493e-96ce-5ea945c82ddd", "type": "item", - "value": {"icon": None, - "link_href": "https://openstax.org/press/openstax-surpasses-1-billion-textbook-savings-wide-ranging-impact-teaching-learning-and-student-success", - "link_text": "Read more", - "description": "$1.8 billion saved in education costs since 2012"}}]}}] - ), - quote=json.dumps( - [{"id": "49d89977-5433-4b1f-9e0f-ed4982ac4201", "type": "content", - "value": {"image": {"link": "", "image": None}, - "quote": "

I believe that knowledge should be free. And that means we shouldn’t be tying up ideas and knowledge in proprietary systems. We should be finding ways to share ideas and knowledge to make the world a better place. – Prof. Richard Baraniuk,

Founder of OpenStax, the C. Sidney Burrus Professor of Electrical and Computer Engineering at Rice University, and Fellow of the American Academy of Arts and Sciences

"}}] - ), - making_a_difference=json.dumps( - [{"id": "c1dc4294-2b44-4320-9d72-2286af863302", "type": "content", - "value": {"heading": "Community Stories", "stories": [ - {"id": "8593338c-752a-491c-8b07-33ef8d42bc10", "type": "item", - "value": {"image": None, - "story_text": "Rahul Kane grew up in India in a family of educators, and upon completing his undergraduate studies, moved to the United States in 2005 to pursue graduate school. Now, he teaches biology at Century College and the University of St. Thomas. Read more about why he prefers open educational resources!", - "linked_story": None, "embedded_video": ""}}, - {"id": "29293e1b-bd9c-4f66-8e7d-d35e279aaee9", "type": "item", - "value": {"image": None, - "story_text": "Megan expects to get her nursing diploma in five years and sees herself as a successful nurse working in a hospital.", - "linked_story": None, "embedded_video": ""}}]}}] - ), - disruption=json.dumps( - [{"id": "81f78c27-d65b-449a-b930-21a3449c11f1", "type": "content", - "value": {"graph": {"image": {"link": "", "image": None}, "image_alt_text": ""}, - "heading": "Positive Disruption", - "description": "The price of textbooks is declining due to open education’s disruption of the college textbook market, removing financial barriers to advanced education, and reducing student debt. According to an economist, “The ‘textbook bubble’ is finally starting to deflate, due to the creative destruction and competition from free/low-cost textbooks from groups like OpenStax” (Mark Perry, AEI, 2019)."}}] - ), - supporter_community=json.dumps( - [{"id": "0d555d88-1c47-483f-bdd7-bd64aa4fae39", "type": "content", - "value": {"image": {"link": "", "image": None}, - "quote": "

OpenStax continues to expand to new subject areas, grade levels, and languages to reach more students. Yet, OpenStax is more than free textbooks. With a team of researchers, educators, and learning engineers at Rice University, OpenStax is creating research-based learning tools to help teachers and learners better personalize the education experience. OpenStax needs your partnership to continue its impact.

Ann Doerr, OpenStax Advisor

", - "heading": "Our Supporter Community", - "link_href": "https://openstax.org/foundation", - "link_text": "View our supporters"}}] - ), - giving=json.dumps( - [{"id": "e5a9a3ad-f8c6-409f-a774-f6c99daf4990", "type": "content", - "value": {"heading": "Students need your help today.", - "link_href": "https://example.com/donate", "link_text": "Give today", - "description": "Together, we can increase educational equity and quality for millions of students worldwide.", - "nonprofit_statement": "As a part of Rice University, a 501(c)(3) nonprofit, gifts to OpenStax are tax deductible to the fullest extent allowed by law. Our tax ID number is 00-111111. Read our latest Annual Report", - "annual_report_link_href": "https://example.com/annual-report", - "annual_report_link_text": "Read our latest OpenStax Brochure"}}] - ) - - ) + impact_page = page_models.Impact(title='Impact Page', + improving_access=json.dumps( + [{"id": "b42f66a2-a4b2-4c84-a581-3535a0fbc20b", "type": "content", + "value": {"image": {"link": "", "image": None}, + "heading": "Why Open Education Matters", + "button_href": "https://riceconnect.rice.edu/donation" + "/support-openstax-impact", + "button_text": "Give today", + "description": "

OpenStax " + "believes that learning is a public good and " + "that every learner has the fundamental right " + "to pursue their education in areas that " + "inspire them most. Our goal is to remain " + "100% free and easily accessible, " + "while improving learning proficiency for " + "every learner. Open education can open " + "doorways to new careers, intellectual " + "pursuits, and the betterment of " + "society.

What " + "might you achieve if you had the right " + "resources at your fingertips? Join us today " + "and help millions of learners experience an " + "affordable and engaging education.

"}}] + ), + reach=json.dumps( + [{"id": "0b1d5fe8-f77a-4eb6-ae6a-1dc58cb08c10", "type": "content", + "value": {"cards": [ + {"id": "ce31a4fb-b929-493e-96ce-5ea945c82ddd", "type": "item", + "value": {"icon": None, + "link_href": "https://openstax.org/press/openstax" + "-surpasses-1-billion-textbook-savings-wide-ranging-impact-teaching-learning-and-student-success", + "link_text": "Read more", + "description": "$1.8 billion saved in education costs " + "since 2012" + } + } + ]}}] + ), + quote=json.dumps( + [{"id": "49d89977-5433-4b1f-9e0f-ed4982ac4201", "type": "content", + "value": {"image": {"link": "", "image": None}, + "quote": "

I believe that " + "knowledge should be free. And that means we " + "shouldn’t be tying up ideas and knowledge in " + "proprietary systems. We should be finding ways to " + "share ideas and knowledge to make the world a " + "better place. – Prof. Richard Baraniuk," + "

Founder of " + "OpenStax, the C. Sidney Burrus Professor " + "of Electrical and Computer Engineering at Rice " + "University, and Fellow of the American Academy of " + "Arts and Sciences

"}}] + ), + making_a_difference=json.dumps( + [{"id": "c1dc4294-2b44-4320-9d72-2286af863302", "type": "content", + "value": {"heading": "Community Stories", "stories": [ + {"id": "8593338c-752a-491c-8b07-33ef8d42bc10", "type": "item", + "value": {"image": None, + "story_text": "Rahul Kane grew up in India in a family " + "of educators, and upon completing his " + "undergraduate studies, moved to the " + "United States in 2005 to pursue graduate " + "school. Now, he teaches biology at " + "Century College and the University of " + "St. Thomas. Read more about why he " + "prefers open educational resources!", + "linked_story": None, "embedded_video": ""}}, + {"id": "29293e1b-bd9c-4f66-8e7d-d35e279aaee9", "type": "item", + "value": {"image": None, + "story_text": "Megan expects to get her nursing diploma " + "in five years and sees herself as a " + "successful nurse working in a hospital.", + "linked_story": None, "embedded_video": ""}}]}}] + ), + disruption=json.dumps( + [{"id": "81f78c27-d65b-449a-b930-21a3449c11f1", "type": "content", + "value": {"graph": {"image": {"link": "", "image": None}, + "image_alt_text": ""}, + "heading": "Positive Disruption", + "description": "The price of textbooks is declining due to " + "open education’s disruption of the college " + "textbook market, removing financial barriers " + "to advanced education, and reducing student " + "debt. According to an economist, " + "“The ‘textbook bubble’ is finally starting " + "to deflate, due to the creative destruction " + "and competition from free/low-cost textbooks " + "from groups like OpenStax” (Mark Perry, AEI, " + "2019)."}}] + ), + supporter_community=json.dumps( + [{"id": "0d555d88-1c47-483f-bdd7-bd64aa4fae39", "type": "content", + "value": {"image": {"link": "", "image": None}, + "quote": "

OpenStax continues to " + "expand to new subject areas, grade levels, " + "and languages to reach more students. Yet, " + "OpenStax is more than free textbooks. With a team " + "of researchers, educators, and learning engineers " + "at Rice University, OpenStax is creating " + "research-based learning tools to help teachers and " + "learners better personalize the education " + "experience. OpenStax needs your partnership to " + "continue its impact.

Ann Doerr, " + "OpenStax Advisor

", + "heading": "Our Supporter Community", + "link_href": "https://openstax.org/foundation", + "link_text": "View our supporters"}}] + ), + giving=json.dumps( + [{"id": "e5a9a3ad-f8c6-409f-a774-f6c99daf4990", "type": "content", + "value": {"heading": "Students need your help today.", + "link_href": "https://example.com/donate", + "link_text": "Give today", + "description": "Together, we can increase educational equity " + "and quality for millions of students " + "worldwide.", + "nonprofit_statement": "As a part of Rice University, a 501(" + "c)(3) nonprofit, gifts to OpenStax " + "are tax deductible to the fullest " + "extent allowed by law. Our tax ID " + "number is 00-111111. Read our latest " + "Annual Report", + "annual_report_link_href": "https://example.com/annual-report", + "annual_report_link_text": "Read our latest OpenStax Brochure"}}] + ) + + ) self.homepage.add_child(instance=impact_page) - self.assertCanCreateAt(HomePage, Impact) + self.assertCanCreateAt(page_models.HomePage, page_models.Impact) retrieved_page = Page.objects.get(id=impact_page.id) self.assertEqual(retrieved_page.title, "Impact Page") - story_page = ImpactStory(title='Impact Story Page', - date=datetime.datetime.now(), - heading='Impact Story', - author='Jane Doe', - body=json.dumps([{"id": "ae6f048b-6eb5-42e7-844f-cfcd459f81b5", "type": "heading", - "value": "Impact Story"}]) - ) + story_page = page_models.ImpactStory(title='Impact Story Page', + date=datetime.datetime.now(), + heading='Impact Story', + author='Jane Doe', + body=json.dumps( + [{"id": "ae6f048b-6eb5-42e7-844f-cfcd459f81b5", "type": "heading", + "value": "Impact Story"}]) + ) impact_page.add_child(instance=story_page) - self.assertCanCreateAt(Impact, ImpactStory) + self.assertCanCreateAt(page_models.Impact, page_models.ImpactStory) retrieved_page = Page.objects.get(id=story_page.id) self.assertEqual(retrieved_page.title, "Impact Story Page") def test_can_create_learning_research_page(self): - research_page = LearningResearchPage(title='Learning Research Page', - mission_body='This is our mission', - research_area_header='Research area header', - research_area_description_mobile='Research area mobile header', - people_header='People header', - publication_header='Publication header' - ) + research_page = page_models.LearningResearchPage(title='Learning Research Page', + mission_body='This is our mission', + research_area_header='Research area header', + research_area_description_mobile='Research area mobile header', + people_header='People header', + publication_header='Publication header' + ) self.homepage.add_child(instance=research_page) - self.assertCanCreateAt(HomePage, LearningResearchPage) + self.assertCanCreateAt(page_models.HomePage, page_models.LearningResearchPage) retrieved_page = Page.objects.get(id=research_page.id) self.assertEqual(retrieved_page.title, "Learning Research Page") def test_can_create_webinar_page(self): - webinar_page = WebinarPage(title='Webinar Page', - heading='Heading', - ) + webinar_page = page_models.WebinarPage(title='Webinar Page', + heading='Heading', + ) self.homepage.add_child(instance=webinar_page) - self.assertCanCreateAt(HomePage, WebinarPage) + self.assertCanCreateAt(page_models.HomePage, page_models.WebinarPage) retrieved_page = Page.objects.get(id=webinar_page.id) self.assertEqual(retrieved_page.title, "Webinar Page") def test_can_create_form_headings_page(self): - form_page = FormHeadings(title='Form Headings Page', - adoption_intro_heading='Adoption intro heading', - adoption_intro_description='Adoption intro description', - interest_intro_heading='Interest intro heading', - interest_intro_description='Interest intro description' - ) + form_page = page_models.FormHeadings(title='Form Headings Page', + adoption_intro_heading='Adoption intro heading', + adoption_intro_description='Adoption intro description', + interest_intro_heading='Interest intro heading', + interest_intro_description='Interest intro description' + ) self.homepage.add_child(instance=form_page) - self.assertCanCreateAt(HomePage, FormHeadings) + self.assertCanCreateAt(page_models.HomePage, page_models.FormHeadings) retrieved_page = Page.objects.get(id=form_page.id) self.assertEqual(retrieved_page.title, "Form Headings Page") def test_can_create_ally_logos_page(self): - ally_page = AllyLogos(title='Ally Logos Page', - heading='Ally logos heading', - description='Alloy logos description', - ally_logos_heading='Ally logos heading', - ally_logos_description='Ally logos description', - ally_logos='', - book_ally_logos_heading='Book ally logo heading', - book_ally_logos_description='Book ally logo description', - book_ally_logos = '', - ) + ally_page = page_models.AllyLogos(title='Ally Logos Page', + heading='Ally logos heading', + description='Alloy logos description', + ally_logos_heading='Ally logos heading', + ally_logos_description='Ally logos description', + ally_logos='', + book_ally_logos_heading='Book ally logo heading', + book_ally_logos_description='Book ally logo description', + book_ally_logos='', + ) self.homepage.add_child(instance=ally_page) - self.assertCanCreateAt(HomePage, AllyLogos) + self.assertCanCreateAt(page_models.HomePage, page_models.AllyLogos) retrieved_page = Page.objects.get(id=ally_page.id) self.assertEqual(retrieved_page.title, "Ally Logos Page") def test_can_create_assignable_page(self): - assignable_page = Assignable(title='Assignable Page', - subheading='Assignable subheading', - heading_description='Assignable heading description', - add_assignable_cta_header='Add assignable heading', - add_assignable_cta_description='Add assignable description', - add_assignable_cta_button_text='Add assignable', - available_courses_header='Available courses header', - courses_coming_soon_header = 'Courses coming soon header', - ) + assignable_page = page_models.Assignable(title='Assignable Page', + subheading='Assignable subheading', + heading_description='Assignable heading description', + add_assignable_cta_header='Add assignable heading', + add_assignable_cta_description='Add assignable description', + add_assignable_cta_button_text='Add assignable', + available_courses_header='Available courses header', + courses_coming_soon_header='Courses coming soon header', + ) self.homepage.add_child(instance=assignable_page) - self.assertCanCreateAt(HomePage, Assignable) + self.assertCanCreateAt(page_models.HomePage, page_models.Assignable) retrieved_page = Page.objects.get(id=assignable_page.id) self.assertEqual(retrieved_page.title, "Assignable Page") @@ -504,18 +509,18 @@ def setUp(self): def test_can_create_errata_list_page(self): root_page = Page.objects.get(title="Root") - homepage = HomePage(title="Hello World", - slug="hello-world", - ) + homepage = page_models.HomePage(title="Hello World", + slug="hello-world", + ) root_page.add_child(instance=homepage) - errata_list_page = ErrataList(title="Errata List Template", - correction_schedule="Some sample correction schedule text.", - new_edition_errata_message="New edition correction text.", - deprecated_errata_message="Deprecated errata message.", - about_header="About our correction schedule.", - about_text="Errata receieved from March through...", - about_popup="Instructor and student resources..." - ) + errata_list_page = page_models.ErrataList(title="Errata List Template", + correction_schedule="Some sample correction schedule text.", + new_edition_errata_message="New edition correction text.", + deprecated_errata_message="Deprecated errata message.", + about_header="About our correction schedule.", + about_text="Errata receieved from March through...", + about_popup="Instructor and student resources..." + ) homepage.add_child(instance=errata_list_page) retrieved_page = Page.objects.get(id=errata_list_page.id) @@ -529,15 +534,15 @@ def setUp(self): def test_can_create_subjects_page(self): root_page = Page.objects.get(title="Root") - homepage = HomePage(title="Hello World", - slug="hello-world", - ) + homepage = page_models.HomePage(title="Hello World", + slug="hello-world", + ) root_page.add_child(instance=homepage) - subjects_page = Subjects(title="Subjects", - heading="Testing Subjects Page", - description="This is a Subjects page test", - philanthropic_support="Please support us", - ) + subjects_page = page_models.Subjects(title="Subjects", + heading="Testing Subjects Page", + description="This is a Subjects page test", + philanthropic_support="Please support us", + ) homepage.add_child(instance=subjects_page) retrieved_page = Page.objects.get(id=subjects_page.id) @@ -551,26 +556,26 @@ def setUp(self): def test_can_create_subject_page(self): root_page = Page.objects.get(title="Root") - homepage = HomePage(title="Hello World", - slug="hello-world", - ) + homepage = page_models.HomePage(title="Hello World", + slug="hello-world", + ) root_page.add_child(instance=homepage) - subjects_page = Subjects(title="Subjects", - heading="Testing Subjects Page", - description="This is a Subjects page test", - philanthropic_support="Please support us", - ) + subjects_page = page_models.Subjects(title="Subjects", + heading="Testing Subjects Page", + description="This is a Subjects page test", + philanthropic_support="Please support us", + ) homepage.add_child(instance=subjects_page) - subject_page = Subject(title="Business", - page_description="Business page", - os_textbook_heading="OpenStax Business Textbooks", - philanthropic_support="Please support us", - book_categories_heading="Business book categories", - learn_more_heading="learn more", - learn_more_blog_posts="Business blog posts", - learn_more_webinars="business webinars", - learn_more_about_books="Learn more about our books", - ) + subject_page = page_models.Subject(title="Business", + page_description="Business page", + os_textbook_heading="OpenStax Business Textbooks", + philanthropic_support="Please support us", + book_categories_heading="Business book categories", + learn_more_heading="learn more", + learn_more_blog_posts="Business blog posts", + learn_more_webinars="business webinars", + learn_more_about_books="Learn more about our books", + ) subjects_page.add_child(instance=subject_page) retrieved_page = Page.objects.get(id=subject_page.id) @@ -594,6 +599,7 @@ def test_redirect(path): response = self.client.get(path) self.assertIn(response.status_code, (301, 302)) return response + return test_redirect def test_admin_link(self): diff --git a/pages/wagtail_hooks.py b/pages/wagtail_hooks.py deleted file mode 100644 index 6f5803767..000000000 --- a/pages/wagtail_hooks.py +++ /dev/null @@ -1,24 +0,0 @@ -from django.templatetags.static import static -from django.utils.html import format_html -from django.urls import reverse - -from wagtail import hooks - -@hooks.register('insert_global_admin_css') -def editor_css(): - return format_html( - '', - static("wagtail_hooks/css/page_wagtail_hooks.css") - ) - - -@hooks.register('insert_editor_js') -def editor_js(): - return format_html( - """ - - """, - reverse('wagtailadmin_choose_page_external_link') - ) diff --git a/snippets/migrations/0037_pagelayout.py b/snippets/migrations/0037_pagelayout.py new file mode 100644 index 000000000..17ca9437a --- /dev/null +++ b/snippets/migrations/0037_pagelayout.py @@ -0,0 +1,55 @@ +# Generated by Django 5.0.4 on 2024-06-28 01:41 + +import django.db.models.deletion +import uuid +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("snippets", "0036_contentwarning"), + ("wagtailcore", "0089_log_entry_data_json_null_to_object"), + ("wagtailimages", "0025_alter_image_file_alter_rendition_file"), + ] + + operations = [ + migrations.CreateModel( + name="PageLayout", + fields=[ + ("id", models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), + ("translation_key", models.UUIDField(default=uuid.uuid4, editable=False)), + ( + "layout", + models.CharField( + choices=[("default", "Default"), ("splash_image", "Splash Image")], + default="default", + max_length=255, + ), + ), + ( + "background_image", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="+", + to="wagtailimages.image", + ), + ), + ( + "locale", + models.ForeignKey( + editable=False, + on_delete=django.db.models.deletion.PROTECT, + related_name="+", + to="wagtailcore.locale", + ), + ), + ], + options={ + "abstract": False, + "unique_together": {("translation_key", "locale")}, + }, + ), + ] diff --git a/snippets/migrations/0038_alter_pagelayout_layout.py b/snippets/migrations/0038_alter_pagelayout_layout.py new file mode 100644 index 000000000..4c7e3e3ce --- /dev/null +++ b/snippets/migrations/0038_alter_pagelayout_layout.py @@ -0,0 +1,18 @@ +# Generated by Django 5.0.4 on 2024-06-28 18:00 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("snippets", "0037_pagelayout"), + ] + + operations = [ + migrations.AlterField( + model_name="pagelayout", + name="layout", + field=models.CharField(choices=[("Default", "default")], default="default", max_length=255), + ), + ] diff --git a/snippets/migrations/0039_alter_pagelayout_layout.py b/snippets/migrations/0039_alter_pagelayout_layout.py new file mode 100644 index 000000000..ba6dcbd57 --- /dev/null +++ b/snippets/migrations/0039_alter_pagelayout_layout.py @@ -0,0 +1,18 @@ +# Generated by Django 5.0.7 on 2024-07-15 23:29 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("snippets", "0038_alter_pagelayout_layout"), + ] + + operations = [ + migrations.AlterField( + model_name="pagelayout", + name="layout", + field=models.CharField(choices=[("default", "Default")], default="default", max_length=255), + ), + ] diff --git a/snippets/migrations/0040_remove_pagelayout_background_image.py b/snippets/migrations/0040_remove_pagelayout_background_image.py new file mode 100644 index 000000000..6c137b75c --- /dev/null +++ b/snippets/migrations/0040_remove_pagelayout_background_image.py @@ -0,0 +1,17 @@ +# Generated by Django 5.0.7 on 2024-07-23 16:39 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ("snippets", "0039_alter_pagelayout_layout"), + ] + + operations = [ + migrations.RemoveField( + model_name="pagelayout", + name="background_image", + ), + ] diff --git a/snippets/migrations/0041_delete_pagelayout.py b/snippets/migrations/0041_delete_pagelayout.py new file mode 100644 index 000000000..5bbe0da19 --- /dev/null +++ b/snippets/migrations/0041_delete_pagelayout.py @@ -0,0 +1,16 @@ +# Generated by Django 5.0.7 on 2024-07-24 23:53 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ("snippets", "0040_remove_pagelayout_background_image"), + ] + + operations = [ + migrations.DeleteModel( + name="PageLayout", + ), + ] diff --git a/snippets/tests.py b/snippets/tests.py index 752e7c49a..f9a5fce86 100644 --- a/snippets/tests.py +++ b/snippets/tests.py @@ -1,86 +1,94 @@ -import json -from urllib.parse import urlencode -from urllib.request import urlopen - from django.test import TestCase, Client -from django.conf import settings -from django.urls import reverse - -from snippets.models import ContentWarning, Subject, ErrataContent, GiveBanner, BlogContentType, NoWebinarMessage, K12Subject, \ - FacultyResource, StudentResource, Role, SharedContent, NewsSource, SubjectCategory, BlogCollection, \ - AmazonBookBlurb, PromoteSnippet -import snippets +from snippets import models as snippets_models class SnippetsTestCase(TestCase): def setUp(self): self.client = Client() - self.math = Subject(name="Math", page_content="Math page content.", seo_title="Math SEO Title", search_description="Math page description.") + self.math = snippets_models.Subject(name="Math", page_content="Math page content.", seo_title="Math SEO Title", + search_description="Math page description.") self.math.save() - self.economics = Subject(name="Economics", page_content="Economics page content.", seo_title="Economics SEO Title", - search_description="Economics page description.") + self.economics = snippets_models.Subject(name="Economics", page_content="Economics page content.", + seo_title="Economics SEO Title", + search_description="Economics page description.") self.economics.save() - self.live = ErrataContent(heading='Errata Content - Live', book_state='live', content='All OpenStax textbooks undergo a rigorous review process. However, like any professional-grade textbook, errors sometimes occur. The good part is, since our books are web-based, we can make updates periodically. If you have a correction to suggest, submit it here. We review your suggestion and make necessary changes.') + self.live = snippets_models.ErrataContent(heading='Errata Content - Live', book_state='live', + content='All OpenStax textbooks undergo a rigorous review process. ' + 'However, like any professional-grade textbook, ' + 'errors sometimes occur. The good part is, since our books ' + 'are web-based, we can make updates periodically. If you ' + 'have a correction to suggest, submit it here. We review ' + 'your suggestion and make necessary changes.') self.live.save() - self.deprecated = ErrataContent(heading='Errata Content - Deprecated', book_state='deprecated', content='No more corrections will be made') + self.deprecated = snippets_models.ErrataContent(heading='Errata Content - Deprecated', book_state='deprecated', + content='No more corrections will be made') self.deprecated.save() - self.give_banner = GiveBanner(html_message="Help students around the world succeed with contributions of $5, $10 or $20", link_text="Make a difference now", link_url='https://example.com') + self.give_banner = snippets_models.GiveBanner( + html_message="Help students around the world succeed with contributions of $5, $10 or $20", + link_text="Make a difference now", link_url='https://example.com') self.give_banner.save() - self.report = BlogContentType(content_type="Report") + self.report = snippets_models.BlogContentType(content_type="Report") self.report.save() - self.video = BlogContentType(content_type="Video") + self.video = snippets_models.BlogContentType(content_type="Video") self.video.save() - self.whitepaper = BlogContentType(content_type="Whitepaper") + self.whitepaper = snippets_models.BlogContentType(content_type="Whitepaper") self.whitepaper.save() - self.no_webinar_message = NoWebinarMessage(no_webinar_message="No webinars currently scheduled. In the meantime, please watch any of our past webinars.") + self.no_webinar_message = snippets_models.NoWebinarMessage( + no_webinar_message="No webinars currently scheduled. In the meantime, please watch any of our past " + "webinars.") self.no_webinar_message.save() - self.k12subject = K12Subject(name="Test Subject", intro_text='Intro text',subject_link="https://example.com/openstaxk12") + self.k12subject = snippets_models.K12Subject(name="Test Subject", intro_text='Intro text', + subject_link="https://example.com/openstaxk12") self.k12subject.save() - self.faculty_resource = FacultyResource(heading="Faculty Resource", description='resource description',unlocked_resource=True) + self.faculty_resource = snippets_models.FacultyResource(heading="Faculty Resource", + description='resource description', + unlocked_resource=True) self.faculty_resource.save() - self.student_resource = StudentResource(heading="Student Resource", description='resource description', - unlocked_resource=True) + self.student_resource = snippets_models.StudentResource(heading="Student Resource", + description='resource description', + unlocked_resource=True) self.student_resource.save() - self.role = Role(display_name="role display name", salesforce_name='role salesforce name') + self.role = snippets_models.Role(display_name="role display name", salesforce_name='role salesforce name') self.role.save() - self.shared_content = SharedContent(title="shared content", heading='shared content heading', content='shared content') + self.shared_content = snippets_models.SharedContent(title="shared content", heading='shared content heading', + content='shared content') self.shared_content.save() - self.news_source = NewsSource(name="news source") + self.news_source = snippets_models.NewsSource(name="news source") self.news_source.save() - self.subject_category = SubjectCategory(subject_category="subject category", description='subject category description') + self.subject_category = snippets_models.SubjectCategory(subject_category="subject category", + description='subject category description') self.subject_category.save() - self.blog_collection = BlogCollection(name="blog collection", description='blog collection description') + self.blog_collection = snippets_models.BlogCollection(name="blog collection", + description='blog collection description') self.blog_collection.save() - self.amazon_book_blurb = AmazonBookBlurb( + self.amazon_book_blurb = snippets_models.AmazonBookBlurb( amazon_book_blurb="Amazon Book Blurb. Amazon Book Blurb. Amazon Book Blurb.") self.amazon_book_blurb.save() - self.content_warning = ContentWarning( - content_warning = "Content Warning" - ) + self.content_warning = snippets_models.ContentWarning(content_warning="Content Warning") self.content_warning.save() - def test_can_create_subject(self): - subject = Subject(name="Science", page_content="Science page content.", seo_title="Science SEO Title", - search_description="Science page description.") + subject = snippets_models.Subject(name="Science", page_content="Science page content.", + seo_title="Science SEO Title", + search_description="Science page description.") subject.save() self.assertEqual(subject.name, "Science") @@ -120,24 +128,24 @@ def test_can_fetch_k12_subject(self): self.assertIn(b"https://example.com/openstaxk12", response.content) def test_can_fetch_faculty_resource(self): - faculty_resource = FacultyResource.objects.all()[0] - self.assertEquals(True, faculty_resource.unlocked_resource) + faculty_resource = snippets_models.FacultyResource.objects.all()[0] + self.assertEqual(True, faculty_resource.unlocked_resource) def test_can_fetch_student_resource(self): - student_resource = StudentResource.objects.all()[0] - self.assertEquals(True, student_resource.unlocked_resource) + student_resource = snippets_models.StudentResource.objects.all()[0] + self.assertEqual(True, student_resource.unlocked_resource) def test_can_fetch_role(self): response = self.client.get('/apps/cms/api/snippets/roles/?format=json') self.assertIn(b"role display name", response.content) def test_can_fetch_shared_content(self): - shared_content = SharedContent.objects.all()[0] - self.assertEquals('shared content', shared_content.title) + shared_content = snippets_models.SharedContent.objects.all()[0] + self.assertEqual('shared content', shared_content.title) def test_can_fetch_news_source(self): - news_source = NewsSource.objects.all()[0] - self.assertEquals('news source', news_source.name) + news_source = snippets_models.NewsSource.objects.all()[0] + self.assertEqual('news source', news_source.name) def test_can_fetch_subject_category(self): response = self.client.get('/apps/cms/api/snippets/subjectcategory/?format=json') @@ -152,7 +160,8 @@ def test_can_fetch_amazon_book_blurb(self): self.assertIn(b"Amazon Book Blurb", response.content) def test_can_create_promote_snippet(self): - promote_snippet = PromoteSnippet(name="Assignable", description="Assignable is available on this book.") + promote_snippet = snippets_models.PromoteSnippet(name="Assignable", + description="Assignable is available on this book.") promote_snippet.save() self.assertEqual(promote_snippet.name, "Assignable")