Skip to content

Commit 03f2118

Browse files
Merge pull request #67 from contentstack/enh/dx-4344
enh: Added support for editable tags
2 parents d70ee40 + 1f441aa commit 03f2118

File tree

7 files changed

+383
-4
lines changed

7 files changed

+383
-4
lines changed

changelog.rst

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,25 @@
22
**CHANGELOG**
33
================
44

5+
*v1.5.0*
6+
============
7+
8+
NEW FEATURE: Variants utility (CDA entry variant aliases).
9+
10+
- Added ``Utils.get_variant_aliases`` to read variant alias strings from ``publish_details.variants`` on a CDA entry (single dict or list of entries). Supports optional ``content_type_uid`` when ``_content_type_uid`` is absent on the entry.
11+
- Added ``Utils.get_variant_metadata_tags`` to build a ``data-csvariants`` HTML data-attribute value (JSON string of the multi-entry alias results).
12+
13+
NEW FEATURE: Live Preview editable tags (CSLP).
14+
15+
- Added JS-parity editable tagging helpers in ``contentstack_utils/entry_editable.py``.
16+
- Added ``addEditableTags`` / ``addTags`` to mutate an entry with a ``$`` map of CSLP tags (supports nested objects, arrays, references, and applied variants; normalizes case for ``contentTypeUid`` and locale similar to JS).
17+
- Added ``getTag`` helper for building tag maps recursively.
18+
- Exported ``addEditableTags``, ``addTags``, and ``getTag`` at package level, and delegated via ``Utils`` for backward compatibility.
19+
20+
BUG FIX: Test compatibility.
21+
22+
- Fixed deprecated unittest assertion usage in ``tests/convert_style.py`` for newer Python versions.
23+
524
*v1.4.0*
625
============
726

contentstack_utils/__init__.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
from contentstack_utils.utils import Utils
1717
from contentstack_utils.gql import GQL
1818
from contentstack_utils.automate import Automate
19+
from contentstack_utils.entry_editable import addEditableTags, addTags, getTag
1920

2021
__all__ = (
2122
"Utils",
@@ -25,7 +26,10 @@
2526
"Automate",
2627
"StyleType",
2728
"ItemType",
28-
"NodeToHtml"
29+
"NodeToHtml",
30+
"addEditableTags",
31+
"addTags",
32+
"getTag",
2933
)
3034

