Skip to content

Commit 92324a4

Browse files
authored
Add Inline include resolution for StrcturedTextField (contentful#37)
1 parent 9e13e77 commit 92324a4

File tree

4 files changed

+183
-13
lines changed

4 files changed

+183
-13
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
# CHANGELOG
22

33
## Unreleased
4+
### Added
5+
* Added support for `StructuredText` inline Entry include resolution.
46

57
## v1.10.1
68
### Added

contentful/content_type_field_types.py

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -150,11 +150,8 @@ class StructuredTextField(BasicField):
150150
"""
151151

152152
def _coerce_link(self, value, includes=None, errors=None, resources=None, default_locale='en-US', locale=None):
153-
if 'data' not in value or 'target' not in value['data']:
154-
return value
155-
156-
if not('target' in value['data'] and value['data']['target']['sys']['type'] == 'Link'):
157-
return value
153+
if value['data']['target']['sys']['type'] != 'Link':
154+
return value['data']
158155

159156
if unresolvable(value['data']['target'], errors):
160157
return None
@@ -184,8 +181,9 @@ def _coerce_block(self, value, includes=None, errors=None, resources=None, defau
184181
return value
185182

186183
invalid_nodes = []
184+
coerced_nodes = {}
187185
for index, node in enumerate(value['content']):
188-
if node['nodeClass'] == 'block' and 'data' in node:
186+
if node.get('data', None) and node['data'].get('target', None):
189187
link = self._coerce_link(
190188
node,
191189
includes=includes,
@@ -198,8 +196,8 @@ def _coerce_block(self, value, includes=None, errors=None, resources=None, defau
198196
node['data'] = link
199197
else:
200198
invalid_nodes.append(index)
201-
elif node['nodeClass'] == 'block' and 'content' in node:
202-
node['content'] = self._coerce_block(
199+
if node.get('content', None):
200+
coerced_nodes[index] = self._coerce_block(
203201
node,
204202
includes=includes,
205203
errors=errors,
@@ -208,6 +206,9 @@ def _coerce_block(self, value, includes=None, errors=None, resources=None, defau
208206
locale=locale
209207
)
210208

209+
for node_index, coerced_node in coerced_nodes.items():
210+
value['content'][node_index] = coerced_node
211+
211212
for node_index in invalid_nodes:
212213
del value['content'][node_index]
213214

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
interactions:
2+
- request:
3+
body: null
4+
headers:
5+
Accept: ['*/*']
6+
Accept-Encoding: ['gzip, deflate']
7+
Authorization: [Bearer 6256b8ef7d66805ca41f2728271daf27e8fa6055873b802a813941a0fe696248]
8+
Connection: [keep-alive]
9+
Content-Type: [application/vnd.contentful.delivery.v1+json]
10+
User-Agent: [python-requests/2.12.1]
11+
X-Contentful-User-Agent: [sdk contentful.py/1.10.1; platform python/3.6.3; os
12+
macOS/16.7.0;]
13+
method: GET
14+
uri: https://cdn.contentful.com/spaces/jd7yc4wnatx3/environments/master/content_types
15+
response:
16+
body:
17+
string: !!binary |
18+
H4sIAAAAAAAAA91VPW/CMBDd+RXIc0Eh0DbKRis6VV3I1IrBxIfk4jipbSgp4r/Xdr6cqKhUReqH
19+
F3xn3727y3tm3+v3kcwlCvt7vdWGyjPQFpoKgXOkfYcLc0elCjPt960l1zTThmcNRhOqtDXyCpsq
20+
SEzCJ5uwSNtBsUgyw7GBqm4UTqcW47DOqqZ7ytfIYDZLo/N1VNY8txk7Fygx7TyT6zyevHKsdmPT
21+
VbUO9d72WSxUxECyBEKAOBnrUm5TroAri+wExgKwAjI180C+NwoGXjDw/WjkhZNJ6F0Pg+Dq0Q3Y
22+
ZORrAcC3VKQ80eAnza5oJcFSgeiO5tS5zhzMT2cnYEslTblhRHm5Hi0iVGYM53cUmP0sy5TkdVWI
23+
48Ryb9YdPCIgY0EzVeRFTcjKZGrYZr5fi09F+y0Yc6eGunELsCfVUOZ5skxZa2SIpTFm9E1TIuyv
24+
MJPgcg0JeNlQceRQd46X7MhhqhWkaVNlbThZ7hb2txzjH1aUVBFIdSY9jcNLbxh4/sl6+jDgf+nJ
25+
KqgiZU3yeXvs31VTC6SlpgcX/vepqVHrud4IJTax2mjNR7BzeW3+Fn/2rdAvxqJ36L0DMymmQeEH
26+
AAA=
27+
headers:
28+
Accept-Ranges: [bytes]
29+
Access-Control-Allow-Headers: ['Accept,Accept-Language,Authorization,Cache-Control,Content-Length,Content-Range,Content-Type,DNT,Destination,Expires,If-Match,If-Modified-Since,If-None-Match,Keep-Alive,Last-Modified,Origin,Pragma,Range,User-Agent,X-Http-Method-Override,X-Mx-ReqToken,X-Requested-With,X-Contentful-Version,X-Contentful-Content-Type,X-Contentful-Organization,X-Contentful-Skip-Transformation,X-Contentful-User-Agent,X-Contentful-Enable-Alpha-Feature']
30+
Access-Control-Allow-Methods: ['GET,HEAD,OPTIONS']
31+
Access-Control-Allow-Origin: ['*']
32+
Access-Control-Expose-Headers: [Etag]
33+
Access-Control-Max-Age: ['86400']
34+
Age: ['0']
35+
Cache-Control: [max-age=0]
36+
Connection: [keep-alive]
37+
Content-Encoding: [gzip]
38+
Content-Length: ['458']
39+
Content-Type: [application/vnd.contentful.delivery.v1+json]
40+
Contentful-Api: [cda_cached]
41+
Date: ['Wed, 12 Sep 2018 10:07:49 GMT']
42+
ETag: [W/"df23ab55e257a1f3eb634ed5ee9759ec"]
43+
Server: [Contentful]
44+
Vary: [Accept-Encoding]
45+
Via: [1.1 varnish]
46+
X-Cache: [MISS]
47+
X-Cache-Hits: ['0']
48+
X-Content-Type-Options: [nosniff]
49+
X-Contentful-Region: [us-east-1]
50+
X-Contentful-Request-Id: [ad7e4567861099ccf465ff7866201ef8]
51+
X-Served-By: [cache-hhn1549-HHN]
52+
X-Timer: ['S1536746869.280947,VS0,VE366']
53+
status: {code: 200, message: OK}
54+
- request:
55+
body: null
56+
headers:
57+
Accept: ['*/*']
58+
Accept-Encoding: ['gzip, deflate']
59+
Authorization: [Bearer 6256b8ef7d66805ca41f2728271daf27e8fa6055873b802a813941a0fe696248]
60+
Connection: [keep-alive]
61+
Content-Type: [application/vnd.contentful.delivery.v1+json]
62+
User-Agent: [python-requests/2.12.1]
63+
X-Contentful-User-Agent: [sdk contentful.py/1.10.1; platform python/3.6.3; os
64+
macOS/16.7.0;]
65+
method: GET
66+
uri: https://cdn.contentful.com/spaces/jd7yc4wnatx3/environments/master/entries?sys.id=6NGLswCREsGA28kGouScyY
67+
response:
68+
body:
69+
string: !!binary |
70+
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
95+
headers:
96+
Accept-Ranges: [bytes]
97+
Access-Control-Allow-Headers: ['Accept,Accept-Language,Authorization,Cache-Control,Content-Length,Content-Range,Content-Type,DNT,Destination,Expires,If-Match,If-Modified-Since,If-None-Match,Keep-Alive,Last-Modified,Origin,Pragma,Range,User-Agent,X-Http-Method-Override,X-Mx-ReqToken,X-Requested-With,X-Contentful-Version,X-Contentful-Content-Type,X-Contentful-Organization,X-Contentful-Skip-Transformation,X-Contentful-User-Agent,X-Contentful-Enable-Alpha-Feature']
98+
Access-Control-Allow-Methods: ['GET,HEAD,OPTIONS']
99+
Access-Control-Allow-Origin: ['*']
100+
Access-Control-Expose-Headers: [Etag]
101+
Access-Control-Max-Age: ['86400']
102+
Age: ['0']
103+
Cache-Control: [max-age=0]
104+
Connection: [keep-alive]
105+
Content-Encoding: [gzip]
106+
Content-Length: ['1409']
107+
Content-Type: [application/vnd.contentful.delivery.v1+json]
108+
Contentful-Api: [cda_cached]
109+
Date: ['Wed, 12 Sep 2018 10:07:49 GMT']
110+
ETag: [W/"ad6896d8b4604d1307178a506ce1b102"]
111+
Server: [Contentful]
112+
Vary: [Accept-Encoding]
113+
Via: [1.1 varnish]
114+
X-Cache: [MISS]
115+
X-Cache-Hits: ['0']
116+
X-Content-Type-Options: [nosniff]
117+
X-Contentful-Region: [us-east-1]
118+
X-Contentful-Request-Id: [f59d940c76009e6aea7642be6cee10d5]
119+
X-Served-By: [cache-hhn1520-HHN]
120+
X-Timer: ['S1536746870.776895,VS0,VE197']
121+
status: {code: 200, message: OK}
122+
version: 1

tests/client_test.py

Lines changed: 50 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
from contentful.client import Client
77
from contentful.content_type_cache import ContentTypeCache
88
from contentful.errors import EntryNotFoundError
9-
from contentful.utils import ConfigurationException, NotSupportedException
9+
from contentful.utils import ConfigurationException
1010
from contentful.entry import Entry
1111

1212

@@ -111,7 +111,7 @@ def test_entry_incoming_references(self):
111111
def test_entry_incoming_references_with_query(self):
112112
client = Client('cfexampleapi', 'b4c0n73n7fu1', content_type_cache=False)
113113
entry = client.entry('nyancat')
114-
entries = entry.incoming_references(client, { 'content_type': 'cat', 'select': ['fields.name'] })
114+
entries = entry.incoming_references(client, {'content_type': 'cat', 'select': ['fields.name']})
115115
self.assertEqual(len(entries), 1)
116116
self.assertEqual(str(entries[0]), "<Entry[cat] id='happycat'>")
117117
self.assertEqual(entries[0].fields(), {'name': 'Happy Cat'})
@@ -136,7 +136,10 @@ def test_client_asset(self):
136136
client = Client('cfexampleapi', 'b4c0n73n7fu1', content_type_cache=False)
137137
asset = client.asset('nyancat')
138138

139-
self.assertEqual(str(asset), "<Asset id='nyancat' url='//images.contentful.com/cfexampleapi/4gp6taAwW4CmSgumq2ekUm/9da0cd1936871b8d72343e895a00d611/Nyan_cat_250px_frame.png'>")
139+
self.assertEqual(
140+
str(asset),
141+
"<Asset id='nyancat' url='//images.contentful.com/cfexampleapi/4gp6taAwW4CmSgumq2ekUm/9da0cd1936871b8d72343e895a00d611/Nyan_cat_250px_frame.png'>"
142+
)
140143

141144
@vcr.use_cassette('fixtures/client/locales_on_environment.yaml')
142145
def test_client_locales_on_environment(self):
@@ -151,14 +154,22 @@ def test_client_assets(self):
151154
client = Client('cfexampleapi', 'b4c0n73n7fu1', content_type_cache=False)
152155
assets = client.assets()
153156

154-
self.assertEqual(str(assets[0]), "<Asset id='1x0xpXu4pSGS4OukSyWGUK' url='//images.contentful.com/cfexampleapi/1x0xpXu4pSGS4OukSyWGUK/cc1239c6385428ef26f4180190532818/doge.jpg'>")
157+
self.assertEqual(
158+
str(assets[0]),
159+
"<Asset id='1x0xpXu4pSGS4OukSyWGUK' url='//images.contentful.com/cfexampleapi/1x0xpXu4pSGS4OukSyWGUK/cc1239c6385428ef26f4180190532818/doge.jpg'>"
160+
)
155161

156162
@vcr.use_cassette('fixtures/client/sync.yaml')
157163
def test_client_sync(self):
158164
client = Client('cfexampleapi', 'b4c0n73n7fu1', content_type_cache=False)
159165
sync = client.sync({'initial': True})
160166

161-
self.assertEqual(str(sync), "<SyncPage next_sync_token='w5ZGw6JFwqZmVcKsE8Kow4grw45QdybCnV_Cg8OASMKpwo1UY8K8bsKFwqJrw7DDhcKnM2RDOVbDt1E-wo7CnDjChMKKGsK1wrzCrBzCqMOpZAwOOcOvCcOAwqHDv0XCiMKaOcOxZA8BJUzDr8K-wo1lNx7DnHE'>")
167+
self.assertEqual(
168+
str(sync),
169+
"<SyncPage next_sync_token='{0}'>".format(
170+
'w5ZGw6JFwqZmVcKsE8Kow4grw45QdybCnV_Cg8OASMKpwo1UY8K8bsKFwqJrw7DDhcKnM2RDOVbDt1E-wo7CnDjChMKKGsK1wrzCrBzCqMOpZAwOOcOvCcOAwqHDv0XCiMKaOcOxZA8BJUzDr8K-wo1lNx7DnHE'
171+
)
172+
)
162173
self.assertEqual(str(sync.items[0]), "<Entry[1t9IbcfdCk6m04uISSsaIK] id='5ETMRzkl9KM4omyMwKAOki'>")
163174

164175
@vcr.use_cassette('fixtures/client/sync_environments.yaml')
@@ -398,3 +409,37 @@ def test_structured_text_field(self):
398409
expected_entry_occurrances -= 1
399410
embedded_entry_index += 1
400411
self.assertEqual(expected_entry_occurrances, 0)
412+
413+
@vcr.use_cassette('fixtures/fields/structured_text_lists_with_embeds.yaml')
414+
def test_structured_text_field_with_embeds_in_lists(self):
415+
client = Client(
416+
'jd7yc4wnatx3',
417+
'6256b8ef7d66805ca41f2728271daf27e8fa6055873b802a813941a0fe696248',
418+
gzip_encoded=False
419+
)
420+
421+
entry = client.entry('6NGLswCREsGA28kGouScyY')
422+
423+
# Hyperlink data is conserved
424+
self.assertEqual(entry.body['content'][0], {
425+
'data': {},
426+
'content': [
427+
{'marks': [], 'value': 'A link to ', 'nodeType': 'text', 'nodeClass': 'text'},
428+
{
429+
'data': {'uri': 'https://google.com'},
430+
'content': [{'marks': [], 'value': 'google', 'nodeType': 'text', 'nodeClass': 'text'}],
431+
'nodeType': 'hyperlink',
432+
'nodeClass': 'inline'
433+
},
434+
{'marks': [], 'value': '', 'nodeType': 'text', 'nodeClass': 'text'}
435+
],
436+
'nodeType': 'paragraph',
437+
'nodeClass': 'block'
438+
})
439+
440+
# Unordered lists and ordered lists can contain embedded entries
441+
self.assertEqual(entry.body['content'][3]['nodeType'], 'unordered-list')
442+
self.assertEqual(str(entry.body['content'][3]['content'][2]['content'][0]['data']), "<Entry[embedded] id='49rofLvvxCOiIMIi6mk8ai'>")
443+
444+
self.assertEqual(entry.body['content'][4]['nodeType'], 'ordered-list')
445+
self.assertEqual(str(entry.body['content'][4]['content'][2]['content'][0]['data']), "<Entry[embedded] id='5ZF9Q4K6iWSYIU2OUs0UaQ'>")

0 commit comments

Comments
 (0)