-
Notifications
You must be signed in to change notification settings - Fork 6
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Pull content for moratorium banner from Contentful. (#2125)
This is an attempt to try pulling content for the Covid moratorium banner text directly from Contentful, so that we don't need to push a change to this repository whenever we want to change the text of the banner. It currently uses the new [`@justfixnyc/contentful-common-strings`](https://www.npmjs.com/package/@justfixnyc/contentful-common-strings) package to help accomplish this. ## How it works The tenant platform now optionally supports retrieving "common strings" from Contentful. By "common strings" we really mean "common [rich text](https://www.contentful.com/developers/docs/concepts/rich-text/) strings". Each common string has an alphanumeric id and a value. The value is a localizable rich text value. For example, in this PR, the common string with id `covidMoratoriumBanner` is expected to have a value containing the rich text to show users about the current state of the COVID Moratorium, localized in both English and Spanish. The platform retrieves all entries from its configured Contentful space [tagged](https://www.contentful.com/help/tags/) `common`. Each of these entries is expected to have a short text field called `id` and a localized rich text field called `value`. The common strings are made available to front-end code,. A helper function on the front-end makes it convenient to retrieve a common string's rich text document representation by its id. The front-end can then render them using the `@contentful/rich-text-react-renderer` package. Note that at present, _all_ locales of the common strings are passed to the front-end, which means that there's a slight amount of bloat--but it shouldn't be _too_ bad since we have very few common strings. The back-end caches the common strings from Contentful for a short period of time--at the time of this writing, it's 5 seconds--to ensure minimal latency during periods of high load. (Note that the back-end is responsible for retrieving the Contentful strings to ensure that they are properly rendered during server-side rendering.)
- Loading branch information
Showing
12 changed files
with
333 additions
and
39 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
import { Document } from "@contentful/rich-text-types"; | ||
import { ContentfulCommonStrings } from "@justfixnyc/contentful-common-strings"; | ||
import { useContext } from "react"; | ||
|
||
import { AppContext } from "./app-context"; | ||
import i18n from "./i18n"; | ||
|
||
/** | ||
* Retrieve a common string (one shared across multiple JustFix properties) | ||
* in the current locale from Contentful and return it. | ||
* | ||
* If the string doesn't exist or Contentful integration is disabled, | ||
* `null` will be returned. | ||
*/ | ||
export function useContentfulCommonString(key: string): Document | null { | ||
const ccs = new ContentfulCommonStrings( | ||
useContext(AppContext).server.contentfulCommonStrings || {} | ||
); | ||
|
||
return ccs.get(key, i18n.locale); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,74 @@ | ||
import logging | ||
from django.conf import settings | ||
from django.core.cache import cache | ||
from typing import Any, Dict, Optional | ||
import requests | ||
|
||
|
||
logger = logging.getLogger(__name__) | ||
|
||
ORIGIN = "https://cdn.contentful.com" | ||
|
||
CommonStrings = Dict[str, Any] | ||
|
||
|
||
def _to_common_strings_map(raw: Any) -> CommonStrings: | ||
""" | ||
Converts the given raw Contentful API result and converts | ||
it into a Contentful common strings mapping. | ||
This is the Python version of the `toCommonStringsMap` | ||
function from the following TypeScript code: | ||
https://github.com/JustFixNYC/justfix-ts/blob/master/packages/contentful-common-strings/src/fetch-common-strings.ts | ||
""" | ||
|
||
result: CommonStrings = {} | ||
|
||
for item in raw["items"]: | ||
fields = item["fields"] | ||
key = fields.get("id", {}).get("en") | ||
value = fields.get("value") | ||
if key and value: | ||
result[key] = value | ||
|
||
return result | ||
|
||
|
||
def get_common_strings() -> Optional[CommonStrings]: | ||
""" | ||
Fetches Contentful common strings and returns them. | ||
Caching is used to ensure that we don't trigger Contentful's rate | ||
limiting or cause undue latency. | ||
If Contentful integration is disabled, or if a network error | ||
occurs and we don't have a cached value, returns `None`. | ||
""" | ||
|
||
if not (settings.CONTENTFUL_ACCESS_TOKEN and settings.CONTENTFUL_SPACE_ID): | ||
# Contentful integration is disabled. | ||
return None | ||
|
||
cache_key = f"contentful_common_strings.{settings.CONTENTFUL_SPACE_ID}" | ||
|
||
result = cache.get(cache_key) | ||
|
||
if result is None: | ||
try: | ||
response = requests.get( | ||
f"{ORIGIN}/spaces/{settings.CONTENTFUL_SPACE_ID}/entries", | ||
{ | ||
"access_token": settings.CONTENTFUL_ACCESS_TOKEN, | ||
"locale": "*", | ||
"metadata.tags.sys.id[in]": settings.CONTENTFUL_COMMON_STRING_TAG, | ||
}, | ||
timeout=settings.CONTENTFUL_TIMEOUT, | ||
) | ||
response.raise_for_status() | ||
result = _to_common_strings_map(response.json()) | ||
cache.set(cache_key, result, settings.CONTENTFUL_CACHE_TIMEOUT) | ||
except Exception: | ||
logger.exception(f"Error while retrieving data from {ORIGIN}") | ||
|
||
return result |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,94 @@ | ||
import pytest | ||
from django.core.cache import cache | ||
|
||
from project.contentful import get_common_strings | ||
|
||
|
||
ENTRIES_URL = "https://cdn.contentful.com/spaces/myspaceid/entries" | ||
|
||
CONTENTFUL_DOC = { | ||
"nodeType": "document", | ||
"data": {}, | ||
"content": [ | ||
{ | ||
"nodeType": "paragraph", | ||
"content": [ | ||
{ | ||
"nodeType": "text", | ||
"value": "Hello!", | ||
"marks": [], | ||
"data": {}, | ||
}, | ||
], | ||
}, | ||
], | ||
} | ||
|
||
RAW_ENTRIES_RESPONSE = { | ||
"sys": {"type": "Array"}, | ||
"total": 1, | ||
"skip": 0, | ||
"limit": 100, | ||
"items": [ | ||
{ | ||
"metadata": {"tags": [{"sys": {"type": "Link", "linkType": "Tag", "id": "common"}}]}, | ||
"sys": { | ||
"space": {"sys": {"type": "Link", "linkType": "Space", "id": "markmr2gi204"}}, | ||
"id": "6JHYqWl0h2QWvObWQfNH4m", | ||
"type": "Entry", | ||
"createdAt": "2021-06-16T11:01:57.811Z", | ||
"updatedAt": "2021-06-16T12:44:37.768Z", | ||
"environment": {"sys": {"id": "master", "type": "Link", "linkType": "Environment"}}, | ||
"revision": 6, | ||
"contentType": {"sys": {"type": "Link", "linkType": "ContentType", "id": "string"}}, | ||
}, | ||
"fields": { | ||
"id": {"en": "covidMoratoriumBanner"}, | ||
"value": { | ||
"en": CONTENTFUL_DOC, | ||
}, | ||
}, | ||
} | ||
], | ||
} | ||
|
||
|
||
@pytest.fixture | ||
def enabled(settings): | ||
settings.CONTENTFUL_ACCESS_TOKEN = "myaccesstoken" | ||
settings.CONTENTFUL_SPACE_ID = "myspaceid" | ||
|
||
|
||
class TestGetCommonStrings: | ||
def setup(self): | ||
cache.clear() | ||
|
||
def test_it_returns_none_when_disabled(self): | ||
assert get_common_strings() is None | ||
|
||
def test_it_returns_none_when_enabled_but_err_occurs(self, enabled, requests_mock): | ||
requests_mock.get(ENTRIES_URL, status_code=500) | ||
assert get_common_strings() is None | ||
|
||
def test_it_works(self, enabled, requests_mock): | ||
requests_mock.get(ENTRIES_URL, json=RAW_ENTRIES_RESPONSE) | ||
assert get_common_strings() == {"covidMoratoriumBanner": {"en": CONTENTFUL_DOC}} | ||
|
||
def test_it_caches_result(self, enabled, requests_mock): | ||
requests_mock.get(ENTRIES_URL, json=RAW_ENTRIES_RESPONSE) | ||
|
||
strings = get_common_strings() | ||
assert strings is not None | ||
|
||
requests_mock.get(ENTRIES_URL, status_code=500) | ||
|
||
assert get_common_strings() == strings | ||
|
||
def test_it_does_not_cache_errors(self, enabled, requests_mock): | ||
requests_mock.get(ENTRIES_URL, status_code=500) | ||
|
||
assert get_common_strings() is None | ||
|
||
requests_mock.get(ENTRIES_URL, json=RAW_ENTRIES_RESPONSE) | ||
|
||
assert get_common_strings() is not None |
Oops, something went wrong.