3135
__title__ = 'contentstack_utils'
Lines changed: 248 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,248 @@
1+
from __future__ import annotations
2+
3+
from typing import Any, Dict, Optional, Union, cast
4+
5+
6+
AppliedVariants = Optional[Dict[str, Any]]
7+
TagValue = Union[str, Dict[str, str]]
8+
9+
10+
def _get_parent_variantised_path(applied_variants: Dict[str, Any], meta_key: str) -> str:
11+
"""
12+
Port of JS getParentVariantisedPath().
13+
Finds the longest variantised field path that is a prefix of meta_key.
14+
"""
15+
try:
16+
if not meta_key:
17+
return ""
18+
variantised_field_paths = sorted(applied_variants.keys(), key=len, reverse=True)
19+
child_fragments = meta_key.split(".")
20+
if not child_fragments or not variantised_field_paths:
21+
return ""
22+
for path in variantised_field_paths:
23+
parent_fragments = str(path).split(".")
24+
if len(parent_fragments) > len(child_fragments):
25+
continue
26+
if all(child_fragments[i] == parent_fragments[i] for i in range(len(parent_fragments))):
27+
return str(path)
28+
return ""
29+
except Exception:
30+
return ""
31+
32+
33+
def _apply_variant_to_data_value(data_value: str, applied_variants: AppliedVariants, meta_key: str, should_apply_variant: bool) -> str:
34+
"""
35+
Port of JS applyVariantToDataValue().
36+
37+
If the current field (or its parent field path) is variantised, prefixes with
38+
'v2:' and appends `_{variant}` to the entry uid segment of the dot-path.
39+
"""
40+
if not should_apply_variant or not applied_variants or not meta_key or not isinstance(applied_variants, dict):
41+
return data_value
42+
43+
variant: Optional[str] = None
44+
if meta_key in applied_variants:
45+
variant = str(applied_variants[meta_key])
46+
else:
47+
parent_path = _get_parent_variantised_path(applied_variants, meta_key)
48+
if parent_path:
49+
variant = str(applied_variants.get(parent_path))
50+
51+
if not variant:
52+
return data_value
53+
54+
parts = ("v2:" + data_value).split(".")
55+
if len(parts) >= 2:
56+
parts[1] = parts[1] + "_" + variant
57+
return ".".join(parts)
58+
59+
60+
def _tags_value(data_value: str, tags_as_object: bool, applied_variants: AppliedVariants, meta_key: str, should_apply_variant: bool) -> TagValue:
61+
resolved = _apply_variant_to_data_value(data_value, applied_variants, meta_key, should_apply_variant)
62+
if tags_as_object:
63+
return {"data-cslp": resolved}
64+
return f"data-cslp={resolved}"
65+
66+
67+
def _parent_tags_value(data_value: str, tags_as_object: bool) -> TagValue:
68+
if tags_as_object:
69+
return {"data-cslp-parent-field": data_value}
70+
return f"data-cslp-parent-field={data_value}"
71+
72+
73+
def getTag( # pylint: disable=invalid-name
74+
content: Any,
75+
prefix: str,
76+
tags_as_object: bool,
77+
locale: str,
78+
applied_variants: AppliedVariants,
79+
should_apply_variant: bool,
80+
meta_key: str = "",
81+
) -> Dict[str, Any]:
82+
"""
83+
Port of JS getTag() from `src/entry-editable.ts`.
84+
85+
Returns a dict mapping field keys to CSLP tag values, and mutates nested objects/refs
86+
by attaching their own `$` tag maps.
87+
"""
88+
if content is None or not isinstance(content, dict):
89+
return {}
90+
91+
tags: Dict[str, Any] = {}
92+
for key, value in content.items():
93+
if key == "$":
94+
continue
95+
96+
meta_uid = ""
97+
if isinstance(value, dict):
98+
meta = value.get("_metadata")
99+
if isinstance(meta, dict) and meta.get("uid"):
100+
meta_uid = str(meta.get("uid"))
101+
102+
meta_key_prefix = (meta_key + ".") if meta_key else ""
103+
updated_meta_key = f"{meta_key_prefix}{key}" if should_apply_variant else ""
104+
if meta_uid and updated_meta_key:
105+
updated_meta_key = updated_meta_key + "." + meta_uid
106+
107+
if isinstance(value, list):
108+
for index, obj in enumerate(value):
109+
if obj is None:
110+
continue
111+
112+
child_key = f"{key}__{index}"
113+
parent_key = f"{key}__parent"
114+
115+
obj_meta_uid = ""
116+
if isinstance(obj, dict):
117+
meta = obj.get("_metadata")
118+
if isinstance(meta, dict) and meta.get("uid"):
119+
obj_meta_uid = str(meta.get("uid"))
120+
121+
array_meta_key = f"{meta_key_prefix}{key}" if should_apply_variant else ""
122+
if obj_meta_uid and array_meta_key:
123+
array_meta_key = array_meta_key + "." + obj_meta_uid
124+
125+
tags[child_key] = _tags_value(
126+
f"{prefix}.{key}.{index}",
127+
tags_as_object,
128+
applied_variants,
129+
array_meta_key,
130+
should_apply_variant,
131+
)
132+
tags[parent_key] = _parent_tags_value(f"{prefix}.{key}", tags_as_object)
133+
134+
# Reference entries in array
135+
if isinstance(obj, dict) and obj.get("_content_type_uid") is not None and obj.get("uid") is not None:
136+
new_applied_variants = obj.get("_applied_variants")
137+
if new_applied_variants is None and isinstance(obj.get("system"), dict):
138+
new_applied_variants = cast(dict, obj["system"]).get("applied_variants")
139+
new_should_apply_variant = bool(new_applied_variants)
140+
141+
obj_locale = obj.get("locale") or locale
142+
obj["$"] = getTag(
143+
obj,
144+
f"{obj.get('_content_type_uid')}.{obj.get('uid')}.{obj_locale}",
145+
tags_as_object,
146+
locale,
147+
cast(AppliedVariants, new_applied_variants),
148+
new_should_apply_variant,
149+
meta_key="",
150+
)
151+
continue
152+
153+
if isinstance(obj, dict):
154+
obj["$"] = getTag(
155+
obj,
156+
f"{prefix}.{key}.{index}",
157+
tags_as_object,
158+
locale,
159+
applied_variants,
160+
should_apply_variant,
161+
meta_key=array_meta_key,
162+
)
163+
164+
tags[key] = _tags_value(
165+
f"{prefix}.{key}",
166+
tags_as_object,
167+
applied_variants,
168+
updated_meta_key,
169+
should_apply_variant,
170+
)
171+
continue
172+
173+
if isinstance(value, dict):
174+
value["$"] = getTag(
175+
value,
176+
f"{prefix}.{key}",
177+
tags_as_object,
178+
locale,
179+
applied_variants,
180+
should_apply_variant,
181+
meta_key=updated_meta_key,
182+
)
183+
tags[key] = _tags_value(
184+
f"{prefix}.{key}",
185+
tags_as_object,
186+
applied_variants,
187+
updated_meta_key,
188+
should_apply_variant,
189+
)
190+
continue
191+
192+
tags[key] = _tags_value(
193+
f"{prefix}.{key}",
194+
tags_as_object,
195+
applied_variants,
196+
updated_meta_key,
197+
should_apply_variant,
198+
)
199+
200+
return tags
201+
202+
203+
def addTags( # pylint: disable=invalid-name
204+
entry: Optional[dict],
205+
contentTypeUid: str,
206+
tagsAsObject: bool,
207+
locale: str = "en-us",
208+
options: Optional[dict] = None,
209+
) -> None:
210+
"""
211+
Port of JS addTags() from `src/entry-editable.ts`.
212+
Mutates `entry` by attaching a `$` dict of CSLP tags.
213+
"""
214+
if not entry:
215+
return
216+
217+
use_lower_case_locale = True
218+
if isinstance(options, dict) and "useLowerCaseLocale" in options:
219+
use_lower_case_locale = bool(options.get("useLowerCaseLocale"))
220+
221+
content_type_uid = (contentTypeUid or "").lower()
222+
resolved_locale = (locale or "en-us")
223+
if use_lower_case_locale:
224+
resolved_locale = resolved_locale.lower()
225+
226+
applied_variants = entry.get("_applied_variants")
227+
if applied_variants is None and isinstance(entry.get("system"), dict):
228+
applied_variants = cast(dict, entry["system"]).get("applied_variants")
229+
should_apply_variant = bool(applied_variants)
230+
231+
entry["$"] = getTag(
232+
entry,
233+
f"{content_type_uid}.{entry.get('uid')}.{resolved_locale}",
234+
tagsAsObject,
235+
resolved_locale,
236+
cast(AppliedVariants, applied_variants),
237+
should_apply_variant,
238+
meta_key="",
239+
)
240+
241+
242+
# JS parity export name
243+
addEditableTags = addTags # pylint: disable=invalid-name
244+
245+
# Pythonic aliases
246+
add_tags = addTags
247+
get_tags = getTag
248+

