Skip to content

Commit

Permalink
New RootPage and FlexPage types to support new homepage (and more) (#…
Browse files Browse the repository at this point in the history
…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 <[email protected]>
  • Loading branch information
mwvolo and TomWoodward authored Jul 25, 2024
1 parent 410b416 commit cd1178f
Show file tree
Hide file tree
Showing 30 changed files with 2,131 additions and 489 deletions.
2 changes: 2 additions & 0 deletions allies/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,3 +88,5 @@ def ally_subject_list(self):
FieldPanel('short_description'),
FieldPanel('long_description'),
]

parent_page_types = ['pages.HomePage']
2 changes: 2 additions & 0 deletions api/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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='')
Expand All @@ -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)
Expand Down
2 changes: 2 additions & 0 deletions api/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from wagtail.documents.models import Document
from api.models import ProgressTracker, CustomizationRequest


class AdopterSerializer(serializers.HyperlinkedModelSerializer):

class Meta:
Expand Down Expand Up @@ -72,6 +73,7 @@ class ModuleListingField(serializers.StringRelatedField):
def to_internal_value(self, value):
return value


class CustomizationRequestSerializer(serializers.ModelSerializer):
modules = ModuleListingField(many=True)

Expand Down
27 changes: 27 additions & 0 deletions books/migrations/0157_alter_book_print_isbn_13_and_more.py
Original file line number Diff line number Diff line change
@@ -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
),
),
]
4 changes: 2 additions & 2 deletions books/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
5 changes: 4 additions & 1 deletion docker/entrypoint
Original file line number Diff line number Diff line change
@@ -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)
Expand Down
5 changes: 4 additions & 1 deletion docker/start
Original file line number Diff line number Diff line change
@@ -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
1 change: 1 addition & 0 deletions global_settings/signals.py
Original file line number Diff line number Diff line change
Expand Up @@ -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')

Expand Down
6 changes: 4 additions & 2 deletions openstax/middleware.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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:]
Expand Down Expand Up @@ -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)



41 changes: 41 additions & 0 deletions openstax/settings/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -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': {
Expand All @@ -417,6 +421,7 @@

ImageFile.LOAD_TRUNCATED_IMAGES = True

WAGTAILIMAGES_EXTENSIONS = ["gif", "jpg", "jpeg", "png", "webp", "svg"]
WAGTAILIMAGES_FORMAT_CONVERSIONS = {
'webp': 'webp',
'jpeg': 'webp',
Expand All @@ -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 #
##########
Expand Down
124 changes: 112 additions & 12 deletions pages/custom_blocks.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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)
Expand All @@ -48,7 +147,7 @@ class FAQBlock(blocks.StructBlock):
document = DocumentChooserBlock(required=False)

class Meta:
icon = 'placeholder'
icon = 'bars'


class BookProviderBlock(blocks.StructBlock):
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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
Expand All @@ -163,4 +264,3 @@ def get_api_representation(self, value, context=None):
'cover': build_document_url(value['cover'].url),
'title': value['title'],
}

Loading

0 comments on commit cd1178f

Please sign in to comment.