class DataParser: def __init__(self, content_type, single_entry=False): self.content_type = content_type self.data_parsed = [] self.renderer = RichTextRenderer() self.single_entry = single_entry def set_data(self, data): self.data = data def parse(self): self.__parse_default() if (self.single_entry == True): return self.data_parsed[0] else: return self.data_parsed def __parse_default(self): items = self.data['items'] for item in items: parsed_item = item.get('fields') self.data_parsed.append(self.__parse_document_field(parsed_item)) def __parse_document_field(self, item): parsed_item = {} for key, value in item.items(): if (isinstance(value, dict)): #is not None means has nodeType value if (value.get('nodeType') and value.get('nodeType') == 'document'): list_content = self.renderer.render(value) value = list_content parsed_item.update({key: value}) return parsed_item
def test_render_with_defaults(self): renderer = RichTextRenderer() self.assertEqual( renderer.render(full_document), "\n".join([ "<h1>Some heading</h1>", "<p></p>", "<div>{0}</div>".format({ "sys": { "id": "49rofLvvxCOiIMIi6mk8ai", "type": "Link", "linkType": "Entry", } }), "<h2>Some subheading</h2>", "<p><b>Some bold</b></p>", "<p><i>Some italics</i></p>", "<p><u>Some underline</u></p>", "<p></p>", "<p></p>", "<div>{0}</div>".format({ "sys": { "id": "5ZF9Q4K6iWSYIU2OUs0UaQ", "type": "Link", "linkType": "Entry", } }), "<p></p>", "<p>Some raw content</p>", "<p></p>", "<p>An unpublished embed:</p>", "<p></p>", "<div>{0}</div>".format({ "sys": { "id": "q2hGXkd5tICym64AcgeKK", "type": "Link", "linkType": "Entry", } }), "<p>Some more content</p>", ]), )
MAP_KEY = os.environ.get("MAP_KEY") DEBUG_STATUS = os.environ.get("DEBUG_STATUS") ENV = os.environ.get("ENV") client = contentful.Client(SPACE_ID, DELIVERY_API_KEY, API_URL, environment=ENV) BaseBlockEntryRenderer.__RENDERERS__ += [ locationBlockEntryRenderer, buttonEntryRenderer, ] renderer = RichTextRenderer({ "embedded-entry-block": BaseBlockEntryRenderer, "embedded-entry-inline": BaseInlineRenderer, }) app = Flask(__name__) Markdown(app) @app.route("/") def home_page(): entry = client.entry("1l3EHYzPbgf9UUV0oEyTDs") return render_template( "home.html", renderer=renderer, title=entry.page_title, page_components=entry.page_component,
class ContentfulPage: # TODO: List: stop list items from being wrapped in paragraph tags # TODO: Error/ Warn / Transform links to allizom client = get_client() _renderer = RichTextRenderer({ "hyperlink": LinkRenderer, "bold": StrongRenderer, "italic": EmphasisRenderer, "unordered-list": UlRenderer, "ordered-list": OlRenderer, "list-item": LiRenderer, "paragraph": PRenderer, "embedded-entry-inline": InlineEntryRenderer, "embedded-asset-block": AssetBlockRenderer, }) SPLIT_LAYOUT_CLASS = { "Even": "", "Narrow": "mzp-l-split-body-narrow", "Wide": "mzp-l-split-body-wide", } SPLIT_MEDIA_WIDTH_CLASS = { "Fill available width": "", "Fill available height": "mzp-l-split-media-constrain-height", "Overflow container": "mzp-l-split-media-overflow", } SPLIT_V_ALIGN_CLASS = { "Top": "mzp-l-split-v-start", "Center": "mzp-l-split-v-center", "Bottom": "mzp-l-split-v-end", } SPLIT_H_ALIGN_CLASS = { "Left": "mzp-l-split-h-start", "Center": "mzp-l-split-h-center", "Right": "mzp-l-split-h-end", } SPLIT_POP_CLASS = { "None": "", "Both": "mzp-l-split-pop", "Top": "mzp-l-split-pop-top", "Bottom": "mzp-l-split-pop-bottom", } CONTENT_TYPE_MAP = { "componentHero": { "proc": "get_hero_data", "css": "c-hero", }, "componentSectionHeading": { "proc": "get_section_data", "css": "c-section-heading", }, "componentSplitBlock": { "proc": "get_split_data", "css": "c-split", }, "componentCallout": { "proc": "get_callout_data", "css": "c-call-out", }, "layout2Cards": { "proc": "get_card_layout_data", "css": "t-card-layout", "js": "c-card" }, "layout3Cards": { "proc": "get_card_layout_data", "css": "t-card-layout", "js": "c-card" }, "layout4Cards": { "proc": "get_card_layout_data", "css": "t-card-layout", "js": "c-card" }, "layout5Cards": { "proc": "get_card_layout_data", "css": "t-card-layout", "js": "c-card" }, "layoutPictoBlocks": { "proc": "get_picto_layout_data", "css": ("c-picto", "t-multi-column"), }, "textOneColumn": { "proc": "get_text_column_data_1", "css": "t-multi-column", }, "textTwoColumns": { "proc": "get_text_column_data_2", "css": "t-multi-column", }, "textThreeColumns": { "proc": "get_text_column_data_3", "css": "t-multi-column", }, "textFourColumns": { "proc": "get_text_column_data_4", "css": "t-multi-column", }, } def __init__(self, request, page_id): set_current_request(request) self.request = request self.page_id = page_id self.locale = get_locale(request) @cached_property def page(self): return self.client.entry( self.page_id, { "include": 10, "locale": self.locale, # ie, get ONLY the page for the specificed locale, as long as # the locale doesn't have a fallback configured in Contentful }, ) def render_rich_text(self, node): return self._renderer.render(node) if node else "" def _get_preview_image_from_fields(self, fields): if "preview_image" in fields: # TODO request proper size image preview_image_url = fields["preview_image"].fields().get( "file", {}).get("url", {}) if preview_image_url: return f"https:{preview_image_url}" def _get_info_data__slug_title_blurb(self, entry_fields, seo_fields): if self.page.content_type.id == COMPOSE_MAIN_PAGE_TYPE: # This means we're dealing with a Compose-structured setup, # and the slug lives not on the Entry, nor the SEO object # but just on the top-level Compose `page` slug = self.page.fields().get("slug") else: # Non-Compose pages slug = entry_fields.get( "slug", "home") # TODO: check if we can use a better fallback title = getattr(self.page, "title", "") title = entry_fields.get("preview_title", title) blurb = entry_fields.get("preview_blurb", "") if seo_fields: # Defer to SEO fields for blurb if appropriate. blurb = seo_fields.get("description", "") return { "slug": slug, "title": title, "blurb": blurb, } def _get_info_data__category_tags_classification(self, entry_fields, page_type): data = {} # TODO: Check with plans for Contentful use - we may # be able to relax this check and use it for page types # once we're in all-Compose mode if page_type == CONTENT_TYPE_PAGE_RESOURCE_CENTER: if "category" in entry_fields: data["category"] = entry_fields["category"] if "tags" in entry_fields: data["tags"] = entry_fields["tags"] if "product" in entry_fields: # NB: this is a re-mapping with an eye on flexibility - pages may not always have # a 'product' key, but they might have something regarding overall classification data["classification"] = entry_fields["product"] return data def _get_info_data__theme_campaign(self, entry_fields, slug): _folder = entry_fields.get("folder", "") _in_firefox = "firefox-" if "firefox" in _folder else "" campaign = f"{_in_firefox}{slug}" theme = "firefox" if "firefox" in _folder else "mozilla" return { "theme": theme, "campaign": campaign, } def _get_info_data__locale(self, page_type, entry_fields, entry_obj): # TODO: update this once we have a robust locale field available (ideally # via Compose's parent `page`), because double-purposing the "name" field # is a bit too brittle. if page_type == "pageHome": locale = entry_fields["name"] else: locale = entry_obj.sys["locale"] return {"locale": locale} def get_info_data(self, entry_obj, seo_obj=None): # TODO, need to enable connectors entry_fields = entry_obj.fields() if seo_obj: seo_fields = seo_obj.fields() else: seo_fields = None page_type = entry_obj.content_type.id data = {} data.update( self._get_info_data__slug_title_blurb(entry_fields, seo_fields)) data.update( self._get_info_data__theme_campaign(entry_fields, data["slug"])) data.update( self._get_info_data__locale(page_type, entry_fields, entry_obj)) campaign = data.pop("campaign") data.update({ # eg www.mozilla.org-firefox-accounts or www.mozilla.org-firefox-sync "utm_source": f"www.mozilla.org-{campaign}", "utm_campaign": campaign, # eg firefox-sync }) _preview_image = self._get_preview_image_from_fields(entry_fields) if _preview_image: data["image"] = _preview_image if seo_fields: _preview_image = self._get_preview_image_from_fields(seo_fields) _seo_fields = deepcopy( seo_fields) # NB: don't mutate the source dict if _preview_image: _seo_fields["image"] = _preview_image # We don't need the preview_image key if we've had it in the past, and # if reading it fails then we don't want it sticking around, either _seo_fields.pop("preview_image", None) data.update({"seo": _seo_fields}) data.update( self._get_info_data__category_tags_classification( entry_fields, page_type, )) return data def get_content(self): # Check if it is a page or a connector, or a Compose page type entry_type = self.page.content_type.id seo_obj = None if entry_type == COMPOSE_MAIN_PAGE_TYPE: # Contentful Compose page, linking to content and SEO models entry_obj = self.page.content # The page with the actual content seo_obj = self.page.seo # The SEO model # Note that the slug lives on self.page, not the seo_obj. elif entry_type.startswith("page"): entry_obj = self.page elif entry_type == "connectHomepage": # Legacy - TODO: remove me once we're no longer using Connect: Homepage entry_obj = self.page.fields()["entry"] else: raise ValueError(f"{entry_type} is not a recognized page type") if not entry_obj: raise Exception( f"No 'Entry' detected for {self.page.content_type.id}") self.request.page_info = self.get_info_data( entry_obj, seo_obj, ) page_type = entry_obj.content_type.id page_css = set() page_js = set() fields = entry_obj.fields() content = None entries = [] def proc(item): content_type = item.sys.get("content_type").id ctype_info = self.CONTENT_TYPE_MAP.get(content_type) if ctype_info: processor = getattr(self, ctype_info["proc"]) entries.append(processor(item)) css = ctype_info.get("css") if css: if isinstance(css, str): css = (css, ) page_css.update(css) js = ctype_info.get("js") if js: if isinstance(js, str): js = (js, ) page_js.update(js) if page_type == CONTENT_TYPE_PAGE_GENERAL: # look through all entries and find content ones for key, value in fields.items(): if key == "component_hero": proc(value) elif key == "body": entries.append(self.get_text_data(value)) elif key == "component_callout": proc(value) elif page_type == CONTENT_TYPE_PAGE_RESOURCE_CENTER: # TODO: can we actually make this generic? Poss not: main_content is a custom field name _content = fields.get("main_content", {}) entries.append(self.get_text_data(_content)) else: # This covers pageVersatile, pageHome, etc content = fields.get("content") if content: # get components from content for item in content: proc(item) return { "page_type": page_type, "page_css": list(page_css), "page_js": list(page_js), "info": self.request.page_info, "entries": entries, } def get_text_data(self, value): data = { "component": "text", "body": self.render_rich_text(value), "width_class": _get_width_class("Medium") } # TODO return data def get_hero_data(self, entry_obj): fields = entry_obj.fields() hero_image_url = _get_image_url(fields["image"], 800) hero_reverse = fields.get("image_side") hero_body = self.render_rich_text(fields.get("body")) product_class = _get_product_class(fields.get( "product_icon")) if fields.get("product_icon") and fields.get( "product_icon") != "None" else "" data = { "component": "hero", "theme_class": _get_theme_class(fields.get("theme")), "product_class": product_class, "title": fields.get("heading"), "tagline": fields.get("tagline"), "body": hero_body, "image": hero_image_url, "image_class": "mzp-l-reverse" if hero_reverse == "Left" else "", "include_cta": True if fields.get("cta") else False, "cta": _make_cta_button(fields.get("cta")) if fields.get("cta") else "", } return data def get_section_data(self, entry_obj): fields = entry_obj.fields() data = { "component": "sectionHeading", "heading": fields.get("heading"), } return data def get_split_data(self, entry_obj): fields = entry_obj.fields() def get_split_class(): block_classes = [ "mzp-l-split-reversed" if fields.get("image_side") == "Left" else "", self.SPLIT_LAYOUT_CLASS.get(fields.get("body_width"), ""), self.SPLIT_POP_CLASS.get(fields.get("image_pop"), ""), ] return " ".join(block_classes) def get_body_class(): body_classes = [ self.SPLIT_V_ALIGN_CLASS.get( fields.get("body_vertical_alignment"), ""), self.SPLIT_H_ALIGN_CLASS.get( fields.get("body_horizontal_alignment"), ""), ] return " ".join(body_classes) def get_media_class(): media_classes = [ self.SPLIT_MEDIA_WIDTH_CLASS.get(fields.get("image_width"), ""), self.SPLIT_V_ALIGN_CLASS.get( fields.get("image_vertical_alignment"), ""), self.SPLIT_H_ALIGN_CLASS.get( fields.get("image_horizontal_alignment"), ""), ] return " ".join(media_classes) def get_mobile_class(): mobile_display = fields.get("mobile_display") if not mobile_display: return "" mobile_classes = [ "mzp-l-split-center-on-sm-md" if "Center content" in mobile_display else "", "mzp-l-split-hide-media-on-sm-md" if "Hide image" in mobile_display else "", ] return " ".join(mobile_classes) split_image_url = _get_image_url(fields["image"], 800) data = { "component": "split", "block_class": get_split_class(), "theme_class": _get_theme_class(fields.get("theme")), "body_class": get_body_class(), "body": self.render_rich_text(fields.get("body")), "media_class": get_media_class(), "image": split_image_url, "mobile_class": get_mobile_class(), } return data def get_callout_data(self, entry_obj): fields = entry_obj.fields() data = { "component": "callout", "theme_class": _get_theme_class(fields.get("theme")), "product_class": _get_product_class(fields.get("product_icon")) if fields.get("product_icon") else "", "title": fields.get("heading"), "body": self.render_rich_text(fields.get("body")) if fields.get("body") else "", "cta": _make_cta_button(fields.get("cta")), } return data def get_card_data(self, entry_obj, aspect_ratio): # need a fallback aspect ratio aspect_ratio = aspect_ratio or "16:9" fields = entry_obj.fields() card_body = self.render_rich_text( fields.get("body")) if fields.get("body") else "" image_url = highres_image_url = "" if "image" in fields: card_image = fields.get("image") # TODO smaller image files when layout allows it if card_image: highres_image_url = _get_card_image_url( card_image, 800, aspect_ratio) image_url = _get_card_image_url(card_image, 800, aspect_ratio) if "you_tube" in fields: # TODO: add youtube JS to page_js youtube_id = _get_youtube_id(fields.get("you_tube")) else: youtube_id = "" data = { "component": "card", "heading": fields.get("heading"), "tag": fields.get("tag"), "link": fields.get("link"), "body": card_body, "aspect_ratio": _get_aspect_ratio_class(aspect_ratio) if image_url != "" else "", "highres_image_url": highres_image_url, "image_url": image_url, "youtube_id": youtube_id, } return data def get_large_card_data(self, entry_obj, card_obj): fields = entry_obj.fields() # get card data card_data = self.get_card_data(card_obj, "16:9") # large card data large_card_image = fields.get("image") if large_card_image: highres_image_url = _get_card_image_url(large_card_image, 1860, "16:9") image_url = _get_card_image_url(large_card_image, 1860, "16:9") # over-write with large values card_data["component"] = "large_card" card_data["highres_image_url"] = highres_image_url card_data["image_url"] = image_url return card_data def get_card_layout_data(self, entry_obj): fields = entry_obj.fields() aspect_ratio = fields.get("aspect_ratio") layout = entry_obj.sys.get("content_type").id data = { "component": "cardLayout", "layout_class": _get_layout_class(layout), "aspect_ratio": aspect_ratio, "cards": [], } follows_large_card = False if layout == "layout5Cards": card_layout_obj = fields.get("large_card") card_obj = fields.get("large_card").fields().get("card") large_card_data = self.get_large_card_data(card_layout_obj, card_obj) data.get("cards").append(large_card_data) follows_large_card = True cards = fields.get("content") for card in cards: if follows_large_card: this_aspect = "1:1" follows_large_card = False else: this_aspect = aspect_ratio card_data = self.get_card_data(card, this_aspect) data.get("cards").append(card_data) return data def get_picto_data(self, picto_obj, image_width): fields = picto_obj.fields() body = self.render_rich_text( fields.get("body")) if fields.get("body") else False if "icon" in fields: picto_image = fields.get("icon") image_url = _get_image_url(picto_image, image_width) else: image_url = "" # TODO: this should cause an error, the macro requires an image return { "component": "picto", "heading": fields.get("heading"), "body": body, "image_url": image_url, } def get_picto_layout_data(self, entry): PICTO_ICON_SIZE = { "Small": 32, "Medium": 48, "Large": 64, "Extra Large": 96, "Extra Extra Large": 192, } fields = entry.fields() # layout = entry.sys.get('content_type').id def get_layout_class(): column_class = _get_column_class(str(fields.get("blocks_per_row"))) layout_classes = [ _get_width_class(fields.get("width")), column_class or "3", "mzp-t-picto-side" if fields.get("icon_position") == "Side" else "", "mzp-t-picto-center" if fields.get("block_alignment") == "Center" else "", _get_theme_class(fields.get("theme")), ] return " ".join(layout_classes) image_width = PICTO_ICON_SIZE.get( fields.get("icon_size")) if fields.get( "icon_size") else PICTO_ICON_SIZE.get("Large") data = { "component": "pictoLayout", "layout_class": get_layout_class(), "heading_level": fields.get("heading_level")[1:] if fields.get("heading_level") else 3, "image_width": image_width, "pictos": [], } pictos = fields.get("content") for picto_obj in pictos: picto_data = self.get_picto_data(picto_obj, image_width) data.get("pictos").append(picto_data) return data def get_text_column_data(self, cols, entry_obj): fields = entry_obj.fields() def get_content_class(): content_classes = [ _get_width_class(fields.get("width")), _get_column_class(str(cols)), _get_theme_class(fields.get("theme")), "mzp-u-center" if fields.get("block_alignment") == "Center" else "", ] return " ".join(content_classes) data = { "component": "textColumns", "layout_class": get_content_class(), "content": [self.render_rich_text(fields.get("body_column_one"))], } if cols > 1: data["content"].append( self.render_rich_text(fields.get("body_column_two"))) if cols > 2: data["content"].append( self.render_rich_text(fields.get("body_column_three"))) if cols > 3: data["content"].append( self.render_rich_text(fields.get("body_column_four"))) return data get_text_column_data_1 = partialmethod(get_text_column_data, 1) get_text_column_data_2 = partialmethod(get_text_column_data, 2) get_text_column_data_3 = partialmethod(get_text_column_data, 3) get_text_column_data_4 = partialmethod(get_text_column_data, 4)
def __init__(self, content_type, single_entry=False): self.content_type = content_type self.data_parsed = [] self.renderer = RichTextRenderer() self.single_entry = single_entry
def article(slug): article = Contentful.get_article_by_slug(slug) renderer = RichTextRenderer() article.html = renderer.render(article.content) return render_template('article.html', navbar=navbar, article=article)
def test_render_with_all_renderers_overridden_for_markdown(self): self.maxDiff = None renderer = RichTextRenderer({ "heading-1": HeadingOneMarkdownRenderer, "heading-2": HeadingTwoMarkdownRenderer, "paragraph": ParagraphMarkdownRenderer, "embedded-entry-block": EntryBlockMarkdownRenderer, "bold": BoldMarkdownRenderer, "italic": ItalicMarkdownRenderer, "underline": UnderlineMarkdownRenderer, }) self.assertEqual( renderer.render(full_document), "\n".join([ "# Some heading", "", "", "", "", "```", "{0}".format({ "sys": { "id": "49rofLvvxCOiIMIi6mk8ai", "type": "Link", "linkType": "Entry", } }), "```", "", "## Some subheading", "", "**Some bold**", "", "", "*Some italics*", "", "", "__Some underline__", "", "", "", "", "", "", "", "", "```", "{0}".format({ "sys": { "id": "5ZF9Q4K6iWSYIU2OUs0UaQ", "type": "Link", "linkType": "Entry", } }), "```", "", "", "", "", "", "Some raw content", "", "", "", "", "", "An unpublished embed:", "", "", "", "", "", "```", "{0}".format({ "sys": { "id": "q2hGXkd5tICym64AcgeKK", "type": "Link", "linkType": "Entry", } }), "```", "", "", "Some more content", "", ]), )
def test_render_with_emojis(self): renderer = RichTextRenderer() self.assertEqual(renderer.render(mock_document_with_unicode), "\n".join(["<p>😇</p>"]))
def test_null_renderer_will_raise_an_error_if_unknown_node_type_is_not_mapped( self): renderer = RichTextRenderer() with self.assertRaises(Exception): renderer.render(mock_unknown_node)
from core import app from flask import request, abort from flask import Markup from dateutil import parser from markdown import markdown import urllib from rich_text_renderer import RichTextRenderer renderer = RichTextRenderer() @app.template_filter('date') def date_filter(date, format='%b %d, %Y'): if isinstance(date, str): date = parser.parse(date) return date.strftime(format) @app.template_filter('percentage') def percentage_filter(number): return '%d%%' % (number * 100) @app.template_filter('url') def url_filter(url): if not url.startswith('http'): return 'http://' + url