diff --git a/.gitignore b/.gitignore index 47224ed..76e1f93 100644 --- a/.gitignore +++ b/.gitignore @@ -84,7 +84,7 @@ target/ # pyenv .python-version -# celery beat schedule file +# celery beat schedule self celerybeat-schedule # SageMath parsed files diff --git a/README.md b/README.md index b9cf9cb..7500757 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ If you are using Contentstack Python SDK in your project by running the followin ## For the specific version ```python - pip install Contentstack==1.4.0 + pip install Contentstack==1.5.1 ``` ## Usage @@ -46,11 +46,9 @@ To render embedded items on the front-end, use the renderContents function, and from contentstack_utils.utils import Utils from contentstack_utils.render.options import Options - json_array # should be type of dictionary or list - rte_content = "html_string" - - callback = Options() - response = Utils.render_content(rte_content, json_array, callback) + json_array = {} # should be type of dictionary or list + option = Options() + response = Utils.render_content('html_string', json_array, option) print(response) ``` @@ -106,7 +104,24 @@ query = stack.content_type("content_type_uid").query() result = query.find() if result is not None and 'entries' in result: entry = result['entries'] - for item in range: + for item in entry: option = Option() Utils.json_to_html(item, ['paragraph_text'], option) ``` + +## GraphQL SRTE + +To get supercharged items from multiple entries, you need to provide the stack API key, delivery token, environment name, and content type’s UID. + +```python +import contentstack + +stack = contentstack.Stack('api_key','delivery_token','environment') +query = stack.content_type("content_type_uid").query() +result = query.find() +if result is not None and 'entries' in result: + entry = result['entries'] + for item in entry: + option = Option() + GQL.json_to_html(item, ['paragraph_text'], option) +``` diff --git a/changelog.rst b/changelog.rst index fda7a6e..14b2f48 100644 --- a/changelog.rst +++ b/changelog.rst @@ -2,9 +2,20 @@ **CHANGELOG** ================ +*v1.2.0* +============ + +NEW FEATURE: GraphQl supercharged RTE + +- GQL.jsonToHtml function support added + + +*v1.1.0* +============ + NEW FEATURE: Supercharged RTE -- jsonToHtml function support added +- Utils.jsonToHtml function support added *v0.2.0* ============ diff --git a/contentstack_utils/__init__.py b/contentstack_utils/__init__.py index d52bcd9..3baf961 100644 --- a/contentstack_utils/__init__.py +++ b/contentstack_utils/__init__.py @@ -17,6 +17,6 @@ __title__ = 'contentstack_utils' __author__ = 'contentstack' __status__ = 'debug' -__version__ = '0.0.1' +__version__ = '1.1.0' __endpoint__ = 'cdn.contentstack.io' __contact__ = 'support@contentstack.com' diff --git a/contentstack_utils/automate.py b/contentstack_utils/automate.py new file mode 100644 index 0000000..11b7409 --- /dev/null +++ b/contentstack_utils/automate.py @@ -0,0 +1,164 @@ +import json + +from contentstack_utils.helper.converter import convert_style +from contentstack_utils.helper.metadata import Metadata +from contentstack_utils.helper.node_to_html import NodeToHtml +from contentstack_utils.render.options import Options + + +class Automate: + + @staticmethod + def _str_from_embed_items(metadata, entry, option): + if isinstance(entry, list): + for node in entry: + uid = node['node']['uid'] + if uid == metadata.get_item_uid: + return option.render_options(node['node'], metadata) + elif isinstance(entry, dict) and '_embedded_items' in entry: + items = entry['_embedded_items'].keys() + for item in items: + items_array = entry['_embedded_items'][item] + content = Automate._find_embedded_entry(items_array, metadata) + if content is not None: + return option.render_options(content, metadata) + return '' + + @staticmethod + def _get_embedded_keys(entry, key_path, option: Options, render_callback): + if '_embedded_items' in entry: + if key_path is not None: + for path in key_path: + Automate._find_embed_keys(entry, path, option, render_callback) + else: + _embedded_items = entry['_embedded_items'] + available_keys: list = _embedded_items.keys() + for path in available_keys: + Automate._find_embed_keys(entry, path, option, render_callback) + + @staticmethod + def _find_embed_keys(entry, path, option: Options, render_callback): + keys = path.split('.') + Automate._get_content(keys, entry, option, render_callback) + + @staticmethod + def _get_content(keys_array, entry, option: Options, render_callback): + if keys_array is not None and len(keys_array) > 0: + key = keys_array[0] + if len(keys_array) == 1 and keys_array[0] in entry: + var_content = entry[key] + if isinstance(var_content, (list, str, dict)): + entry[key] = render_callback(var_content, entry, option) + else: + keys_array.remove(key) + if key in entry and isinstance(entry[key], dict): + Automate._get_content(keys_array, entry[key], option, render_callback) + elif key in entry and isinstance(entry[key], list): + list_json = entry[key] + for node in list_json: + Automate._get_content(keys_array, node, option, render_callback) + + @staticmethod + def is_json(self: object) -> bool: + try: + json.dumps(self) + return True + except ValueError: + return False + + @staticmethod + def find_embed_keys(entry, path, option: Options, render_callback): + keys = path.split('.') + Automate.get_content(keys, entry, option, render_callback) + + @staticmethod + def get_content(keys_array, entry, option: Options, render_callback): + if keys_array is not None and len(keys_array) > 0: + key = keys_array[0] + if len(keys_array) == 1 and keys_array[0] in entry: + var_content = entry[key] + if isinstance(var_content, (list, str, dict)): + entry[key] = render_callback(var_content, entry, option) + else: + keys_array.remove(key) + if key in entry and isinstance(entry[key], dict): + Automate.get_content(keys_array, entry[key], option, render_callback) + elif key in entry and isinstance(entry[key], list): + list_json = entry[key] + for node in list_json: + Automate.get_content(keys_array, node, option, render_callback) + + @staticmethod + def _enumerate_content(content, entry, option): + if len(content) > 0: + if isinstance(content, list): + array_content = [] + for item in content: + result = Automate._enumerate_content(item, entry, option) + array_content.append(result) + return array_content + if isinstance(content, dict): + if 'type' and 'children' in content: + if content['type'] == 'doc': + return Automate._raw_processing(content['children'], entry, option) + return '' + + @staticmethod + def _raw_processing(children, entry, option): + array_container = [] + for item in children: + if isinstance(item, dict): + array_container.append(Automate._extract_keys(item, entry, option)) + temp = ''.join(array_container) + return temp + + @staticmethod + def _extract_keys(item, entry, option: Options): + if 'type' not in item.keys() and 'text' in item.keys(): + return NodeToHtml.text_node_to_html(item, option) + + elif 'type' in item.keys(): + node_style = item['type'] + if node_style == 'reference': + metadata = Automate._return_metadata(item, node_style) + return Automate._str_from_embed_items(metadata=metadata, entry=entry, option=option) + else: + def call(children): + return Automate._raw_processing(children, entry, option) + + return option.render_node(node_style, item, callback=call) + return '' + + @staticmethod + def _find_embedded_entry(list_json: list, metadata: Metadata): + for obj in list_json: + if obj['uid'] == metadata.get_item_uid: + return obj + return None + + @staticmethod + def _return_metadata(item, node_style): + attr = item['attrs'] + text = Automate._get_child_text(item) + style = convert_style(attr['display-type']) + if attr['type'] == 'asset': + return Metadata(text, node_style, + attr['asset-uid'], + 'sys-asset', + style, '', '') + else: + return Metadata(text, node_style, + attr['entry-uid'], + attr['content-type-uid'], + style, '', '') + + @staticmethod + def _get_child_text(item): + text = '' + if 'children' in item.keys() and len(item['children']) > 0: + children = item['children'] + for child in children: + if text in child.keys(): + text = child['text'] + break + return text diff --git a/contentstack_utils/gql.py b/contentstack_utils/gql.py new file mode 100644 index 0000000..58d32ea --- /dev/null +++ b/contentstack_utils/gql.py @@ -0,0 +1,36 @@ +from contentstack_utils import Utils +from contentstack_utils.automate import Automate +from contentstack_utils.render.options import Options + + +class GQL(Automate): + + @staticmethod + def json_to_html(gql_entry: dict, paths: list, option: Options): + if not Automate.is_json(gql_entry): + raise FileNotFoundError("Can't process invalid object") + if len(paths) > 0: + for path in paths: + Automate.find_embed_keys(gql_entry, path, option, render_callback=GQL._json_matcher) + + @staticmethod + def __filter_content(content_dict): + embedded_items = None + if content_dict is not None and 'embedded_itemsConnection' in content_dict: + embedded_connection = content_dict['embedded_itemsConnection'] + if 'edges' in embedded_connection: + embedded_items = embedded_connection['edges'] + return embedded_items + + @staticmethod + def _json_matcher(content_dict, entry, option): + embedded_items = GQL.__filter_content(content_dict) + if 'json' in content_dict: + json = content_dict['json'] + if isinstance(json, dict): + return Automate._enumerate_content(json, entry=embedded_items, option=option) + elif isinstance(json, list): + json_container = [] + for item in json: + json_container.append(Automate._enumerate_content(item, entry=embedded_items, option=option)) + return json_container diff --git a/contentstack_utils/helper/converter.py b/contentstack_utils/helper/converter.py index e18f7d8..803bac9 100644 --- a/contentstack_utils/helper/converter.py +++ b/contentstack_utils/helper/converter.py @@ -4,11 +4,11 @@ def convert_style(style) -> StyleType: if style == 'block': return StyleType.BLOCK - elif style == 'inline': + if style == 'inline': return StyleType.INLINE - elif style == 'link': + if style == 'link': return StyleType.LINK - elif style == 'display': + if style == 'display': return StyleType.DISPLAY - elif style == 'download': + if style == 'download': return StyleType.DOWNLOAD diff --git a/contentstack_utils/render/options.py b/contentstack_utils/render/options.py index 1857263..7c63ecb 100644 --- a/contentstack_utils/render/options.py +++ b/contentstack_utils/render/options.py @@ -28,8 +28,11 @@ class Options: @staticmethod def render_options(_obj: dict, metadata: Metadata): if metadata.style_type.value == 'block': + _content_type_uid = '' + if '_content_type_uid' in _obj: + _content_type_uid = _obj['_content_type_uid'] return '