contentstack_utils/utils.py

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,69 @@
11
# pylint: disable=missing-function-docstring
22

33
import json
4-
from typing import Any, Dict, List, Union
4+
from typing import Any, Dict, List, Optional, Union
55

66
from lxml import etree
77

88
from contentstack_utils.automate import Automate
9+
from contentstack_utils.entry_editable import addEditableTags as _addEditableTags
10+
from contentstack_utils.entry_editable import addTags as _addTags
11+
from contentstack_utils.entry_editable import getTag as _getTag
912
from contentstack_utils.helper.converter import convert_style
1013
from contentstack_utils.helper.metadata import Metadata
1114
from contentstack_utils.render.options import Options
1215

1316

1417
class Utils(Automate):
18+
# JS parity helpers (moved to `contentstack_utils/entry_editable.py`)
19+
@staticmethod
20+
def addTags( # pylint: disable=invalid-name
21+
entry: dict,
22+
contentTypeUid: str,
23+
tagsAsObject: Optional[bool] = None,
24+
locale: str = "en-us",
25+
options: Optional[dict] = None,
26+
**kwargs,
27+
) -> None:
28+
# Support pythonic kwarg name too (backward compatibility with earlier port).
29+
if tagsAsObject is None and "tags_as_object" in kwargs:
30+
tagsAsObject = bool(kwargs["tags_as_object"])
31+
if tagsAsObject is None:
32+
tagsAsObject = False
33+
return _addTags(entry, contentTypeUid, tagsAsObject, locale, options)
34+
35+
@staticmethod
36+
def addEditableTags( # pylint: disable=invalid-name
37+
entry: dict,
38+
contentTypeUid: str,
39+
tagsAsObject: Optional[bool] = None,
40+
locale: str = "en-us",
41+
options: Optional[dict] = None,
42+
**kwargs,
43+
) -> None:
44+
if tagsAsObject is None and "tags_as_object" in kwargs:
45+
tagsAsObject = bool(kwargs["tags_as_object"])
46+
if tagsAsObject is None:
47+
tagsAsObject = False
48+
return _addEditableTags(entry, contentTypeUid, tagsAsObject, locale, options)
49+
50+
@staticmethod
51+
def getTag( # pylint: disable=invalid-name
52+
content: Any,
53+
prefix: str,
54+
tagsAsObject: bool,
55+
locale: str,
56+
appliedVariants: Optional[dict],
57+
shouldApplyVariant: bool,
58+
metaKey: str = "",
59+
) -> Dict[str, Any]:
60+
# Keep JS argument names for parity.
61+
return _getTag(content, prefix, tagsAsObject, locale, appliedVariants, shouldApplyVariant, metaKey)
62+
63+
# Pythonic aliases
64+
add_tags = addTags
65+
get_tags = getTag
66+
get_tag = getTag
1567

1668
@staticmethod
1769
def _variants_map_from_entry(entry: dict) -> dict:

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
long_description_content_type="text/markdown",
1616
url="https://github.com/contentstack/contentstack-utils-python",
1717
license='MIT',
18-
version='1.4.0',
18+
version='1.5.0',
1919
install_requires=[
2020

2121
],

tests/convert_style.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ def setUp(self):
1010

1111
def test_converter_style_block(self):
1212
_returns = converter.convert_style('block')
13-
self.assertEquals(StyleType.BLOCK, _returns)
13+
self.assertEqual(StyleType.BLOCK, _returns)
1414

1515
def test_converter_style_inline(self):
1616
_returns = converter.convert_style('inline')

0 commit comments

Comments
 (0)