0001-plone-restapi-add-a-plone.restapi-connector-57258.patch
passerelle/apps/plone_restapi/forms.py | ||
---|---|---|
1 |
# passerelle - uniform access to multiple data sources and services |
|
2 |
# Copyright (C) 2021 Entr'ouvert |
|
3 |
# |
|
4 |
# This program is free software: you can redistribute it and/or modify it |
|
5 |
# under the terms of the GNU Affero General Public License as published |
|
6 |
# by the Free Software Foundation, either version 3 of the License, or |
|
7 |
# (at your option) any later version. |
|
8 |
# |
|
9 |
# This program is distributed in the hope that it will be useful, |
|
10 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 |
# GNU Affero General Public License for more details. |
|
13 |
# |
|
14 |
# You should have received a copy of the GNU Affero General Public License |
|
15 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
16 | ||
17 |
from django import forms |
|
18 | ||
19 |
from passerelle.base.forms import BaseQueryFormMixin |
|
20 | ||
21 |
from . import models |
|
22 | ||
23 | ||
24 |
class QueryForm(BaseQueryFormMixin, forms.ModelForm): |
|
25 |
class Meta: |
|
26 |
model = models.Query |
|
27 |
fields = '__all__' |
|
28 |
exclude = ['resource'] |
passerelle/apps/plone_restapi/migrations/0001_initial.py | ||
---|---|---|
1 |
# Generated by Django 2.2.19 on 2021-10-15 10:15 |
|
2 | ||
3 |
import django.db.models.deletion |
|
4 |
from django.db import migrations, models |
|
5 | ||
6 |
import passerelle.utils.templates |
|
7 | ||
8 | ||
9 |
class Migration(migrations.Migration): |
|
10 | ||
11 |
initial = True |
|
12 | ||
13 |
dependencies = [ |
|
14 |
('base', '0029_auto_20210202_1627'), |
|
15 |
] |
|
16 | ||
17 |
operations = [ |
|
18 |
migrations.CreateModel( |
|
19 |
name='PloneRestApi', |
|
20 |
fields=[ |
|
21 |
( |
|
22 |
'id', |
|
23 |
models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), |
|
24 |
), |
|
25 |
('title', models.CharField(max_length=50, verbose_name='Title')), |
|
26 |
('slug', models.SlugField(unique=True, verbose_name='Identifier')), |
|
27 |
('description', models.TextField(verbose_name='Description')), |
|
28 |
( |
|
29 |
'service_url', |
|
30 |
models.CharField( |
|
31 |
help_text='ex: https://demo.plone.org', max_length=256, verbose_name='Site URL' |
|
32 |
), |
|
33 |
), |
|
34 |
( |
|
35 |
'token_ws_url', |
|
36 |
models.CharField( |
|
37 |
blank=True, |
|
38 |
help_text='ex: https://IDP/idp/oidc/token/ or unset for anonymous acces', |
|
39 |
max_length=256, |
|
40 |
verbose_name='Token webservice URL', |
|
41 |
), |
|
42 |
), |
|
43 |
( |
|
44 |
'client_id', |
|
45 |
models.CharField( |
|
46 |
blank=True, |
|
47 |
help_text='OIDC id of the connector', |
|
48 |
max_length=128, |
|
49 |
verbose_name='OIDC id', |
|
50 |
), |
|
51 |
), |
|
52 |
( |
|
53 |
'client_secret', |
|
54 |
models.CharField( |
|
55 |
blank=True, |
|
56 |
help_text='Share secret secret for webservice call authentication', |
|
57 |
max_length=128, |
|
58 |
verbose_name='Shared secret', |
|
59 |
), |
|
60 |
), |
|
61 |
('username', models.CharField(blank=True, max_length=128, verbose_name='Username')), |
|
62 |
('password', models.CharField(blank=True, max_length=128, verbose_name='Password')), |
|
63 |
( |
|
64 |
'users', |
|
65 |
models.ManyToManyField( |
|
66 |
blank=True, |
|
67 |
related_name='_plonerestapi_users_+', |
|
68 |
related_query_name='+', |
|
69 |
to='base.ApiUser', |
|
70 |
), |
|
71 |
), |
|
72 |
], |
|
73 |
options={ |
|
74 |
'verbose_name': 'Plone REST API Web Service', |
|
75 |
}, |
|
76 |
), |
|
77 |
migrations.CreateModel( |
|
78 |
name='Query', |
|
79 |
fields=[ |
|
80 |
( |
|
81 |
'id', |
|
82 |
models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), |
|
83 |
), |
|
84 |
('name', models.CharField(max_length=128, verbose_name='Name')), |
|
85 |
('slug', models.SlugField(max_length=128, verbose_name='Slug')), |
|
86 |
('description', models.TextField(blank=True, verbose_name='Description')), |
|
87 |
( |
|
88 |
'uri', |
|
89 |
models.CharField( |
|
90 |
blank=True, help_text='uri to query', max_length=128, verbose_name='Uri' |
|
91 |
), |
|
92 |
), |
|
93 |
( |
|
94 |
'text_template', |
|
95 |
models.TextField( |
|
96 |
blank=True, |
|
97 |
help_text="Use Django's template syntax. Attributes can be accessed through {{ attributes.name }}", |
|
98 |
validators=[passerelle.utils.templates.validate_template], |
|
99 |
verbose_name='Text template', |
|
100 |
), |
|
101 |
), |
|
102 |
( |
|
103 |
'filter_expression', |
|
104 |
models.TextField( |
|
105 |
blank=True, |
|
106 |
help_text='Specify more URL parameters (key=value) separated by lines', |
|
107 |
verbose_name='filter', |
|
108 |
), |
|
109 |
), |
|
110 |
( |
|
111 |
'sort', |
|
112 |
models.CharField( |
|
113 |
blank=True, |
|
114 |
help_text='Sorts results by the specified field', |
|
115 |
max_length=256, |
|
116 |
verbose_name='Sort field', |
|
117 |
), |
|
118 |
), |
|
119 |
( |
|
120 |
'order', |
|
121 |
models.BooleanField( |
|
122 |
default=True, |
|
123 |
help_text='Unset to use descending sort order', |
|
124 |
verbose_name='Ascending sort order', |
|
125 |
), |
|
126 |
), |
|
127 |
( |
|
128 |
'limit', |
|
129 |
models.PositiveIntegerField( |
|
130 |
default=10, |
|
131 |
help_text='Number of results to return in a single call', |
|
132 |
verbose_name='Limit', |
|
133 |
), |
|
134 |
), |
|
135 |
( |
|
136 |
'resource', |
|
137 |
models.ForeignKey( |
|
138 |
on_delete=django.db.models.deletion.CASCADE, |
|
139 |
related_name='queries', |
|
140 |
to='plone_restapi.PloneRestApi', |
|
141 |
verbose_name='Resource', |
|
142 |
), |
|
143 |
), |
|
144 |
], |
|
145 |
options={ |
|
146 |
'verbose_name': 'Query', |
|
147 |
'ordering': ['name'], |
|
148 |
'abstract': False, |
|
149 |
'unique_together': {('resource', 'name'), ('resource', 'slug')}, |
|
150 |
}, |
|
151 |
), |
|
152 |
] |
passerelle/apps/plone_restapi/models.py | ||
---|---|---|
1 |
# passerelle - uniform access to multiple data sources and services |
|
2 |
# Copyright (C) 2021 Entr'ouvert |
|
3 |
# |
|
4 |
# This program is free software: you can redistribute it and/or modify it |
|
5 |
# under the terms of the GNU Affero General Public License as published |
|
6 |
# by the Free Software Foundation, either version 3 of the License, or |
|
7 |
# (at your option) any later version. |
|
8 |
# |
|
9 |
# This program is distributed in the hope that it will be useful, |
|
10 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 |
# GNU Affero General Public License for more details. |
|
13 |
# |
|
14 |
# You should have received a copy of the GNU Affero General Public License |
|
15 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
16 | ||
17 |
from urllib.parse import parse_qsl, urlsplit, urlunsplit |
|
18 | ||
19 |
from django.core.cache import cache |
|
20 |
from django.db import models |
|
21 |
from django.shortcuts import get_object_or_404 |
|
22 |
from django.urls import reverse |
|
23 |
from django.utils.translation import ugettext_lazy as _ |
|
24 |
from requests import RequestException |
|
25 | ||
26 |
from passerelle.base.models import BaseQuery, BaseResource |
|
27 |
from passerelle.compat import json_loads |
|
28 |
from passerelle.utils.api import endpoint |
|
29 |
from passerelle.utils.http_authenticators import HttpBearerAuth |
|
30 |
from passerelle.utils.json import unflatten |
|
31 |
from passerelle.utils.jsonresponse import APIError |
|
32 |
from passerelle.utils.templates import render_to_string, validate_template |
|
33 | ||
34 | ||
35 |
class ParameterTypeError(Exception): |
|
36 |
http_status = 400 |
|
37 |
log_error = False |
|
38 | ||
39 | ||
40 |
class PloneRestApi(BaseResource): |
|
41 |
service_url = models.CharField( |
|
42 |
_('Site URL'), |
|
43 |
max_length=256, |
|
44 |
blank=False, |
|
45 |
help_text=_('ex: https://demo.plone.org'), |
|
46 |
) |
|
47 |
token_ws_url = models.CharField( |
|
48 |
_('Token webservice URL'), |
|
49 |
max_length=256, |
|
50 |
blank=True, |
|
51 |
help_text=_('ex: https://IDP/idp/oidc/token/ or unset for anonymous acces'), |
|
52 |
) |
|
53 |
client_id = models.CharField( |
|
54 |
_('OIDC id'), |
|
55 |
max_length=128, |
|
56 |
blank=True, |
|
57 |
help_text=_('OIDC id of the connector'), |
|
58 |
) |
|
59 |
client_secret = models.CharField( |
|
60 |
_('Shared secret'), |
|
61 |
max_length=128, |
|
62 |
blank=True, |
|
63 |
help_text=_('Share secret secret for webservice call authentication'), |
|
64 |
) |
|
65 |
username = models.CharField(_('Username'), max_length=128, blank=True) |
|
66 |
password = models.CharField(_('Password'), max_length=128, blank=True) |
|
67 | ||
68 |
category = _('Data Sources') |
|
69 |
plone_keys_to_rename = ['@id', '@type', '@components'] |
|
70 | ||
71 |
class Meta: |
|
72 |
verbose_name = _('Plone REST API Web Service') |
|
73 | ||
74 |
def export_json(self): |
|
75 |
data = super(PloneRestApi, self).export_json() |
|
76 |
data['queries'] = [query.export_json() for query in self.queries.all()] |
|
77 |
return data |
|
78 | ||
79 |
@classmethod |
|
80 |
def import_json_real(cls, overwrite, instance, data, **kwargs): |
|
81 |
data_queries = data.pop('queries', []) |
|
82 |
instance = super(PloneRestApi, cls).import_json_real(overwrite, instance, data, **kwargs) |
|
83 |
queries = [] |
|
84 |
if instance and overwrite: |
|
85 |
Query.objects.filter(resource=instance).delete() |
|
86 |
for data_query in data_queries: |
|
87 |
query = Query.import_json(data_query) |
|
88 |
query.resource = instance |
|
89 |
queries.append(query) |
|
90 |
Query.objects.bulk_create(queries) |
|
91 |
return instance |
|
92 | ||
93 |
def adapt_id_and_type_plone_attributes(self, data): |
|
94 |
"""Rename keys starting with '@' from plone response |
|
95 |
ex: '@id' is renammed into 'PLONE_id'""" |
|
96 |
if isinstance(data, list): |
|
97 |
for value in list(data): |
|
98 |
self.adapt_id_and_type_plone_attributes(value) |
|
99 |
elif isinstance(data, dict): |
|
100 |
for key, value in list(data.items()): |
|
101 |
self.adapt_id_and_type_plone_attributes(value) |
|
102 |
if key in self.plone_keys_to_rename and key[0] == '@': |
|
103 |
data['PLONE_%s' % key[1:]] = value |
|
104 |
del data[key] |
|
105 | ||
106 |
def adapt_record(self, text_template, record): |
|
107 |
self.adapt_id_and_type_plone_attributes(record) |
|
108 |
data = {} |
|
109 |
for key, value in record.items(): |
|
110 |
# backup original id and text fields |
|
111 |
if key in ('id', 'text'): |
|
112 |
key = 'original_%s' % key |
|
113 |
data[key] = value |
|
114 |
data['id'] = record.get('UID') |
|
115 |
data['text'] = render_to_string(text_template, data).strip() |
|
116 |
return data |
|
117 | ||
118 |
def get_token(self, renew=False): |
|
119 |
token_key = 'plone-restapi-%s-token' % self.id |
|
120 |
if not renew and cache.get(token_key): |
|
121 |
return cache.get(token_key) |
|
122 |
payload = { |
|
123 |
'grant_type': 'password', |
|
124 |
'client_id': str(self.client_id), |
|
125 |
'client_secret': str(self.client_secret), |
|
126 |
'username': self.username, |
|
127 |
'password': self.password, |
|
128 |
'scope': ['openid'], |
|
129 |
} |
|
130 |
headers = { |
|
131 |
'Content-Type': 'application/x-www-form-urlencoded', |
|
132 |
} |
|
133 |
response = self.requests.post(self.token_ws_url, headers=headers, data=payload) |
|
134 |
if not response.status_code // 100 == 2: |
|
135 |
raise APIError(response.content) |
|
136 |
token = response.json().get('id_token') |
|
137 |
cache.set(token_key, token, 30) |
|
138 |
return token |
|
139 | ||
140 |
def request(self, uri='', uid='', method='GET', params=None, json=None): |
|
141 |
scheme, netloc, path, query, fragment = urlsplit(self.service_url) |
|
142 |
if uri: |
|
143 |
path += '/%s' % uri |
|
144 |
if uid: |
|
145 |
path += '/%s' % uid |
|
146 |
url = urlunsplit((scheme, netloc, path, '', fragment)) |
|
147 |
headers = {'Accept': 'application/json'} |
|
148 |
auth = HttpBearerAuth(self.get_token()) if self.token_ws_url else None |
|
149 |
try: |
|
150 |
response = self.requests.request( |
|
151 |
method=method, url=url, headers=headers, params=params, json=json, auth=auth |
|
152 |
) |
|
153 |
except RequestException as e: |
|
154 |
raise APIError('PloneRestApi: %s' % e) |
|
155 |
json_response = None |
|
156 |
if response.status_code != 204: # No Content |
|
157 |
try: |
|
158 |
json_response = response.json() |
|
159 |
except ValueError as e: |
|
160 |
raise APIError('PloneRestApi: bad JSON response') |
|
161 |
try: |
|
162 |
response.raise_for_status() |
|
163 |
except RequestException as e: |
|
164 |
raise APIError('PloneRestApi: %s "%s"' % (e, json_response)) |
|
165 |
return json_response |
|
166 | ||
167 |
def call_search( |
|
168 |
self, |
|
169 |
uri='', |
|
170 |
text_template='', |
|
171 |
filter_expression='', |
|
172 |
sort=None, |
|
173 |
order=True, |
|
174 |
limit=None, |
|
175 |
id=None, |
|
176 |
q=None, |
|
177 |
): |
|
178 |
query = urlsplit(self.service_url).query |
|
179 |
params = dict(parse_qsl(query)) |
|
180 |
if id: |
|
181 |
params['UID'] = id |
|
182 |
else: |
|
183 |
if q is not None: |
|
184 |
params['SearchableText'] = q |
|
185 |
if sort: |
|
186 |
params['sort_on'] = sort |
|
187 |
if order: |
|
188 |
params['sort_order'] = 'ascending' |
|
189 |
else: |
|
190 |
params['sort_order'] = 'descending' |
|
191 |
if limit: |
|
192 |
params['b_size'] = limit |
|
193 |
params.update(parse_qsl(filter_expression)) |
|
194 |
params['fullobjects'] = 'y' |
|
195 |
json_response = self.request(uri=uri, uid='@search', method='GET', params=params) |
|
196 | ||
197 |
result = [] |
|
198 |
for record in json_response.get('items'): |
|
199 |
data = self.adapt_record(text_template, record) |
|
200 |
result.append(data) |
|
201 |
return result |
|
202 | ||
203 |
@endpoint( |
|
204 |
perm='can_access', |
|
205 |
description=_('Fetch'), |
|
206 |
parameters={ |
|
207 |
'uri': {'description': _('Uri')}, |
|
208 |
'uid': {'description': _('Uid')}, |
|
209 |
'text_template': {'description': _('Text template')}, |
|
210 |
}, |
|
211 |
display_order=1, |
|
212 |
) |
|
213 |
def fetch(self, request, uid, uri='', text_template=''): |
|
214 |
json_response = self.request(uri=uri, uid=uid, method='GET') |
|
215 |
data = self.adapt_record(text_template, json_response) |
|
216 |
return {'data': data} |
|
217 | ||
218 |
@endpoint( |
|
219 |
perm='can_access', |
|
220 |
description=_('Creates'), |
|
221 |
parameters={ |
|
222 |
'uri': {'description': _('Uri')}, |
|
223 |
}, |
|
224 |
methods=['post'], |
|
225 |
display_order=2, |
|
226 |
) |
|
227 |
def create(self, request, uri): |
|
228 |
try: |
|
229 |
post_data = json_loads(request.body) |
|
230 |
except ValueError as e: |
|
231 |
raise ParameterTypeError(str(e)) |
|
232 |
post_data = unflatten(post_data) |
|
233 |
response = self.request(uri=uri, method='POST', json=post_data) |
|
234 |
return {'data': {'uid': response['UID'], 'created': True}} |
|
235 | ||
236 |
@endpoint( |
|
237 |
perm='can_access', |
|
238 |
description=_('Update'), |
|
239 |
parameters={ |
|
240 |
'uri': {'description': _('Uri')}, |
|
241 |
'uid': {'description': _('Uid')}, |
|
242 |
}, |
|
243 |
methods=['post'], |
|
244 |
display_order=3, |
|
245 |
) |
|
246 |
def update(self, request, uid, uri=''): |
|
247 |
try: |
|
248 |
post_data = json_loads(request.body) |
|
249 |
except ValueError as e: |
|
250 |
raise ParameterTypeError(str(e)) |
|
251 |
post_data = unflatten(post_data) |
|
252 |
self.request(uri=uri, uid=uid, method='PATCH', json=post_data) |
|
253 |
return {'data': {'uid': uid, 'updated': True}} |
|
254 | ||
255 |
@endpoint( |
|
256 |
perm='can_access', |
|
257 |
description=_('Remove'), |
|
258 |
parameters={ |
|
259 |
'uri': {'description': _('Uri')}, |
|
260 |
'uid': {'description': _('Uid')}, |
|
261 |
}, |
|
262 |
methods=['delete'], |
|
263 |
display_order=4, |
|
264 |
) |
|
265 |
def remove(self, request, uid, uri=''): |
|
266 |
self.request(method='DELETE', uri=uri, uid=uid) |
|
267 |
return {'data': {'uid': uid, 'removed': True}} |
|
268 | ||
269 |
@endpoint( |
|
270 |
perm='can_access', |
|
271 |
description=_('Search'), |
|
272 |
parameters={ |
|
273 |
'uri': {'description': _('Uri')}, |
|
274 |
'text_template': {'description': _('Text template')}, |
|
275 |
'sort': {'description': _('Sort field')}, |
|
276 |
'order': {'description': _('Ascending sort order'), 'type': 'bool'}, |
|
277 |
'limit': {'description': _('Maximum items')}, |
|
278 |
'id': {'description': _('Record identifier')}, |
|
279 |
'q': {'description': _('Full text query')}, |
|
280 |
}, |
|
281 |
) |
|
282 |
def search( |
|
283 |
self, |
|
284 |
request, |
|
285 |
uri='', |
|
286 |
text_template='', |
|
287 |
sort=None, |
|
288 |
order=True, |
|
289 |
limit=None, |
|
290 |
id=None, |
|
291 |
q=None, |
|
292 |
**kwargs, |
|
293 |
): |
|
294 |
result = self.call_search(uri, text_template, '', sort, order, limit, id, q) |
|
295 |
return {'data': result} |
|
296 | ||
297 |
@endpoint( |
|
298 |
name='q', |
|
299 |
description=_('Query'), |
|
300 |
pattern=r'^(?P<query_slug>[\w:_-]+)/$', |
|
301 |
perm='can_access', |
|
302 |
show=False, |
|
303 |
) |
|
304 |
def q(self, request, query_slug, **kwargs): |
|
305 |
query = get_object_or_404(Query, resource=self, slug=query_slug) |
|
306 |
result = query.q(request, **kwargs) |
|
307 |
meta = {'label': query.name, 'description': query.description} |
|
308 |
return {'data': result, 'meta': meta} |
|
309 | ||
310 |
def create_query_url(self): |
|
311 |
return reverse('plone-restapi-query-new', kwargs={'slug': self.slug}) |
|
312 | ||
313 | ||
314 |
class Query(BaseQuery): |
|
315 |
resource = models.ForeignKey( |
|
316 |
to=PloneRestApi, related_name='queries', verbose_name=_('Resource'), on_delete=models.CASCADE |
|
317 |
) |
|
318 |
uri = models.CharField( |
|
319 |
verbose_name=_('Uri'), |
|
320 |
max_length=128, |
|
321 |
help_text=_('uri to query'), |
|
322 |
blank=True, |
|
323 |
) |
|
324 |
text_template = models.TextField( |
|
325 |
verbose_name=_('Text template'), |
|
326 |
help_text=_("Use Django's template syntax. Attributes can be accessed through {{ attributes.name }}"), |
|
327 |
validators=[validate_template], |
|
328 |
blank=True, |
|
329 |
) |
|
330 |
filter_expression = models.TextField( |
|
331 |
verbose_name=_('filter'), |
|
332 |
help_text=_('Specify more URL parameters (key=value) separated by lines'), |
|
333 |
blank=True, |
|
334 |
) |
|
335 |
sort = models.CharField( |
|
336 |
verbose_name=_('Sort field'), |
|
337 |
help_text=_('Sorts results by the specified field'), |
|
338 |
max_length=256, |
|
339 |
blank=True, |
|
340 |
) |
|
341 |
order = models.BooleanField( |
|
342 |
verbose_name=_('Ascending sort order'), |
|
343 |
help_text=_("Unset to use descending sort order"), |
|
344 |
default=True, |
|
345 |
) |
|
346 |
limit = models.PositiveIntegerField( |
|
347 |
default=10, |
|
348 |
verbose_name=_('Limit'), |
|
349 |
help_text=_('Number of results to return in a single call'), |
|
350 |
) |
|
351 | ||
352 |
delete_view = 'plone-restapi-query-delete' |
|
353 |
edit_view = 'plone-restapi-query-edit' |
|
354 | ||
355 |
def q(self, request, **kwargs): |
|
356 |
return self.resource.call_search( |
|
357 |
uri=self.uri, |
|
358 |
text_template=self.text_template, |
|
359 |
filter_expression='&'.join( |
|
360 |
[x.strip() for x in str(self.filter_expression).splitlines() if x.strip()] |
|
361 |
), |
|
362 |
sort=self.sort, |
|
363 |
order=self.order, |
|
364 |
limit=self.limit, |
|
365 |
id=kwargs.get('id'), |
|
366 |
q=kwargs.get('q'), |
|
367 |
) |
|
368 | ||
369 |
def as_endpoint(self): |
|
370 |
endpoint = super(Query, self).as_endpoint(path=self.resource.q.endpoint_info.name) |
|
371 | ||
372 |
search_endpoint = self.resource.search.endpoint_info |
|
373 |
endpoint.func = search_endpoint.func |
|
374 |
endpoint.show_undocumented_params = False |
|
375 | ||
376 |
# Copy generic params descriptions from original endpoint |
|
377 |
# if they are not overloaded by the query |
|
378 |
for param in search_endpoint.parameters: |
|
379 |
if param in ('uri', 'text_template') and getattr(self, param): |
|
380 |
continue |
|
381 |
endpoint.parameters[param] = search_endpoint.parameters[param] |
|
382 |
return endpoint |
passerelle/apps/plone_restapi/urls.py | ||
---|---|---|
1 |
# passerelle - uniform access to multiple data sources and services |
|
2 |
# Copyright (C) 2021 Entr'ouvert |
|
3 |
# |
|
4 |
# This program is free software: you can redistribute it and/or modify it |
|
5 |
# under the terms of the GNU Affero General Public License as published |
|
6 |
# by the Free Software Foundation, either version 3 of the License, or |
|
7 |
# (at your option) any later version. |
|
8 |
# |
|
9 |
# This program is distributed in the hope that it will be useful, |
|
10 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 |
# GNU Affero General Public License for more details. |
|
13 |
# |
|
14 |
# You should have received a copy of the GNU Affero General Public License |
|
15 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
16 | ||
17 |
from django.conf.urls import url |
|
18 | ||
19 |
from . import views |
|
20 | ||
21 |
management_urlpatterns = [ |
|
22 |
url(r'^(?P<slug>[\w,-]+)/query/new/$', views.QueryNew.as_view(), name='plone-restapi-query-new'), |
|
23 |
url( |
|
24 |
r'^(?P<slug>[\w,-]+)/query/(?P<pk>\d+)/$', views.QueryEdit.as_view(), name='plone-restapi-query-edit' |
|
25 |
), |
|
26 |
url( |
|
27 |
r'^(?P<slug>[\w,-]+)/query/(?P<pk>\d+)/delete/$', |
|
28 |
views.QueryDelete.as_view(), |
|
29 |
name='plone-restapi-query-delete', |
|
30 |
), |
|
31 |
] |
passerelle/apps/plone_restapi/views.py | ||
---|---|---|
1 |
# passerelle - uniform access to multiple data sources and services |
|
2 |
# Copyright (C) 2021 Entr'ouvert |
|
3 |
# |
|
4 |
# This program is free software: you can redistribute it and/or modify it |
|
5 |
# under the terms of the GNU Affero General Public License as published |
|
6 |
# by the Free Software Foundation, either version 3 of the License, or |
|
7 |
# (at your option) any later version. |
|
8 |
# |
|
9 |
# This program is distributed in the hope that it will be useful, |
|
10 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 |
# GNU Affero General Public License for more details. |
|
13 |
# |
|
14 |
# You should have received a copy of the GNU Affero General Public License |
|
15 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
16 | ||
17 |
from django.views.generic import CreateView, DeleteView, UpdateView |
|
18 | ||
19 |
from passerelle.base.mixins import ResourceChildViewMixin |
|
20 | ||
21 |
from . import models |
|
22 |
from .forms import QueryForm |
|
23 | ||
24 | ||
25 |
class QueryNew(ResourceChildViewMixin, CreateView): |
|
26 |
model = models.Query |
|
27 |
form_class = QueryForm |
|
28 |
template_name = "passerelle/manage/resource_child_form.html" |
|
29 | ||
30 |
def get_form_kwargs(self): |
|
31 |
kwargs = super(QueryNew, self).get_form_kwargs() |
|
32 |
kwargs['instance'] = self.model(resource=self.resource) |
|
33 |
return kwargs |
|
34 | ||
35 | ||
36 |
class QueryEdit(ResourceChildViewMixin, UpdateView): |
|
37 |
model = models.Query |
|
38 |
form_class = QueryForm |
|
39 |
template_name = "passerelle/manage/resource_child_form.html" |
|
40 | ||
41 | ||
42 |
class QueryDelete(ResourceChildViewMixin, DeleteView): |
|
43 |
model = models.Query |
|
44 |
template_name = "passerelle/manage/resource_child_confirm_delete.html" |
passerelle/settings.py | ||
---|---|---|
154 | 154 |
'passerelle.apps.okina', |
155 | 155 |
'passerelle.apps.opendatasoft', |
156 | 156 |
'passerelle.apps.opengis', |
157 | 157 |
'passerelle.apps.orange', |
158 | 158 |
'passerelle.apps.ovh', |
159 | 159 |
'passerelle.apps.oxyd', |
160 | 160 |
'passerelle.apps.phonecalls', |
161 | 161 |
'passerelle.apps.photon', |
162 |
'passerelle.apps.plone_restapi', |
|
162 | 163 |
'passerelle.apps.sector', |
163 | 164 |
'passerelle.apps.solis', |
164 | 165 |
'passerelle.apps.twilio', |
165 | 166 |
'passerelle.apps.vivaticket', |
166 | 167 |
# backoffice templates and static |
167 | 168 |
'gadjo', |
168 | 169 |
) |
169 | 170 |
passerelle/static/css/style.css | ||
---|---|---|
181 | 181 |
li.connector.dpark a::before { |
182 | 182 |
content: "\f1b9"; /* car */ |
183 | 183 |
} |
184 | 184 | |
185 | 185 |
li.connector.cryptor a::before { |
186 | 186 |
content: "\f023"; /* lock */ |
187 | 187 |
} |
188 | 188 | |
189 |
li.connector.plonerestapi a::before { |
|
190 |
content: "\f1c0"; /* database */ |
|
191 |
} |
|
192 | ||
189 | 193 |
li.connector.opendatasoft a::before { |
190 | 194 |
content: "\f1c0"; /* database */ |
191 | 195 |
} |
192 | 196 | |
193 | 197 | |
194 | 198 |
li.connector.status-down span.connector-name::after { |
195 | 199 |
font-family: FontAwesome; |
196 | 200 |
content: "\f00d"; /* times */ |
tests/data/plone_restapi/fetch.json | ||
---|---|---|
1 |
{ |
|
2 |
"@components": { |
|
3 |
"actions": { |
|
4 |
"@id": "https://annuaire.preprod.imio.be/braine-l-alleud/c44f1b32f0ce436eb7a042ca8933b287/@actions" |
|
5 |
}, |
|
6 |
"breadcrumbs": { |
|
7 |
"@id": "https://annuaire.preprod.imio.be/braine-l-alleud/c44f1b32f0ce436eb7a042ca8933b287/@breadcrumbs" |
|
8 |
}, |
|
9 |
"contextnavigation": { |
|
10 |
"@id": "https://annuaire.preprod.imio.be/braine-l-alleud/c44f1b32f0ce436eb7a042ca8933b287/@contextnavigation" |
|
11 |
}, |
|
12 |
"navigation": { |
|
13 |
"@id": "https://annuaire.preprod.imio.be/braine-l-alleud/c44f1b32f0ce436eb7a042ca8933b287/@navigation" |
|
14 |
}, |
|
15 |
"types": { |
|
16 |
"@id": "https://annuaire.preprod.imio.be/braine-l-alleud/c44f1b32f0ce436eb7a042ca8933b287/@types" |
|
17 |
}, |
|
18 |
"workflow": { |
|
19 |
"@id": "https://annuaire.preprod.imio.be/braine-l-alleud/c44f1b32f0ce436eb7a042ca8933b287/@workflow" |
|
20 |
} |
|
21 |
}, |
|
22 |
"@id": "https://annuaire.preprod.imio.be/braine-l-alleud/c44f1b32f0ce436eb7a042ca8933b287", |
|
23 |
"@type": "imio.directory.Contact", |
|
24 |
"UID": "dccd85d12cf54b6899dff41e5a56ee7f", |
|
25 |
"allow_discussion": false, |
|
26 |
"city": "Braine-l'Alleud", |
|
27 |
"complement": null, |
|
28 |
"country": { |
|
29 |
"title": "Belgique", |
|
30 |
"token": "be" |
|
31 |
}, |
|
32 |
"created": "2021-07-28T07:53:01+00:00", |
|
33 |
"description": "Ouvert du lundi au samedi toute l'ann\u00e9e.\r\n\r\nContact : Thierry Vou\u00e9", |
|
34 |
"exceptional_closure": [], |
|
35 |
"facilities": null, |
|
36 |
"geolocation": { |
|
37 |
"latitude": 50.4989185, |
|
38 |
"longitude": 4.7184485 |
|
39 |
}, |
|
40 |
"iam": [ |
|
41 |
{ |
|
42 |
"title": "Jeune", |
|
43 |
"token": "young" |
|
44 |
} |
|
45 |
], |
|
46 |
"id": "c44f1b32f0ce436eb7a042ca8933b287", |
|
47 |
"image": null, |
|
48 |
"image_caption": null, |
|
49 |
"is_folderish": true, |
|
50 |
"is_geolocated": true, |
|
51 |
"items": [], |
|
52 |
"items_total": 0, |
|
53 |
"language": { |
|
54 |
"title": "Fran\u00e7ais", |
|
55 |
"token": "fr" |
|
56 |
}, |
|
57 |
"layout": "view", |
|
58 |
"logo": { |
|
59 |
"content-type": "image/png", |
|
60 |
"download": "https://annuaire.preprod.imio.be/braine-l-alleud/c44f1b32f0ce436eb7a042ca8933b287/@@images/d4e7b99f-98c2-4c85-87fa-7fb0ebb31c16.png", |
|
61 |
"filename": "maison jeunes le prisme.png", |
|
62 |
"height": 1536, |
|
63 |
"scales": { |
|
64 |
"banner": { |
|
65 |
"download": "https://annuaire.preprod.imio.be/braine-l-alleud/c44f1b32f0ce436eb7a042ca8933b287/@@images/02ef9609-1182-4a3d-9ce6-eb0449309b55.png", |
|
66 |
"height": 590, |
|
67 |
"width": 1920 |
|
68 |
}, |
|
69 |
"extralarge": { |
|
70 |
"download": "https://annuaire.preprod.imio.be/braine-l-alleud/c44f1b32f0ce436eb7a042ca8933b287/@@images/5be772a5-dc46-417c-aab6-ae4352536a48.png", |
|
71 |
"height": 405, |
|
72 |
"width": 1320 |
|
73 |
}, |
|
74 |
"icon": { |
|
75 |
"download": "https://annuaire.preprod.imio.be/braine-l-alleud/c44f1b32f0ce436eb7a042ca8933b287/@@images/21e0bb31-8a45-4e42-a4ef-60f8070d7ef9.png", |
|
76 |
"height": 9, |
|
77 |
"width": 32 |
|
78 |
}, |
|
79 |
"large": { |
|
80 |
"download": "https://annuaire.preprod.imio.be/braine-l-alleud/c44f1b32f0ce436eb7a042ca8933b287/@@images/1cd53cba-e9ff-4abb-a43a-9e859c6959dc.png", |
|
81 |
"height": 236, |
|
82 |
"width": 768 |
|
83 |
}, |
|
84 |
"listing": { |
|
85 |
"download": "https://annuaire.preprod.imio.be/braine-l-alleud/c44f1b32f0ce436eb7a042ca8933b287/@@images/09c2223f-0fb0-4fcb-bcb2-2501af8543cd.png", |
|
86 |
"height": 4, |
|
87 |
"width": 16 |
|
88 |
}, |
|
89 |
"medium": { |
|
90 |
"download": "https://annuaire.preprod.imio.be/braine-l-alleud/c44f1b32f0ce436eb7a042ca8933b287/@@images/9db56c58-9cf9-4b72-afdf-1e2d167e7fee.png", |
|
91 |
"height": 184, |
|
92 |
"width": 600 |
|
93 |
}, |
|
94 |
"mini": { |
|
95 |
"download": "https://annuaire.preprod.imio.be/braine-l-alleud/c44f1b32f0ce436eb7a042ca8933b287/@@images/67a49d61-159d-4ca7-9e06-0de39838e5c9.png", |
|
96 |
"height": 61, |
|
97 |
"width": 200 |
|
98 |
}, |
|
99 |
"preview": { |
|
100 |
"download": "https://annuaire.preprod.imio.be/braine-l-alleud/c44f1b32f0ce436eb7a042ca8933b287/@@images/2ff94086-ce56-45cc-b293-fd796931dbe5.png", |
|
101 |
"height": 123, |
|
102 |
"width": 400 |
|
103 |
}, |
|
104 |
"thumb": { |
|
105 |
"download": "https://annuaire.preprod.imio.be/braine-l-alleud/c44f1b32f0ce436eb7a042ca8933b287/@@images/0ac5cdc9-c5c2-4aed-8e1d-9260311236f2.png", |
|
106 |
"height": 39, |
|
107 |
"width": 128 |
|
108 |
}, |
|
109 |
"tile": { |
|
110 |
"download": "https://annuaire.preprod.imio.be/braine-l-alleud/c44f1b32f0ce436eb7a042ca8933b287/@@images/e439acab-b379-46e4-b939-52f5a7ba67a6.png", |
|
111 |
"height": 19, |
|
112 |
"width": 64 |
|
113 |
} |
|
114 |
}, |
|
115 |
"size": 1268077, |
|
116 |
"width": 4995 |
|
117 |
}, |
|
118 |
"mails": [ |
|
119 |
{ |
|
120 |
"label": null, |
|
121 |
"mail_address": "info@leprisme.be", |
|
122 |
"type": "work" |
|
123 |
} |
|
124 |
], |
|
125 |
"modified": "2021-10-01T17:07:32+00:00", |
|
126 |
"multi_schedule": [], |
|
127 |
"next_item": { |
|
128 |
"@id": "https://annuaire.preprod.imio.be/braine-l-alleud/c812043a3ed44e00815e342de34a61c9", |
|
129 |
"@type": "imio.directory.Contact", |
|
130 |
"description": "", |
|
131 |
"title": "Parc \u00e0 conteneurs de Braine-l'Alleud" |
|
132 |
}, |
|
133 |
"number": "103", |
|
134 |
"parent": { |
|
135 |
"@id": "https://annuaire.preprod.imio.be/braine-l-alleud", |
|
136 |
"@type": "imio.directory.Entity", |
|
137 |
"description": "", |
|
138 |
"review_state": "published", |
|
139 |
"title": "Braine-l'Alleud" |
|
140 |
}, |
|
141 |
"phones": [ |
|
142 |
{ |
|
143 |
"label": null, |
|
144 |
"number": "+3223870926", |
|
145 |
"type": "work" |
|
146 |
}, |
|
147 |
{ |
|
148 |
"label": null, |
|
149 |
"number": "+32475916819", |
|
150 |
"type": "cell" |
|
151 |
} |
|
152 |
], |
|
153 |
"previous_item": { |
|
154 |
"@id": "https://annuaire.preprod.imio.be/braine-l-alleud/3d595a01fa814af09cf9aac35a11b9b0", |
|
155 |
"@type": "imio.directory.Contact", |
|
156 |
"description": "", |
|
157 |
"title": "Kinepolis Imagibraine" |
|
158 |
}, |
|
159 |
"review_state": "published", |
|
160 |
"schedule": { |
|
161 |
"friday": { |
|
162 |
"afternoonend": "", |
|
163 |
"afternoonstart": "", |
|
164 |
"comment": "", |
|
165 |
"morningend": "17:00", |
|
166 |
"morningstart": "10:00" |
|
167 |
}, |
|
168 |
"monday": { |
|
169 |
"afternoonend": "", |
|
170 |
"afternoonstart": "", |
|
171 |
"comment": "", |
|
172 |
"morningend": "17:00", |
|
173 |
"morningstart": "10:00" |
|
174 |
}, |
|
175 |
"saturday": { |
|
176 |
"afternoonend": "", |
|
177 |
"afternoonstart": "", |
|
178 |
"comment": "", |
|
179 |
"morningend": "", |
|
180 |
"morningstart": "" |
|
181 |
}, |
|
182 |
"sunday": { |
|
183 |
"afternoonend": "", |
|
184 |
"afternoonstart": "", |
|
185 |
"comment": "", |
|
186 |
"morningend": "", |
|
187 |
"morningstart": "" |
|
188 |
}, |
|
189 |
"thursday": { |
|
190 |
"afternoonend": "", |
|
191 |
"afternoonstart": "", |
|
192 |
"comment": "", |
|
193 |
"morningend": "17:00", |
|
194 |
"morningstart": "10:00" |
|
195 |
}, |
|
196 |
"tuesday": { |
|
197 |
"afternoonend": "", |
|
198 |
"afternoonstart": "", |
|
199 |
"comment": "", |
|
200 |
"morningend": "17:00", |
|
201 |
"morningstart": "10:00" |
|
202 |
}, |
|
203 |
"wednesday": { |
|
204 |
"afternoonend": "", |
|
205 |
"afternoonstart": "", |
|
206 |
"comment": "", |
|
207 |
"morningend": "17:00", |
|
208 |
"morningstart": "10:00" |
|
209 |
} |
|
210 |
}, |
|
211 |
"selected_entities": [ |
|
212 |
{ |
|
213 |
"title": "Braine-l'Alleud", |
|
214 |
"token": "f571b73a16f34832a5fdd3683533b3cc" |
|
215 |
} |
|
216 |
], |
|
217 |
"street": "Avenue Alphonse Allard", |
|
218 |
"subjects": [ |
|
219 |
"mj" |
|
220 |
], |
|
221 |
"subtitle": "Maison de Jeunes de Braine-l'Alleud", |
|
222 |
"taxonomy_contact_category": [ |
|
223 |
{ |
|
224 |
"title": "Loisirs \u00bb Mouvements et associations \u00bb Jeunesse", |
|
225 |
"token": "oqa05qwd45" |
|
226 |
} |
|
227 |
], |
|
228 |
"title": "Le Prisme", |
|
229 |
"topics": [ |
|
230 |
{ |
|
231 |
"title": "Activit\u00e9s et divertissement", |
|
232 |
"token": "entertainment" |
|
233 |
}, |
|
234 |
{ |
|
235 |
"title": "Culture", |
|
236 |
"token": "culture" |
|
237 |
}, |
|
238 |
{ |
|
239 |
"title": "Sports", |
|
240 |
"token": "sports" |
|
241 |
}, |
|
242 |
{ |
|
243 |
"title": "Participation citoyenne", |
|
244 |
"token": "citizen_participation" |
|
245 |
} |
|
246 |
], |
|
247 |
"type": { |
|
248 |
"title": "Organisation (service administratif, commerce, profession lib\u00e9rale, club sportif, association, etc.)", |
|
249 |
"token": "organization" |
|
250 |
}, |
|
251 |
"urls": [ |
|
252 |
{ |
|
253 |
"type": "website", |
|
254 |
"url": "http://www.leprisme.be/" |
|
255 |
} |
|
256 |
], |
|
257 |
"vat_number": null, |
|
258 |
"version": "current", |
|
259 |
"working_copy": null, |
|
260 |
"working_copy_of": null, |
|
261 |
"zipcode": 1420 |
|
262 |
} |
tests/data/plone_restapi/id_search.json | ||
---|---|---|
1 |
{ |
|
2 |
"@id": "https://annuaire.preprod.imio.be/braine-l-alleud/@search?UID=23a32197d6c841259963b43b24747854&fullobjects=y", |
|
3 |
"items": [ |
|
4 |
{ |
|
5 |
"@components": { |
|
6 |
"actions": { |
|
7 |
"@id": "https://annuaire.preprod.imio.be/braine-l-alleud/3378d97243854ddfa90510f6ceb9fcdb/@actions" |
|
8 |
}, |
|
9 |
"breadcrumbs": { |
|
10 |
"@id": "https://annuaire.preprod.imio.be/braine-l-alleud/3378d97243854ddfa90510f6ceb9fcdb/@breadcrumbs" |
|
11 |
}, |
|
12 |
"contextnavigation": { |
|
13 |
"@id": "https://annuaire.preprod.imio.be/braine-l-alleud/3378d97243854ddfa90510f6ceb9fcdb/@contextnavigation" |
|
14 |
}, |
|
15 |
"navigation": { |
|
16 |
"@id": "https://annuaire.preprod.imio.be/braine-l-alleud/3378d97243854ddfa90510f6ceb9fcdb/@navigation" |
|
17 |
}, |
|
18 |
"types": { |
|
19 |
"@id": "https://annuaire.preprod.imio.be/braine-l-alleud/3378d97243854ddfa90510f6ceb9fcdb/@types" |
|
20 |
}, |
|
21 |
"workflow": { |
|
22 |
"@id": "https://annuaire.preprod.imio.be/braine-l-alleud/3378d97243854ddfa90510f6ceb9fcdb/@workflow" |
|
23 |
} |
|
24 |
}, |
|
25 |
"@id": "https://annuaire.preprod.imio.be/braine-l-alleud/3378d97243854ddfa90510f6ceb9fcdb", |
|
26 |
"@type": "imio.directory.Contact", |
|
27 |
"UID": "23a32197d6c841259963b43b24747854", |
|
28 |
"allow_discussion": false, |
|
29 |
"city": "Braine-l'Alleud", |
|
30 |
"complement": null, |
|
31 |
"country": { |
|
32 |
"title": "Belgique", |
|
33 |
"token": "be" |
|
34 |
}, |
|
35 |
"created": "2021-07-28T07:10:02+00:00", |
|
36 |
"description": "Contact : Jean-Pascal Hinnekens (directeur)", |
|
37 |
"exceptional_closure": [], |
|
38 |
"facilities": null, |
|
39 |
"geolocation": { |
|
40 |
"latitude": 50.4989185, |
|
41 |
"longitude": 4.7184485 |
|
42 |
}, |
|
43 |
"iam": [ |
|
44 |
{ |
|
45 |
"title": "Jeune", |
|
46 |
"token": "young" |
|
47 |
}, |
|
48 |
{ |
|
49 |
"title": "Nouvel arrivant", |
|
50 |
"token": "newcomer" |
|
51 |
}, |
|
52 |
{ |
|
53 |
"title": "Parent", |
|
54 |
"token": "parent" |
|
55 |
} |
|
56 |
], |
|
57 |
"id": "3378d97243854ddfa90510f6ceb9fcdb", |
|
58 |
"image": null, |
|
59 |
"image_caption": null, |
|
60 |
"is_folderish": true, |
|
61 |
"is_geolocated": true, |
|
62 |
"language": { |
|
63 |
"title": "Fran\u00e7ais", |
|
64 |
"token": "fr" |
|
65 |
}, |
|
66 |
"layout": "view", |
|
67 |
"logo": { |
|
68 |
"content-type": "image/png", |
|
69 |
"download": "https://annuaire.preprod.imio.be/braine-l-alleud/3378d97243854ddfa90510f6ceb9fcdb/@@images/b5785773-138a-4907-9ec6-b29100f18e85.png", |
|
70 |
"filename": "acad\u00e9mie musique braine-l'alleud.png", |
|
71 |
"height": 591, |
|
72 |
"scales": { |
|
73 |
"banner": { |
|
74 |
"download": "https://annuaire.preprod.imio.be/braine-l-alleud/3378d97243854ddfa90510f6ceb9fcdb/@@images/64373ca2-151b-439d-a489-288c83a94de6.png", |
|
75 |
"height": 591, |
|
76 |
"width": 559 |
|
77 |
}, |
|
78 |
"extralarge": { |
|
79 |
"download": "https://annuaire.preprod.imio.be/braine-l-alleud/3378d97243854ddfa90510f6ceb9fcdb/@@images/cb8f259c-1255-4d5d-9b1a-fdace828f74c.png", |
|
80 |
"height": 591, |
|
81 |
"width": 559 |
|
82 |
}, |
|
83 |
"icon": { |
|
84 |
"download": "https://annuaire.preprod.imio.be/braine-l-alleud/3378d97243854ddfa90510f6ceb9fcdb/@@images/77276cc0-e251-450b-8a4c-968e1d5c4ac4.png", |
|
85 |
"height": 32, |
|
86 |
"width": 31 |
|
87 |
}, |
|
88 |
"large": { |
|
89 |
"download": "https://annuaire.preprod.imio.be/braine-l-alleud/3378d97243854ddfa90510f6ceb9fcdb/@@images/6ec18ddf-645a-4764-b7b4-fedfe7c1d9e0.png", |
|
90 |
"height": 591, |
|
91 |
"width": 559 |
|
92 |
}, |
|
93 |
"listing": { |
|
94 |
"download": "https://annuaire.preprod.imio.be/braine-l-alleud/3378d97243854ddfa90510f6ceb9fcdb/@@images/07b35265-e56f-4fa0-9717-b1982604f9b4.png", |
|
95 |
"height": 16, |
|
96 |
"width": 16 |
|
97 |
}, |
|
98 |
"medium": { |
|
99 |
"download": "https://annuaire.preprod.imio.be/braine-l-alleud/3378d97243854ddfa90510f6ceb9fcdb/@@images/4470f78e-19fb-41aa-b897-4f9b0f5f356e.png", |
|
100 |
"height": 591, |
|
101 |
"width": 559 |
|
102 |
}, |
|
103 |
"mini": { |
|
104 |
"download": "https://annuaire.preprod.imio.be/braine-l-alleud/3378d97243854ddfa90510f6ceb9fcdb/@@images/548c616b-d37b-43be-b931-42c17b462127.png", |
|
105 |
"height": 200, |
|
106 |
"width": 189 |
|
107 |
}, |
|
108 |
"preview": { |
|
109 |
"download": "https://annuaire.preprod.imio.be/braine-l-alleud/3378d97243854ddfa90510f6ceb9fcdb/@@images/84394ebe-2d4d-4894-b331-1636e93ccd38.png", |
|
110 |
"height": 400, |
|
111 |
"width": 379 |
|
112 |
}, |
|
113 |
"thumb": { |
|
114 |
"download": "https://annuaire.preprod.imio.be/braine-l-alleud/3378d97243854ddfa90510f6ceb9fcdb/@@images/087c5b17-5ae3-4e9d-a794-a95440e5aa43.png", |
|
115 |
"height": 128, |
|
116 |
"width": 121 |
|
117 |
}, |
|
118 |
"tile": { |
|
119 |
"download": "https://annuaire.preprod.imio.be/braine-l-alleud/3378d97243854ddfa90510f6ceb9fcdb/@@images/34278749-0375-4000-924f-b56993076bbd.png", |
|
120 |
"height": 64, |
|
121 |
"width": 61 |
|
122 |
} |
|
123 |
}, |
|
124 |
"size": 232832, |
|
125 |
"width": 559 |
|
126 |
}, |
|
127 |
"mails": [ |
|
128 |
{ |
|
129 |
"label": null, |
|
130 |
"mail_address": "academie.musique@braine-lalleud.be", |
|
131 |
"type": "work" |
|
132 |
} |
|
133 |
], |
|
134 |
"modified": "2021-10-14T10:48:57+00:00", |
|
135 |
"multi_schedule": [], |
|
136 |
"next_item": { |
|
137 |
"@id": "https://annuaire.preprod.imio.be/braine-l-alleud/f82d2c079131433ea6ab20f9f7f49442", |
|
138 |
"@type": "imio.directory.Contact", |
|
139 |
"description": "\u00ab Accueil et Orientation Volontariat \u00bb guide et conseille les candidats volontaires. Cette ASBL est pluraliste et poursuit les objectifs suivants : - faciliter et encourager la pratique de volontariat aupr\u00e8s des associations locales, - informer et recruter des candidats en vue d\u2019un volontariat citoyen de qualit\u00e9. Si vous d\u00e9sirez vous engager dans une action de volontariat, vous pouvez contacter le centre d\u2019orientation afin de prendre rendez-vous. Vous serez re\u00e7u par 2 volontaires et pourrez choisir les activit\u00e9s convenant le mieux \u00e0 vos aspirations, \u00e0 vos comp\u00e9tences et vos disponibilit\u00e9s. Permanences le vendredi matin (uniquement sur rendez-vous) \u00e0 l\u2019H\u00f4tel communal (Maison des Associations)", |
|
140 |
"title": "Accueil et Orientation Volontariat (A.O.V.)" |
|
141 |
}, |
|
142 |
"number": "49", |
|
143 |
"parent": { |
|
144 |
"@id": "https://annuaire.preprod.imio.be/braine-l-alleud", |
|
145 |
"@type": "imio.directory.Entity", |
|
146 |
"description": "", |
|
147 |
"review_state": "published", |
|
148 |
"title": "Braine-l'Alleud" |
|
149 |
}, |
|
150 |
"phones": [ |
|
151 |
{ |
|
152 |
"label": null, |
|
153 |
"number": "+3228540720", |
|
154 |
"type": "work" |
|
155 |
}, |
|
156 |
{ |
|
157 |
"label": null, |
|
158 |
"number": "+3228540729", |
|
159 |
"type": "fax" |
|
160 |
} |
|
161 |
], |
|
162 |
"previous_item": { |
|
163 |
"@id": "https://annuaire.preprod.imio.be/braine-l-alleud/30bc56007a5140358de0a5ad897b7a47", |
|
164 |
"@type": "imio.directory.Contact", |
|
165 |
"description": "Contact : Vinciane Vrielinck", |
|
166 |
"title": "Cabinet du Bourgmestre de la Commune de Braine-l'Alleud" |
|
167 |
}, |
|
168 |
"review_state": "published", |
|
169 |
"schedule": { |
|
170 |
"friday": { |
|
171 |
"afternoonend": "", |
|
172 |
"afternoonstart": "", |
|
173 |
"comment": "", |
|
174 |
"morningend": "", |
|
175 |
"morningstart": "" |
|
176 |
}, |
|
177 |
"monday": { |
|
178 |
"afternoonend": "", |
|
179 |
"afternoonstart": "", |
|
180 |
"comment": "", |
|
181 |
"morningend": "", |
|
182 |
"morningstart": "" |
|
183 |
}, |
|
184 |
"saturday": { |
|
185 |
"afternoonend": "", |
|
186 |
"afternoonstart": "", |
|
187 |
"comment": "", |
|
188 |
"morningend": "", |
|
189 |
"morningstart": "" |
|
190 |
}, |
|
191 |
"sunday": { |
|
192 |
"afternoonend": "", |
|
193 |
"afternoonstart": "", |
|
194 |
"comment": "", |
|
195 |
"morningend": "", |
|
196 |
"morningstart": "" |
|
197 |
}, |
|
198 |
"thursday": { |
|
199 |
"afternoonend": "", |
|
200 |
"afternoonstart": "", |
|
201 |
"comment": "", |
|
202 |
"morningend": "", |
|
203 |
"morningstart": "" |
|
204 |
}, |
|
205 |
"tuesday": { |
|
206 |
"afternoonend": "", |
|
207 |
"afternoonstart": "", |
|
208 |
"comment": "", |
|
209 |
"morningend": "", |
|
210 |
"morningstart": "" |
|
211 |
}, |
|
212 |
"wednesday": { |
|
213 |
"afternoonend": "", |
|
214 |
"afternoonstart": "", |
|
215 |
"comment": "", |
|
216 |
"morningend": "", |
|
217 |
"morningstart": "" |
|
218 |
} |
|
219 |
}, |
|
220 |
"selected_entities": [ |
|
221 |
{ |
|
222 |
"title": "Braine-l'Alleud", |
|
223 |
"token": "f571b73a16f34832a5fdd3683533b3cc" |
|
224 |
} |
|
225 |
], |
|
226 |
"street": "Rue du Ch\u00e2teau", |
|
227 |
"subjects": [ |
|
228 |
"\u00e9cole" |
|
229 |
], |
|
230 |
"subtitle": null, |
|
231 |
"taxonomy_contact_category": [ |
|
232 |
{ |
|
233 |
"title": "Loisirs \u00bb Cours et activit\u00e9s \u00bb Musique", |
|
234 |
"token": "3qaeiq8v2p" |
|
235 |
} |
|
236 |
], |
|
237 |
"title": "Acad\u00e9mie de Musique de Braine-l'Alleud", |
|
238 |
"topics": [ |
|
239 |
{ |
|
240 |
"title": "Culture", |
|
241 |
"token": "culture" |
|
242 |
}, |
|
243 |
{ |
|
244 |
"title": "\u00c9ducation", |
|
245 |
"token": "education" |
|
246 |
} |
|
247 |
], |
|
248 |
"type": { |
|
249 |
"title": "Organisation (service administratif, commerce, profession lib\u00e9rale, club sportif, association, etc.)", |
|
250 |
"token": "organization" |
|
251 |
}, |
|
252 |
"urls": [ |
|
253 |
{ |
|
254 |
"type": "website", |
|
255 |
"url": "http://academie-de-musique.braine-lalleud.be/" |
|
256 |
} |
|
257 |
], |
|
258 |
"vat_number": null, |
|
259 |
"version": "current", |
|
260 |
"working_copy": null, |
|
261 |
"working_copy_of": null, |
|
262 |
"zipcode": 1420 |
|
263 |
} |
|
264 |
], |
|
265 |
"items_total": 1 |
|
266 |
} |
tests/data/plone_restapi/q_search.json | ||
---|---|---|
1 |
{ |
|
2 |
"@id": "https://annuaire.preprod.imio.be/braine-l-alleud/@search?portal_type=imio.directory.Contact&review_state=published&fullobjects=y", |
|
3 |
"batching": { |
|
4 |
"@id": "https://annuaire.preprod.imio.be/braine-l-alleud/@search?b_size=3&portal_type=imio.directory.Contact&review_state=published&fullobjects=y", |
|
5 |
"first": "https://annuaire.preprod.imio.be/braine-l-alleud/@search?b_start=0&b_size=3&portal_type=imio.directory.Contact&review_state=published&fullobjects=y", |
|
6 |
"last": "https://annuaire.preprod.imio.be/braine-l-alleud/@search?b_start=261&b_size=3&portal_type=imio.directory.Contact&review_state=published&fullobjects=y", |
|
7 |
"next": "https://annuaire.preprod.imio.be/braine-l-alleud/@search?b_start=3&b_size=3&portal_type=imio.directory.Contact&review_state=published&fullobjects=y" |
|
8 |
}, |
|
9 |
"items": [ |
|
10 |
{ |
|
11 |
"@components": { |
|
12 |
"actions": { |
|
13 |
"@id": "https://annuaire.preprod.imio.be/braine-l-alleud/30bc56007a5140358de0a5ad897b7a47/@actions" |
|
14 |
}, |
|
15 |
"breadcrumbs": { |
|
16 |
"@id": "https://annuaire.preprod.imio.be/braine-l-alleud/30bc56007a5140358de0a5ad897b7a47/@breadcrumbs" |
|
17 |
}, |
|
18 |
"contextnavigation": { |
|
19 |
"@id": "https://annuaire.preprod.imio.be/braine-l-alleud/30bc56007a5140358de0a5ad897b7a47/@contextnavigation" |
|
20 |
}, |
|
21 |
"navigation": { |
|
22 |
"@id": "https://annuaire.preprod.imio.be/braine-l-alleud/30bc56007a5140358de0a5ad897b7a47/@navigation" |
|
23 |
}, |
|
24 |
"types": { |
|
25 |
"@id": "https://annuaire.preprod.imio.be/braine-l-alleud/30bc56007a5140358de0a5ad897b7a47/@types" |
|
26 |
}, |
|
27 |
"workflow": { |
|
28 |
"@id": "https://annuaire.preprod.imio.be/braine-l-alleud/30bc56007a5140358de0a5ad897b7a47/@workflow" |
|
29 |
} |
|
30 |
}, |
|
31 |
"@id": "https://annuaire.preprod.imio.be/braine-l-alleud/30bc56007a5140358de0a5ad897b7a47", |
|
32 |
"@type": "imio.directory.Contact", |
|
33 |
"UID": "dea9d26baab944beb7e54d4024d35a33", |
|
34 |
"allow_discussion": false, |
|
35 |
"city": "Braine-l'Alleud", |
|
36 |
"complement": null, |
|
37 |
"country": { |
|
38 |
"title": "Belgique", |
|
39 |
"token": "be" |
|
40 |
}, |
|
41 |
"created": "2021-07-28T07:23:24+00:00", |
|
42 |
"description": "Contact : Vinciane Vrielinck", |
|
43 |
"exceptional_closure": [], |
|
44 |
"facilities": null, |
|
45 |
"geolocation": { |
|
46 |
"latitude": 50.4989185, |
|
47 |
"longitude": 4.7184485 |
|
48 |
}, |
|
49 |
"iam": [ |
|
50 |
{ |
|
51 |
"title": "Nouvel arrivant", |
|
52 |
"token": "newcomer" |
|
53 |
} |
|
54 |
], |
|
55 |
"id": "30bc56007a5140358de0a5ad897b7a47", |
|
56 |
"image": null, |
|
57 |
"image_caption": null, |
|
58 |
"is_folderish": true, |
|
59 |
"is_geolocated": true, |
|
60 |
"language": { |
|
61 |
"title": "Fran\u00e7ais", |
|
62 |
"token": "fr" |
|
63 |
}, |
|
64 |
"layout": "view", |
|
65 |
"logo": null, |
|
66 |
"mails": [ |
|
67 |
{ |
|
68 |
"label": null, |
|
69 |
"mail_address": "bourgmestre@braine-lalleud.be", |
|
70 |
"type": "work" |
|
71 |
} |
|
72 |
], |
|
73 |
"modified": "2021-09-22T13:15:16+00:00", |
|
74 |
"multi_schedule": [], |
|
75 |
"next_item": { |
|
76 |
"@id": "https://annuaire.preprod.imio.be/braine-l-alleud/3378d97243854ddfa90510f6ceb9fcdb", |
|
77 |
"@type": "imio.directory.Contact", |
|
78 |
"description": "Contact : Jean-Pascal Hinnekens (directeur)", |
|
79 |
"title": "Acad\u00e9mie de Musique de Braine-l'Alleud" |
|
80 |
}, |
|
81 |
"number": "1", |
|
82 |
"parent": { |
|
83 |
"@id": "https://annuaire.preprod.imio.be/braine-l-alleud", |
|
84 |
"@type": "imio.directory.Entity", |
|
85 |
"description": "", |
|
86 |
"review_state": "published", |
|
87 |
"title": "Braine-l'Alleud" |
|
88 |
}, |
|
89 |
"phones": [ |
|
90 |
{ |
|
91 |
"label": null, |
|
92 |
"number": "+3228540500", |
|
93 |
"type": "work" |
|
94 |
} |
|
95 |
], |
|
96 |
"previous_item": {}, |
|
97 |
"review_state": "published", |
|
98 |
"schedule": { |
|
99 |
"friday": { |
|
100 |
"afternoonend": "", |
|
101 |
"afternoonstart": "", |
|
102 |
"comment": "", |
|
103 |
"morningend": "", |
|
104 |
"morningstart": "" |
|
105 |
}, |
|
106 |
"monday": { |
|
107 |
"afternoonend": "", |
|
108 |
"afternoonstart": "", |
|
109 |
"comment": "", |
|
110 |
"morningend": "", |
|
111 |
"morningstart": "" |
|
112 |
}, |
|
113 |
"saturday": { |
|
114 |
"afternoonend": "", |
|
115 |
"afternoonstart": "", |
|
116 |
"comment": "", |
|
117 |
"morningend": "", |
|
118 |
"morningstart": "" |
|
119 |
}, |
|
120 |
"sunday": { |
|
121 |
"afternoonend": "", |
|
122 |
"afternoonstart": "", |
|
123 |
"comment": "", |
|
124 |
"morningend": "", |
|
125 |
"morningstart": "" |
|
126 |
}, |
|
127 |
"thursday": { |
|
128 |
"afternoonend": "", |
|
129 |
"afternoonstart": "", |
|
130 |
"comment": "", |
|
131 |
"morningend": "", |
|
132 |
"morningstart": "" |
|
133 |
}, |
|
134 |
"tuesday": { |
|
135 |
"afternoonend": "", |
|
136 |
"afternoonstart": "", |
|
137 |
"comment": "", |
|
138 |
"morningend": "", |
|
139 |
"morningstart": "" |
|
140 |
}, |
|
141 |
"wednesday": { |
|
142 |
"afternoonend": "", |
|
143 |
"afternoonstart": "", |
|
144 |
"comment": "", |
|
145 |
"morningend": "", |
|
146 |
"morningstart": "" |
|
147 |
} |
|
148 |
}, |
|
149 |
"selected_entities": [ |
|
150 |
{ |
|
151 |
"title": "Braine-l'Alleud", |
|
152 |
"token": "f571b73a16f34832a5fdd3683533b3cc" |
|
153 |
} |
|
154 |
], |
|
155 |
"street": "Avenue du 21 Juillet", |
|
156 |
"subjects": [ |
|
157 |
"scourneau", |
|
158 |
"mr" |
|
159 |
], |
|
160 |
"subtitle": null, |
|
161 |
"taxonomy_contact_category": [ |
|
162 |
{ |
|
163 |
"title": "Service public \u00bb Administration communale", |
|
164 |
"token": "xhowidw6kd" |
|
165 |
} |
|
166 |
], |
|
167 |
"title": "Cabinet du Bourgmestre de la Commune de Braine-l'Alleud", |
|
168 |
"topics": [ |
|
169 |
{ |
|
170 |
"title": "Politique", |
|
171 |
"token": "politics" |
|
172 |
} |
|
173 |
], |
|
174 |
"type": { |
|
175 |
"title": "Organisation (service administratif, commerce, profession lib\u00e9rale, club sportif, association, etc.)", |
|
176 |
"token": "organization" |
|
177 |
}, |
|
178 |
"urls": [], |
|
179 |
"vat_number": null, |
|
180 |
"version": "current", |
|
181 |
"working_copy": null, |
|
182 |
"working_copy_of": null, |
|
183 |
"zipcode": 1420 |
|
184 |
}, |
|
185 |
{ |
|
186 |
"@components": { |
|
187 |
"actions": { |
|
188 |
"@id": "https://annuaire.preprod.imio.be/braine-l-alleud/3378d97243854ddfa90510f6ceb9fcdb/@actions" |
|
189 |
}, |
|
190 |
"breadcrumbs": { |
|
191 |
"@id": "https://annuaire.preprod.imio.be/braine-l-alleud/3378d97243854ddfa90510f6ceb9fcdb/@breadcrumbs" |
|
192 |
}, |
|
193 |
"contextnavigation": { |
|
194 |
"@id": "https://annuaire.preprod.imio.be/braine-l-alleud/3378d97243854ddfa90510f6ceb9fcdb/@contextnavigation" |
|
195 |
}, |
|
196 |
"navigation": { |
|
197 |
"@id": "https://annuaire.preprod.imio.be/braine-l-alleud/3378d97243854ddfa90510f6ceb9fcdb/@navigation" |
|
198 |
}, |
|
199 |
"types": { |
|
200 |
"@id": "https://annuaire.preprod.imio.be/braine-l-alleud/3378d97243854ddfa90510f6ceb9fcdb/@types" |
|
201 |
}, |
|
202 |
"workflow": { |
|
203 |
"@id": "https://annuaire.preprod.imio.be/braine-l-alleud/3378d97243854ddfa90510f6ceb9fcdb/@workflow" |
|
204 |
} |
|
205 |
}, |
|
206 |
"@id": "https://annuaire.preprod.imio.be/braine-l-alleud/3378d97243854ddfa90510f6ceb9fcdb", |
|
207 |
"@type": "imio.directory.Contact", |
|
208 |
"UID": "23a32197d6c841259963b43b24747854", |
|
209 |
"allow_discussion": false, |
|
210 |
"city": "Braine-l'Alleud", |
|
211 |
"complement": null, |
|
212 |
"country": { |
|
213 |
"title": "Belgique", |
|
214 |
"token": "be" |
|
215 |
}, |
|
216 |
"created": "2021-07-28T07:10:02+00:00", |
|
217 |
"description": "Contact : Jean-Pascal Hinnekens (directeur)", |
|
218 |
"exceptional_closure": [], |
|
219 |
"facilities": null, |
|
220 |
"geolocation": { |
|
221 |
"latitude": 50.4989185, |
|
222 |
"longitude": 4.7184485 |
|
223 |
}, |
|
224 |
"iam": [ |
|
225 |
{ |
|
226 |
"title": "Jeune", |
|
227 |
"token": "young" |
|
228 |
}, |
|
229 |
{ |
|
230 |
"title": "Nouvel arrivant", |
|
231 |
"token": "newcomer" |
|
232 |
}, |
|
233 |
{ |
|
234 |
"title": "Parent", |
|
235 |
"token": "parent" |
|
236 |
} |
|
237 |
], |
|
238 |
"id": "3378d97243854ddfa90510f6ceb9fcdb", |
|
239 |
"image": null, |
|
240 |
"image_caption": null, |
|
241 |
"is_folderish": true, |
|
242 |
"is_geolocated": true, |
|
243 |
"language": { |
|
244 |
"title": "Fran\u00e7ais", |
|
245 |
"token": "fr" |
|
246 |
}, |
|
247 |
"layout": "view", |
|
248 |
"logo": { |
|
249 |
"content-type": "image/png", |
|
250 |
"download": "https://annuaire.preprod.imio.be/braine-l-alleud/3378d97243854ddfa90510f6ceb9fcdb/@@images/b5785773-138a-4907-9ec6-b29100f18e85.png", |
|
251 |
"filename": "acad\u00e9mie musique braine-l'alleud.png", |
|
252 |
"height": 591, |
|
253 |
"scales": { |
|
254 |
"banner": { |
|
255 |
"download": "https://annuaire.preprod.imio.be/braine-l-alleud/3378d97243854ddfa90510f6ceb9fcdb/@@images/64373ca2-151b-439d-a489-288c83a94de6.png", |
|
256 |
"height": 591, |
|
257 |
"width": 559 |
|
258 |
}, |
|
259 |
"extralarge": { |
|
260 |
"download": "https://annuaire.preprod.imio.be/braine-l-alleud/3378d97243854ddfa90510f6ceb9fcdb/@@images/cb8f259c-1255-4d5d-9b1a-fdace828f74c.png", |
|
261 |
"height": 591, |
|
262 |
"width": 559 |
|
263 |
}, |
|
264 |
"icon": { |
|
265 |
"download": "https://annuaire.preprod.imio.be/braine-l-alleud/3378d97243854ddfa90510f6ceb9fcdb/@@images/77276cc0-e251-450b-8a4c-968e1d5c4ac4.png", |
|
266 |
"height": 32, |
|
267 |
"width": 31 |
|
268 |
}, |
|
269 |
"large": { |
|
270 |
"download": "https://annuaire.preprod.imio.be/braine-l-alleud/3378d97243854ddfa90510f6ceb9fcdb/@@images/6ec18ddf-645a-4764-b7b4-fedfe7c1d9e0.png", |
|
271 |
"height": 591, |
|
272 |
"width": 559 |
|
273 |
}, |
|
274 |
"listing": { |
|
275 |
"download": "https://annuaire.preprod.imio.be/braine-l-alleud/3378d97243854ddfa90510f6ceb9fcdb/@@images/07b35265-e56f-4fa0-9717-b1982604f9b4.png", |
|
276 |
"height": 16, |
|
277 |
"width": 16 |
|
278 |
}, |
|
279 |
"medium": { |
|
280 |
"download": "https://annuaire.preprod.imio.be/braine-l-alleud/3378d97243854ddfa90510f6ceb9fcdb/@@images/4470f78e-19fb-41aa-b897-4f9b0f5f356e.png", |
|
281 |
"height": 591, |
|
282 |
"width": 559 |
|
283 |
}, |
|
284 |
"mini": { |
|
285 |
"download": "https://annuaire.preprod.imio.be/braine-l-alleud/3378d97243854ddfa90510f6ceb9fcdb/@@images/548c616b-d37b-43be-b931-42c17b462127.png", |
|
286 |
"height": 200, |
|
287 |
"width": 189 |
|
288 |
}, |
|
289 |
"preview": { |
|
290 |
"download": "https://annuaire.preprod.imio.be/braine-l-alleud/3378d97243854ddfa90510f6ceb9fcdb/@@images/84394ebe-2d4d-4894-b331-1636e93ccd38.png", |
|
291 |
"height": 400, |
|
292 |
"width": 379 |
|
293 |
}, |
|
294 |
"thumb": { |
|
295 |
"download": "https://annuaire.preprod.imio.be/braine-l-alleud/3378d97243854ddfa90510f6ceb9fcdb/@@images/087c5b17-5ae3-4e9d-a794-a95440e5aa43.png", |
|
296 |
"height": 128, |
|
297 |
"width": 121 |
|
298 |
}, |
|
299 |
"tile": { |
|
300 |
"download": "https://annuaire.preprod.imio.be/braine-l-alleud/3378d97243854ddfa90510f6ceb9fcdb/@@images/34278749-0375-4000-924f-b56993076bbd.png", |
|
301 |
"height": 64, |
|
302 |
"width": 61 |
|
303 |
} |
|
304 |
}, |
|
305 |
"size": 232832, |
|
306 |
"width": 559 |
|
307 |
}, |
|
308 |
"mails": [ |
|
309 |
{ |
|
310 |
"label": null, |
|
311 |
"mail_address": "academie.musique@braine-lalleud.be", |
|
312 |
"type": "work" |
|
313 |
} |
|
314 |
], |
|
315 |
"modified": "2021-10-14T10:48:57+00:00", |
|
316 |
"multi_schedule": [], |
|
317 |
"next_item": { |
|
318 |
"@id": "https://annuaire.preprod.imio.be/braine-l-alleud/f82d2c079131433ea6ab20f9f7f49442", |
|
319 |
"@type": "imio.directory.Contact", |
|
320 |
"description": "\u00ab Accueil et Orientation Volontariat \u00bb guide et conseille les candidats volontaires. Cette ASBL est pluraliste et poursuit les objectifs suivants : - faciliter et encourager la pratique de volontariat aupr\u00e8s des associations locales, - informer et recruter des candidats en vue d\u2019un volontariat citoyen de qualit\u00e9. Si vous d\u00e9sirez vous engager dans une action de volontariat, vous pouvez contacter le centre d\u2019orientation afin de prendre rendez-vous. Vous serez re\u00e7u par 2 volontaires et pourrez choisir les activit\u00e9s convenant le mieux \u00e0 vos aspirations, \u00e0 vos comp\u00e9tences et vos disponibilit\u00e9s. Permanences le vendredi matin (uniquement sur rendez-vous) \u00e0 l\u2019H\u00f4tel communal (Maison des Associations)", |
|
321 |
"title": "Accueil et Orientation Volontariat (A.O.V.)" |
|
322 |
}, |
|
323 |
"number": "49", |
|
324 |
"parent": { |
|
325 |
"@id": "https://annuaire.preprod.imio.be/braine-l-alleud", |
|
326 |
"@type": "imio.directory.Entity", |
|
327 |
"description": "", |
|
328 |
"review_state": "published", |
|
329 |
"title": "Braine-l'Alleud" |
|
330 |
}, |
|
331 |
"phones": [ |
|
332 |
{ |
|
333 |
"label": null, |
|
334 |
"number": "+3228540720", |
|
335 |
"type": "work" |
|
336 |
}, |
|
337 |
{ |
|
338 |
"label": null, |
|
339 |
"number": "+3228540729", |
|
340 |
"type": "fax" |
|
341 |
} |
|
342 |
], |
|
343 |
"previous_item": { |
|
344 |
"@id": "https://annuaire.preprod.imio.be/braine-l-alleud/30bc56007a5140358de0a5ad897b7a47", |
|
345 |
"@type": "imio.directory.Contact", |
|
346 |
"description": "Contact : Vinciane Vrielinck", |
|
347 |
"title": "Cabinet du Bourgmestre de la Commune de Braine-l'Alleud" |
|
348 |
}, |
|
349 |
"review_state": "published", |
|
350 |
"schedule": { |
|
351 |
"friday": { |
|
352 |
"afternoonend": "", |
|
353 |
"afternoonstart": "", |
|
354 |
"comment": "", |
|
355 |
"morningend": "", |
|
356 |
"morningstart": "" |
|
357 |
}, |
|
358 |
"monday": { |
|
359 |
"afternoonend": "", |
|
360 |
"afternoonstart": "", |
|
361 |
"comment": "", |
|
362 |
"morningend": "", |
|
363 |
"morningstart": "" |
|
364 |
}, |
|
365 |
"saturday": { |
|
366 |
"afternoonend": "", |
|
367 |
"afternoonstart": "", |
|
368 |
"comment": "", |
|
369 |
"morningend": "", |
|
370 |
"morningstart": "" |
|
371 |
}, |
|
372 |
"sunday": { |
|
373 |
"afternoonend": "", |
|
374 |
"afternoonstart": "", |
|
375 |
"comment": "", |
|
376 |
"morningend": "", |
|
377 |
"morningstart": "" |
|
378 |
}, |
|
379 |
"thursday": { |
|
380 |
"afternoonend": "", |
|
381 |
"afternoonstart": "", |
|
382 |
"comment": "", |
|
383 |
"morningend": "", |
|
384 |
"morningstart": "" |
|
385 |
}, |
|
386 |
"tuesday": { |
|
387 |
"afternoonend": "", |
|
388 |
"afternoonstart": "", |
|
389 |
"comment": "", |
|
390 |
"morningend": "", |
|
391 |
"morningstart": "" |
|
392 |
}, |
|
393 |
"wednesday": { |
|
394 |
"afternoonend": "", |
|
395 |
"afternoonstart": "", |
|
396 |
"comment": "", |
|
397 |
"morningend": "", |
|
398 |
"morningstart": "" |
|
399 |
} |
|
400 |
}, |
|
401 |
"selected_entities": [ |
|
402 |
{ |
|
403 |
"title": "Braine-l'Alleud", |
|
404 |
"token": "f571b73a16f34832a5fdd3683533b3cc" |
|
405 |
} |
|
406 |
], |
|
407 |
"street": "Rue du Ch\u00e2teau", |
|
408 |
"subjects": [ |
|
409 |
"\u00e9cole" |
|
410 |
], |
|
411 |
"subtitle": null, |
|
412 |
"taxonomy_contact_category": [ |
|
413 |
{ |
|
414 |
"title": "Loisirs \u00bb Cours et activit\u00e9s \u00bb Musique", |
|
415 |
"token": "3qaeiq8v2p" |
|
416 |
} |
|
417 |
], |
|
418 |
"title": "Acad\u00e9mie de Musique de Braine-l'Alleud", |
|
419 |
"topics": [ |
|
420 |
{ |
|
421 |
"title": "Culture", |
|
422 |
"token": "culture" |
|
423 |
}, |
|
424 |
{ |
|
425 |
"title": "\u00c9ducation", |
|
426 |
"token": "education" |
|
427 |
} |
|
428 |
], |
|
429 |
"type": { |
|
430 |
"title": "Organisation (service administratif, commerce, profession lib\u00e9rale, club sportif, association, etc.)", |
|
431 |
"token": "organization" |
|
432 |
}, |
|
433 |
"urls": [ |
|
434 |
{ |
|
435 |
"type": "website", |
|
436 |
"url": "http://academie-de-musique.braine-lalleud.be/" |
|
437 |
} |
|
438 |
], |
|
439 |
"vat_number": null, |
|
440 |
"version": "current", |
|
441 |
"working_copy": null, |
|
442 |
"working_copy_of": null, |
|
443 |
"zipcode": 1420 |
|
444 |
}, |
|
445 |
{ |
|
446 |
"@components": { |
|
447 |
"actions": { |
|
448 |
"@id": "https://annuaire.preprod.imio.be/braine-l-alleud/f82d2c079131433ea6ab20f9f7f49442/@actions" |
|
449 |
}, |
|
450 |
"breadcrumbs": { |
|
451 |
"@id": "https://annuaire.preprod.imio.be/braine-l-alleud/f82d2c079131433ea6ab20f9f7f49442/@breadcrumbs" |
|
452 |
}, |
|
453 |
"contextnavigation": { |
|
454 |
"@id": "https://annuaire.preprod.imio.be/braine-l-alleud/f82d2c079131433ea6ab20f9f7f49442/@contextnavigation" |
|
455 |
}, |
|
456 |
"navigation": { |
|
457 |
"@id": "https://annuaire.preprod.imio.be/braine-l-alleud/f82d2c079131433ea6ab20f9f7f49442/@navigation" |
|
458 |
}, |
|
459 |
"types": { |
|
460 |
"@id": "https://annuaire.preprod.imio.be/braine-l-alleud/f82d2c079131433ea6ab20f9f7f49442/@types" |
|
461 |
}, |
|
462 |
"workflow": { |
|
463 |
"@id": "https://annuaire.preprod.imio.be/braine-l-alleud/f82d2c079131433ea6ab20f9f7f49442/@workflow" |
|
464 |
} |
|
465 |
}, |
|
466 |
"@id": "https://annuaire.preprod.imio.be/braine-l-alleud/f82d2c079131433ea6ab20f9f7f49442", |
|
467 |
"@type": "imio.directory.Contact", |
|
468 |
"UID": "f82d2c079131433ea6ab20f9f7f49442", |
|
469 |
"allow_discussion": false, |
|
470 |
"city": "Braine-l'Alleud", |
|
471 |
"complement": null, |
|
472 |
"country": { |
|
473 |
"title": "Belgique", |
|
474 |
"token": "be" |
|
475 |
}, |
|
476 |
"created": "2021-08-20T09:27:46+00:00", |
|
477 |
"description": "\u00ab Accueil et Orientation Volontariat \u00bb guide et conseille les candidats volontaires.\r\n\r\nCette ASBL est pluraliste et poursuit les objectifs suivants :\r\n\r\n- faciliter et encourager la pratique de volontariat aupr\u00e8s des associations locales,\r\n- informer et recruter des candidats en vue d\u2019un volontariat citoyen de qualit\u00e9.\r\n\r\nSi vous d\u00e9sirez vous engager dans une action de volontariat, vous pouvez contacter le centre d\u2019orientation afin de prendre rendez-vous. Vous serez re\u00e7u par 2 volontaires et pourrez choisir les activit\u00e9s convenant le mieux \u00e0 vos aspirations, \u00e0 vos comp\u00e9tences et vos disponibilit\u00e9s.\r\n\r\nPermanences le vendredi matin (uniquement sur rendez-vous) \u00e0 l\u2019H\u00f4tel communal (Maison des Associations)", |
|
478 |
"exceptional_closure": [], |
|
479 |
"facilities": null, |
|
480 |
"geolocation": { |
|
481 |
"latitude": 50.4989185, |
|
482 |
"longitude": 4.7184485 |
|
483 |
}, |
|
484 |
"iam": [ |
|
485 |
{ |
|
486 |
"title": "Jeune", |
|
487 |
"token": "young" |
|
488 |
}, |
|
489 |
{ |
|
490 |
"title": "Nouvel arrivant", |
|
491 |
"token": "newcomer" |
|
492 |
} |
|
493 |
], |
|
494 |
"id": "f82d2c079131433ea6ab20f9f7f49442", |
|
495 |
"image": null, |
|
496 |
"image_caption": null, |
|
497 |
"is_folderish": true, |
|
498 |
"is_geolocated": true, |
|
499 |
"language": { |
|
500 |
"title": "Fran\u00e7ais", |
|
501 |
"token": "fr" |
|
502 |
}, |
|
503 |
"layout": "view", |
|
504 |
"logo": null, |
|
505 |
"mails": [], |
|
506 |
"modified": "2021-09-23T11:50:05+00:00", |
|
507 |
"multi_schedule": [], |
|
508 |
"next_item": { |
|
509 |
"@id": "https://annuaire.preprod.imio.be/braine-l-alleud/813f4b02118d498ab779ec8542315e66", |
|
510 |
"@type": "imio.directory.Contact", |
|
511 |
"description": "", |
|
512 |
"title": "Association des commer\u00e7ants et artisans de Braine-l\u2019Alleud" |
|
513 |
}, |
|
514 |
"number": "3", |
|
515 |
"parent": { |
|
516 |
"@id": "https://annuaire.preprod.imio.be/braine-l-alleud", |
|
517 |
"@type": "imio.directory.Entity", |
|
518 |
"description": "", |
|
519 |
"review_state": "published", |
|
520 |
"title": "Braine-l'Alleud" |
|
521 |
}, |
|
522 |
"phones": [ |
|
523 |
{ |
|
524 |
"label": null, |
|
525 |
"number": "+3223846945", |
|
526 |
"type": "work" |
|
527 |
} |
|
528 |
], |
|
529 |
"previous_item": { |
|
530 |
"@id": "https://annuaire.preprod.imio.be/braine-l-alleud/3378d97243854ddfa90510f6ceb9fcdb", |
|
531 |
"@type": "imio.directory.Contact", |
|
532 |
"description": "Contact : Jean-Pascal Hinnekens (directeur)", |
|
533 |
"title": "Acad\u00e9mie de Musique de Braine-l'Alleud" |
|
534 |
}, |
|
535 |
"review_state": "published", |
|
536 |
"schedule": { |
|
537 |
"friday": { |
|
538 |
"afternoonend": "", |
|
539 |
"afternoonstart": "", |
|
540 |
"comment": "", |
|
541 |
"morningend": "", |
|
542 |
"morningstart": "" |
|
543 |
}, |
|
544 |
"monday": { |
|
545 |
"afternoonend": "", |
|
546 |
"afternoonstart": "", |
|
547 |
"comment": "", |
|
548 |
"morningend": "", |
|
549 |
"morningstart": "" |
|
550 |
}, |
|
551 |
"saturday": { |
|
552 |
"afternoonend": "", |
|
553 |
"afternoonstart": "", |
|
554 |
"comment": "", |
|
555 |
"morningend": "", |
|
556 |
"morningstart": "" |
|
557 |
}, |
|
558 |
"sunday": { |
|
559 |
"afternoonend": "", |
|
560 |
"afternoonstart": "", |
|
561 |
"comment": "", |
|
562 |
"morningend": "", |
|
563 |
"morningstart": "" |
|
564 |
}, |
|
565 |
"thursday": { |
|
566 |
"afternoonend": "", |
|
567 |
"afternoonstart": "", |
|
568 |
"comment": "", |
|
569 |
"morningend": "", |
|
570 |
"morningstart": "" |
|
571 |
}, |
|
572 |
"tuesday": { |
|
573 |
"afternoonend": "", |
|
574 |
"afternoonstart": "", |
|
575 |
"comment": "", |
|
576 |
"morningend": "", |
|
577 |
"morningstart": "" |
|
578 |
}, |
|
579 |
"wednesday": { |
|
580 |
"afternoonend": "", |
|
581 |
"afternoonstart": "", |
|
582 |
"comment": "", |
|
583 |
"morningend": "", |
|
584 |
"morningstart": "" |
|
585 |
} |
|
586 |
}, |
|
587 |
"selected_entities": [ |
|
588 |
{ |
|
589 |
"title": "Braine-l'Alleud", |
|
590 |
"token": "f571b73a16f34832a5fdd3683533b3cc" |
|
591 |
} |
|
592 |
], |
|
593 |
"street": "Grand-Place Baudouin 1er", |
|
594 |
"subjects": [ |
|
595 |
"b\u00e9n\u00e9volat" |
|
596 |
], |
|
597 |
"subtitle": null, |
|
598 |
"taxonomy_contact_category": [ |
|
599 |
{ |
|
600 |
"title": "Loisirs \u00bb Mouvements et associations", |
|
601 |
"token": "13drlsiykl" |
|
602 |
} |
|
603 |
], |
|
604 |
"title": "Accueil et Orientation Volontariat (A.O.V.)", |
|
605 |
"topics": [ |
|
606 |
{ |
|
607 |
"title": "Participation citoyenne", |
|
608 |
"token": "citizen_participation" |
|
609 |
} |
|
610 |
], |
|
611 |
"type": { |
|
612 |
"title": "Organisation (service administratif, commerce, profession lib\u00e9rale, club sportif, association, etc.)", |
|
613 |
"token": "organization" |
|
614 |
}, |
|
615 |
"urls": [ |
|
616 |
{ |
|
617 |
"type": "website", |
|
618 |
"url": "https://www.aovolontariat.be/" |
|
619 |
} |
|
620 |
], |
|
621 |
"vat_number": null, |
|
622 |
"version": "current", |
|
623 |
"working_copy": null, |
|
624 |
"working_copy_of": null, |
|
625 |
"zipcode": 1420 |
|
626 |
} |
|
627 |
], |
|
628 |
"items_total": 264 |
|
629 |
} |
tests/test_plone_restapi.py | ||
---|---|---|
1 |
# -*- coding: utf-8 -*- |
|
2 |
# passerelle - uniform access to multiple data sources and services |
|
3 |
# Copyright (C) 202 Entr'ouvert |
|
4 |
# |
|
5 |
# This program is free software: you can redistribute it and/or modify it |
|
6 |
# under the terms of the GNU Affero General Public License as published |
|
7 |
# by the Free Software Foundation, either version 3 of the License, or |
|
8 |
# (at your option) any later version. |
|
9 |
# |
|
10 |
# This program is distributed in the hope that it will be useful, |
|
11 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
12 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
13 |
# GNU Affero General Public License for more details. |
|
14 |
# |
|
15 |
# You should have received a copy of the GNU Affero General Public License |
|
16 |
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
17 | ||
18 |
import json |
|
19 |
import os |
|
20 | ||
21 |
import pytest |
|
22 |
import utils |
|
23 |
from requests.exceptions import ConnectionError |
|
24 |
from test_manager import login |
|
25 | ||
26 |
from passerelle.apps.plone_restapi.models import PloneRestApi, Query |
|
27 |
from passerelle.utils import import_site |
|
28 |
from passerelle.utils.jsonresponse import APIError |
|
29 | ||
30 |
pytestmark = pytest.mark.django_db |
|
31 | ||
32 |
TEST_BASE_DIR = os.path.join(os.path.dirname(__file__), 'data', 'plone_restapi') |
|
33 | ||
34 |
TOKEN_RESPONSE = { |
|
35 |
'access_token': 'd319258e-48b9-4853-88e8-7a2ad6883c7f', |
|
36 |
'token_type': 'Bearer', |
|
37 |
'expires_in': 28800, |
|
38 |
'id_token': 'acd...def', |
|
39 |
} |
|
40 | ||
41 |
TOKEN_ERROR_RESPONSE = { |
|
42 |
"error": "access_denied", |
|
43 |
"error_description": "Mauvaises informations de connexion de l'utilisateur", |
|
44 |
} |
|
45 | ||
46 | ||
47 |
def json_get_data(filename): |
|
48 |
with open(os.path.join(TEST_BASE_DIR, "%s.json" % filename)) as fd: |
|
49 |
return json.load(fd) |
|
50 | ||
51 | ||
52 |
@pytest.fixture |
|
53 |
def connector(): |
|
54 |
return utils.setup_access_rights( |
|
55 |
PloneRestApi.objects.create( |
|
56 |
slug='my_connector', |
|
57 |
service_url='http://www.example.net', |
|
58 |
token_ws_url='http://www.example.net/idp/oidc/token/', |
|
59 |
client_id='aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee', |
|
60 |
client_secret='11111111-2222-3333-4444-555555555555', |
|
61 |
username='jdoe', |
|
62 |
password='secret', |
|
63 |
) |
|
64 |
) |
|
65 | ||
66 | ||
67 |
@pytest.fixture |
|
68 |
def token(connector): |
|
69 |
with utils.mock_url(url=connector.token_ws_url, response=TOKEN_RESPONSE) as mocked: |
|
70 |
yield mocked |
|
71 | ||
72 | ||
73 |
@pytest.fixture |
|
74 |
def query(connector): |
|
75 |
return Query.objects.create( |
|
76 |
resource=connector, |
|
77 |
name='demo query', |
|
78 |
slug='my_query', |
|
79 |
description="Annuaire de Braine-l'Alleud", |
|
80 |
uri='braine-l-alleud', |
|
81 |
text_template='{{ title }} ({{ PLONE_type }})', |
|
82 |
filter_expression=''' |
|
83 |
portal_type=Document |
|
84 |
review_state=published |
|
85 |
''', |
|
86 |
sort='UID', |
|
87 |
order=False, |
|
88 |
limit=3, |
|
89 |
) |
|
90 | ||
91 | ||
92 |
def test_views(db, admin_user, app, connector): |
|
93 |
app = login(app) |
|
94 |
resp = app.get('/plone-restapi/my_connector/', status=200) |
|
95 |
resp = resp.click('New Query') |
|
96 |
resp.form['name'] = 'my query' |
|
97 |
resp.form['slug'] = 'my-query' |
|
98 |
resp.form['uri'] = 'my-uri' |
|
99 |
resp = resp.form.submit() |
|
100 |
resp = resp.follow() |
|
101 |
assert resp.html.find('div', {'id': 'queries'}).ul.li.a.text == 'my query' |
|
102 | ||
103 | ||
104 |
def test_views_query_unicity(admin_user, app, connector, query): |
|
105 |
connector2 = PloneRestApi.objects.create( |
|
106 |
slug='my_connector2', |
|
107 |
) |
|
108 |
Query.objects.create( |
|
109 |
resource=connector2, |
|
110 |
slug='foo-bar', |
|
111 |
name='Foo Bar', |
|
112 |
) |
|
113 | ||
114 |
# create |
|
115 |
app = login(app) |
|
116 |
resp = app.get('/manage/plone-restapi/%s/query/new/' % connector.slug) |
|
117 |
resp.form['slug'] = query.slug |
|
118 |
resp.form['name'] = 'Foo Bar' |
|
119 |
resp = resp.form.submit() |
|
120 |
assert resp.status_code == 200 |
|
121 |
assert 'A query with this slug already exists' in resp.text |
|
122 |
assert Query.objects.filter(resource=connector).count() == 1 |
|
123 |
resp.form['slug'] = 'foo-bar' |
|
124 |
resp.form['name'] = query.name |
|
125 |
resp = resp.form.submit() |
|
126 |
assert resp.status_code == 200 |
|
127 |
assert 'A query with this name already exists' in resp.text |
|
128 |
assert Query.objects.filter(resource=connector).count() == 1 |
|
129 |
resp.form['slug'] = 'foo-bar' |
|
130 |
resp.form['name'] = 'Foo Bar' |
|
131 |
resp = resp.form.submit() |
|
132 |
assert resp.status_code == 302 |
|
133 |
assert Query.objects.filter(resource=connector).count() == 2 |
|
134 |
new_query = Query.objects.latest('pk') |
|
135 |
assert new_query.resource == connector |
|
136 |
assert new_query.slug == 'foo-bar' |
|
137 |
assert new_query.name == 'Foo Bar' |
|
138 | ||
139 |
# update |
|
140 |
resp = app.get('/manage/plone-restapi/%s/query/%s/' % (connector.slug, new_query.pk)) |
|
141 |
resp.form['slug'] = query.slug |
|
142 |
resp.form['name'] = 'Foo Bar' |
|
143 |
resp = resp.form.submit() |
|
144 |
assert resp.status_code == 200 |
|
145 |
assert 'A query with this slug already exists' in resp.text |
|
146 |
resp.form['slug'] = 'foo-bar' |
|
147 |
resp.form['name'] = query.name |
|
148 |
resp = resp.form.submit() |
|
149 |
assert resp.status_code == 200 |
|
150 |
assert 'A query with this name already exists' in resp.text |
|
151 |
resp.form['slug'] = 'foo-bar' |
|
152 |
resp.form['name'] = 'Foo Bar' |
|
153 |
resp.form['uri'] = 'fr' |
|
154 |
resp = resp.form.submit() |
|
155 |
assert resp.status_code == 302 |
|
156 |
query = Query.objects.get(resource=connector, slug='foo-bar') |
|
157 |
assert query.uri == 'fr' |
|
158 | ||
159 | ||
160 |
def test_export_import(query): |
|
161 |
assert PloneRestApi.objects.count() == 1 |
|
162 |
assert Query.objects.count() == 1 |
|
163 |
serialization = {'resources': [query.resource.export_json()]} |
|
164 |
PloneRestApi.objects.all().delete() |
|
165 |
assert PloneRestApi.objects.count() == 0 |
|
166 |
assert Query.objects.count() == 0 |
|
167 |
import_site(serialization) |
|
168 |
assert PloneRestApi.objects.count() == 1 |
|
169 |
assert str(PloneRestApi.objects.get().client_id) == 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee' |
|
170 |
assert Query.objects.count() == 1 |
|
171 | ||
172 | ||
173 |
def test_adapt_id_and_type_plone_attributes(connector): |
|
174 |
plone_response = { |
|
175 |
'@type': '@value', |
|
176 |
'@dict': { |
|
177 |
'@array': [ |
|
178 |
{ |
|
179 |
'@id': '123', |
|
180 |
'@type': '@value', |
|
181 |
} |
|
182 |
] |
|
183 |
}, |
|
184 |
} |
|
185 |
connector.adapt_id_and_type_plone_attributes(plone_response) |
|
186 |
assert plone_response == { |
|
187 |
'PLONE_type': '@value', |
|
188 |
'@dict': {'@array': [{'PLONE_id': '123', 'PLONE_type': '@value'}]}, |
|
189 |
} |
|
190 | ||
191 | ||
192 |
def test_adapt_record(connector, token): |
|
193 |
data = { |
|
194 |
'@id': 'plone id', |
|
195 |
'UID': 'plone uid', |
|
196 |
'id': 'foo', |
|
197 |
'text': 'bar', |
|
198 |
} |
|
199 |
template = '{{ PLONE_id }}, {{ id }}, {{original_id }}, {{ original_text }}' |
|
200 |
assert connector.adapt_record(template, data) == { |
|
201 |
'PLONE_id': 'plone id', |
|
202 |
'UID': 'plone uid', |
|
203 |
'id': 'plone uid', |
|
204 |
'text': 'plone id, plone uid, foo, bar', |
|
205 |
'original_id': 'foo', |
|
206 |
'original_text': 'bar', |
|
207 |
} |
|
208 | ||
209 | ||
210 |
def test_get_token(app, connector): |
|
211 |
with pytest.raises(APIError): |
|
212 |
with utils.mock_url(url=connector.token_ws_url, response=TOKEN_ERROR_RESPONSE, status_code=404): |
|
213 |
connector.get_token() |
|
214 |
with utils.mock_url(url=connector.token_ws_url, response=TOKEN_RESPONSE) as mocked: |
|
215 |
result = connector.get_token() |
|
216 |
assert mocked.handlers[0].call['count'] == 1 |
|
217 |
assert 'secret' in mocked.handlers[0].call['requests'][0].body |
|
218 |
assert result == 'acd...def' |
|
219 | ||
220 |
# make sure the token from cache is used |
|
221 |
connector.get_token() |
|
222 |
assert mocked.handlers[0].call['count'] == 1 |
|
223 |
connector.get_token(True) |
|
224 |
assert mocked.handlers[0].call['count'] == 2 |
|
225 | ||
226 | ||
227 |
def test_fetch(app, connector, token): |
|
228 |
endpoint = utils.generic_endpoint_url('plone-restapi', 'fetch', slug=connector.slug) |
|
229 |
assert endpoint == '/plone-restapi/my_connector/fetch' |
|
230 |
url = connector.service_url + '/braine-l-alleud/dccd85d12cf54b6899dff41e5a56ee7f' |
|
231 |
params = { |
|
232 |
'uid': 'dccd85d12cf54b6899dff41e5a56ee7f', |
|
233 |
'uri': 'braine-l-alleud', |
|
234 |
'text_template': '{{ title }} ({{ topics.0.title }})', |
|
235 |
} |
|
236 |
with utils.mock_url(url=url, response=json_get_data('fetch')): |
|
237 |
resp = app.get(endpoint, params=params) |
|
238 |
assert not resp.json['err'] |
|
239 |
assert resp.json['data']['id'] == 'dccd85d12cf54b6899dff41e5a56ee7f' |
|
240 |
assert resp.json['data']['text'] == 'Le Prisme (Activités et divertissement)' |
|
241 |
assert token.handlers[0].call['count'] == 1 |
|
242 | ||
243 | ||
244 |
def test_request_anonymously(app, connector, token): |
|
245 |
connector.token_ws_url = '' |
|
246 |
connector.save() |
|
247 |
endpoint = utils.generic_endpoint_url('plone-restapi', 'fetch', slug=connector.slug) |
|
248 |
assert endpoint == '/plone-restapi/my_connector/fetch' |
|
249 |
url = connector.service_url + '/braine-l-alleud/dccd85d12cf54b6899dff41e5a56ee7f' |
|
250 |
params = { |
|
251 |
'uid': 'dccd85d12cf54b6899dff41e5a56ee7f', |
|
252 |
'uri': 'braine-l-alleud', |
|
253 |
'text_template': '{{ title }} ({{ topics.0.title }})', |
|
254 |
} |
|
255 |
with utils.mock_url(url=url, response=json_get_data('fetch')): |
|
256 |
resp = app.get(endpoint, params=params) |
|
257 |
assert not resp.json['err'] |
|
258 |
assert resp.json['data']['id'] == 'dccd85d12cf54b6899dff41e5a56ee7f' |
|
259 |
assert resp.json['data']['text'] == 'Le Prisme (Activités et divertissement)' |
|
260 |
assert token.handlers[0].call['count'] == 0 |
|
261 | ||
262 | ||
263 |
@pytest.mark.parametrize( |
|
264 |
'exception, status_code, response, err_desc', |
|
265 |
[ |
|
266 |
[ConnectionError('plop'), None, None, 'plop'], |
|
267 |
[None, 200, 'not json', 'bad JSON response'], |
|
268 |
[None, 404, {'message': 'Resource not found: ...', 'type': 'NotFound'}, '404 Client Error'], |
|
269 |
], |
|
270 |
) |
|
271 |
def test_request_error(app, connector, token, exception, status_code, response, err_desc): |
|
272 |
endpoint = utils.generic_endpoint_url('plone-restapi', 'fetch', slug=connector.slug) |
|
273 |
assert endpoint == '/plone-restapi/my_connector/fetch' |
|
274 |
url = connector.service_url + '/braine-l-alleud/plop' |
|
275 |
params = { |
|
276 |
'uid': 'plop', |
|
277 |
'uri': 'braine-l-alleud', |
|
278 |
'text_template': '{{ title }} ({{ PLONE_type }})', |
|
279 |
} |
|
280 |
with utils.mock_url(url=url, response=response, status_code=status_code, exception=exception): |
|
281 |
resp = app.get(endpoint, params=params) |
|
282 |
assert resp.json['err'] |
|
283 |
assert err_desc in resp.json['err_desc'] |
|
284 | ||
285 | ||
286 |
def test_create(app, connector, token): |
|
287 |
endpoint = utils.generic_endpoint_url('plone-restapi', 'create', slug=connector.slug) |
|
288 |
assert endpoint == '/plone-restapi/my_connector/create' |
|
289 |
url = connector.service_url + '/braine-l-alleud' |
|
290 |
payload = { |
|
291 |
'@type': 'imio.directory.Contact', |
|
292 |
'title': "Test Entr'ouvert", |
|
293 |
'type': 'organization', |
|
294 |
'schedule': {}, |
|
295 |
'topics/0/title': 'Tourisme', |
|
296 |
'topics/0/token': 'tourism', |
|
297 |
} |
|
298 |
with utils.mock_url(url=url, response=json_get_data('fetch'), status_code=201) as mocked: |
|
299 |
resp = app.post_json(endpoint + '?uri=braine-l-alleud', params=payload) |
|
300 |
body = json.loads(mocked.handlers[0].call['requests'][1].body) |
|
301 |
assert body['topics'] == [{'title': 'Tourisme', 'token': 'tourism'}] |
|
302 |
assert not resp.json['err'] |
|
303 |
assert resp.json['data'] == {'uid': 'dccd85d12cf54b6899dff41e5a56ee7f', 'created': True} |
|
304 | ||
305 | ||
306 |
def test_create_wrong_payload(app, connector, token): |
|
307 |
endpoint = utils.generic_endpoint_url('plone-restapi', 'create', slug=connector.slug) |
|
308 |
assert endpoint == '/plone-restapi/my_connector/create' |
|
309 |
url = connector.service_url + '/braine-l-alleud' |
|
310 |
payload = 'not json' |
|
311 |
resp = app.post(endpoint + '?uri=braine-l-alleud', params=payload, status=400) |
|
312 |
assert resp.json['err'] |
|
313 |
assert resp.json['err_desc'] == 'Expecting value: line 1 column 1 (char 0)' |
|
314 |
assert resp.json['err_class'] == 'passerelle.apps.plone_restapi.models.ParameterTypeError' |
|
315 | ||
316 | ||
317 |
def test_update(app, connector, token): |
|
318 |
endpoint = utils.generic_endpoint_url('plone-restapi', 'update', slug=connector.slug) |
|
319 |
assert endpoint == '/plone-restapi/my_connector/update' |
|
320 |
url = connector.service_url + '/braine-l-alleud/dccd85d12cf54b6899dff41e5a56ee7f' |
|
321 |
query_string = '?uri=braine-l-alleud&uid=dccd85d12cf54b6899dff41e5a56ee7f' |
|
322 |
payload = { |
|
323 |
'title': 'Test update', |
|
324 |
'topics/0/token': 'social', |
|
325 |
} |
|
326 |
with utils.mock_url(url=url, response='', status_code=204) as mocked: |
|
327 |
resp = app.post_json(endpoint + query_string, params=payload) |
|
328 |
body = json.loads(mocked.handlers[0].call['requests'][1].body) |
|
329 |
assert body['topics'] == [{'token': 'social'}] |
|
330 |
assert not resp.json['err'] |
|
331 |
assert resp.json['data'] == {'uid': 'dccd85d12cf54b6899dff41e5a56ee7f', 'updated': True} |
|
332 | ||
333 | ||
334 |
def test_update_wrong_payload(app, connector, token): |
|
335 |
endpoint = utils.generic_endpoint_url('plone-restapi', 'update', slug=connector.slug) |
|
336 |
assert endpoint == '/plone-restapi/my_connector/update' |
|
337 |
url = connector.service_url + '/braine-l-alleud/dccd85d12cf54b6899dff41e5a56ee7f' |
|
338 |
query_string = '?uri=braine-l-alleud&uid=dccd85d12cf54b6899dff41e5a56ee7f' |
|
339 |
payload = 'not json' |
|
340 |
resp = app.post(endpoint + query_string, params=payload, status=400) |
|
341 |
assert resp.json['err'] |
|
342 |
assert resp.json['err_desc'] == 'Expecting value: line 1 column 1 (char 0)' |
|
343 |
assert resp.json['err_class'] == 'passerelle.apps.plone_restapi.models.ParameterTypeError' |
|
344 | ||
345 | ||
346 |
def test_remove(app, connector, token): |
|
347 |
endpoint = utils.generic_endpoint_url('plone-restapi', 'remove', slug=connector.slug) |
|
348 |
assert endpoint == '/plone-restapi/my_connector/remove' |
|
349 |
url = connector.service_url + '/braine-l-alleud/dccd85d12cf54b6899dff41e5a56ee7f' |
|
350 |
query_string = '?uri=braine-l-alleud&uid=dccd85d12cf54b6899dff41e5a56ee7f' |
|
351 |
with utils.mock_url(url=url, response='', status_code=204): |
|
352 |
resp = app.delete(endpoint + query_string) |
|
353 |
assert resp.json['data'] == {'uid': 'dccd85d12cf54b6899dff41e5a56ee7f', 'removed': True} |
|
354 |
assert not resp.json['err'] |
|
355 | ||
356 | ||
357 |
def test_search(app, connector, token): |
|
358 |
endpoint = utils.generic_endpoint_url('plone-restapi', 'search', slug=connector.slug) |
|
359 |
assert endpoint == '/plone-restapi/my_connector/search' |
|
360 |
url = connector.service_url + '/braine-l-alleud/@search' |
|
361 |
params = { |
|
362 |
'uri': 'braine-l-alleud', |
|
363 |
'text_template': '{{ title }} ({{ PLONE_type }})', |
|
364 |
'sort': 'UID', |
|
365 |
'order': False, |
|
366 |
'limit': 3, |
|
367 |
} |
|
368 |
qs = {} |
|
369 |
with utils.mock_url(url=url, response=json_get_data('q_search'), qs=qs): |
|
370 |
resp = app.get(endpoint, params=params) |
|
371 |
assert token.handlers[0].call['count'] == 1 |
|
372 |
assert qs == {'sort_on': 'UID', 'sort_order': 'descending', 'b_size': '3', 'fullobjects': 'y'} |
|
373 |
assert not resp.json['err'] |
|
374 |
assert len(resp.json['data']) == 3 |
|
375 |
assert [(x['id'], x['text']) for x in resp.json['data']] == [ |
|
376 |
( |
|
377 |
'dea9d26baab944beb7e54d4024d35a33', |
|
378 |
"Cabinet du Bourgmestre de la Commune de Braine-l'Alleud (imio.directory.Contact)", |
|
379 |
), |
|
380 |
( |
|
381 |
'23a32197d6c841259963b43b24747854', |
|
382 |
"Académie de Musique de Braine-l'Alleud (imio.directory.Contact)", |
|
383 |
), |
|
384 |
( |
|
385 |
'f82d2c079131433ea6ab20f9f7f49442', |
|
386 |
'Accueil et Orientation Volontariat (A.O.V.) (imio.directory.Contact)', |
|
387 |
), |
|
388 |
] |
|
389 | ||
390 | ||
391 |
def test_search_using_q(app, connector, token): |
|
392 |
endpoint = utils.generic_endpoint_url('plone-restapi', 'search', slug=connector.slug) |
|
393 |
assert endpoint == '/plone-restapi/my_connector/search' |
|
394 |
url = connector.service_url + '/braine-l-alleud/@search' |
|
395 |
params = { |
|
396 |
'uri': 'braine-l-alleud', |
|
397 |
'text_template': '{{ title }} ({{ PLONE_type }})', |
|
398 |
'sort': 'title', |
|
399 |
'order': True, |
|
400 |
'limit': '3', |
|
401 |
'q': 'Página dentro', |
|
402 |
} |
|
403 |
qs = {} |
|
404 |
with utils.mock_url(url=url, response=json_get_data('q_search'), qs=qs): |
|
405 |
resp = app.get(endpoint, params=params) |
|
406 |
assert qs == { |
|
407 |
'SearchableText': 'Página dentro', |
|
408 |
'sort_on': 'title', |
|
409 |
'sort_order': 'ascending', |
|
410 |
'b_size': '3', |
|
411 |
'fullobjects': 'y', |
|
412 |
} |
|
413 |
assert not resp.json['err'] |
|
414 |
assert len(resp.json['data']) == 3 |
|
415 |
assert [(x['id'], x['text']) for x in resp.json['data']] == [ |
|
416 |
( |
|
417 |
'dea9d26baab944beb7e54d4024d35a33', |
|
418 |
"Cabinet du Bourgmestre de la Commune de Braine-l'Alleud (imio.directory.Contact)", |
|
419 |
), |
|
420 |
( |
|
421 |
'23a32197d6c841259963b43b24747854', |
|
422 |
"Académie de Musique de Braine-l'Alleud (imio.directory.Contact)", |
|
423 |
), |
|
424 |
( |
|
425 |
'f82d2c079131433ea6ab20f9f7f49442', |
|
426 |
'Accueil et Orientation Volontariat (A.O.V.) (imio.directory.Contact)', |
|
427 |
), |
|
428 |
] |
|
429 | ||
430 | ||
431 |
def test_search_using_id(app, connector, token): |
|
432 |
endpoint = utils.generic_endpoint_url('plone-restapi', 'search', slug=connector.slug) |
|
433 |
assert endpoint == '/plone-restapi/my_connector/search' |
|
434 |
url = connector.service_url + '/braine-l-alleud/@search' |
|
435 |
params = { |
|
436 |
'uri': 'braine-l-alleud', |
|
437 |
'text_template': '{{ title }} ({{ PLONE_type }})', |
|
438 |
'id': '9fbb2afd499e465983434f974fce8404', |
|
439 |
} |
|
440 |
qs = {} |
|
441 |
with utils.mock_url(url=url, response=json_get_data('id_search'), qs=qs): |
|
442 |
resp = app.get(endpoint, params=params) |
|
443 |
assert qs == {'UID': '9fbb2afd499e465983434f974fce8404', 'fullobjects': 'y'} |
|
444 |
assert len(resp.json['data']) == 1 |
|
445 |
assert resp.json['data'][0]['text'] == "Académie de Musique de Braine-l'Alleud (imio.directory.Contact)" |
|
446 | ||
447 | ||
448 |
def test_query_q(app, query, token): |
|
449 |
endpoint = '/plone-restapi/my_connector/q/my_query/' |
|
450 |
url = query.resource.service_url + '/braine-l-alleud/@search' |
|
451 |
params = { |
|
452 |
'limit': 3, |
|
453 |
} |
|
454 |
qs = {} |
|
455 |
with utils.mock_url(url=url, response=json_get_data('q_search'), qs=qs): |
|
456 |
resp = app.get(endpoint, params=params) |
|
457 |
assert qs == { |
|
458 |
'sort_on': 'UID', |
|
459 |
'sort_order': 'descending', |
|
460 |
'b_size': '3', |
|
461 |
'portal_type': 'Document', |
|
462 |
'review_state': 'published', |
|
463 |
'fullobjects': 'y', |
|
464 |
} |
|
465 |
assert not resp.json['err'] |
|
466 |
assert len(resp.json['data']) == 3 |
|
467 |
assert resp.json['meta'] == {'label': 'demo query', 'description': "Annuaire de Braine-l'Alleud"} |
|
468 | ||
469 | ||
470 |
def test_query_q_using_q(app, query, token): |
|
471 |
endpoint = '/plone-restapi/my_connector/q/my_query/' |
|
472 |
url = query.resource.service_url + '/braine-l-alleud/@search' |
|
473 |
params = { |
|
474 |
'q': 'Página dentro', |
|
475 |
} |
|
476 |
qs = {} |
|
477 |
with utils.mock_url(url=url, response=json_get_data('q_search'), qs=qs): |
|
478 |
resp = app.get(endpoint, params=params) |
|
479 |
assert qs == { |
|
480 |
'SearchableText': 'Página dentro', |
|
481 |
'sort_on': 'UID', |
|
482 |
'sort_order': 'descending', |
|
483 |
'b_size': '3', |
|
484 |
'portal_type': 'Document', |
|
485 |
'review_state': 'published', |
|
486 |
'fullobjects': 'y', |
|
487 |
} |
|
488 |
assert not resp.json['err'] |
|
489 |
assert len(resp.json['data']) == 3 |
|
490 |
assert resp.json['meta'] == {'label': 'demo query', 'description': "Annuaire de Braine-l'Alleud"} |
|
491 | ||
492 | ||
493 |
def test_query_q_using_id(app, query, token): |
|
494 |
endpoint = '/plone-restapi/my_connector/q/my_query/' |
|
495 |
url = query.resource.service_url + '/braine-l-alleud/@search' |
|
496 |
params = { |
|
497 |
'id': '9fbb2afd499e465983434f974fce8404', |
|
498 |
} |
|
499 |
qs = {} |
|
500 |
with utils.mock_url(url=url, response=json_get_data('id_search'), qs=qs): |
|
501 |
resp = app.get(endpoint, params=params) |
|
502 |
assert qs == { |
|
503 |
'UID': '9fbb2afd499e465983434f974fce8404', |
|
504 |
'fullobjects': 'y', |
|
505 |
} |
|
506 |
assert len(resp.json['data']) == 1 |
|
507 |
assert resp.json['data'][0]['text'] == "Académie de Musique de Braine-l'Alleud (imio.directory.Contact)" |
|
508 |
assert resp.json['meta'] == {'label': 'demo query', 'description': "Annuaire de Braine-l'Alleud"} |
tests/utils.py | ||
---|---|---|
23 | 23 | |
24 | 24 |
class FakedResponse(mock.Mock): |
25 | 25 |
headers = {} |
26 | 26 | |
27 | 27 |
def json(self): |
28 | 28 |
return json_loads(self.content) |
29 | 29 | |
30 | 30 | |
31 |
def mock_url(url=None, response='', status_code=200, headers=None, reason=None, exception=None): |
|
31 |
def mock_url(url=None, response='', status_code=200, headers=None, reason=None, exception=None, qs=None):
|
|
32 | 32 |
urlmatch_kwargs = {} |
33 | 33 |
if url: |
34 | 34 |
parsed = urlparse.urlparse(url) |
35 | 35 |
if parsed.netloc: |
36 | 36 |
urlmatch_kwargs['netloc'] = parsed.netloc |
37 | 37 |
if parsed.path: |
38 | 38 |
urlmatch_kwargs['path'] = parsed.path |
39 | 39 | |
40 | 40 |
if not isinstance(response, str): |
41 | 41 |
response = json.dumps(response) |
42 | 42 | |
43 | 43 |
@httmock.remember_called |
44 | 44 |
@httmock.urlmatch(**urlmatch_kwargs) |
45 | 45 |
def mocked(url, request): |
46 |
if qs is not None: |
|
47 |
qs.update(urlparse.parse_qsl(url.query)) |
|
46 | 48 |
if exception: |
47 | 49 |
raise exception |
48 | 50 |
return httmock.response(status_code, response, headers, reason, request=request) |
49 | 51 | |
50 | 52 |
return httmock.HTTMock(mocked) |
51 | 53 | |
52 | 54 | |
53 | 55 |
def make_resource(model_class, **kwargs): |
54 |
- |