Projet

Général

Profil

0001-plone-restapi-add-a-plone.restapi-connector-57258.patch

Nicolas Roche, 18 octobre 2021 15:59

Télécharger (119 ko)

Voir les différences:

Subject: [PATCH] plone-restapi: add a plone.restapi connector (#57258)

 passerelle/apps/plone_restapi/__init__.py     |   0
 passerelle/apps/plone_restapi/forms.py        |  28 +
 .../plone_restapi/migrations/0001_initial.py  | 152 ++++
 .../apps/plone_restapi/migrations/__init__.py |   0
 passerelle/apps/plone_restapi/models.py       | 441 +++++++++++
 passerelle/apps/plone_restapi/urls.py         |  31 +
 passerelle/apps/plone_restapi/views.py        |  44 ++
 passerelle/settings.py                        |   1 +
 passerelle/static/css/style.css               |   4 +
 tests/data/plone_restapi/fetch.json           | 262 +++++++
 .../data/plone_restapi/get_content_type.json  | 692 ++++++++++++++++++
 .../data/plone_restapi/get_content_types.json |  52 ++
 .../data/plone_restapi/get_field_choices.json |  74 ++
 tests/data/plone_restapi/id_search.json       | 266 +++++++
 tests/data/plone_restapi/q_search.json        | 629 ++++++++++++++++
 .../data/plone_restapi/workflow_publish.json  |   8 +
 tests/test_plone_restapi.py                   | 606 +++++++++++++++
 tests/utils.py                                |   4 +-
 18 files changed, 3293 insertions(+), 1 deletion(-)
 create mode 100644 passerelle/apps/plone_restapi/__init__.py
 create mode 100644 passerelle/apps/plone_restapi/forms.py
 create mode 100644 passerelle/apps/plone_restapi/migrations/0001_initial.py
 create mode 100644 passerelle/apps/plone_restapi/migrations/__init__.py
 create mode 100644 passerelle/apps/plone_restapi/models.py
 create mode 100644 passerelle/apps/plone_restapi/urls.py
 create mode 100644 passerelle/apps/plone_restapi/views.py
 create mode 100644 tests/data/plone_restapi/fetch.json
 create mode 100644 tests/data/plone_restapi/get_content_type.json
 create mode 100644 tests/data/plone_restapi/get_content_types.json
 create mode 100644 tests/data/plone_restapi/get_field_choices.json
 create mode 100644 tests/data/plone_restapi/id_search.json
 create mode 100644 tests/data/plone_restapi/q_search.json
 create mode 100644 tests/data/plone_restapi/workflow_publish.json
 create mode 100644 tests/test_plone_restapi.py
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_payload(self, payload):
107
        # convert image format
108
        for file_field in payload.values():
109
            if isinstance(file_field, dict) and file_field.get('filename'):
110
                file_field['encoding'] = 'base64'
111
                file_field['data'] = file_field['content']
112
                file_field['content-type'] = file_field['content_type']
113
                del file_field['content']
114

  
115
    def adapt_record(
116
        self,
117
        record,
118
        text_template='{{ id }}',
119
        id_key='UID',
120
    ):
121
        self.adapt_id_and_type_plone_attributes(record)
122
        for key, value in list(record.items()):
123
            # backup original id and text fields
124
            if key in ('id', 'text'):
125
                key = 'original_%s' % key
126
            record[key] = value
127
        record['id'] = record.get(id_key)
128
        record['text'] = render_to_string(text_template, record).strip()
129

  
130
    def get_token(self, renew=False):
131
        token_key = 'plone-restapi-%s-token' % self.id
132
        if not renew and cache.get(token_key):
133
            return cache.get(token_key)
134
        payload = {
135
            'grant_type': 'password',
136
            'client_id': str(self.client_id),
137
            'client_secret': str(self.client_secret),
138
            'username': self.username,
139
            'password': self.password,
140
            'scope': ['openid'],
141
        }
142
        headers = {
143
            'Content-Type': 'application/x-www-form-urlencoded',
144
        }
145
        response = self.requests.post(self.token_ws_url, headers=headers, data=payload)
146
        if not response.status_code // 100 == 2:
147
            raise APIError(response.content)
148
        token = response.json().get('id_token')
149
        cache.set(token_key, token, 30)
150
        return token
151

  
152
    def request(self, uri='', uid='', method='GET', params=None, json=None):
153
        scheme, netloc, path, query, fragment = urlsplit(self.service_url)
154
        if uri:
155
            path += '/%s' % uri
156
        if uid:
157
            path += '/%s' % uid
158
        url = urlunsplit((scheme, netloc, path, '', fragment))
159
        headers = {'Accept': 'application/json'}
160
        auth = HttpBearerAuth(self.get_token()) if self.token_ws_url else None
161
        try:
162
            response = self.requests.request(
163
                method=method, url=url, headers=headers, params=params, json=json, auth=auth
164
            )
165
        except RequestException as e:
166
            raise APIError('PloneRestApi: %s' % e)
167
        json_response = None
168
        if response.status_code != 204:  # No Content
169
            try:
170
                json_response = response.json()
171
            except ValueError as e:
172
                raise APIError('PloneRestApi: bad JSON response')
173
        try:
174
            response.raise_for_status()
175
        except RequestException as e:
176
            raise APIError('PloneRestApi: %s "%s"' % (e, json_response))
177
        return json_response
178

  
179
    def call_search(
180
        self,
181
        uri='',
182
        text_template='',
183
        filter_expression='',
184
        sort=None,
185
        order=True,
186
        limit=None,
187
        id=None,
188
        q=None,
189
    ):
190
        query = urlsplit(self.service_url).query
191
        params = dict(parse_qsl(query))
192
        if id:
193
            params['UID'] = id
194
        else:
195
            if q is not None:
196
                params['SearchableText'] = q
197
            if sort:
198
                params['sort_on'] = sort
199
                if order:
200
                    params['sort_order'] = 'ascending'
201
                else:
202
                    params['sort_order'] = 'descending'
203
            if limit:
204
                params['b_size'] = limit
205
            params.update(parse_qsl(filter_expression))
206
        params['fullobjects'] = 'y'
207
        response = self.request(uri=uri, uid='@search', method='GET', params=params)
208

  
209
        for record in response.get('items') or []:
210
            self.adapt_record(record, text_template)
211
        return response.get('items') or []
212

  
213
    @endpoint(
214
        perm='can_access',
215
        description=_('Get content types'),
216
        display_order=1,
217
    )
218
    def get_content_types(self, request):
219
        response = self.request(uri='@types', method='GET')
220
        for record in response or []:
221
            self.adapt_record(record, '{{ title }}', id_key='PLONE_id')
222
            record['id'] = record['id'].split('/')[-1]
223
        return {'data': response or []}
224

  
225
    @endpoint(
226
        perm='can_access',
227
        description=_('Get content type'),
228
        parameters={
229
            'id': {'description': _('Content type identifier'), 'example_value': 'imio.directory.Contact'}
230
        },
231
        display_order=2,
232
    )
233
    def get_content_type(self, request, id):
234
        response = self.request(uri='@types', uid=id, method='GET')
235
        return {'data': response}
236

  
237
    @endpoint(
238
        perm='can_access',
239
        description=_('Get field choices'),
240
        parameters={
241
            'id': {'description': _('Field identifier'), 'example_value': 'imio.smartweb.vocabulary.Topics'}
242
        },
243
        display_order=3,
244
    )
245
    def get_field_choices(self, request, id):
246
        response = self.request(uri='@vocabularies', uid=id, method='GET')
247
        for record in response.get('items') or []:
248
            self.adapt_record(record, '{{ title }}', id_key='token')
249
        return {'data': response.get('items') or []}
250

  
251
    @endpoint(
252
        perm='can_access',
253
        description=_('Fetch'),
254
        parameters={
255
            'uri': {'description': _('Uri')},
256
            'uid': {'description': _('Uid')},
257
            'text_template': {'description': _('Text template')},
258
        },
259
        display_order=4,
260
    )
261
    def fetch(self, request, uid, uri='', text_template=''):
262
        response = self.request(uri=uri, uid=uid, method='GET')
263
        self.adapt_record(response, text_template)
264
        return {'data': response}
265

  
266
    @endpoint(
267
        perm='can_access',
268
        description=_('Creates'),
269
        parameters={
270
            'uri': {'description': _('Uri')},
271
            'publish': {'description': _('Do publish content (default is false)')},
272
        },
273
        methods=['post'],
274
        display_order=5,
275
    )
276
    def create(self, request, uri, publish=False):
277
        try:
278
            post_data = json_loads(request.body)
279
        except ValueError as e:
280
            raise ParameterTypeError(str(e))
281
        post_data = unflatten(post_data)
282
        self.adapt_payload(post_data)
283
        response = self.request(uri=uri, method='POST', json=post_data)
284
        uid = response.get('UID')
285

  
286
        review_state = None
287
        if uid and bool(publish):
288
            uri += '/%s' % uid
289
            response = self.request(uri, uid='@workflow/publish', method='POST')
290
            review_state = response.get('review_state')
291

  
292
        return {'data': {'uid': uid, 'created': True, 'review_state': review_state}}
293

  
294
    @endpoint(
295
        perm='can_access',
296
        description=_('Update'),
297
        parameters={
298
            'uri': {'description': _('Uri')},
299
            'uid': {'description': _('Uid')},
300
        },
301
        methods=['post'],
302
        display_order=6,
303
    )
304
    def update(self, request, uid, uri=''):
305
        try:
306
            post_data = json_loads(request.body)
307
        except ValueError as e:
308
            raise ParameterTypeError(str(e))
309
        post_data = unflatten(post_data)
310
        self.adapt_payload(post_data)
311
        self.request(uri=uri, uid=uid, method='PATCH', json=post_data)
312
        return {'data': {'uid': uid, 'updated': True}}
313

  
314
    @endpoint(
315
        perm='can_access',
316
        description=_('Remove'),
317
        parameters={
318
            'uri': {'description': _('Uri')},
319
            'uid': {'description': _('Uid')},
320
        },
321
        methods=['delete'],
322
        display_order=7,
323
    )
324
    def remove(self, request, uid, uri=''):
325
        self.request(method='DELETE', uri=uri, uid=uid)
326
        return {'data': {'uid': uid, 'removed': True}}
327

  
328
    @endpoint(
329
        perm='can_access',
330
        description=_('Search'),
331
        parameters={
332
            'uri': {'description': _('Uri')},
333
            'text_template': {'description': _('Text template')},
334
            'sort': {'description': _('Sort field')},
335
            'order': {'description': _('Ascending sort order'), 'type': 'bool'},
336
            'limit': {'description': _('Maximum items')},
337
            'id': {'description': _('Record identifier')},
338
            'q': {'description': _('Full text query')},
339
        },
340
    )
341
    def search(
342
        self,
343
        request,
344
        uri='',
345
        text_template='',
346
        sort=None,
347
        order=True,
348
        limit=None,
349
        id=None,
350
        q=None,
351
        **kwargs,
352
    ):
353
        result = self.call_search(uri, text_template, '', sort, order, limit, id, q)
354
        return {'data': result}
355

  
356
    @endpoint(
357
        name='q',
358
        description=_('Query'),
359
        pattern=r'^(?P<query_slug>[\w:_-]+)/$',
360
        perm='can_access',
361
        show=False,
362
    )
363
    def q(self, request, query_slug, **kwargs):
364
        query = get_object_or_404(Query, resource=self, slug=query_slug)
365
        result = query.q(request, **kwargs)
366
        meta = {'label': query.name, 'description': query.description}
367
        return {'data': result, 'meta': meta}
368

  
369
    def create_query_url(self):
370
        return reverse('plone-restapi-query-new', kwargs={'slug': self.slug})
371

  
372

  
373
class Query(BaseQuery):
374
    resource = models.ForeignKey(
375
        to=PloneRestApi, related_name='queries', verbose_name=_('Resource'), on_delete=models.CASCADE
376
    )
377
    uri = models.CharField(
378
        verbose_name=_('Uri'),
379
        max_length=128,
380
        help_text=_('uri to query'),
381
        blank=True,
382
    )
383
    text_template = models.TextField(
384
        verbose_name=_('Text template'),
385
        help_text=_("Use Django's template syntax. Attributes can be accessed through {{ attributes.name }}"),
386
        validators=[validate_template],
387
        blank=True,
388
    )
389
    filter_expression = models.TextField(
390
        verbose_name=_('filter'),
391
        help_text=_('Specify more URL parameters (key=value) separated by lines'),
392
        blank=True,
393
    )
394
    sort = models.CharField(
395
        verbose_name=_('Sort field'),
396
        help_text=_('Sorts results by the specified field'),
397
        max_length=256,
398
        blank=True,
399
    )
400
    order = models.BooleanField(
401
        verbose_name=_('Ascending sort order'),
402
        help_text=_("Unset to use descending sort order"),
403
        default=True,
404
    )
405
    limit = models.PositiveIntegerField(
406
        default=10,
407
        verbose_name=_('Limit'),
408
        help_text=_('Number of results to return in a single call'),
409
    )
410

  
411
    delete_view = 'plone-restapi-query-delete'
412
    edit_view = 'plone-restapi-query-edit'
413

  
414
    def q(self, request, **kwargs):
415
        return self.resource.call_search(
416
            uri=self.uri,
417
            text_template=self.text_template,
418
            filter_expression='&'.join(
419
                [x.strip() for x in str(self.filter_expression).splitlines() if x.strip()]
420
            ),
421
            sort=self.sort,
422
            order=self.order,
423
            limit=self.limit,
424
            id=kwargs.get('id'),
425
            q=kwargs.get('q'),
426
        )
427

  
428
    def as_endpoint(self):
429
        endpoint = super(Query, self).as_endpoint(path=self.resource.q.endpoint_info.name)
430

  
431
        search_endpoint = self.resource.search.endpoint_info
432
        endpoint.func = search_endpoint.func
433
        endpoint.show_undocumented_params = False
434

  
435
        # Copy generic params descriptions from original endpoint
436
        # if they are not overloaded by the query
437
        for param in search_endpoint.parameters:
438
            if param in ('uri', 'text_template') and getattr(self, param):
439
                continue
440
            endpoint.parameters[param] = search_endpoint.parameters[param]
441
        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/get_content_type.json
1
{
2
  "fieldsets": [
3
    {
4
      "behavior": "plone",
5
      "fields": [
6
        "title",
7
        "description",
8
        "type",
9
        "subtitle",
10
        "logo",
11
        "image",
12
        "image_caption",
13
        "geolocation"
14
      ],
15
      "id": "default",
16
      "title": "G\u00e9n\u00e9ral"
17
    },
18
    {
19
      "behavior": "plone",
20
      "fields": [
21
        "street",
22
        "number",
23
        "complement",
24
        "zipcode",
25
        "city",
26
        "country"
27
      ],
28
      "id": "address",
29
      "title": "Adresse"
30
    },
31
    {
32
      "behavior": "plone",
33
      "fields": [
34
        "vat_number",
35
        "phones",
36
        "mails",
37
        "urls"
38
      ],
39
      "id": "contact_informations",
40
      "title": "Coordonn\u00e9es"
41
    },
42
    {
43
      "behavior": "plone.dexterity.schema.generated",
44
      "fields": [
45
        "selected_entities",
46
        "facilities",
47
        "taxonomy_contact_category",
48
        "topics",
49
        "iam",
50
        "subjects",
51
        "language"
52
      ],
53
      "id": "categorization",
54
      "title": "Cat\u00e9gorisation"
55
    },
56
    {
57
      "behavior": "plone",
58
      "fields": [
59
        "schedule",
60
        "multi_schedule"
61
      ],
62
      "id": "multischedule",
63
      "title": "Horaire multiple"
64
    },
65
    {
66
      "behavior": "plone",
67
      "fields": [
68
        "exceptional_closure"
69
      ],
70
      "id": "exceptionalclosure",
71
      "title": "Fermeture exceptionnelle"
72
    }
73
  ],
74
  "layouts": [
75
    "view"
76
  ],
77
  "properties": {
78
    "city": {
79
      "behavior": "imio.directory.core.contents.contact.content.IContact",
80
      "description": "",
81
      "factory": "Text line (String)",
82
      "title": "Commune",
83
      "type": "string"
84
    },
85
    "complement": {
86
      "behavior": "imio.directory.core.contents.contact.content.IContact",
87
      "description": "",
88
      "factory": "Text line (String)",
89
      "title": "Compl\u00e9ment",
90
      "type": "string"
91
    },
92
    "country": {
93
      "behavior": "imio.directory.core.contents.contact.content.IContact",
94
      "description": "",
95
      "factory": "Choice",
96
      "title": "Pays",
97
      "type": "string",
98
      "vocabulary": {
99
        "@id": "https://annuaire.preprod.imio.be/@vocabularies/imio.smartweb.vocabulary.Countries"
100
      }
101
    },
102
    "description": {
103
      "behavior": "plone.basic",
104
      "description": "Utilis\u00e9 dans les pages listant des \u00e9l\u00e9ments et les pages de r\u00e9sultats de recherche.",
105
      "factory": "Text",
106
      "maxLength": 700,
107
      "title": "Description",
108
      "type": "string",
109
      "widget": "textarea"
110
    },
111
    "exceptional_closure": {
112
      "additionalItems": true,
113
      "behavior": "collective.schedulefield.behavior.IExceptionalClosureContent",
114
      "description": "",
115
      "factory": "Collection",
116
      "items": {
117
        "description": "",
118
        "factory": "File",
119
        "properties": {
120
          "ExceptionalClosure.date": {
121
            "description": "",
122
            "factory": "Date",
123
            "title": "Date",
124
            "type": "string",
125
            "widget": "date"
126
          },
127
          "ExceptionalClosure.title": {
128
            "description": "",
129
            "factory": "Text",
130
            "title": "Titre",
131
            "type": "string",
132
            "widget": "textarea"
133
          }
134
        },
135
        "title": "",
136
        "type": "object"
137
      },
138
      "title": "Dates",
139
      "type": "array",
140
      "uniqueItems": false
141
    },
142
    "facilities": {
143
      "additionalItems": true,
144
      "behavior": "imio.directory.core.contents.contact.content.IContact",
145
      "description": "Important! Ces cat\u00e9gories permettent de mettre en avant et de g\u00e9olocaliser certains services de base",
146
      "factory": "List",
147
      "items": {
148
        "description": "",
149
        "factory": "Choice",
150
        "title": "",
151
        "type": "string",
152
        "vocabulary": {
153
          "@id": "https://annuaire.preprod.imio.be/@vocabularies/imio.directory.vocabulary.Facilities"
154
        }
155
      },
156
      "title": "Facilit\u00e9s",
157
      "type": "array",
158
      "uniqueItems": true
159
    },
160
    "geolocation": {
161
      "behavior": "geolocatable",
162
      "description": "Cliquez sur la carte pour s\u00e9lectionner une localisation, ou utilisez le champ texte pour rechercher par adresse.",
163
      "factory": "File",
164
      "properties": {
165
        "geolocation.latitude": {
166
          "description": "",
167
          "factory": "Floating-point number",
168
          "title": "Latitude",
169
          "type": "number"
170
        },
171
        "geolocation.longitude": {
172
          "description": "",
173
          "factory": "Floating-point number",
174
          "title": "Longitude",
175
          "type": "number"
176
        }
177
      },
178
      "title": "G\u00e9olocalisation",
179
      "type": "object"
180
    },
181
    "iam": {
182
      "additionalItems": true,
183
      "behavior": "imio.smartweb.iam",
184
      "description": "Important! Ces cat\u00e9gories sont utilis\u00e9es pour cr\u00e9er des listes accessibles via le menu navigation",
185
      "factory": "List",
186
      "items": {
187
        "description": "",
188
        "factory": "Choice",
189
        "title": "",
190
        "type": "string",
191
        "vocabulary": {
192
          "@id": "https://annuaire.preprod.imio.be/@vocabularies/imio.smartweb.vocabulary.IAm"
193
        }
194
      },
195
      "title": "Je suis",
196
      "type": "array",
197
      "uniqueItems": true
198
    },
199
    "image": {
200
      "behavior": "plone.leadimage",
201
      "description": "",
202
      "factory": "Image",
203
      "properties": {
204
        "image.contentType": {
205
          "default": "",
206
          "description": "The content type identifies the type of data.",
207
          "factory": "Text line (String)",
208
          "title": "Content Type",
209
          "type": "string"
210
        },
211
        "image.data": {
212
          "default": "",
213
          "description": "The actual content of the object.",
214
          "factory": "Text line (String)",
215
          "title": "Data",
216
          "type": "string"
217
        },
218
        "image.filename": {
219
          "description": "",
220
          "factory": "Text line (String)",
221
          "title": "Filename",
222
          "type": "string"
223
        }
224
      },
225
      "title": "Image principale",
226
      "type": "object"
227
    },
228
    "image_caption": {
229
      "behavior": "plone.leadimage",
230
      "description": "",
231
      "factory": "Text line (String)",
232
      "title": "L\u00e9gende de l'image principale",
233
      "type": "string"
234
    },
235
    "language": {
236
      "behavior": "plone.categorization",
237
      "default": "fr",
238
      "description": "",
239
      "factory": "Choice",
240
      "title": "Langue",
241
      "type": "string",
242
      "vocabulary": {
243
        "@id": "https://annuaire.preprod.imio.be/@vocabularies/plone.app.vocabularies.SupportedContentLanguages"
244
      }
245
    },
246
    "logo": {
247
      "behavior": "imio.directory.core.contents.contact.content.IContact",
248
      "description": "",
249
      "factory": "Image",
250
      "properties": {
251
        "logo.contentType": {
252
          "default": "",
253
          "description": "The content type identifies the type of data.",
254
          "factory": "Text line (String)",
255
          "title": "Content Type",
256
          "type": "string"
257
        },
258
        "logo.data": {
259
          "default": "",
260
          "description": "The actual content of the object.",
261
          "factory": "Text line (String)",
262
          "title": "Data",
263
          "type": "string"
264
        },
265
        "logo.filename": {
266
          "description": "",
267
          "factory": "Text line (String)",
268
          "title": "Filename",
269
          "type": "string"
270
        }
271
      },
272
      "title": "Logo",
273
      "type": "object"
274
    },
275
    "mails": {
276
      "additionalItems": true,
277
      "behavior": "imio.directory.core.contents.contact.content.IContact",
278
      "description": "",
279
      "factory": "List",
280
      "items": {
281
        "description": "",
282
        "factory": "File",
283
        "properties": {
284
          "label": {
285
            "description": "",
286
            "factory": "Text line (String)",
287
            "title": "Intitul\u00e9 (Secr\u00e9tariat, Bureau de la direction, Ventes, ...)",
288
            "type": "string"
289
          },
290
          "mail_address": {
291
            "description": "",
292
            "factory": "Email",
293
            "title": "Adresse e-mail",
294
            "type": "string",
295
            "widget": "email"
296
          },
297
          "type": {
298
            "description": "",
299
            "factory": "Choice",
300
            "title": "Type",
301
            "type": "string",
302
            "vocabulary": {
303
              "@id": "https://annuaire.preprod.imio.be/@vocabularies/imio.directory.vocabulary.MailTypes"
304
            }
305
          }
306
        },
307
        "title": "Value",
308
        "type": "object"
309
      },
310
      "title": "E-mails",
311
      "type": "array",
312
      "uniqueItems": false
313
    },
314
    "multi_schedule": {
315
      "additionalItems": true,
316
      "behavior": "collective.schedulefield.behavior.IMultiScheduledContent",
317
      "description": "",
318
      "factory": "Collection",
319
      "items": {
320
        "description": "",
321
        "factory": "File",
322
        "properties": {
323
          "MultiSchedule.dates": {
324
            "additionalItems": true,
325
            "description": "",
326
            "factory": "List",
327
            "items": {
328
              "description": "",
329
              "factory": "File",
330
              "properties": {
331
                "DateRange.end_date": {
332
                  "description": "",
333
                  "factory": "Date",
334
                  "title": "Date de fin",
335
                  "type": "string",
336
                  "widget": "date"
337
                },
338
                "DateRange.start_date": {
339
                  "description": "",
340
                  "factory": "Date",
341
                  "title": "Date de d\u00e9but",
342
                  "type": "string",
343
                  "widget": "date"
344
                }
345
              },
346
              "title": "",
347
              "type": "object"
348
            },
349
            "title": "Dates",
350
            "type": "array",
351
            "uniqueItems": false
352
          },
353
          "MultiSchedule.schedule": {
354
            "description": "",
355
            "key_type": {
356
              "additional": {},
357
              "schema": {
358
                "description": "",
359
                "factory": "Text line (String)",
360
                "title": "",
361
                "type": "string"
362
              }
363
            },
364
            "title": "Horaire",
365
            "type": "dict",
366
            "value_type": {
367
              "additional": {
368
                "key_type": {
369
                  "additional": {},
370
                  "schema": {
371
                    "description": "",
372
                    "factory": "Text line (String)",
373
                    "title": "",
374
                    "type": "string"
375
                  }
376
                },
377
                "value_type": {
378
                  "additional": {},
379
                  "schema": {
380
                    "description": "",
381
                    "factory": "Text line (String)",
382
                    "title": "",
383
                    "type": "string"
384
                  }
385
                }
386
              },
387
              "schema": {
388
                "description": "",
389
                "key_type": {
390
                  "additional": {},
391
                  "schema": {
392
                    "description": "",
393
                    "factory": "Text line (String)",
394
                    "title": "",
395
                    "type": "string"
396
                  }
397
                },
398
                "title": "",
399
                "type": "dict",
400
                "value_type": {
401
                  "additional": {},
402
                  "schema": {
403
                    "description": "",
404
                    "factory": "Text line (String)",
405
                    "title": "",
406
                    "type": "string"
407
                  }
408
                }
409
              }
410
            }
411
          },
412
          "MultiSchedule.title": {
413
            "description": "",
414
            "factory": "Text",
415
            "title": "Titre",
416
            "type": "string",
417
            "widget": "textarea"
418
          }
419
        },
420
        "title": "",
421
        "type": "object"
422
      },
423
      "title": "Horaire multiple",
424
      "type": "array",
425
      "uniqueItems": false
426
    },
427
    "number": {
428
      "behavior": "imio.directory.core.contents.contact.content.IContact",
429
      "description": "",
430
      "factory": "Text line (String)",
431
      "title": "Num\u00e9ro",
432
      "type": "string"
433
    },
434
    "phones": {
435
      "additionalItems": true,
436
      "behavior": "imio.directory.core.contents.contact.content.IContact",
437
      "description": "",
438
      "factory": "List",
439
      "items": {
440
        "description": "",
441
        "factory": "File",
442
        "properties": {
443
          "label": {
444
            "description": "",
445
            "factory": "Text line (String)",
446
            "title": "Intitul\u00e9 (direction, num\u00e9ro principal, ...)",
447
            "type": "string"
448
          },
449
          "number": {
450
            "description": "",
451
            "factory": "Text line (String)",
452
            "title": "Num\u00e9ro (format: +32475010203)",
453
            "type": "string"
454
          },
455
          "type": {
456
            "description": "",
457
            "factory": "Choice",
458
            "title": "Type",
459
            "type": "string",
460
            "vocabulary": {
461
              "@id": "https://annuaire.preprod.imio.be/@vocabularies/imio.directory.vocabulary.PhoneTypes"
462
            }
463
          }
464
        },
465
        "title": "Value",
466
        "type": "object"
467
      },
468
      "title": "T\u00e9l\u00e9phones",
469
      "type": "array",
470
      "uniqueItems": false
471
    },
472
    "schedule": {
473
      "behavior": "collective.schedulefield.behavior.IMultiScheduledContent",
474
      "description": "",
475
      "key_type": {
476
        "additional": {},
477
        "schema": {
478
          "description": "",
479
          "factory": "Text line (String)",
480
          "title": "",
481
          "type": "string"
482
        }
483
      },
484
      "title": "Horaire",
485
      "type": "dict",
486
      "value_type": {
487
        "additional": {
488
          "key_type": {
489
            "additional": {},
490
            "schema": {
491
              "description": "",
492
              "factory": "Text line (String)",
493
              "title": "",
494
              "type": "string"
495
            }
496
          },
497
          "value_type": {
498
            "additional": {},
499
            "schema": {
500
              "description": "",
501
              "factory": "Text line (String)",
502
              "title": "",
503
              "type": "string"
504
            }
505
          }
506
        },
507
        "schema": {
508
          "description": "",
509
          "key_type": {
510
            "additional": {},
511
            "schema": {
512
              "description": "",
513
              "factory": "Text line (String)",
514
              "title": "",
515
              "type": "string"
516
            }
517
          },
518
          "title": "",
519
          "type": "dict",
520
          "value_type": {
521
            "additional": {},
522
            "schema": {
523
              "description": "",
524
              "factory": "Text line (String)",
525
              "title": "",
526
              "type": "string"
527
            }
528
          }
529
        }
530
      }
531
    },
532
    "selected_entities": {
533
      "additionalItems": true,
534
      "behavior": "imio.directory.core.contents.contact.content.IContact",
535
      "default": [],
536
      "description": "S\u00e9lectionnez les entit\u00e9s dans lesquelles ce contact sera affich\u00e9. L'entit\u00e9 courante sera toujours s\u00e9lectionn\u00e9e.",
537
      "factory": "List",
538
      "items": {
539
        "description": "",
540
        "factory": "Choice",
541
        "title": "",
542
        "type": "string",
543
        "vocabulary": {
544
          "@id": "https://annuaire.preprod.imio.be/@vocabularies/imio.directory.vocabulary.EntitiesUIDs"
545
        }
546
      },
547
      "title": "Entit\u00e9s concern\u00e9es",
548
      "type": "array",
549
      "uniqueItems": true
550
    },
551
    "street": {
552
      "behavior": "imio.directory.core.contents.contact.content.IContact",
553
      "description": "",
554
      "factory": "Text line (String)",
555
      "title": "Rue",
556
      "type": "string"
557
    },
558
    "subjects": {
559
      "additionalItems": true,
560
      "behavior": "plone.categorization",
561
      "description": "Les mots cl\u00e9s sont utilis\u00e9s couramment pour organiser le contenu de fa\u00e7on ad hoc.",
562
      "factory": "Tuple",
563
      "items": {
564
        "description": "",
565
        "factory": "Text line (String)",
566
        "title": "",
567
        "type": "string"
568
      },
569
      "title": "Mots cl\u00e9s",
570
      "type": "array",
571
      "uniqueItems": true,
572
      "widgetOptions": {
573
        "vocabulary": {
574
          "@id": "https://annuaire.preprod.imio.be/@vocabularies/plone.app.vocabularies.Keywords"
575
        }
576
      }
577
    },
578
    "subtitle": {
579
      "behavior": "imio.directory.core.contents.contact.content.IContact",
580
      "description": "",
581
      "factory": "Text line (String)",
582
      "title": "Sous-titre",
583
      "type": "string"
584
    },
585
    "taxonomy_contact_category": {
586
      "additionalItems": true,
587
      "behavior": "collective.taxonomy.generated.contact_category",
588
      "description": "Important! Ces cat\u00e9gories sont utilis\u00e9es pour am\u00e9liorer la recherche et filtrer les fiches dans l'annuaire",
589
      "factory": "List",
590
      "items": {
591
        "description": "",
592
        "factory": "Choice",
593
        "title": "",
594
        "type": "string",
595
        "vocabulary": {
596
          "@id": "https://annuaire.preprod.imio.be/@vocabularies/collective.taxonomy.contact_category"
597
        }
598
      },
599
      "title": "Cat\u00e9gorie",
600
      "type": "array",
601
      "uniqueItems": true
602
    },
603
    "title": {
604
      "behavior": "plone.basic",
605
      "description": "",
606
      "factory": "Text line (String)",
607
      "title": "Titre",
608
      "type": "string"
609
    },
610
    "topics": {
611
      "additionalItems": true,
612
      "behavior": "imio.smartweb.topics",
613
      "description": "Important! Les th\u00e9matiques sont utilis\u00e9es pour filtrer les r\u00e9sultats de la recherche et cr\u00e9er des listes",
614
      "factory": "List",
615
      "items": {
616
        "description": "",
617
        "factory": "Choice",
618
        "title": "",
619
        "type": "string",
620
        "vocabulary": {
621
          "@id": "https://annuaire.preprod.imio.be/@vocabularies/imio.smartweb.vocabulary.Topics"
622
        }
623
      },
624
      "title": "Th\u00e9matiques",
625
      "type": "array",
626
      "uniqueItems": true
627
    },
628
    "type": {
629
      "behavior": "imio.directory.core.contents.contact.content.IContact",
630
      "description": "",
631
      "factory": "Choice",
632
      "title": "Type",
633
      "type": "string",
634
      "vocabulary": {
635
        "@id": "https://annuaire.preprod.imio.be/@vocabularies/imio.directory.vocabulary.ContactTypes"
636
      }
637
    },
638
    "urls": {
639
      "additionalItems": true,
640
      "behavior": "imio.directory.core.contents.contact.content.IContact",
641
      "description": "",
642
      "factory": "List",
643
      "items": {
644
        "description": "",
645
        "factory": "File",
646
        "properties": {
647
          "type": {
648
            "description": "",
649
            "factory": "Choice",
650
            "title": "Type",
651
            "type": "string",
652
            "vocabulary": {
653
              "@id": "https://annuaire.preprod.imio.be/@vocabularies/imio.directory.vocabulary.SiteTypes"
654
            }
655
          },
656
          "url": {
657
            "description": "",
658
            "factory": "URL",
659
            "title": "URL",
660
            "type": "string",
661
            "widget": "url"
662
          }
663
        },
664
        "title": "Value",
665
        "type": "object"
666
      },
667
      "title": "URLs",
668
      "type": "array",
669
      "uniqueItems": false
670
    },
671
    "vat_number": {
672
      "behavior": "imio.directory.core.contents.contact.content.IContact",
673
      "description": "",
674
      "factory": "Text line (String)",
675
      "title": "Num\u00e9ro d'entreprise / TVA",
676
      "type": "string"
677
    },
678
    "zipcode": {
679
      "behavior": "imio.directory.core.contents.contact.content.IContact",
680
      "description": "",
681
      "factory": "Integer",
682
      "title": "Code postal",
683
      "type": "integer"
684
    }
685
  },
686
  "required": [
687
    "title",
688
    "type"
689
  ],
690
  "title": "Contact",
691
  "type": "object"
692
}
tests/data/plone_restapi/get_content_types.json
1
[
2
  {
3
    "@id": "https://annuaire.preprod.imio.be/@types/News Item",
4
    "addable": false,
5
    "title": "Actualit\u00e9"
6
  },
7
  {
8
    "@id": "https://annuaire.preprod.imio.be/@types/Collection",
9
    "addable": false,
10
    "title": "Collection"
11
  },
12
  {
13
    "@id": "https://annuaire.preprod.imio.be/@types/imio.directory.Contact",
14
    "addable": false,
15
    "title": "Contact"
16
  },
17
  {
18
    "@id": "https://annuaire.preprod.imio.be/@types/Folder",
19
    "addable": false,
20
    "title": "Dossier"
21
  },
22
  {
23
    "@id": "https://annuaire.preprod.imio.be/@types/imio.directory.Entity",
24
    "addable": true,
25
    "title": "Entit\u00e9"
26
  },
27
  {
28
    "@id": "https://annuaire.preprod.imio.be/@types/File",
29
    "addable": false,
30
    "title": "Fichier"
31
  },
32
  {
33
    "@id": "https://annuaire.preprod.imio.be/@types/Image",
34
    "addable": false,
35
    "title": "Image"
36
  },
37
  {
38
    "@id": "https://annuaire.preprod.imio.be/@types/Link",
39
    "addable": false,
40
    "title": "Lien"
41
  },
42
  {
43
    "@id": "https://annuaire.preprod.imio.be/@types/Document",
44
    "addable": false,
45
    "title": "Page Web"
46
  },
47
  {
48
    "@id": "https://annuaire.preprod.imio.be/@types/Event",
49
    "addable": false,
50
    "title": "\u00c9v\u00e9nement"
51
  }
52
]
tests/data/plone_restapi/get_field_choices.json
1
{
2
  "@id": "https://annuaire.preprod.imio.be/@vocabularies/imio.smartweb.vocabulary.Topics",
3
  "items": [
4
    {
5
      "title": "Activit\u00e9s et divertissement",
6
      "token": "entertainment"
7
    },
8
    {
9
      "title": "Agriculture",
10
      "token": "agriculture"
11
    },
12
    {
13
      "title": "Citoyennet\u00e9",
14
      "token": "citizenship"
15
    },
16
    {
17
      "title": "Culture",
18
      "token": "culture"
19
    },
20
    {
21
      "title": "\u00c9conomie",
22
      "token": "economics"
23
    },
24
    {
25
      "title": "\u00c9ducation",
26
      "token": "education"
27
    },
28
    {
29
      "title": "Environnement",
30
      "token": "environment"
31
    },
32
    {
33
      "title": "Habitat et urbanisme",
34
      "token": "habitat_town_planning"
35
    },
36
    {
37
      "title": "Mobilit\u00e9",
38
      "token": "mobility"
39
    },
40
    {
41
      "title": "Participation citoyenne",
42
      "token": "citizen_participation"
43
    },
44
    {
45
      "title": "Politique",
46
      "token": "politics"
47
    },
48
    {
49
      "title": "Sant\u00e9",
50
      "token": "health"
51
    },
52
    {
53
      "title": "S\u00e9curit\u00e9 et pr\u00e9vention",
54
      "token": "safety_prevention"
55
    },
56
    {
57
      "title": "Social",
58
      "token": "social"
59
    },
60
    {
61
      "title": "Sports",
62
      "token": "sports"
63
    },
64
    {
65
      "title": "Territoire et espace public",
66
      "token": "territory_public_space"
67
    },
68
    {
69
      "title": "Tourisme",
70
      "token": "tourism"
71
    }
72
  ],
73
  "items_total": 17
74
}
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/data/plone_restapi/workflow_publish.json
1
{
2
  "action": "publish",
3
  "actor": "brainelalleud-iateleservice",
4
  "comments": "",
5
  "review_state": "published",
6
  "time": "2021-10-18T12:40:11+00:00",
7
  "title": "Publi\u00e9"
8
}
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
    record = {
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
    connector.adapt_record(record, template)
201
    assert record == {
202
        'PLONE_id': 'plone id',
203
        'UID': 'plone uid',
204
        'id': 'plone uid',
205
        'text': 'plone id, plone uid, foo, bar',
206
        'original_id': 'foo',
207
        'original_text': 'bar',
208
    }
209

  
210

  
211
def test_get_token(app, connector):
212
    with pytest.raises(APIError):
213
        with utils.mock_url(url=connector.token_ws_url, response=TOKEN_ERROR_RESPONSE, status_code=404):
214
            connector.get_token()
215
    with utils.mock_url(url=connector.token_ws_url, response=TOKEN_RESPONSE) as mocked:
216
        result = connector.get_token()
217
        assert mocked.handlers[0].call['count'] == 1
218
        assert 'secret' in mocked.handlers[0].call['requests'][0].body
219
        assert result == 'acd...def'
220

  
221
        # make sure the token from cache is used
222
        connector.get_token()
223
        assert mocked.handlers[0].call['count'] == 1
224
        connector.get_token(True)
225
        assert mocked.handlers[0].call['count'] == 2
226

  
227

  
228
def test_fetch(app, connector, token):
229
    endpoint = utils.generic_endpoint_url('plone-restapi', 'fetch', slug=connector.slug)
230
    assert endpoint == '/plone-restapi/my_connector/fetch'
231
    url = connector.service_url + '/braine-l-alleud/dccd85d12cf54b6899dff41e5a56ee7f'
232
    params = {
233
        'uid': 'dccd85d12cf54b6899dff41e5a56ee7f',
234
        'uri': 'braine-l-alleud',
235
        'text_template': '{{ title }} ({{ topics.0.title }})',
236
    }
237
    with utils.mock_url(url=url, response=json_get_data('fetch')):
238
        resp = app.get(endpoint, params=params)
239
    assert not resp.json['err']
240
    assert resp.json['data']['id'] == 'dccd85d12cf54b6899dff41e5a56ee7f'
241
    assert resp.json['data']['text'] == 'Le Prisme (Activités et divertissement)'
242
    assert token.handlers[0].call['count'] == 1
243

  
244

  
245
def test_request_anonymously(app, connector, token):
246
    connector.token_ws_url = ''
247
    connector.save()
248
    endpoint = utils.generic_endpoint_url('plone-restapi', 'fetch', slug=connector.slug)
249
    assert endpoint == '/plone-restapi/my_connector/fetch'
250
    url = connector.service_url + '/braine-l-alleud/dccd85d12cf54b6899dff41e5a56ee7f'
251
    params = {
252
        'uid': 'dccd85d12cf54b6899dff41e5a56ee7f',
253
        'uri': 'braine-l-alleud',
254
        'text_template': '{{ title }} ({{ topics.0.title }})',
255
    }
256
    with utils.mock_url(url=url, response=json_get_data('fetch')):
257
        resp = app.get(endpoint, params=params)
258
    assert not resp.json['err']
259
    assert resp.json['data']['id'] == 'dccd85d12cf54b6899dff41e5a56ee7f'
260
    assert resp.json['data']['text'] == 'Le Prisme (Activités et divertissement)'
261
    assert token.handlers[0].call['count'] == 0
262

  
263

  
264
@pytest.mark.parametrize(
265
    'exception, status_code, response, err_desc',
266
    [
267
        [ConnectionError('plop'), None, None, 'plop'],
268
        [None, 200, 'not json', 'bad JSON response'],
269
        [None, 404, {'message': 'Resource not found: ...', 'type': 'NotFound'}, '404 Client Error'],
270
    ],
271
)
272
def test_request_error(app, connector, token, exception, status_code, response, err_desc):
273
    endpoint = utils.generic_endpoint_url('plone-restapi', 'fetch', slug=connector.slug)
274
    assert endpoint == '/plone-restapi/my_connector/fetch'
275
    url = connector.service_url + '/braine-l-alleud/plop'
276
    params = {
277
        'uid': 'plop',
278
        'uri': 'braine-l-alleud',
279
        'text_template': '{{ title }} ({{ PLONE_type }})',
280
    }
281
    with utils.mock_url(url=url, response=response, status_code=status_code, exception=exception):
282
        resp = app.get(endpoint, params=params)
283
    assert resp.json['err']
284
    assert err_desc in resp.json['err_desc']
285

  
286

  
287
def test_get_content_types(app, connector, token):
288
    endpoint = utils.generic_endpoint_url('plone-restapi', 'get_content_types', slug=connector.slug)
289
    assert endpoint == '/plone-restapi/my_connector/get_content_types'
290
    url = connector.service_url + '/@types'
291
    with utils.mock_url(url=url, response=json_get_data('get_content_types')):
292
        resp = app.get(endpoint)
293
    assert not resp.json['err']
294
    assert len(resp.json['data']) == 10
295
    assert resp.json['data'][2]['id'] == 'imio.directory.Contact'
296
    assert resp.json['data'][2]['text'] == 'Contact'
297

  
298

  
299
def test_get_content_type(app, connector, token):
300
    endpoint = utils.generic_endpoint_url('plone-restapi', 'get_content_type', slug=connector.slug)
301
    assert endpoint == '/plone-restapi/my_connector/get_content_type'
302
    url = connector.service_url + '/@types/imio.directory.Contact'
303
    params = {'id': 'imio.directory.Contact'}
304
    with utils.mock_url(url=url, response=json_get_data('get_content_type')):
305
        resp = app.get(endpoint, params=params)
306
    assert not resp.json['err']
307
    assert resp.json['data']['title'] == 'Contact'
308
    assert resp.json['data']['required'] == ['title', 'type']
309
    assert len(resp.json['data']['properties']) == 28
310
    assert (
311
        resp.json['data']['properties']['topics']['items']['vocabulary']['@id']
312
        == 'https://annuaire.preprod.imio.be/@vocabularies/imio.smartweb.vocabulary.Topics'
313
    )
314

  
315

  
316
def test_get_field_choices(app, connector, token):
317
    endpoint = utils.generic_endpoint_url('plone-restapi', 'get_field_choices', slug=connector.slug)
318
    assert endpoint == '/plone-restapi/my_connector/get_field_choices'
319
    url = connector.service_url + '/@vocabularies/imio.smartweb.vocabulary.Topics'
320
    params = {'id': 'imio.smartweb.vocabulary.Topics'}
321
    with utils.mock_url(url=url, response=json_get_data('get_field_choices')):
322
        resp = app.get(endpoint, params=params)
323
    assert not resp.json['err']
324
    assert len(resp.json['data']) == 17
325
    assert resp.json['data'][16]['id'] == 'tourism'
326
    assert resp.json['data'][16]['text'] == 'Tourisme'
327

  
328

  
329
def test_create(app, connector, token):
330
    endpoint = utils.generic_endpoint_url('plone-restapi', 'create', slug=connector.slug)
331
    assert endpoint == '/plone-restapi/my_connector/create'
332
    url = connector.service_url + '/braine-l-alleud'
333
    payload = {
334
        '@type': 'imio.directory.Contact',
335
        'title': "Test Entr'ouvert",
336
        'type': 'organization',
337
        'schedule': {},
338
        'topics/0/title': 'Tourisme',
339
        'topics/0/token': 'tourism',
340
        'image': {'filename': 'foo.jpg', 'content_type': 'image/jpeg', 'content': '...'},
341
    }
342
    with utils.mock_url(url=url, response=json_get_data('fetch'), status_code=201) as mocked:
343
        resp = app.post_json(endpoint + '?uri=braine-l-alleud&publish=false', params=payload)
344
        body = json.loads(mocked.handlers[0].call['requests'][1].body)
345
        assert body['topics'] == [{'title': 'Tourisme', 'token': 'tourism'}]
346
        assert body['image'] == {
347
            'filename': 'foo.jpg',
348
            'content_type': 'image/jpeg',
349
            'encoding': 'base64',
350
            'data': '...',
351
            'content-type': 'image/jpeg',
352
        }
353
    assert not resp.json['err']
354
    assert resp.json['data'] == {
355
        'uid': 'dccd85d12cf54b6899dff41e5a56ee7f',
356
        'created': True,
357
        'review_state': None,
358
    }
359

  
360

  
361
def test_create_and_publish(app, connector, token):
362
    endpoint = utils.generic_endpoint_url('plone-restapi', 'create', slug=connector.slug)
363
    assert endpoint == '/plone-restapi/my_connector/create'
364
    url = connector.service_url + '/braine-l-alleud'
365
    payload = {
366
        '@type': 'imio.directory.Contact',
367
        'title': "Test Entr'ouvert",
368
        'type': 'organization',
369
        'schedule': {},
370
        'topics/0/title': 'Tourisme',
371
        'topics/0/token': 'tourism',
372
        'image': {'filename': 'foo.jpg', 'content_type': 'image/jpeg', 'content': '...'},
373
    }
374
    publish_url = url + '/%s/@workflow/publish' % 'dccd85d12cf54b6899dff41e5a56ee7f'
375
    with utils.mock_url(url=url, response=json_get_data('fetch'), status_code=201) as mocked:
376
        with utils.mock_url(url=publish_url, response=json_get_data('workflow_publish'), status_code=200):
377

  
378
            resp = app.post_json(endpoint + '?uri=braine-l-alleud&publish=true', params=payload)
379
            body = json.loads(mocked.handlers[0].call['requests'][1].body)
380
            assert body['topics'] == [{'title': 'Tourisme', 'token': 'tourism'}]
381
            assert body['image'] == {
382
                'filename': 'foo.jpg',
383
                'content_type': 'image/jpeg',
384
                'encoding': 'base64',
385
                'data': '...',
386
                'content-type': 'image/jpeg',
387
            }
388
    assert not resp.json['err']
389
    assert resp.json['data'] == {
390
        'uid': 'dccd85d12cf54b6899dff41e5a56ee7f',
391
        'created': True,
392
        'review_state': 'published',
393
    }
394

  
395

  
396
def test_create_wrong_payload(app, connector, token):
397
    endpoint = utils.generic_endpoint_url('plone-restapi', 'create', slug=connector.slug)
398
    assert endpoint == '/plone-restapi/my_connector/create'
399
    url = connector.service_url + '/braine-l-alleud'
400
    payload = 'not json'
401
    resp = app.post(endpoint + '?uri=braine-l-alleud', params=payload, status=400)
402
    assert resp.json['err']
403
    assert resp.json['err_desc'] == 'Expecting value: line 1 column 1 (char 0)'
404
    assert resp.json['err_class'] == 'passerelle.apps.plone_restapi.models.ParameterTypeError'
405

  
406

  
407
def test_update(app, connector, token):
408
    endpoint = utils.generic_endpoint_url('plone-restapi', 'update', slug=connector.slug)
409
    assert endpoint == '/plone-restapi/my_connector/update'
410
    url = connector.service_url + '/braine-l-alleud/dccd85d12cf54b6899dff41e5a56ee7f'
411
    query_string = '?uri=braine-l-alleud&uid=dccd85d12cf54b6899dff41e5a56ee7f'
412
    payload = {
413
        'title': 'Test update',
414
        'topics/0/token': 'social',
415
        'image': {'filename': 'foo.jpg', 'content_type': 'image/jpeg', 'content': '...'},
416
    }
417
    with utils.mock_url(url=url, response='', status_code=204) as mocked:
418
        resp = app.post_json(endpoint + query_string, params=payload)
419
        body = json.loads(mocked.handlers[0].call['requests'][1].body)
420
        assert body['topics'] == [{'token': 'social'}]
421
        assert body['image'] == {
422
            'filename': 'foo.jpg',
423
            'content_type': 'image/jpeg',
424
            'encoding': 'base64',
425
            'data': '...',
426
            'content-type': 'image/jpeg',
427
        }
428
    assert not resp.json['err']
429
    assert resp.json['data'] == {'uid': 'dccd85d12cf54b6899dff41e5a56ee7f', 'updated': True}
430

  
431

  
432
def test_update_wrong_payload(app, connector, token):
433
    endpoint = utils.generic_endpoint_url('plone-restapi', 'update', slug=connector.slug)
434
    assert endpoint == '/plone-restapi/my_connector/update'
435
    url = connector.service_url + '/braine-l-alleud/dccd85d12cf54b6899dff41e5a56ee7f'
436
    query_string = '?uri=braine-l-alleud&uid=dccd85d12cf54b6899dff41e5a56ee7f'
437
    payload = 'not json'
438
    resp = app.post(endpoint + query_string, params=payload, status=400)
439
    assert resp.json['err']
440
    assert resp.json['err_desc'] == 'Expecting value: line 1 column 1 (char 0)'
441
    assert resp.json['err_class'] == 'passerelle.apps.plone_restapi.models.ParameterTypeError'
442

  
443

  
444
def test_remove(app, connector, token):
445
    endpoint = utils.generic_endpoint_url('plone-restapi', 'remove', slug=connector.slug)
446
    assert endpoint == '/plone-restapi/my_connector/remove'
447
    url = connector.service_url + '/braine-l-alleud/dccd85d12cf54b6899dff41e5a56ee7f'
448
    query_string = '?uri=braine-l-alleud&uid=dccd85d12cf54b6899dff41e5a56ee7f'
449
    with utils.mock_url(url=url, response='', status_code=204):
450
        resp = app.delete(endpoint + query_string)
451
    assert resp.json['data'] == {'uid': 'dccd85d12cf54b6899dff41e5a56ee7f', 'removed': True}
452
    assert not resp.json['err']
453

  
454

  
455
def test_search(app, connector, token):
456
    endpoint = utils.generic_endpoint_url('plone-restapi', 'search', slug=connector.slug)
457
    assert endpoint == '/plone-restapi/my_connector/search'
458
    url = connector.service_url + '/braine-l-alleud/@search'
459
    params = {
460
        'uri': 'braine-l-alleud',
461
        'text_template': '{{ title }} ({{ PLONE_type }})',
462
        'sort': 'UID',
463
        'order': False,
464
        'limit': 3,
465
    }
466
    qs = {}
467
    with utils.mock_url(url=url, response=json_get_data('q_search'), qs=qs):
468
        resp = app.get(endpoint, params=params)
469
    assert token.handlers[0].call['count'] == 1
470
    assert qs == {'sort_on': 'UID', 'sort_order': 'descending', 'b_size': '3', 'fullobjects': 'y'}
471
    assert not resp.json['err']
472
    assert len(resp.json['data']) == 3
473
    assert [(x['id'], x['text']) for x in resp.json['data']] == [
474
        (
475
            'dea9d26baab944beb7e54d4024d35a33',
476
            "Cabinet du Bourgmestre de la Commune de Braine-l'Alleud (imio.directory.Contact)",
477
        ),
478
        (
479
            '23a32197d6c841259963b43b24747854',
480
            "Académie de Musique de Braine-l'Alleud (imio.directory.Contact)",
481
        ),
482
        (
483
            'f82d2c079131433ea6ab20f9f7f49442',
484
            'Accueil et Orientation Volontariat (A.O.V.) (imio.directory.Contact)',
485
        ),
486
    ]
487

  
488

  
489
def test_search_using_q(app, connector, token):
490
    endpoint = utils.generic_endpoint_url('plone-restapi', 'search', slug=connector.slug)
491
    assert endpoint == '/plone-restapi/my_connector/search'
492
    url = connector.service_url + '/braine-l-alleud/@search'
493
    params = {
494
        'uri': 'braine-l-alleud',
495
        'text_template': '{{ title }} ({{ PLONE_type }})',
496
        'sort': 'title',
497
        'order': True,
498
        'limit': '3',
499
        'q': 'Página dentro',
500
    }
501
    qs = {}
502
    with utils.mock_url(url=url, response=json_get_data('q_search'), qs=qs):
503
        resp = app.get(endpoint, params=params)
504
    assert qs == {
505
        'SearchableText': 'Página dentro',
506
        'sort_on': 'title',
507
        'sort_order': 'ascending',
508
        'b_size': '3',
509
        'fullobjects': 'y',
510
    }
511
    assert not resp.json['err']
512
    assert len(resp.json['data']) == 3
513
    assert [(x['id'], x['text']) for x in resp.json['data']] == [
514
        (
515
            'dea9d26baab944beb7e54d4024d35a33',
516
            "Cabinet du Bourgmestre de la Commune de Braine-l'Alleud (imio.directory.Contact)",
517
        ),
518
        (
519
            '23a32197d6c841259963b43b24747854',
520
            "Académie de Musique de Braine-l'Alleud (imio.directory.Contact)",
521
        ),
522
        (
523
            'f82d2c079131433ea6ab20f9f7f49442',
524
            'Accueil et Orientation Volontariat (A.O.V.) (imio.directory.Contact)',
525
        ),
526
    ]
527

  
528

  
529
def test_search_using_id(app, connector, token):
530
    endpoint = utils.generic_endpoint_url('plone-restapi', 'search', slug=connector.slug)
531
    assert endpoint == '/plone-restapi/my_connector/search'
532
    url = connector.service_url + '/braine-l-alleud/@search'
533
    params = {
534
        'uri': 'braine-l-alleud',
535
        'text_template': '{{ title }} ({{ PLONE_type }})',
536
        'id': '9fbb2afd499e465983434f974fce8404',
537
    }
538
    qs = {}
539
    with utils.mock_url(url=url, response=json_get_data('id_search'), qs=qs):
540
        resp = app.get(endpoint, params=params)
541
    assert qs == {'UID': '9fbb2afd499e465983434f974fce8404', 'fullobjects': 'y'}
542
    assert len(resp.json['data']) == 1
543
    assert resp.json['data'][0]['text'] == "Académie de Musique de Braine-l'Alleud (imio.directory.Contact)"
544

  
545

  
546
def test_query_q(app, query, token):
547
    endpoint = '/plone-restapi/my_connector/q/my_query/'
548
    url = query.resource.service_url + '/braine-l-alleud/@search'
549
    params = {
550
        'limit': 3,
551
    }
552
    qs = {}
553
    with utils.mock_url(url=url, response=json_get_data('q_search'), qs=qs):
554
        resp = app.get(endpoint, params=params)
555
        assert qs == {
556
            'sort_on': 'UID',
557
            'sort_order': 'descending',
558
            'b_size': '3',
559
            'portal_type': 'Document',
560
            'review_state': 'published',
561
            'fullobjects': 'y',
562
        }
563
    assert not resp.json['err']
564
    assert len(resp.json['data']) == 3
565
    assert resp.json['meta'] == {'label': 'demo query', 'description': "Annuaire de Braine-l'Alleud"}
566

  
567

  
568
def test_query_q_using_q(app, query, token):
569
    endpoint = '/plone-restapi/my_connector/q/my_query/'
570
    url = query.resource.service_url + '/braine-l-alleud/@search'
571
    params = {
572
        'q': 'Página dentro',
573
    }
574
    qs = {}
575
    with utils.mock_url(url=url, response=json_get_data('q_search'), qs=qs):
576
        resp = app.get(endpoint, params=params)
577
        assert qs == {
578
            'SearchableText': 'Página dentro',
579
            'sort_on': 'UID',
580
            'sort_order': 'descending',
581
            'b_size': '3',
582
            'portal_type': 'Document',
583
            'review_state': 'published',
584
            'fullobjects': 'y',
585
        }
586
    assert not resp.json['err']
587
    assert len(resp.json['data']) == 3
588
    assert resp.json['meta'] == {'label': 'demo query', 'description': "Annuaire de Braine-l'Alleud"}
589

  
590

  
591
def test_query_q_using_id(app, query, token):
592
    endpoint = '/plone-restapi/my_connector/q/my_query/'
593
    url = query.resource.service_url + '/braine-l-alleud/@search'
594
    params = {
595
        'id': '9fbb2afd499e465983434f974fce8404',
596
    }
597
    qs = {}
598
    with utils.mock_url(url=url, response=json_get_data('id_search'), qs=qs):
599
        resp = app.get(endpoint, params=params)
600
    assert qs == {
601
        'UID': '9fbb2afd499e465983434f974fce8404',
602
        'fullobjects': 'y',
603
    }
604
    assert len(resp.json['data']) == 1
605
    assert resp.json['data'][0]['text'] == "Académie de Musique de Braine-l'Alleud (imio.directory.Contact)"
606
    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
-