' + _title_or_uid(_obj) \ - + '

Content type: ' + _obj['_content_type_uid'] \ + + '

Content type: ' + _content_type_uid \ + '

' if metadata.style_type.value == 'inline': return '' + _title_or_uid(_obj) + '' @@ -67,11 +70,11 @@ def render_node(node_type, node_obj: dict, callback): if node_type == 'p': return "

" + inner_html + "

" if node_type == 'a': - return "" + inner_html + "" + return "{}".format(node_obj["attrs"]["href"], inner_html) if node_type == 'img': - return "" + inner_html + "" + return "{}".format(node_obj["attrs"]["src"], inner_html) if node_type == 'embed': - return "" + return "".format(node_obj["attrs"]["src"], inner_html) if node_type == 'h1': return "

" + inner_html + "

" if node_type == 'h2': @@ -102,7 +105,6 @@ def render_node(node_type, node_obj: dict, callback): return "" + inner_html + "" if node_type == 'tr': return "" + inner_html + "" - if node_type == 'th': return "" + inner_html + "" if node_type == 'td': diff --git a/contentstack_utils/utils.py b/contentstack_utils/utils.py index ca08a16..8ead7a6 100644 --- a/contentstack_utils/utils.py +++ b/contentstack_utils/utils.py @@ -1,25 +1,18 @@ # pylint: disable=missing-function-docstring -""" -Utils module helps to get access of public functions like: - render - render_content - get_embedded_objects - get_embedded_entry -""" -import json from lxml import etree + +from contentstack_utils.automate import Automate from contentstack_utils.helper.converter import convert_style from contentstack_utils.helper.metadata import Metadata -from contentstack_utils.helper.node_to_html import NodeToHtml from contentstack_utils.render.options import Options -class Utils: +class Utils(Automate): @staticmethod def render(entry_obj, key_path: list, option: Options): - valid = Utils.__is_json(entry_obj) + valid = Automate.is_json(entry_obj) if not valid: raise FileNotFoundError('Invalid file found') @@ -28,55 +21,21 @@ def render(entry_obj, key_path: list, option: Options): Utils.render(entry, key_path, option) if isinstance(entry_obj, dict): - Utils.__get_embedded_keys(entry_obj, key_path, option, render_callback=Utils.render_content) - - @staticmethod - def __get_embedded_keys(entry, key_path, option: Options, render_callback): - if '_embedded_items' in entry: - if key_path is not None: - for path in key_path: - Utils.__find_embed_keys(entry, path, option, render_callback) - else: - _embedded_items = entry['_embedded_items'] - available_keys: list = _embedded_items.keys() - for path in available_keys: - Utils.__find_embed_keys(entry, path, option, render_callback) + Automate._get_embedded_keys(entry_obj, key_path, option, render_callback=Utils.render_content) @staticmethod - def __find_embed_keys(entry, path, option: Options, render_callback): - keys = path.split('.') - Utils.__get_content(keys, entry, option, render_callback) - - @staticmethod - def __get_content(keys_array, entry, option: Options, render_callback): - if keys_array is not None and len(keys_array) > 0: - key = keys_array[0] - if len(keys_array) == 1 and keys_array[0] in entry: - var_content = entry[key] - if isinstance(var_content, (list, str, dict)): - entry[key] = render_callback(var_content, entry, option) - else: - keys_array.remove(key) - if key in entry and isinstance(entry[key], dict): - Utils.__get_content(keys_array, entry[key], option, render_callback) - elif key in entry and isinstance(entry[key], list): - list_json = entry[key] - for node in list_json: - Utils.__get_content(keys_array, node, option, render_callback) - - @staticmethod - def render_content(rte_content, embed_obj: dict, callback: Options) -> object: + def render_content(rte_content, embed_obj: dict, option: Options) -> object: if isinstance(rte_content, str): - return Utils.__get_embedded_objects(rte_content, embed_obj, callback) + return Utils.__get_embedded_objects(rte_content, embed_obj, option) elif isinstance(rte_content, list): render_callback = [] for rte in rte_content: - render_callback.append(Utils.render_content(rte, embed_obj, callback)) + render_callback.append(Utils.render_content(rte, embed_obj, option)) return render_callback return rte_content @staticmethod - def __get_embedded_objects(html_doc, embedded_obj, callback): + def __get_embedded_objects(html_doc, entry, option): import re document = f"{html_doc}" tag = etree.fromstring(document) @@ -84,20 +43,20 @@ def __get_embedded_objects(html_doc, embedded_obj, callback): html_doc = re.sub('(?ms)<%s[^>]*>(.*)' % (tag.tag, tag.tag), '\\1', html_doc) elements = tag.xpath("//*[contains(@class, 'embedded-asset') or contains(@class, 'embedded-entry')]") metadata = Utils.__get_metadata(elements) - return Utils.__get_html_doc(embedded_obj, metadata, callback, html_doc) + string_content = Utils._str_from_embed_items(metadata=metadata, entry=entry, option=option) + html_doc = html_doc.replace(metadata.outer_html, string_content) + return html_doc @staticmethod - def __get_html_doc(embedded_obj, metadata, callback, html_doc): - if '_embedded_items' in embedded_obj: - keys = embedded_obj['_embedded_items'].keys() - for key in keys: - items_array = embedded_obj['_embedded_items'][key] - item = Utils.__find_embedded_entry(items_array, metadata) - if item is not None: - replaceable_str = callback.render_options(item, metadata) - html_doc = html_doc.replace(metadata.outer_html, replaceable_str) - break - return html_doc + def _str_from_embed_items(metadata, entry, option): + if '_embedded_items' in entry: + items = entry['_embedded_items'].keys() + for item in items: + items_array = entry['_embedded_items'][item] + content = Automate._find_embedded_entry(items_array, metadata) + if content is not None: + return option.render_options(content, metadata) + return '' @staticmethod def __get_metadata(elements): @@ -120,110 +79,15 @@ def __get_metadata(elements): # SUPERCHARGED # #################################################### - @staticmethod - def __is_json(file): - try: - json.dumps(file) - return True - except ValueError: - return False - @staticmethod def json_to_html(entry_obj, key_path: list, option: Options): - if not Utils.__is_json(entry_obj): + if not Automate.is_json(entry_obj): raise FileNotFoundError('Could not process invalid content') if isinstance(entry_obj, list): for entry in entry_obj: return Utils.json_to_html(entry, key_path, option) if isinstance(entry_obj, dict): - render_callback = Utils.__enumerate_content + render_callback = Automate._enumerate_content if key_path is not None: for path in key_path: - Utils.__find_embed_keys(entry_obj, path, option, render_callback) - - @staticmethod - def __enumerate_content(content, entry, option): - if len(content) > 0: - if isinstance(content, list): - array_content = [] - for item in content: - result = Utils.__enumerate_content(item, entry, option) - array_content.append(result) - return array_content - if isinstance(content, dict): - if 'type' and 'children' in content: - if content['type'] == 'doc': - return Utils.__raw_processing(content['children'], entry, option) - return '' - - @staticmethod - def __raw_processing(children, entry, option): - array_container = [] - for item in children: - if isinstance(item, dict): - array_container.append(Utils.__extract_keys(item, entry, option)) - temp = ''.join(array_container) - return temp - - @staticmethod - def __extract_keys(item, entry, option: Options): - if 'type' not in item.keys() and 'text' in item.keys(): - return NodeToHtml.text_node_to_html(item, option) - - elif 'type' in item.keys(): - node_style = item['type'] - if node_style == 'reference': - metadata = Utils.__return_metadata(item, node_style) - if '_embedded_items' in entry: - keys = entry['_embedded_items'].keys() - for key in keys: - items_array = entry['_embedded_items'][key] - content = Utils.__find_embedded_entry(items_array, metadata) - return Utils.__get_string_option(option, metadata, content) - else: - def call(children): - return Utils.__raw_processing(children, entry, option) - - return option.render_node(node_style, item, callback=call) - return '' - - @staticmethod - def __find_embedded_entry(list_json: list, metadata: Metadata): - for obj in list_json: - if obj['uid'] == metadata.get_item_uid: - return obj - return None - - @staticmethod - def __get_string_option(option: Options, metadata: Metadata, content: dict): - string_option = option.render_options(content, metadata) - if string_option is None: - string_option = Options().render_options(content, metadata) - return string_option - - @staticmethod - def __return_metadata(item, node_style): - attr = item['attrs'] - text = Utils.__get_child_text(item) - style = convert_style(attr['display-type']) - if attr['type'] == 'asset': - return Metadata(text, node_style, - attr['asset-uid'], - 'sys-asset', - style, '', '') - else: - return Metadata(text, node_style, - attr['entry-uid'], - attr['content-type-uid'], - style, '', '') - - @staticmethod - def __get_child_text(item): - text = '' - if 'children' in item.keys() and len(item['children']) > 0: - children = item['children'] - for child in children: - if text in child.keys(): - text = child['text'] - break - return text + Automate._find_embed_keys(entry_obj, path, option, render_callback) diff --git a/setup.py b/setup.py index 0ae3b3e..3906062 100644 --- a/setup.py +++ b/setup.py @@ -15,7 +15,7 @@ long_description_content_type="text/markdown", url="https://github.com/contentstack/contentstack-utils-python", license='MIT', - version='0.2.0', + version='1.2.0', install_requires=[ ], diff --git a/tests/convert_style.py b/tests/convert_style.py index 8ba96db..52eb132 100644 --- a/tests/convert_style.py +++ b/tests/convert_style.py @@ -9,21 +9,21 @@ def setUp(self): print("logger for ConvertStyle") def test_converter_style_block(self): - _returns = converter.convert_style("block") + _returns = converter.convert_style('block') self.assertEquals(StyleType.BLOCK, _returns) def test_converter_style_inline(self): - _returns = converter.convert_style("inline") + _returns = converter.convert_style('inline') self.assertEqual(StyleType.INLINE, _returns) def test_converter_style_link(self): - _returns = converter.convert_style("link") + _returns = converter.convert_style('link') self.assertEqual(StyleType.LINK, _returns) def test_converter_style_display(self): - _returns = converter.convert_style("display") + _returns = converter.convert_style('display') self.assertEqual(StyleType.DISPLAY, _returns) def test_converter_style_download(self): - _returns = converter.convert_style("download") + _returns = converter.convert_style('download') self.assertEqual(StyleType.DOWNLOAD, _returns) diff --git a/tests/mocks/graphqlmock/content.json b/tests/mocks/graphqlmock/content.json new file mode 100644 index 0000000..6da0458 --- /dev/null +++ b/tests/mocks/graphqlmock/content.json @@ -0,0 +1,51 @@ +{ + "srte": { + "json": [ + { + "type": "doc", + "attrs": {}, + "uid": "sameple_uid", + "children": [ + { + "type": "p", + "attrs": {}, + "uid": "sameple_uid", + "children": [ + { + "text": "" + } + ] + }, + { + "uid": "sameple_uid", + "type": "reference", + "attrs": { + "display-type": "block", + "type": "entry", + "class-name": "embedded-entry redactor-component block-entry", + "entry-uid": "145ae29d7b00e4", + "locale": "en-us", + "content-type-uid": "abcd" + }, + "children": [ + { + "text": "" + } + ] + } + ], + "_version": 2 + } + ], + "embedded_itemsConnection": { + "edges": [ + { + "node": { + "title": "Abcd Three", + "uid": "145ae29d7b00e4" + } + } + ] + } + } +} \ No newline at end of file diff --git a/tests/mocks/supercharged/supercharged.json b/tests/mocks/supercharged/supercharged.json index e9cdd0d..7954e44 100644 --- a/tests/mocks/supercharged/supercharged.json +++ b/tests/mocks/supercharged/supercharged.json @@ -1460,7 +1460,7 @@ "uid": "removed_for_security_reasons", "type": "img", "attrs": { - "url": "https://images.contentstack.com/v3/assets/Donald.jog.png", + "src": "https://images.contentstack.com/v3/assets/Donald.jog.png", "width": 33.69418132611637, "height": "auto", "redactor-attributes": { @@ -1698,7 +1698,7 @@ "uid": "removed_for_security_reasons", "type": "a", "attrs": { - "url": "LINK.com", + "href": "LINK.com", "target": "_self" }, "children": [ diff --git a/tests/test-report/test-report.html b/tests/test-report/test-report.html index f0bcab4..0396eb2 100644 --- a/tests/test-report/test-report.html +++ b/tests/test-report/test-report.html @@ -6,7 +6,7 @@