plone-restapi: add a plone.restapi connector (#57258)

This commit is contained in:
Nicolas Roche 2021-09-21 17:16:06 +02:00
parent b07dba98ee
commit 7fc18248dd
18 changed files with 3293 additions and 1 deletions

View File

@ -0,0 +1,28 @@
# passerelle - uniform access to multiple data sources and services
# Copyright (C) 2021 Entr'ouvert
#
# This program is free software: you can redistribute it and/or modify it
# under the terms of the GNU Affero General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from django import forms
from passerelle.base.forms import BaseQueryFormMixin
from . import models
class QueryForm(BaseQueryFormMixin, forms.ModelForm):
class Meta:
model = models.Query
fields = '__all__'
exclude = ['resource']

View File

@ -0,0 +1,152 @@
# Generated by Django 2.2.19 on 2021-10-15 10:15
import django.db.models.deletion
from django.db import migrations, models
import passerelle.utils.templates
class Migration(migrations.Migration):
initial = True
dependencies = [
('base', '0029_auto_20210202_1627'),
]
operations = [
migrations.CreateModel(
name='PloneRestApi',
fields=[
(
'id',
models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
),
('title', models.CharField(max_length=50, verbose_name='Title')),
('slug', models.SlugField(unique=True, verbose_name='Identifier')),
('description', models.TextField(verbose_name='Description')),
(
'service_url',
models.CharField(
help_text='ex: https://demo.plone.org', max_length=256, verbose_name='Site URL'
),
),
(
'token_ws_url',
models.CharField(
blank=True,
help_text='ex: https://IDP/idp/oidc/token/ or unset for anonymous acces',
max_length=256,
verbose_name='Token webservice URL',
),
),
(
'client_id',
models.CharField(
blank=True,
help_text='OIDC id of the connector',
max_length=128,
verbose_name='OIDC id',
),
),
(
'client_secret',
models.CharField(
blank=True,
help_text='Share secret secret for webservice call authentication',
max_length=128,
verbose_name='Shared secret',
),
),
('username', models.CharField(blank=True, max_length=128, verbose_name='Username')),
('password', models.CharField(blank=True, max_length=128, verbose_name='Password')),
(
'users',
models.ManyToManyField(
blank=True,
related_name='_plonerestapi_users_+',
related_query_name='+',
to='base.ApiUser',
),
),
],
options={
'verbose_name': 'Plone REST API Web Service',
},
),
migrations.CreateModel(
name='Query',
fields=[
(
'id',
models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
),
('name', models.CharField(max_length=128, verbose_name='Name')),
('slug', models.SlugField(max_length=128, verbose_name='Slug')),
('description', models.TextField(blank=True, verbose_name='Description')),
(
'uri',
models.CharField(
blank=True, help_text='uri to query', max_length=128, verbose_name='Uri'
),
),
(
'text_template',
models.TextField(
blank=True,
help_text="Use Django's template syntax. Attributes can be accessed through {{ attributes.name }}",
validators=[passerelle.utils.templates.validate_template],
verbose_name='Text template',
),
),
(
'filter_expression',
models.TextField(
blank=True,
help_text='Specify more URL parameters (key=value) separated by lines',
verbose_name='filter',
),
),
(
'sort',
models.CharField(
blank=True,
help_text='Sorts results by the specified field',
max_length=256,
verbose_name='Sort field',
),
),
(
'order',
models.BooleanField(
default=True,
help_text='Unset to use descending sort order',
verbose_name='Ascending sort order',
),
),
(
'limit',
models.PositiveIntegerField(
default=10,
help_text='Number of results to return in a single call',
verbose_name='Limit',
),
),
(
'resource',
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name='queries',
to='plone_restapi.PloneRestApi',
verbose_name='Resource',
),
),
],
options={
'verbose_name': 'Query',
'ordering': ['name'],
'abstract': False,
'unique_together': {('resource', 'name'), ('resource', 'slug')},
},
),
]

View File

@ -0,0 +1,441 @@
# passerelle - uniform access to multiple data sources and services
# Copyright (C) 2021 Entr'ouvert
#
# This program is free software: you can redistribute it and/or modify it
# under the terms of the GNU Affero General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from urllib.parse import parse_qsl, urlsplit, urlunsplit
from django.core.cache import cache
from django.db import models
from django.shortcuts import get_object_or_404
from django.urls import reverse
from django.utils.translation import ugettext_lazy as _
from requests import RequestException
from passerelle.base.models import BaseQuery, BaseResource
from passerelle.compat import json_loads
from passerelle.utils.api import endpoint
from passerelle.utils.http_authenticators import HttpBearerAuth
from passerelle.utils.json import unflatten
from passerelle.utils.jsonresponse import APIError
from passerelle.utils.templates import render_to_string, validate_template
class ParameterTypeError(Exception):
http_status = 400
log_error = False
class PloneRestApi(BaseResource):
service_url = models.CharField(
_('Site URL'),
max_length=256,
blank=False,
help_text=_('ex: https://demo.plone.org'),
)
token_ws_url = models.CharField(
_('Token webservice URL'),
max_length=256,
blank=True,
help_text=_('ex: https://IDP/idp/oidc/token/ or unset for anonymous acces'),
)
client_id = models.CharField(
_('OIDC id'),
max_length=128,
blank=True,
help_text=_('OIDC id of the connector'),
)
client_secret = models.CharField(
_('Shared secret'),
max_length=128,
blank=True,
help_text=_('Share secret secret for webservice call authentication'),
)
username = models.CharField(_('Username'), max_length=128, blank=True)
password = models.CharField(_('Password'), max_length=128, blank=True)
category = _('Data Sources')
plone_keys_to_rename = ['@id', '@type', '@components']
class Meta:
verbose_name = _('Plone REST API Web Service')
def export_json(self):
data = super(PloneRestApi, self).export_json()
data['queries'] = [query.export_json() for query in self.queries.all()]
return data
@classmethod
def import_json_real(cls, overwrite, instance, data, **kwargs):
data_queries = data.pop('queries', [])
instance = super(PloneRestApi, cls).import_json_real(overwrite, instance, data, **kwargs)
queries = []
if instance and overwrite:
Query.objects.filter(resource=instance).delete()
for data_query in data_queries:
query = Query.import_json(data_query)
query.resource = instance
queries.append(query)
Query.objects.bulk_create(queries)
return instance
def adapt_id_and_type_plone_attributes(self, data):
"""Rename keys starting with '@' from plone response
ex: '@id' is renammed into 'PLONE_id'"""
if isinstance(data, list):
for value in list(data):
self.adapt_id_and_type_plone_attributes(value)
elif isinstance(data, dict):
for key, value in list(data.items()):
self.adapt_id_and_type_plone_attributes(value)
if key in self.plone_keys_to_rename and key[0] == '@':
data['PLONE_%s' % key[1:]] = value
del data[key]
def adapt_payload(self, payload):
# convert image format
for file_field in payload.values():
if isinstance(file_field, dict) and file_field.get('filename'):
file_field['encoding'] = 'base64'
file_field['data'] = file_field['content']
file_field['content-type'] = file_field['content_type']
del file_field['content']
def adapt_record(
self,
record,
text_template='{{ id }}',
id_key='UID',
):
self.adapt_id_and_type_plone_attributes(record)
for key, value in list(record.items()):
# backup original id and text fields
if key in ('id', 'text'):
key = 'original_%s' % key
record[key] = value
record['id'] = record.get(id_key)
record['text'] = render_to_string(text_template, record).strip()
def get_token(self, renew=False):
token_key = 'plone-restapi-%s-token' % self.id
if not renew and cache.get(token_key):
return cache.get(token_key)
payload = {
'grant_type': 'password',
'client_id': str(self.client_id),
'client_secret': str(self.client_secret),
'username': self.username,
'password': self.password,
'scope': ['openid'],
}
headers = {
'Content-Type': 'application/x-www-form-urlencoded',
}
response = self.requests.post(self.token_ws_url, headers=headers, data=payload)
if not response.status_code // 100 == 2:
raise APIError(response.content)
token = response.json().get('id_token')
cache.set(token_key, token, 30)
return token
def request(self, uri='', uid='', method='GET', params=None, json=None):
scheme, netloc, path, query, fragment = urlsplit(self.service_url)
if uri:
path += '/%s' % uri
if uid:
path += '/%s' % uid
url = urlunsplit((scheme, netloc, path, '', fragment))
headers = {'Accept': 'application/json'}
auth = HttpBearerAuth(self.get_token()) if self.token_ws_url else None
try:
response = self.requests.request(
method=method, url=url, headers=headers, params=params, json=json, auth=auth
)
except RequestException as e:
raise APIError('PloneRestApi: %s' % e)
json_response = None
if response.status_code != 204: # No Content
try:
json_response = response.json()
except ValueError as e:
raise APIError('PloneRestApi: bad JSON response')
try:
response.raise_for_status()
except RequestException as e:
raise APIError('PloneRestApi: %s "%s"' % (e, json_response))
return json_response
def call_search(
self,
uri='',
text_template='',
filter_expression='',
sort=None,
order=True,
limit=None,
id=None,
q=None,
):
query = urlsplit(self.service_url).query
params = dict(parse_qsl(query))
if id:
params['UID'] = id
else:
if q is not None:
params['SearchableText'] = q
if sort:
params['sort_on'] = sort
if order:
params['sort_order'] = 'ascending'
else:
params['sort_order'] = 'descending'
if limit:
params['b_size'] = limit
params.update(parse_qsl(filter_expression))
params['fullobjects'] = 'y'
response = self.request(uri=uri, uid='@search', method='GET', params=params)
for record in response.get('items') or []:
self.adapt_record(record, text_template)
return response.get('items') or []
@endpoint(
perm='can_access',
description=_('Get content types'),
display_order=1,
)
def get_content_types(self, request):
response = self.request(uri='@types', method='GET')
for record in response or []:
self.adapt_record(record, '{{ title }}', id_key='PLONE_id')
record['id'] = record['id'].split('/')[-1]
return {'data': response or []}
@endpoint(
perm='can_access',
description=_('Get content type'),
parameters={
'id': {'description': _('Content type identifier'), 'example_value': 'imio.directory.Contact'}
},
display_order=2,
)
def get_content_type(self, request, id):
response = self.request(uri='@types', uid=id, method='GET')
return {'data': response}
@endpoint(
perm='can_access',
description=_('Get field choices'),
parameters={
'id': {'description': _('Field identifier'), 'example_value': 'imio.smartweb.vocabulary.Topics'}
},
display_order=3,
)
def get_field_choices(self, request, id):
response = self.request(uri='@vocabularies', uid=id, method='GET')
for record in response.get('items') or []:
self.adapt_record(record, '{{ title }}', id_key='token')
return {'data': response.get('items') or []}
@endpoint(
perm='can_access',
description=_('Fetch'),
parameters={
'uri': {'description': _('Uri')},
'uid': {'description': _('Uid')},
'text_template': {'description': _('Text template')},
},
display_order=4,
)
def fetch(self, request, uid, uri='', text_template=''):
response = self.request(uri=uri, uid=uid, method='GET')
self.adapt_record(response, text_template)
return {'data': response}
@endpoint(
perm='can_access',
description=_('Creates'),
parameters={
'uri': {'description': _('Uri')},
'publish': {'description': _('Do publish content (default is false)')},
},
methods=['post'],
display_order=5,
)
def create(self, request, uri, publish=False):
try:
post_data = json_loads(request.body)
except ValueError as e:
raise ParameterTypeError(str(e))
post_data = unflatten(post_data)
self.adapt_payload(post_data)
response = self.request(uri=uri, method='POST', json=post_data)
uid = response.get('UID')
review_state = None
if uid and bool(publish):
uri += '/%s' % uid
response = self.request(uri, uid='@workflow/publish', method='POST')
review_state = response.get('review_state')
return {'data': {'uid': uid, 'created': True, 'review_state': review_state}}
@endpoint(
perm='can_access',
description=_('Update'),
parameters={
'uri': {'description': _('Uri')},
'uid': {'description': _('Uid')},
},
methods=['post'],
display_order=6,
)
def update(self, request, uid, uri=''):
try:
post_data = json_loads(request.body)
except ValueError as e:
raise ParameterTypeError(str(e))
post_data = unflatten(post_data)
self.adapt_payload(post_data)
self.request(uri=uri, uid=uid, method='PATCH', json=post_data)
return {'data': {'uid': uid, 'updated': True}}
@endpoint(
perm='can_access',
description=_('Remove'),
parameters={
'uri': {'description': _('Uri')},
'uid': {'description': _('Uid')},
},
methods=['delete'],
display_order=7,
)
def remove(self, request, uid, uri=''):
self.request(method='DELETE', uri=uri, uid=uid)
return {'data': {'uid': uid, 'removed': True}}
@endpoint(
perm='can_access',
description=_('Search'),
parameters={
'uri': {'description': _('Uri')},
'text_template': {'description': _('Text template')},
'sort': {'description': _('Sort field')},
'order': {'description': _('Ascending sort order'), 'type': 'bool'},
'limit': {'description': _('Maximum items')},
'id': {'description': _('Record identifier')},
'q': {'description': _('Full text query')},
},
)
def search(
self,
request,
uri='',
text_template='',
sort=None,
order=True,
limit=None,
id=None,
q=None,
**kwargs,
):
result = self.call_search(uri, text_template, '', sort, order, limit, id, q)
return {'data': result}
@endpoint(
name='q',
description=_('Query'),
pattern=r'^(?P<query_slug>[\w:_-]+)/$',
perm='can_access',
show=False,
)
def q(self, request, query_slug, **kwargs):
query = get_object_or_404(Query, resource=self, slug=query_slug)
result = query.q(request, **kwargs)
meta = {'label': query.name, 'description': query.description}
return {'data': result, 'meta': meta}
def create_query_url(self):
return reverse('plone-restapi-query-new', kwargs={'slug': self.slug})
class Query(BaseQuery):
resource = models.ForeignKey(
to=PloneRestApi, related_name='queries', verbose_name=_('Resource'), on_delete=models.CASCADE
)
uri = models.CharField(
verbose_name=_('Uri'),
max_length=128,
help_text=_('uri to query'),
blank=True,
)
text_template = models.TextField(
verbose_name=_('Text template'),
help_text=_("Use Django's template syntax. Attributes can be accessed through {{ attributes.name }}"),
validators=[validate_template],
blank=True,
)
filter_expression = models.TextField(
verbose_name=_('filter'),
help_text=_('Specify more URL parameters (key=value) separated by lines'),
blank=True,
)
sort = models.CharField(
verbose_name=_('Sort field'),
help_text=_('Sorts results by the specified field'),
max_length=256,
blank=True,
)
order = models.BooleanField(
verbose_name=_('Ascending sort order'),
help_text=_("Unset to use descending sort order"),
default=True,
)
limit = models.PositiveIntegerField(
default=10,
verbose_name=_('Limit'),
help_text=_('Number of results to return in a single call'),
)
delete_view = 'plone-restapi-query-delete'
edit_view = 'plone-restapi-query-edit'
def q(self, request, **kwargs):
return self.resource.call_search(
uri=self.uri,
text_template=self.text_template,
filter_expression='&'.join(
[x.strip() for x in str(self.filter_expression).splitlines() if x.strip()]
),
sort=self.sort,
order=self.order,
limit=self.limit,
id=kwargs.get('id'),
q=kwargs.get('q'),
)
def as_endpoint(self):
endpoint = super(Query, self).as_endpoint(path=self.resource.q.endpoint_info.name)
search_endpoint = self.resource.search.endpoint_info
endpoint.func = search_endpoint.func
endpoint.show_undocumented_params = False
# Copy generic params descriptions from original endpoint
# if they are not overloaded by the query
for param in search_endpoint.parameters:
if param in ('uri', 'text_template') and getattr(self, param):
continue
endpoint.parameters[param] = search_endpoint.parameters[param]
return endpoint

View File

@ -0,0 +1,31 @@
# passerelle - uniform access to multiple data sources and services
# Copyright (C) 2021 Entr'ouvert
#
# This program is free software: you can redistribute it and/or modify it
# under the terms of the GNU Affero General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from django.conf.urls import url
from . import views
management_urlpatterns = [
url(r'^(?P<slug>[\w,-]+)/query/new/$', views.QueryNew.as_view(), name='plone-restapi-query-new'),
url(
r'^(?P<slug>[\w,-]+)/query/(?P<pk>\d+)/$', views.QueryEdit.as_view(), name='plone-restapi-query-edit'
),
url(
r'^(?P<slug>[\w,-]+)/query/(?P<pk>\d+)/delete/$',
views.QueryDelete.as_view(),
name='plone-restapi-query-delete',
),
]

View File

@ -0,0 +1,44 @@
# passerelle - uniform access to multiple data sources and services
# Copyright (C) 2021 Entr'ouvert
#
# This program is free software: you can redistribute it and/or modify it
# under the terms of the GNU Affero General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from django.views.generic import CreateView, DeleteView, UpdateView
from passerelle.base.mixins import ResourceChildViewMixin
from . import models
from .forms import QueryForm
class QueryNew(ResourceChildViewMixin, CreateView):
model = models.Query
form_class = QueryForm
template_name = "passerelle/manage/resource_child_form.html"
def get_form_kwargs(self):
kwargs = super(QueryNew, self).get_form_kwargs()
kwargs['instance'] = self.model(resource=self.resource)
return kwargs
class QueryEdit(ResourceChildViewMixin, UpdateView):
model = models.Query
form_class = QueryForm
template_name = "passerelle/manage/resource_child_form.html"
class QueryDelete(ResourceChildViewMixin, DeleteView):
model = models.Query
template_name = "passerelle/manage/resource_child_confirm_delete.html"

View File

@ -159,6 +159,7 @@ INSTALLED_APPS = (
'passerelle.apps.oxyd',
'passerelle.apps.phonecalls',
'passerelle.apps.photon',
'passerelle.apps.plone_restapi',
'passerelle.apps.sector',
'passerelle.apps.solis',
'passerelle.apps.twilio',

View File

@ -186,6 +186,10 @@ li.connector.cryptor a::before {
content: "\f023"; /* lock */
}
li.connector.plonerestapi a::before {
content: "\f1c0"; /* database */
}
li.connector.opendatasoft a::before {
content: "\f1c0"; /* database */
}

View File

@ -0,0 +1,262 @@
{
"@components": {
"actions": {
"@id": "https://annuaire.preprod.imio.be/braine-l-alleud/c44f1b32f0ce436eb7a042ca8933b287/@actions"
},
"breadcrumbs": {
"@id": "https://annuaire.preprod.imio.be/braine-l-alleud/c44f1b32f0ce436eb7a042ca8933b287/@breadcrumbs"
},
"contextnavigation": {
"@id": "https://annuaire.preprod.imio.be/braine-l-alleud/c44f1b32f0ce436eb7a042ca8933b287/@contextnavigation"
},
"navigation": {
"@id": "https://annuaire.preprod.imio.be/braine-l-alleud/c44f1b32f0ce436eb7a042ca8933b287/@navigation"
},
"types": {
"@id": "https://annuaire.preprod.imio.be/braine-l-alleud/c44f1b32f0ce436eb7a042ca8933b287/@types"
},
"workflow": {
"@id": "https://annuaire.preprod.imio.be/braine-l-alleud/c44f1b32f0ce436eb7a042ca8933b287/@workflow"
}
},
"@id": "https://annuaire.preprod.imio.be/braine-l-alleud/c44f1b32f0ce436eb7a042ca8933b287",
"@type": "imio.directory.Contact",
"UID": "dccd85d12cf54b6899dff41e5a56ee7f",
"allow_discussion": false,
"city": "Braine-l'Alleud",
"complement": null,
"country": {
"title": "Belgique",
"token": "be"
},
"created": "2021-07-28T07:53:01+00:00",
"description": "Ouvert du lundi au samedi toute l'ann\u00e9e.\r\n\r\nContact : Thierry Vou\u00e9",
"exceptional_closure": [],
"facilities": null,
"geolocation": {
"latitude": 50.4989185,
"longitude": 4.7184485
},
"iam": [
{
"title": "Jeune",
"token": "young"
}
],
"id": "c44f1b32f0ce436eb7a042ca8933b287",
"image": null,
"image_caption": null,
"is_folderish": true,
"is_geolocated": true,
"items": [],
"items_total": 0,
"language": {
"title": "Fran\u00e7ais",
"token": "fr"
},
"layout": "view",
"logo": {
"content-type": "image/png",
"download": "https://annuaire.preprod.imio.be/braine-l-alleud/c44f1b32f0ce436eb7a042ca8933b287/@@images/d4e7b99f-98c2-4c85-87fa-7fb0ebb31c16.png",
"filename": "maison jeunes le prisme.png",
"height": 1536,
"scales": {
"banner": {
"download": "https://annuaire.preprod.imio.be/braine-l-alleud/c44f1b32f0ce436eb7a042ca8933b287/@@images/02ef9609-1182-4a3d-9ce6-eb0449309b55.png",
"height": 590,
"width": 1920
},
"extralarge": {
"download": "https://annuaire.preprod.imio.be/braine-l-alleud/c44f1b32f0ce436eb7a042ca8933b287/@@images/5be772a5-dc46-417c-aab6-ae4352536a48.png",
"height": 405,
"width": 1320
},
"icon": {
"download": "https://annuaire.preprod.imio.be/braine-l-alleud/c44f1b32f0ce436eb7a042ca8933b287/@@images/21e0bb31-8a45-4e42-a4ef-60f8070d7ef9.png",
"height": 9,
"width": 32
},
"large": {
"download": "https://annuaire.preprod.imio.be/braine-l-alleud/c44f1b32f0ce436eb7a042ca8933b287/@@images/1cd53cba-e9ff-4abb-a43a-9e859c6959dc.png",
"height": 236,
"width": 768
},
"listing": {
"download": "https://annuaire.preprod.imio.be/braine-l-alleud/c44f1b32f0ce436eb7a042ca8933b287/@@images/09c2223f-0fb0-4fcb-bcb2-2501af8543cd.png",
"height": 4,
"width": 16
},
"medium": {
"download": "https://annuaire.preprod.imio.be/braine-l-alleud/c44f1b32f0ce436eb7a042ca8933b287/@@images/9db56c58-9cf9-4b72-afdf-1e2d167e7fee.png",
"height": 184,
"width": 600
},
"mini": {
"download": "https://annuaire.preprod.imio.be/braine-l-alleud/c44f1b32f0ce436eb7a042ca8933b287/@@images/67a49d61-159d-4ca7-9e06-0de39838e5c9.png",
"height": 61,
"width": 200
},
"preview": {
"download": "https://annuaire.preprod.imio.be/braine-l-alleud/c44f1b32f0ce436eb7a042ca8933b287/@@images/2ff94086-ce56-45cc-b293-fd796931dbe5.png",
"height": 123,
"width": 400
},
"thumb": {
"download": "https://annuaire.preprod.imio.be/braine-l-alleud/c44f1b32f0ce436eb7a042ca8933b287/@@images/0ac5cdc9-c5c2-4aed-8e1d-9260311236f2.png",
"height": 39,
"width": 128
},
"tile": {
"download": "https://annuaire.preprod.imio.be/braine-l-alleud/c44f1b32f0ce436eb7a042ca8933b287/@@images/e439acab-b379-46e4-b939-52f5a7ba67a6.png",
"height": 19,
"width": 64
}
},
"size": 1268077,
"width": 4995
},
"mails": [
{
"label": null,
"mail_address": "info@leprisme.be",
"type": "work"
}
],
"modified": "2021-10-01T17:07:32+00:00",
"multi_schedule": [],
"next_item": {
"@id": "https://annuaire.preprod.imio.be/braine-l-alleud/c812043a3ed44e00815e342de34a61c9",
"@type": "imio.directory.Contact",
"description": "",
"title": "Parc \u00e0 conteneurs de Braine-l'Alleud"
},
"number": "103",
"parent": {
"@id": "https://annuaire.preprod.imio.be/braine-l-alleud",
"@type": "imio.directory.Entity",
"description": "",
"review_state": "published",
"title": "Braine-l'Alleud"
},
"phones": [
{
"label": null,
"number": "+3223870926",
"type": "work"
},
{
"label": null,
"number": "+32475916819",
"type": "cell"
}
],
"previous_item": {
"@id": "https://annuaire.preprod.imio.be/braine-l-alleud/3d595a01fa814af09cf9aac35a11b9b0",
"@type": "imio.directory.Contact",
"description": "",
"title": "Kinepolis Imagibraine"
},
"review_state": "published",
"schedule": {
"friday": {
"afternoonend": "",
"afternoonstart": "",
"comment": "",
"morningend": "17:00",
"morningstart": "10:00"
},
"monday": {
"afternoonend": "",
"afternoonstart": "",
"comment": "",
"morningend": "17:00",
"morningstart": "10:00"
},
"saturday": {
"afternoonend": "",
"afternoonstart": "",
"comment": "",
"morningend": "",
"morningstart": ""
},
"sunday": {
"afternoonend": "",
"afternoonstart": "",
"comment": "",
"morningend": "",
"morningstart": ""
},
"thursday": {
"afternoonend": "",
"afternoonstart": "",
"comment": "",
"morningend": "17:00",
"morningstart": "10:00"
},
"tuesday": {
"afternoonend": "",
"afternoonstart": "",
"comment": "",
"morningend": "17:00",
"morningstart": "10:00"
},
"wednesday": {
"afternoonend": "",
"afternoonstart": "",
"comment": "",
"morningend": "17:00",
"morningstart": "10:00"
}
},
"selected_entities": [
{
"title": "Braine-l'Alleud",
"token": "f571b73a16f34832a5fdd3683533b3cc"
}
],
"street": "Avenue Alphonse Allard",
"subjects": [
"mj"
],
"subtitle": "Maison de Jeunes de Braine-l'Alleud",
"taxonomy_contact_category": [
{
"title": "Loisirs \u00bb Mouvements et associations \u00bb Jeunesse",
"token": "oqa05qwd45"
}
],
"title": "Le Prisme",
"topics": [
{
"title": "Activit\u00e9s et divertissement",
"token": "entertainment"
},
{
"title": "Culture",
"token": "culture"
},
{
"title": "Sports",
"token": "sports"
},
{
"title": "Participation citoyenne",
"token": "citizen_participation"
}
],
"type": {
"title": "Organisation (service administratif, commerce, profession lib\u00e9rale, club sportif, association, etc.)",
"token": "organization"
},
"urls": [
{
"type": "website",
"url": "http://www.leprisme.be/"
}
],
"vat_number": null,
"version": "current",
"working_copy": null,
"working_copy_of": null,
"zipcode": 1420
}

View File

@ -0,0 +1,692 @@
{
"fieldsets": [
{
"behavior": "plone",
"fields": [
"title",
"description",
"type",
"subtitle",
"logo",
"image",
"image_caption",
"geolocation"
],
"id": "default",
"title": "G\u00e9n\u00e9ral"
},
{
"behavior": "plone",
"fields": [
"street",
"number",
"complement",
"zipcode",
"city",
"country"
],
"id": "address",
"title": "Adresse"
},
{
"behavior": "plone",
"fields": [
"vat_number",
"phones",
"mails",
"urls"
],
"id": "contact_informations",
"title": "Coordonn\u00e9es"
},
{
"behavior": "plone.dexterity.schema.generated",
"fields": [
"selected_entities",
"facilities",
"taxonomy_contact_category",
"topics",
"iam",
"subjects",
"language"
],
"id": "categorization",
"title": "Cat\u00e9gorisation"
},
{
"behavior": "plone",
"fields": [
"schedule",
"multi_schedule"
],
"id": "multischedule",
"title": "Horaire multiple"
},
{
"behavior": "plone",
"fields": [
"exceptional_closure"
],
"id": "exceptionalclosure",
"title": "Fermeture exceptionnelle"
}
],
"layouts": [
"view"
],
"properties": {
"city": {
"behavior": "imio.directory.core.contents.contact.content.IContact",
"description": "",
"factory": "Text line (String)",
"title": "Commune",
"type": "string"
},
"complement": {
"behavior": "imio.directory.core.contents.contact.content.IContact",
"description": "",
"factory": "Text line (String)",
"title": "Compl\u00e9ment",
"type": "string"
},
"country": {
"behavior": "imio.directory.core.contents.contact.content.IContact",
"description": "",
"factory": "Choice",
"title": "Pays",
"type": "string",
"vocabulary": {
"@id": "https://annuaire.preprod.imio.be/@vocabularies/imio.smartweb.vocabulary.Countries"
}
},
"description": {
"behavior": "plone.basic",
"description": "Utilis\u00e9 dans les pages listant des \u00e9l\u00e9ments et les pages de r\u00e9sultats de recherche.",
"factory": "Text",
"maxLength": 700,
"title": "Description",
"type": "string",
"widget": "textarea"
},
"exceptional_closure": {
"additionalItems": true,
"behavior": "collective.schedulefield.behavior.IExceptionalClosureContent",
"description": "",
"factory": "Collection",
"items": {
"description": "",
"factory": "File",
"properties": {
"ExceptionalClosure.date": {
"description": "",
"factory": "Date",
"title": "Date",
"type": "string",
"widget": "date"
},
"ExceptionalClosure.title": {
"description": "",
"factory": "Text",
"title": "Titre",
"type": "string",
"widget": "textarea"
}
},
"title": "",
"type": "object"
},
"title": "Dates",
"type": "array",
"uniqueItems": false
},
"facilities": {
"additionalItems": true,
"behavior": "imio.directory.core.contents.contact.content.IContact",
"description": "Important! Ces cat\u00e9gories permettent de mettre en avant et de g\u00e9olocaliser certains services de base",
"factory": "List",
"items": {
"description": "",
"factory": "Choice",
"title": "",
"type": "string",
"vocabulary": {
"@id": "https://annuaire.preprod.imio.be/@vocabularies/imio.directory.vocabulary.Facilities"
}
},
"title": "Facilit\u00e9s",
"type": "array",
"uniqueItems": true
},
"geolocation": {
"behavior": "geolocatable",
"description": "Cliquez sur la carte pour s\u00e9lectionner une localisation, ou utilisez le champ texte pour rechercher par adresse.",
"factory": "File",
"properties": {
"geolocation.latitude": {
"description": "",
"factory": "Floating-point number",
"title": "Latitude",
"type": "number"
},
"geolocation.longitude": {
"description": "",
"factory": "Floating-point number",
"title": "Longitude",
"type": "number"
}
},
"title": "G\u00e9olocalisation",
"type": "object"
},
"iam": {
"additionalItems": true,
"behavior": "imio.smartweb.iam",
"description": "Important! Ces cat\u00e9gories sont utilis\u00e9es pour cr\u00e9er des listes accessibles via le menu navigation",
"factory": "List",
"items": {
"description": "",
"factory": "Choice",
"title": "",
"type": "string",
"vocabulary": {
"@id": "https://annuaire.preprod.imio.be/@vocabularies/imio.smartweb.vocabulary.IAm"
}
},
"title": "Je suis",
"type": "array",
"uniqueItems": true
},
"image": {
"behavior": "plone.leadimage",
"description": "",
"factory": "Image",
"properties": {
"image.contentType": {
"default": "",
"description": "The content type identifies the type of data.",
"factory": "Text line (String)",
"title": "Content Type",
"type": "string"
},
"image.data": {
"default": "",
"description": "The actual content of the object.",
"factory": "Text line (String)",
"title": "Data",
"type": "string"
},
"image.filename": {
"description": "",
"factory": "Text line (String)",
"title": "Filename",
"type": "string"
}
},
"title": "Image principale",
"type": "object"
},
"image_caption": {
"behavior": "plone.leadimage",
"description": "",
"factory": "Text line (String)",
"title": "L\u00e9gende de l'image principale",
"type": "string"
},
"language": {
"behavior": "plone.categorization",
"default": "fr",
"description": "",
"factory": "Choice",
"title": "Langue",
"type": "string",
"vocabulary": {
"@id": "https://annuaire.preprod.imio.be/@vocabularies/plone.app.vocabularies.SupportedContentLanguages"
}
},
"logo": {
"behavior": "imio.directory.core.contents.contact.content.IContact",
"description": "",
"factory": "Image",
"properties": {
"logo.contentType": {
"default": "",
"description": "The content type identifies the type of data.",
"factory": "Text line (String)",
"title": "Content Type",
"type": "string"
},
"logo.data": {
"default": "",
"description": "The actual content of the object.",
"factory": "Text line (String)",
"title": "Data",
"type": "string"
},
"logo.filename": {
"description": "",
"factory": "Text line (String)",
"title": "Filename",
"type": "string"
}
},
"title": "Logo",
"type": "object"
},
"mails": {
"additionalItems": true,
"behavior": "imio.directory.core.contents.contact.content.IContact",
"description": "",
"factory": "List",
"items": {
"description": "",
"factory": "File",
"properties": {
"label": {
"description": "",
"factory": "Text line (String)",
"title": "Intitul\u00e9 (Secr\u00e9tariat, Bureau de la direction, Ventes, ...)",
"type": "string"
},
"mail_address": {
"description": "",
"factory": "Email",
"title": "Adresse e-mail",
"type": "string",
"widget": "email"
},
"type": {
"description": "",
"factory": "Choice",
"title": "Type",
"type": "string",
"vocabulary": {
"@id": "https://annuaire.preprod.imio.be/@vocabularies/imio.directory.vocabulary.MailTypes"
}
}
},
"title": "Value",
"type": "object"
},
"title": "E-mails",
"type": "array",
"uniqueItems": false
},
"multi_schedule": {
"additionalItems": true,
"behavior": "collective.schedulefield.behavior.IMultiScheduledContent",
"description": "",
"factory": "Collection",
"items": {
"description": "",
"factory": "File",
"properties": {
"MultiSchedule.dates": {
"additionalItems": true,
"description": "",
"factory": "List",
"items": {
"description": "",
"factory": "File",
"properties": {
"DateRange.end_date": {
"description": "",
"factory": "Date",
"title": "Date de fin",
"type": "string",
"widget": "date"
},
"DateRange.start_date": {
"description": "",
"factory": "Date",
"title": "Date de d\u00e9but",
"type": "string",
"widget": "date"
}
},
"title": "",
"type": "object"
},
"title": "Dates",
"type": "array",
"uniqueItems": false
},
"MultiSchedule.schedule": {
"description": "",
"key_type": {
"additional": {},
"schema": {
"description": "",
"factory": "Text line (String)",
"title": "",
"type": "string"
}
},
"title": "Horaire",
"type": "dict",
"value_type": {
"additional": {
"key_type": {
"additional": {},
"schema": {
"description": "",
"factory": "Text line (String)",
"title": "",
"type": "string"
}
},
"value_type": {
"additional": {},
"schema": {
"description": "",
"factory": "Text line (String)",
"title": "",
"type": "string"
}
}
},
"schema": {
"description": "",
"key_type": {
"additional": {},
"schema": {
"description": "",
"factory": "Text line (String)",
"title": "",
"type": "string"
}
},
"title": "",
"type": "dict",
"value_type": {
"additional": {},
"schema": {
"description": "",
"factory": "Text line (String)",
"title": "",
"type": "string"
}
}
}
}
},
"MultiSchedule.title": {
"description": "",
"factory": "Text",
"title": "Titre",
"type": "string",
"widget": "textarea"
}
},
"title": "",
"type": "object"
},
"title": "Horaire multiple",
"type": "array",
"uniqueItems": false
},
"number": {
"behavior": "imio.directory.core.contents.contact.content.IContact",
"description": "",
"factory": "Text line (String)",
"title": "Num\u00e9ro",
"type": "string"
},
"phones": {
"additionalItems": true,
"behavior": "imio.directory.core.contents.contact.content.IContact",
"description": "",
"factory": "List",
"items": {
"description": "",
"factory": "File",
"properties": {
"label": {
"description": "",
"factory": "Text line (String)",
"title": "Intitul\u00e9 (direction, num\u00e9ro principal, ...)",
"type": "string"
},
"number": {
"description": "",
"factory": "Text line (String)",
"title": "Num\u00e9ro (format: +32475010203)",
"type": "string"
},
"type": {
"description": "",
"factory": "Choice",
"title": "Type",
"type": "string",
"vocabulary": {
"@id": "https://annuaire.preprod.imio.be/@vocabularies/imio.directory.vocabulary.PhoneTypes"
}
}
},
"title": "Value",
"type": "object"
},
"title": "T\u00e9l\u00e9phones",
"type": "array",
"uniqueItems": false
},
"schedule": {
"behavior": "collective.schedulefield.behavior.IMultiScheduledContent",
"description": "",
"key_type": {
"additional": {},
"schema": {
"description": "",
"factory": "Text line (String)",
"title": "",
"type": "string"
}
},
"title": "Horaire",
"type": "dict",
"value_type": {
"additional": {
"key_type": {
"additional": {},
"schema": {
"description": "",
"factory": "Text line (String)",
"title": "",
"type": "string"
}
},
"value_type": {
"additional": {},
"schema": {
"description": "",
"factory": "Text line (String)",
"title": "",
"type": "string"
}
}
},
"schema": {
"description": "",
"key_type": {
"additional": {},
"schema": {
"description": "",
"factory": "Text line (String)",
"title": "",
"type": "string"
}
},
"title": "",
"type": "dict",
"value_type": {
"additional": {},
"schema": {
"description": "",
"factory": "Text line (String)",
"title": "",
"type": "string"
}
}
}
}
},
"selected_entities": {
"additionalItems": true,
"behavior": "imio.directory.core.contents.contact.content.IContact",
"default": [],
"description": "S\u00e9lectionnez les entit\u00e9s dans lesquelles ce contact sera affich\u00e9. L'entit\u00e9 courante sera toujours s\u00e9lectionn\u00e9e.",
"factory": "List",
"items": {
"description": "",
"factory": "Choice",
"title": "",
"type": "string",
"vocabulary": {
"@id": "https://annuaire.preprod.imio.be/@vocabularies/imio.directory.vocabulary.EntitiesUIDs"
}
},
"title": "Entit\u00e9s concern\u00e9es",
"type": "array",
"uniqueItems": true
},
"street": {
"behavior": "imio.directory.core.contents.contact.content.IContact",
"description": "",
"factory": "Text line (String)",
"title": "Rue",
"type": "string"
},
"subjects": {
"additionalItems": true,
"behavior": "plone.categorization",
"description": "Les mots cl\u00e9s sont utilis\u00e9s couramment pour organiser le contenu de fa\u00e7on ad hoc.",
"factory": "Tuple",
"items": {
"description": "",
"factory": "Text line (String)",
"title": "",
"type": "string"
},
"title": "Mots cl\u00e9s",
"type": "array",
"uniqueItems": true,
"widgetOptions": {
"vocabulary": {
"@id": "https://annuaire.preprod.imio.be/@vocabularies/plone.app.vocabularies.Keywords"
}
}
},
"subtitle": {
"behavior": "imio.directory.core.contents.contact.content.IContact",
"description": "",
"factory": "Text line (String)",
"title": "Sous-titre",
"type": "string"
},
"taxonomy_contact_category": {
"additionalItems": true,
"behavior": "collective.taxonomy.generated.contact_category",
"description": "Important! Ces cat\u00e9gories sont utilis\u00e9es pour am\u00e9liorer la recherche et filtrer les fiches dans l'annuaire",
"factory": "List",
"items": {
"description": "",
"factory": "Choice",
"title": "",
"type": "string",
"vocabulary": {
"@id": "https://annuaire.preprod.imio.be/@vocabularies/collective.taxonomy.contact_category"
}
},
"title": "Cat\u00e9gorie",
"type": "array",
"uniqueItems": true
},
"title": {
"behavior": "plone.basic",
"description": "",
"factory": "Text line (String)",
"title": "Titre",
"type": "string"
},
"topics": {
"additionalItems": true,
"behavior": "imio.smartweb.topics",
"description": "Important! Les th\u00e9matiques sont utilis\u00e9es pour filtrer les r\u00e9sultats de la recherche et cr\u00e9er des listes",
"factory": "List",
"items": {
"description": "",
"factory": "Choice",
"title": "",
"type": "string",
"vocabulary": {
"@id": "https://annuaire.preprod.imio.be/@vocabularies/imio.smartweb.vocabulary.Topics"
}
},
"title": "Th\u00e9matiques",
"type": "array",
"uniqueItems": true
},
"type": {
"behavior": "imio.directory.core.contents.contact.content.IContact",
"description": "",
"factory": "Choice",
"title": "Type",
"type": "string",
"vocabulary": {
"@id": "https://annuaire.preprod.imio.be/@vocabularies/imio.directory.vocabulary.ContactTypes"
}
},
"urls": {
"additionalItems": true,
"behavior": "imio.directory.core.contents.contact.content.IContact",
"description": "",
"factory": "List",
"items": {
"description": "",
"factory": "File",
"properties": {
"type": {
"description": "",
"factory": "Choice",
"title": "Type",
"type": "string",
"vocabulary": {
"@id": "https://annuaire.preprod.imio.be/@vocabularies/imio.directory.vocabulary.SiteTypes"
}
},
"url": {
"description": "",
"factory": "URL",
"title": "URL",
"type": "string",
"widget": "url"
}
},
"title": "Value",
"type": "object"
},
"title": "URLs",
"type": "array",
"uniqueItems": false
},
"vat_number": {
"behavior": "imio.directory.core.contents.contact.content.IContact",
"description": "",
"factory": "Text line (String)",
"title": "Num\u00e9ro d'entreprise / TVA",
"type": "string"
},
"zipcode": {
"behavior": "imio.directory.core.contents.contact.content.IContact",
"description": "",
"factory": "Integer",
"title": "Code postal",
"type": "integer"
}
},
"required": [
"title",
"type"
],
"title": "Contact",
"type": "object"
}

View File

@ -0,0 +1,52 @@
[
{
"@id": "https://annuaire.preprod.imio.be/@types/News Item",
"addable": false,
"title": "Actualit\u00e9"
},
{
"@id": "https://annuaire.preprod.imio.be/@types/Collection",
"addable": false,
"title": "Collection"
},
{
"@id": "https://annuaire.preprod.imio.be/@types/imio.directory.Contact",
"addable": false,
"title": "Contact"
},
{
"@id": "https://annuaire.preprod.imio.be/@types/Folder",
"addable": false,
"title": "Dossier"
},
{
"@id": "https://annuaire.preprod.imio.be/@types/imio.directory.Entity",
"addable": true,
"title": "Entit\u00e9"
},
{
"@id": "https://annuaire.preprod.imio.be/@types/File",
"addable": false,
"title": "Fichier"
},
{
"@id": "https://annuaire.preprod.imio.be/@types/Image",
"addable": false,
"title": "Image"
},
{
"@id": "https://annuaire.preprod.imio.be/@types/Link",
"addable": false,
"title": "Lien"
},
{
"@id": "https://annuaire.preprod.imio.be/@types/Document",
"addable": false,
"title": "Page Web"
},
{
"@id": "https://annuaire.preprod.imio.be/@types/Event",
"addable": false,
"title": "\u00c9v\u00e9nement"
}
]

View File

@ -0,0 +1,74 @@
{
"@id": "https://annuaire.preprod.imio.be/@vocabularies/imio.smartweb.vocabulary.Topics",
"items": [
{
"title": "Activit\u00e9s et divertissement",
"token": "entertainment"
},
{
"title": "Agriculture",
"token": "agriculture"
},
{
"title": "Citoyennet\u00e9",
"token": "citizenship"
},
{
"title": "Culture",
"token": "culture"
},
{
"title": "\u00c9conomie",
"token": "economics"
},
{
"title": "\u00c9ducation",
"token": "education"
},
{
"title": "Environnement",
"token": "environment"
},
{
"title": "Habitat et urbanisme",
"token": "habitat_town_planning"
},
{
"title": "Mobilit\u00e9",
"token": "mobility"
},
{
"title": "Participation citoyenne",
"token": "citizen_participation"
},
{
"title": "Politique",
"token": "politics"
},
{
"title": "Sant\u00e9",
"token": "health"
},
{
"title": "S\u00e9curit\u00e9 et pr\u00e9vention",
"token": "safety_prevention"
},
{
"title": "Social",
"token": "social"
},
{
"title": "Sports",
"token": "sports"
},
{
"title": "Territoire et espace public",
"token": "territory_public_space"
},
{
"title": "Tourisme",
"token": "tourism"
}
],
"items_total": 17
}

View File

@ -0,0 +1,266 @@
{
"@id": "https://annuaire.preprod.imio.be/braine-l-alleud/@search?UID=23a32197d6c841259963b43b24747854&fullobjects=y",
"items": [
{
"@components": {
"actions": {
"@id": "https://annuaire.preprod.imio.be/braine-l-alleud/3378d97243854ddfa90510f6ceb9fcdb/@actions"
},
"breadcrumbs": {
"@id": "https://annuaire.preprod.imio.be/braine-l-alleud/3378d97243854ddfa90510f6ceb9fcdb/@breadcrumbs"
},
"contextnavigation": {
"@id": "https://annuaire.preprod.imio.be/braine-l-alleud/3378d97243854ddfa90510f6ceb9fcdb/@contextnavigation"
},
"navigation": {
"@id": "https://annuaire.preprod.imio.be/braine-l-alleud/3378d97243854ddfa90510f6ceb9fcdb/@navigation"
},
"types": {
"@id": "https://annuaire.preprod.imio.be/braine-l-alleud/3378d97243854ddfa90510f6ceb9fcdb/@types"
},
"workflow": {
"@id": "https://annuaire.preprod.imio.be/braine-l-alleud/3378d97243854ddfa90510f6ceb9fcdb/@workflow"
}
},
"@id": "https://annuaire.preprod.imio.be/braine-l-alleud/3378d97243854ddfa90510f6ceb9fcdb",
"@type": "imio.directory.Contact",
"UID": "23a32197d6c841259963b43b24747854",
"allow_discussion": false,
"city": "Braine-l'Alleud",
"complement": null,
"country": {
"title": "Belgique",
"token": "be"
},
"created": "2021-07-28T07:10:02+00:00",
"description": "Contact : Jean-Pascal Hinnekens (directeur)",
"exceptional_closure": [],
"facilities": null,
"geolocation": {
"latitude": 50.4989185,
"longitude": 4.7184485
},
"iam": [
{
"title": "Jeune",
"token": "young"
},
{
"title": "Nouvel arrivant",
"token": "newcomer"
},
{
"title": "Parent",
"token": "parent"
}
],
"id": "3378d97243854ddfa90510f6ceb9fcdb",
"image": null,
"image_caption": null,
"is_folderish": true,
"is_geolocated": true,
"language": {
"title": "Fran\u00e7ais",
"token": "fr"
},
"layout": "view",
"logo": {
"content-type": "image/png",
"download": "https://annuaire.preprod.imio.be/braine-l-alleud/3378d97243854ddfa90510f6ceb9fcdb/@@images/b5785773-138a-4907-9ec6-b29100f18e85.png",
"filename": "acad\u00e9mie musique braine-l'alleud.png",
"height": 591,
"scales": {
"banner": {
"download": "https://annuaire.preprod.imio.be/braine-l-alleud/3378d97243854ddfa90510f6ceb9fcdb/@@images/64373ca2-151b-439d-a489-288c83a94de6.png",
"height": 591,
"width": 559
},
"extralarge": {
"download": "https://annuaire.preprod.imio.be/braine-l-alleud/3378d97243854ddfa90510f6ceb9fcdb/@@images/cb8f259c-1255-4d5d-9b1a-fdace828f74c.png",
"height": 591,
"width": 559
},
"icon": {
"download": "https://annuaire.preprod.imio.be/braine-l-alleud/3378d97243854ddfa90510f6ceb9fcdb/@@images/77276cc0-e251-450b-8a4c-968e1d5c4ac4.png",
"height": 32,
"width": 31
},
"large": {
"download": "https://annuaire.preprod.imio.be/braine-l-alleud/3378d97243854ddfa90510f6ceb9fcdb/@@images/6ec18ddf-645a-4764-b7b4-fedfe7c1d9e0.png",
"height": 591,
"width": 559
},
"listing": {
"download": "https://annuaire.preprod.imio.be/braine-l-alleud/3378d97243854ddfa90510f6ceb9fcdb/@@images/07b35265-e56f-4fa0-9717-b1982604f9b4.png",
"height": 16,
"width": 16
},
"medium": {
"download": "https://annuaire.preprod.imio.be/braine-l-alleud/3378d97243854ddfa90510f6ceb9fcdb/@@images/4470f78e-19fb-41aa-b897-4f9b0f5f356e.png",
"height": 591,
"width": 559
},
"mini": {
"download": "https://annuaire.preprod.imio.be/braine-l-alleud/3378d97243854ddfa90510f6ceb9fcdb/@@images/548c616b-d37b-43be-b931-42c17b462127.png",
"height": 200,
"width": 189
},
"preview": {
"download": "https://annuaire.preprod.imio.be/braine-l-alleud/3378d97243854ddfa90510f6ceb9fcdb/@@images/84394ebe-2d4d-4894-b331-1636e93ccd38.png",
"height": 400,
"width": 379
},
"thumb": {
"download": "https://annuaire.preprod.imio.be/braine-l-alleud/3378d97243854ddfa90510f6ceb9fcdb/@@images/087c5b17-5ae3-4e9d-a794-a95440e5aa43.png",
"height": 128,
"width": 121
},
"tile": {
"download": "https://annuaire.preprod.imio.be/braine-l-alleud/3378d97243854ddfa90510f6ceb9fcdb/@@images/34278749-0375-4000-924f-b56993076bbd.png",
"height": 64,
"width": 61
}
},
"size": 232832,
"width": 559
},
"mails": [
{
"label": null,
"mail_address": "academie.musique@braine-lalleud.be",
"type": "work"
}
],
"modified": "2021-10-14T10:48:57+00:00",
"multi_schedule": [],
"next_item": {
"@id": "https://annuaire.preprod.imio.be/braine-l-alleud/f82d2c079131433ea6ab20f9f7f49442",
"@type": "imio.directory.Contact",
"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)",
"title": "Accueil et Orientation Volontariat (A.O.V.)"
},
"number": "49",
"parent": {
"@id": "https://annuaire.preprod.imio.be/braine-l-alleud",
"@type": "imio.directory.Entity",
"description": "",
"review_state": "published",
"title": "Braine-l'Alleud"
},
"phones": [
{
"label": null,
"number": "+3228540720",
"type": "work"
},
{
"label": null,
"number": "+3228540729",
"type": "fax"
}
],
"previous_item": {
"@id": "https://annuaire.preprod.imio.be/braine-l-alleud/30bc56007a5140358de0a5ad897b7a47",
"@type": "imio.directory.Contact",
"description": "Contact : Vinciane Vrielinck",
"title": "Cabinet du Bourgmestre de la Commune de Braine-l'Alleud"
},
"review_state": "published",
"schedule": {
"friday": {
"afternoonend": "",
"afternoonstart": "",
"comment": "",
"morningend": "",
"morningstart": ""
},
"monday": {
"afternoonend": "",
"afternoonstart": "",
"comment": "",
"morningend": "",
"morningstart": ""
},
"saturday": {
"afternoonend": "",
"afternoonstart": "",
"comment": "",
"morningend": "",
"morningstart": ""
},
"sunday": {
"afternoonend": "",
"afternoonstart": "",
"comment": "",
"morningend": "",
"morningstart": ""
},
"thursday": {
"afternoonend": "",
"afternoonstart": "",
"comment": "",
"morningend": "",
"morningstart": ""
},
"tuesday": {
"afternoonend": "",
"afternoonstart": "",
"comment": "",
"morningend": "",
"morningstart": ""
},
"wednesday": {
"afternoonend": "",
"afternoonstart": "",
"comment": "",
"morningend": "",
"morningstart": ""
}
},
"selected_entities": [
{
"title": "Braine-l'Alleud",
"token": "f571b73a16f34832a5fdd3683533b3cc"
}
],
"street": "Rue du Ch\u00e2teau",
"subjects": [
"\u00e9cole"
],
"subtitle": null,
"taxonomy_contact_category": [
{
"title": "Loisirs \u00bb Cours et activit\u00e9s \u00bb Musique",
"token": "3qaeiq8v2p"
}
],
"title": "Acad\u00e9mie de Musique de Braine-l'Alleud",
"topics": [
{
"title": "Culture",
"token": "culture"
},
{
"title": "\u00c9ducation",
"token": "education"
}
],
"type": {
"title": "Organisation (service administratif, commerce, profession lib\u00e9rale, club sportif, association, etc.)",
"token": "organization"
},
"urls": [
{
"type": "website",
"url": "http://academie-de-musique.braine-lalleud.be/"
}
],
"vat_number": null,
"version": "current",
"working_copy": null,
"working_copy_of": null,
"zipcode": 1420
}
],
"items_total": 1
}

View File

@ -0,0 +1,629 @@
{
"@id": "https://annuaire.preprod.imio.be/braine-l-alleud/@search?portal_type=imio.directory.Contact&review_state=published&fullobjects=y",
"batching": {
"@id": "https://annuaire.preprod.imio.be/braine-l-alleud/@search?b_size=3&portal_type=imio.directory.Contact&review_state=published&fullobjects=y",
"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",
"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",
"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"
},
"items": [
{
"@components": {
"actions": {
"@id": "https://annuaire.preprod.imio.be/braine-l-alleud/30bc56007a5140358de0a5ad897b7a47/@actions"
},
"breadcrumbs": {
"@id": "https://annuaire.preprod.imio.be/braine-l-alleud/30bc56007a5140358de0a5ad897b7a47/@breadcrumbs"
},
"contextnavigation": {
"@id": "https://annuaire.preprod.imio.be/braine-l-alleud/30bc56007a5140358de0a5ad897b7a47/@contextnavigation"
},
"navigation": {
"@id": "https://annuaire.preprod.imio.be/braine-l-alleud/30bc56007a5140358de0a5ad897b7a47/@navigation"
},
"types": {
"@id": "https://annuaire.preprod.imio.be/braine-l-alleud/30bc56007a5140358de0a5ad897b7a47/@types"
},
"workflow": {
"@id": "https://annuaire.preprod.imio.be/braine-l-alleud/30bc56007a5140358de0a5ad897b7a47/@workflow"
}
},
"@id": "https://annuaire.preprod.imio.be/braine-l-alleud/30bc56007a5140358de0a5ad897b7a47",
"@type": "imio.directory.Contact",
"UID": "dea9d26baab944beb7e54d4024d35a33",
"allow_discussion": false,
"city": "Braine-l'Alleud",
"complement": null,
"country": {
"title": "Belgique",
"token": "be"
},
"created": "2021-07-28T07:23:24+00:00",
"description": "Contact : Vinciane Vrielinck",
"exceptional_closure": [],
"facilities": null,
"geolocation": {
"latitude": 50.4989185,
"longitude": 4.7184485
},
"iam": [
{
"title": "Nouvel arrivant",
"token": "newcomer"
}
],
"id": "30bc56007a5140358de0a5ad897b7a47",
"image": null,
"image_caption": null,
"is_folderish": true,
"is_geolocated": true,
"language": {
"title": "Fran\u00e7ais",
"token": "fr"
},
"layout": "view",
"logo": null,
"mails": [
{
"label": null,
"mail_address": "bourgmestre@braine-lalleud.be",
"type": "work"
}
],
"modified": "2021-09-22T13:15:16+00:00",
"multi_schedule": [],
"next_item": {
"@id": "https://annuaire.preprod.imio.be/braine-l-alleud/3378d97243854ddfa90510f6ceb9fcdb",
"@type": "imio.directory.Contact",
"description": "Contact : Jean-Pascal Hinnekens (directeur)",
"title": "Acad\u00e9mie de Musique de Braine-l'Alleud"
},
"number": "1",
"parent": {
"@id": "https://annuaire.preprod.imio.be/braine-l-alleud",
"@type": "imio.directory.Entity",
"description": "",
"review_state": "published",
"title": "Braine-l'Alleud"
},
"phones": [
{
"label": null,
"number": "+3228540500",
"type": "work"
}
],
"previous_item": {},
"review_state": "published",
"schedule": {
"friday": {
"afternoonend": "",
"afternoonstart": "",
"comment": "",
"morningend": "",
"morningstart": ""
},
"monday": {
"afternoonend": "",
"afternoonstart": "",
"comment": "",
"morningend": "",
"morningstart": ""
},
"saturday": {
"afternoonend": "",
"afternoonstart": "",
"comment": "",
"morningend": "",
"morningstart": ""
},
"sunday": {
"afternoonend": "",
"afternoonstart": "",
"comment": "",
"morningend": "",
"morningstart": ""
},
"thursday": {
"afternoonend": "",
"afternoonstart": "",
"comment": "",
"morningend": "",
"morningstart": ""
},
"tuesday": {
"afternoonend": "",
"afternoonstart": "",
"comment": "",
"morningend": "",
"morningstart": ""
},
"wednesday": {
"afternoonend": "",
"afternoonstart": "",
"comment": "",
"morningend": "",
"morningstart": ""
}
},
"selected_entities": [
{
"title": "Braine-l'Alleud",
"token": "f571b73a16f34832a5fdd3683533b3cc"
}
],
"street": "Avenue du 21 Juillet",
"subjects": [
"scourneau",
"mr"
],
"subtitle": null,
"taxonomy_contact_category": [
{
"title": "Service public \u00bb Administration communale",
"token": "xhowidw6kd"
}
],
"title": "Cabinet du Bourgmestre de la Commune de Braine-l'Alleud",
"topics": [
{
"title": "Politique",
"token": "politics"
}
],
"type": {
"title": "Organisation (service administratif, commerce, profession lib\u00e9rale, club sportif, association, etc.)",
"token": "organization"
},
"urls": [],
"vat_number": null,
"version": "current",
"working_copy": null,
"working_copy_of": null,
"zipcode": 1420
},
{
"@components": {
"actions": {
"@id": "https://annuaire.preprod.imio.be/braine-l-alleud/3378d97243854ddfa90510f6ceb9fcdb/@actions"
},
"breadcrumbs": {
"@id": "https://annuaire.preprod.imio.be/braine-l-alleud/3378d97243854ddfa90510f6ceb9fcdb/@breadcrumbs"
},
"contextnavigation": {
"@id": "https://annuaire.preprod.imio.be/braine-l-alleud/3378d97243854ddfa90510f6ceb9fcdb/@contextnavigation"
},
"navigation": {
"@id": "https://annuaire.preprod.imio.be/braine-l-alleud/3378d97243854ddfa90510f6ceb9fcdb/@navigation"
},
"types": {
"@id": "https://annuaire.preprod.imio.be/braine-l-alleud/3378d97243854ddfa90510f6ceb9fcdb/@types"
},
"workflow": {
"@id": "https://annuaire.preprod.imio.be/braine-l-alleud/3378d97243854ddfa90510f6ceb9fcdb/@workflow"
}
},
"@id": "https://annuaire.preprod.imio.be/braine-l-alleud/3378d97243854ddfa90510f6ceb9fcdb",
"@type": "imio.directory.Contact",
"UID": "23a32197d6c841259963b43b24747854",
"allow_discussion": false,
"city": "Braine-l'Alleud",
"complement": null,
"country": {
"title": "Belgique",
"token": "be"
},
"created": "2021-07-28T07:10:02+00:00",
"description": "Contact : Jean-Pascal Hinnekens (directeur)",
"exceptional_closure": [],
"facilities": null,
"geolocation": {
"latitude": 50.4989185,
"longitude": 4.7184485
},
"iam": [
{
"title": "Jeune",
"token": "young"
},
{
"title": "Nouvel arrivant",
"token": "newcomer"
},
{
"title": "Parent",
"token": "parent"
}
],
"id": "3378d97243854ddfa90510f6ceb9fcdb",
"image": null,
"image_caption": null,
"is_folderish": true,
"is_geolocated": true,
"language": {
"title": "Fran\u00e7ais",
"token": "fr"
},
"layout": "view",
"logo": {
"content-type": "image/png",
"download": "https://annuaire.preprod.imio.be/braine-l-alleud/3378d97243854ddfa90510f6ceb9fcdb/@@images/b5785773-138a-4907-9ec6-b29100f18e85.png",
"filename": "acad\u00e9mie musique braine-l'alleud.png",
"height": 591,
"scales": {
"banner": {
"download": "https://annuaire.preprod.imio.be/braine-l-alleud/3378d97243854ddfa90510f6ceb9fcdb/@@images/64373ca2-151b-439d-a489-288c83a94de6.png",
"height": 591,
"width": 559
},
"extralarge": {
"download": "https://annuaire.preprod.imio.be/braine-l-alleud/3378d97243854ddfa90510f6ceb9fcdb/@@images/cb8f259c-1255-4d5d-9b1a-fdace828f74c.png",
"height": 591,
"width": 559
},
"icon": {
"download": "https://annuaire.preprod.imio.be/braine-l-alleud/3378d97243854ddfa90510f6ceb9fcdb/@@images/77276cc0-e251-450b-8a4c-968e1d5c4ac4.png",
"height": 32,
"width": 31
},
"large": {
"download": "https://annuaire.preprod.imio.be/braine-l-alleud/3378d97243854ddfa90510f6ceb9fcdb/@@images/6ec18ddf-645a-4764-b7b4-fedfe7c1d9e0.png",
"height": 591,
"width": 559
},
"listing": {
"download": "https://annuaire.preprod.imio.be/braine-l-alleud/3378d97243854ddfa90510f6ceb9fcdb/@@images/07b35265-e56f-4fa0-9717-b1982604f9b4.png",
"height": 16,
"width": 16
},
"medium": {
"download": "https://annuaire.preprod.imio.be/braine-l-alleud/3378d97243854ddfa90510f6ceb9fcdb/@@images/4470f78e-19fb-41aa-b897-4f9b0f5f356e.png",
"height": 591,
"width": 559
},
"mini": {
"download": "https://annuaire.preprod.imio.be/braine-l-alleud/3378d97243854ddfa90510f6ceb9fcdb/@@images/548c616b-d37b-43be-b931-42c17b462127.png",
"height": 200,
"width": 189
},
"preview": {
"download": "https://annuaire.preprod.imio.be/braine-l-alleud/3378d97243854ddfa90510f6ceb9fcdb/@@images/84394ebe-2d4d-4894-b331-1636e93ccd38.png",
"height": 400,
"width": 379
},
"thumb": {
"download": "https://annuaire.preprod.imio.be/braine-l-alleud/3378d97243854ddfa90510f6ceb9fcdb/@@images/087c5b17-5ae3-4e9d-a794-a95440e5aa43.png",
"height": 128,
"width": 121
},
"tile": {
"download": "https://annuaire.preprod.imio.be/braine-l-alleud/3378d97243854ddfa90510f6ceb9fcdb/@@images/34278749-0375-4000-924f-b56993076bbd.png",
"height": 64,
"width": 61
}
},
"size": 232832,
"width": 559
},
"mails": [
{
"label": null,
"mail_address": "academie.musique@braine-lalleud.be",
"type": "work"
}
],
"modified": "2021-10-14T10:48:57+00:00",
"multi_schedule": [],
"next_item": {
"@id": "https://annuaire.preprod.imio.be/braine-l-alleud/f82d2c079131433ea6ab20f9f7f49442",
"@type": "imio.directory.Contact",
"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)",
"title": "Accueil et Orientation Volontariat (A.O.V.)"
},
"number": "49",
"parent": {
"@id": "https://annuaire.preprod.imio.be/braine-l-alleud",
"@type": "imio.directory.Entity",
"description": "",
"review_state": "published",
"title": "Braine-l'Alleud"
},
"phones": [
{
"label": null,
"number": "+3228540720",
"type": "work"
},
{
"label": null,
"number": "+3228540729",
"type": "fax"
}
],
"previous_item": {
"@id": "https://annuaire.preprod.imio.be/braine-l-alleud/30bc56007a5140358de0a5ad897b7a47",
"@type": "imio.directory.Contact",
"description": "Contact : Vinciane Vrielinck",
"title": "Cabinet du Bourgmestre de la Commune de Braine-l'Alleud"
},
"review_state": "published",
"schedule": {
"friday": {
"afternoonend": "",
"afternoonstart": "",
"comment": "",
"morningend": "",
"morningstart": ""
},
"monday": {
"afternoonend": "",
"afternoonstart": "",
"comment": "",
"morningend": "",
"morningstart": ""
},
"saturday": {
"afternoonend": "",
"afternoonstart": "",
"comment": "",
"morningend": "",
"morningstart": ""
},
"sunday": {
"afternoonend": "",
"afternoonstart": "",
"comment": "",
"morningend": "",
"morningstart": ""
},
"thursday": {
"afternoonend": "",
"afternoonstart": "",
"comment": "",
"morningend": "",
"morningstart": ""
},
"tuesday": {
"afternoonend": "",
"afternoonstart": "",
"comment": "",
"morningend": "",
"morningstart": ""
},
"wednesday": {
"afternoonend": "",
"afternoonstart": "",
"comment": "",
"morningend": "",
"morningstart": ""
}
},
"selected_entities": [
{
"title": "Braine-l'Alleud",
"token": "f571b73a16f34832a5fdd3683533b3cc"
}
],
"street": "Rue du Ch\u00e2teau",
"subjects": [
"\u00e9cole"
],
"subtitle": null,
"taxonomy_contact_category": [
{
"title": "Loisirs \u00bb Cours et activit\u00e9s \u00bb Musique",
"token": "3qaeiq8v2p"
}
],
"title": "Acad\u00e9mie de Musique de Braine-l'Alleud",
"topics": [
{
"title": "Culture",
"token": "culture"
},
{
"title": "\u00c9ducation",
"token": "education"
}
],
"type": {
"title": "Organisation (service administratif, commerce, profession lib\u00e9rale, club sportif, association, etc.)",
"token": "organization"
},
"urls": [
{
"type": "website",
"url": "http://academie-de-musique.braine-lalleud.be/"
}
],
"vat_number": null,
"version": "current",
"working_copy": null,
"working_copy_of": null,
"zipcode": 1420
},
{
"@components": {
"actions": {
"@id": "https://annuaire.preprod.imio.be/braine-l-alleud/f82d2c079131433ea6ab20f9f7f49442/@actions"
},
"breadcrumbs": {
"@id": "https://annuaire.preprod.imio.be/braine-l-alleud/f82d2c079131433ea6ab20f9f7f49442/@breadcrumbs"
},
"contextnavigation": {
"@id": "https://annuaire.preprod.imio.be/braine-l-alleud/f82d2c079131433ea6ab20f9f7f49442/@contextnavigation"
},
"navigation": {
"@id": "https://annuaire.preprod.imio.be/braine-l-alleud/f82d2c079131433ea6ab20f9f7f49442/@navigation"
},
"types": {
"@id": "https://annuaire.preprod.imio.be/braine-l-alleud/f82d2c079131433ea6ab20f9f7f49442/@types"
},
"workflow": {
"@id": "https://annuaire.preprod.imio.be/braine-l-alleud/f82d2c079131433ea6ab20f9f7f49442/@workflow"
}
},
"@id": "https://annuaire.preprod.imio.be/braine-l-alleud/f82d2c079131433ea6ab20f9f7f49442",
"@type": "imio.directory.Contact",
"UID": "f82d2c079131433ea6ab20f9f7f49442",
"allow_discussion": false,
"city": "Braine-l'Alleud",
"complement": null,
"country": {
"title": "Belgique",
"token": "be"
},
"created": "2021-08-20T09:27:46+00:00",
"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)",
"exceptional_closure": [],
"facilities": null,
"geolocation": {
"latitude": 50.4989185,
"longitude": 4.7184485
},
"iam": [
{
"title": "Jeune",
"token": "young"
},
{
"title": "Nouvel arrivant",
"token": "newcomer"
}
],
"id": "f82d2c079131433ea6ab20f9f7f49442",
"image": null,
"image_caption": null,
"is_folderish": true,
"is_geolocated": true,
"language": {
"title": "Fran\u00e7ais",
"token": "fr"
},
"layout": "view",
"logo": null,
"mails": [],
"modified": "2021-09-23T11:50:05+00:00",
"multi_schedule": [],
"next_item": {
"@id": "https://annuaire.preprod.imio.be/braine-l-alleud/813f4b02118d498ab779ec8542315e66",
"@type": "imio.directory.Contact",
"description": "",
"title": "Association des commer\u00e7ants et artisans de Braine-l\u2019Alleud"
},
"number": "3",
"parent": {
"@id": "https://annuaire.preprod.imio.be/braine-l-alleud",
"@type": "imio.directory.Entity",
"description": "",
"review_state": "published",
"title": "Braine-l'Alleud"
},
"phones": [
{
"label": null,
"number": "+3223846945",
"type": "work"
}
],
"previous_item": {
"@id": "https://annuaire.preprod.imio.be/braine-l-alleud/3378d97243854ddfa90510f6ceb9fcdb",
"@type": "imio.directory.Contact",
"description": "Contact : Jean-Pascal Hinnekens (directeur)",
"title": "Acad\u00e9mie de Musique de Braine-l'Alleud"
},
"review_state": "published",
"schedule": {
"friday": {
"afternoonend": "",
"afternoonstart": "",
"comment": "",
"morningend": "",
"morningstart": ""
},
"monday": {
"afternoonend": "",
"afternoonstart": "",
"comment": "",
"morningend": "",
"morningstart": ""
},
"saturday": {
"afternoonend": "",
"afternoonstart": "",
"comment": "",
"morningend": "",
"morningstart": ""
},
"sunday": {
"afternoonend": "",
"afternoonstart": "",
"comment": "",
"morningend": "",
"morningstart": ""
},
"thursday": {
"afternoonend": "",
"afternoonstart": "",
"comment": "",
"morningend": "",
"morningstart": ""
},
"tuesday": {
"afternoonend": "",
"afternoonstart": "",
"comment": "",
"morningend": "",
"morningstart": ""
},
"wednesday": {
"afternoonend": "",
"afternoonstart": "",
"comment": "",
"morningend": "",
"morningstart": ""
}
},
"selected_entities": [
{
"title": "Braine-l'Alleud",
"token": "f571b73a16f34832a5fdd3683533b3cc"
}
],
"street": "Grand-Place Baudouin 1er",
"subjects": [
"b\u00e9n\u00e9volat"
],
"subtitle": null,
"taxonomy_contact_category": [
{
"title": "Loisirs \u00bb Mouvements et associations",
"token": "13drlsiykl"
}
],
"title": "Accueil et Orientation Volontariat (A.O.V.)",
"topics": [
{
"title": "Participation citoyenne",
"token": "citizen_participation"
}
],
"type": {
"title": "Organisation (service administratif, commerce, profession lib\u00e9rale, club sportif, association, etc.)",
"token": "organization"
},
"urls": [
{
"type": "website",
"url": "https://www.aovolontariat.be/"
}
],
"vat_number": null,
"version": "current",
"working_copy": null,
"working_copy_of": null,
"zipcode": 1420
}
],
"items_total": 264
}

View File

@ -0,0 +1,8 @@
{
"action": "publish",
"actor": "brainelalleud-iateleservice",
"comments": "",
"review_state": "published",
"time": "2021-10-18T12:40:11+00:00",
"title": "Publi\u00e9"
}

606
tests/test_plone_restapi.py Normal file
View File

@ -0,0 +1,606 @@
# -*- coding: utf-8 -*-
# passerelle - uniform access to multiple data sources and services
# Copyright (C) 202 Entr'ouvert
#
# This program is free software: you can redistribute it and/or modify it
# under the terms of the GNU Affero General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import json
import os
import pytest
import utils
from requests.exceptions import ConnectionError
from test_manager import login
from passerelle.apps.plone_restapi.models import PloneRestApi, Query
from passerelle.utils import import_site
from passerelle.utils.jsonresponse import APIError
pytestmark = pytest.mark.django_db
TEST_BASE_DIR = os.path.join(os.path.dirname(__file__), 'data', 'plone_restapi')
TOKEN_RESPONSE = {
'access_token': 'd319258e-48b9-4853-88e8-7a2ad6883c7f',
'token_type': 'Bearer',
'expires_in': 28800,
'id_token': 'acd...def',
}
TOKEN_ERROR_RESPONSE = {
"error": "access_denied",
"error_description": "Mauvaises informations de connexion de l'utilisateur",
}
def json_get_data(filename):
with open(os.path.join(TEST_BASE_DIR, "%s.json" % filename)) as fd:
return json.load(fd)
@pytest.fixture
def connector():
return utils.setup_access_rights(
PloneRestApi.objects.create(
slug='my_connector',
service_url='http://www.example.net',
token_ws_url='http://www.example.net/idp/oidc/token/',
client_id='aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee',
client_secret='11111111-2222-3333-4444-555555555555',
username='jdoe',
password='secret',
)
)
@pytest.fixture
def token(connector):
with utils.mock_url(url=connector.token_ws_url, response=TOKEN_RESPONSE) as mocked:
yield mocked
@pytest.fixture
def query(connector):
return Query.objects.create(
resource=connector,
name='demo query',
slug='my_query',
description="Annuaire de Braine-l'Alleud",
uri='braine-l-alleud',
text_template='{{ title }} ({{ PLONE_type }})',
filter_expression='''
portal_type=Document
review_state=published
''',
sort='UID',
order=False,
limit=3,
)
def test_views(db, admin_user, app, connector):
app = login(app)
resp = app.get('/plone-restapi/my_connector/', status=200)
resp = resp.click('New Query')
resp.form['name'] = 'my query'
resp.form['slug'] = 'my-query'
resp.form['uri'] = 'my-uri'
resp = resp.form.submit()
resp = resp.follow()
assert resp.html.find('div', {'id': 'queries'}).ul.li.a.text == 'my query'
def test_views_query_unicity(admin_user, app, connector, query):
connector2 = PloneRestApi.objects.create(
slug='my_connector2',
)
Query.objects.create(
resource=connector2,
slug='foo-bar',
name='Foo Bar',
)
# create
app = login(app)
resp = app.get('/manage/plone-restapi/%s/query/new/' % connector.slug)
resp.form['slug'] = query.slug
resp.form['name'] = 'Foo Bar'
resp = resp.form.submit()
assert resp.status_code == 200
assert 'A query with this slug already exists' in resp.text
assert Query.objects.filter(resource=connector).count() == 1
resp.form['slug'] = 'foo-bar'
resp.form['name'] = query.name
resp = resp.form.submit()
assert resp.status_code == 200
assert 'A query with this name already exists' in resp.text
assert Query.objects.filter(resource=connector).count() == 1
resp.form['slug'] = 'foo-bar'
resp.form['name'] = 'Foo Bar'
resp = resp.form.submit()
assert resp.status_code == 302
assert Query.objects.filter(resource=connector).count() == 2
new_query = Query.objects.latest('pk')
assert new_query.resource == connector
assert new_query.slug == 'foo-bar'
assert new_query.name == 'Foo Bar'
# update
resp = app.get('/manage/plone-restapi/%s/query/%s/' % (connector.slug, new_query.pk))
resp.form['slug'] = query.slug
resp.form['name'] = 'Foo Bar'
resp = resp.form.submit()
assert resp.status_code == 200
assert 'A query with this slug already exists' in resp.text
resp.form['slug'] = 'foo-bar'
resp.form['name'] = query.name
resp = resp.form.submit()
assert resp.status_code == 200
assert 'A query with this name already exists' in resp.text
resp.form['slug'] = 'foo-bar'
resp.form['name'] = 'Foo Bar'
resp.form['uri'] = 'fr'
resp = resp.form.submit()
assert resp.status_code == 302
query = Query.objects.get(resource=connector, slug='foo-bar')
assert query.uri == 'fr'
def test_export_import(query):
assert PloneRestApi.objects.count() == 1
assert Query.objects.count() == 1
serialization = {'resources': [query.resource.export_json()]}
PloneRestApi.objects.all().delete()
assert PloneRestApi.objects.count() == 0
assert Query.objects.count() == 0
import_site(serialization)
assert PloneRestApi.objects.count() == 1
assert str(PloneRestApi.objects.get().client_id) == 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee'
assert Query.objects.count() == 1
def test_adapt_id_and_type_plone_attributes(connector):
plone_response = {
'@type': '@value',
'@dict': {
'@array': [
{
'@id': '123',
'@type': '@value',
}
]
},
}
connector.adapt_id_and_type_plone_attributes(plone_response)
assert plone_response == {
'PLONE_type': '@value',
'@dict': {'@array': [{'PLONE_id': '123', 'PLONE_type': '@value'}]},
}
def test_adapt_record(connector, token):
record = {
'@id': 'plone id',
'UID': 'plone uid',
'id': 'foo',
'text': 'bar',
}
template = '{{ PLONE_id }}, {{ id }}, {{original_id }}, {{ original_text }}'
connector.adapt_record(record, template)
assert record == {
'PLONE_id': 'plone id',
'UID': 'plone uid',
'id': 'plone uid',
'text': 'plone id, plone uid, foo, bar',
'original_id': 'foo',
'original_text': 'bar',
}
def test_get_token(app, connector):
with pytest.raises(APIError):
with utils.mock_url(url=connector.token_ws_url, response=TOKEN_ERROR_RESPONSE, status_code=404):
connector.get_token()
with utils.mock_url(url=connector.token_ws_url, response=TOKEN_RESPONSE) as mocked:
result = connector.get_token()
assert mocked.handlers[0].call['count'] == 1
assert 'secret' in mocked.handlers[0].call['requests'][0].body
assert result == 'acd...def'
# make sure the token from cache is used
connector.get_token()
assert mocked.handlers[0].call['count'] == 1
connector.get_token(True)
assert mocked.handlers[0].call['count'] == 2
def test_fetch(app, connector, token):
endpoint = utils.generic_endpoint_url('plone-restapi', 'fetch', slug=connector.slug)
assert endpoint == '/plone-restapi/my_connector/fetch'
url = connector.service_url + '/braine-l-alleud/dccd85d12cf54b6899dff41e5a56ee7f'
params = {
'uid': 'dccd85d12cf54b6899dff41e5a56ee7f',
'uri': 'braine-l-alleud',
'text_template': '{{ title }} ({{ topics.0.title }})',
}
with utils.mock_url(url=url, response=json_get_data('fetch')):
resp = app.get(endpoint, params=params)
assert not resp.json['err']
assert resp.json['data']['id'] == 'dccd85d12cf54b6899dff41e5a56ee7f'
assert resp.json['data']['text'] == 'Le Prisme (Activités et divertissement)'
assert token.handlers[0].call['count'] == 1
def test_request_anonymously(app, connector, token):
connector.token_ws_url = ''
connector.save()
endpoint = utils.generic_endpoint_url('plone-restapi', 'fetch', slug=connector.slug)
assert endpoint == '/plone-restapi/my_connector/fetch'
url = connector.service_url + '/braine-l-alleud/dccd85d12cf54b6899dff41e5a56ee7f'
params = {
'uid': 'dccd85d12cf54b6899dff41e5a56ee7f',
'uri': 'braine-l-alleud',
'text_template': '{{ title }} ({{ topics.0.title }})',
}
with utils.mock_url(url=url, response=json_get_data('fetch')):
resp = app.get(endpoint, params=params)
assert not resp.json['err']
assert resp.json['data']['id'] == 'dccd85d12cf54b6899dff41e5a56ee7f'
assert resp.json['data']['text'] == 'Le Prisme (Activités et divertissement)'
assert token.handlers[0].call['count'] == 0
@pytest.mark.parametrize(
'exception, status_code, response, err_desc',
[
[ConnectionError('plop'), None, None, 'plop'],
[None, 200, 'not json', 'bad JSON response'],
[None, 404, {'message': 'Resource not found: ...', 'type': 'NotFound'}, '404 Client Error'],
],
)
def test_request_error(app, connector, token, exception, status_code, response, err_desc):
endpoint = utils.generic_endpoint_url('plone-restapi', 'fetch', slug=connector.slug)
assert endpoint == '/plone-restapi/my_connector/fetch'
url = connector.service_url + '/braine-l-alleud/plop'
params = {
'uid': 'plop',
'uri': 'braine-l-alleud',
'text_template': '{{ title }} ({{ PLONE_type }})',
}
with utils.mock_url(url=url, response=response, status_code=status_code, exception=exception):
resp = app.get(endpoint, params=params)
assert resp.json['err']
assert err_desc in resp.json['err_desc']
def test_get_content_types(app, connector, token):
endpoint = utils.generic_endpoint_url('plone-restapi', 'get_content_types', slug=connector.slug)
assert endpoint == '/plone-restapi/my_connector/get_content_types'
url = connector.service_url + '/@types'
with utils.mock_url(url=url, response=json_get_data('get_content_types')):
resp = app.get(endpoint)
assert not resp.json['err']
assert len(resp.json['data']) == 10
assert resp.json['data'][2]['id'] == 'imio.directory.Contact'
assert resp.json['data'][2]['text'] == 'Contact'
def test_get_content_type(app, connector, token):
endpoint = utils.generic_endpoint_url('plone-restapi', 'get_content_type', slug=connector.slug)
assert endpoint == '/plone-restapi/my_connector/get_content_type'
url = connector.service_url + '/@types/imio.directory.Contact'
params = {'id': 'imio.directory.Contact'}
with utils.mock_url(url=url, response=json_get_data('get_content_type')):
resp = app.get(endpoint, params=params)
assert not resp.json['err']
assert resp.json['data']['title'] == 'Contact'
assert resp.json['data']['required'] == ['title', 'type']
assert len(resp.json['data']['properties']) == 28
assert (
resp.json['data']['properties']['topics']['items']['vocabulary']['@id']
== 'https://annuaire.preprod.imio.be/@vocabularies/imio.smartweb.vocabulary.Topics'
)
def test_get_field_choices(app, connector, token):
endpoint = utils.generic_endpoint_url('plone-restapi', 'get_field_choices', slug=connector.slug)
assert endpoint == '/plone-restapi/my_connector/get_field_choices'
url = connector.service_url + '/@vocabularies/imio.smartweb.vocabulary.Topics'
params = {'id': 'imio.smartweb.vocabulary.Topics'}
with utils.mock_url(url=url, response=json_get_data('get_field_choices')):
resp = app.get(endpoint, params=params)
assert not resp.json['err']
assert len(resp.json['data']) == 17
assert resp.json['data'][16]['id'] == 'tourism'
assert resp.json['data'][16]['text'] == 'Tourisme'
def test_create(app, connector, token):
endpoint = utils.generic_endpoint_url('plone-restapi', 'create', slug=connector.slug)
assert endpoint == '/plone-restapi/my_connector/create'
url = connector.service_url + '/braine-l-alleud'
payload = {
'@type': 'imio.directory.Contact',
'title': "Test Entr'ouvert",
'type': 'organization',
'schedule': {},
'topics/0/title': 'Tourisme',
'topics/0/token': 'tourism',
'image': {'filename': 'foo.jpg', 'content_type': 'image/jpeg', 'content': '...'},
}
with utils.mock_url(url=url, response=json_get_data('fetch'), status_code=201) as mocked:
resp = app.post_json(endpoint + '?uri=braine-l-alleud&publish=false', params=payload)
body = json.loads(mocked.handlers[0].call['requests'][1].body)
assert body['topics'] == [{'title': 'Tourisme', 'token': 'tourism'}]
assert body['image'] == {
'filename': 'foo.jpg',
'content_type': 'image/jpeg',
'encoding': 'base64',
'data': '...',
'content-type': 'image/jpeg',
}
assert not resp.json['err']
assert resp.json['data'] == {
'uid': 'dccd85d12cf54b6899dff41e5a56ee7f',
'created': True,
'review_state': None,
}
def test_create_and_publish(app, connector, token):
endpoint = utils.generic_endpoint_url('plone-restapi', 'create', slug=connector.slug)
assert endpoint == '/plone-restapi/my_connector/create'
url = connector.service_url + '/braine-l-alleud'
payload = {
'@type': 'imio.directory.Contact',
'title': "Test Entr'ouvert",
'type': 'organization',
'schedule': {},
'topics/0/title': 'Tourisme',
'topics/0/token': 'tourism',
'image': {'filename': 'foo.jpg', 'content_type': 'image/jpeg', 'content': '...'},
}
publish_url = url + '/%s/@workflow/publish' % 'dccd85d12cf54b6899dff41e5a56ee7f'
with utils.mock_url(url=url, response=json_get_data('fetch'), status_code=201) as mocked:
with utils.mock_url(url=publish_url, response=json_get_data('workflow_publish'), status_code=200):
resp = app.post_json(endpoint + '?uri=braine-l-alleud&publish=true', params=payload)
body = json.loads(mocked.handlers[0].call['requests'][1].body)
assert body['topics'] == [{'title': 'Tourisme', 'token': 'tourism'}]
assert body['image'] == {
'filename': 'foo.jpg',
'content_type': 'image/jpeg',
'encoding': 'base64',
'data': '...',
'content-type': 'image/jpeg',
}
assert not resp.json['err']
assert resp.json['data'] == {
'uid': 'dccd85d12cf54b6899dff41e5a56ee7f',
'created': True,
'review_state': 'published',
}
def test_create_wrong_payload(app, connector, token):
endpoint = utils.generic_endpoint_url('plone-restapi', 'create', slug=connector.slug)
assert endpoint == '/plone-restapi/my_connector/create'
url = connector.service_url + '/braine-l-alleud'
payload = 'not json'
resp = app.post(endpoint + '?uri=braine-l-alleud', params=payload, status=400)
assert resp.json['err']
assert resp.json['err_desc'] == 'Expecting value: line 1 column 1 (char 0)'
assert resp.json['err_class'] == 'passerelle.apps.plone_restapi.models.ParameterTypeError'
def test_update(app, connector, token):
endpoint = utils.generic_endpoint_url('plone-restapi', 'update', slug=connector.slug)
assert endpoint == '/plone-restapi/my_connector/update'
url = connector.service_url + '/braine-l-alleud/dccd85d12cf54b6899dff41e5a56ee7f'
query_string = '?uri=braine-l-alleud&uid=dccd85d12cf54b6899dff41e5a56ee7f'
payload = {
'title': 'Test update',
'topics/0/token': 'social',
'image': {'filename': 'foo.jpg', 'content_type': 'image/jpeg', 'content': '...'},
}
with utils.mock_url(url=url, response='', status_code=204) as mocked:
resp = app.post_json(endpoint + query_string, params=payload)
body = json.loads(mocked.handlers[0].call['requests'][1].body)
assert body['topics'] == [{'token': 'social'}]
assert body['image'] == {
'filename': 'foo.jpg',
'content_type': 'image/jpeg',
'encoding': 'base64',
'data': '...',
'content-type': 'image/jpeg',
}
assert not resp.json['err']
assert resp.json['data'] == {'uid': 'dccd85d12cf54b6899dff41e5a56ee7f', 'updated': True}
def test_update_wrong_payload(app, connector, token):
endpoint = utils.generic_endpoint_url('plone-restapi', 'update', slug=connector.slug)
assert endpoint == '/plone-restapi/my_connector/update'
url = connector.service_url + '/braine-l-alleud/dccd85d12cf54b6899dff41e5a56ee7f'
query_string = '?uri=braine-l-alleud&uid=dccd85d12cf54b6899dff41e5a56ee7f'
payload = 'not json'
resp = app.post(endpoint + query_string, params=payload, status=400)
assert resp.json['err']
assert resp.json['err_desc'] == 'Expecting value: line 1 column 1 (char 0)'
assert resp.json['err_class'] == 'passerelle.apps.plone_restapi.models.ParameterTypeError'
def test_remove(app, connector, token):
endpoint = utils.generic_endpoint_url('plone-restapi', 'remove', slug=connector.slug)
assert endpoint == '/plone-restapi/my_connector/remove'
url = connector.service_url + '/braine-l-alleud/dccd85d12cf54b6899dff41e5a56ee7f'
query_string = '?uri=braine-l-alleud&uid=dccd85d12cf54b6899dff41e5a56ee7f'
with utils.mock_url(url=url, response='', status_code=204):
resp = app.delete(endpoint + query_string)
assert resp.json['data'] == {'uid': 'dccd85d12cf54b6899dff41e5a56ee7f', 'removed': True}
assert not resp.json['err']
def test_search(app, connector, token):
endpoint = utils.generic_endpoint_url('plone-restapi', 'search', slug=connector.slug)
assert endpoint == '/plone-restapi/my_connector/search'
url = connector.service_url + '/braine-l-alleud/@search'
params = {
'uri': 'braine-l-alleud',
'text_template': '{{ title }} ({{ PLONE_type }})',
'sort': 'UID',
'order': False,
'limit': 3,
}
qs = {}
with utils.mock_url(url=url, response=json_get_data('q_search'), qs=qs):
resp = app.get(endpoint, params=params)
assert token.handlers[0].call['count'] == 1
assert qs == {'sort_on': 'UID', 'sort_order': 'descending', 'b_size': '3', 'fullobjects': 'y'}
assert not resp.json['err']
assert len(resp.json['data']) == 3
assert [(x['id'], x['text']) for x in resp.json['data']] == [
(
'dea9d26baab944beb7e54d4024d35a33',
"Cabinet du Bourgmestre de la Commune de Braine-l'Alleud (imio.directory.Contact)",
),
(
'23a32197d6c841259963b43b24747854',
"Académie de Musique de Braine-l'Alleud (imio.directory.Contact)",
),
(
'f82d2c079131433ea6ab20f9f7f49442',
'Accueil et Orientation Volontariat (A.O.V.) (imio.directory.Contact)',
),
]
def test_search_using_q(app, connector, token):
endpoint = utils.generic_endpoint_url('plone-restapi', 'search', slug=connector.slug)
assert endpoint == '/plone-restapi/my_connector/search'
url = connector.service_url + '/braine-l-alleud/@search'
params = {
'uri': 'braine-l-alleud',
'text_template': '{{ title }} ({{ PLONE_type }})',
'sort': 'title',
'order': True,
'limit': '3',
'q': 'Página dentro',
}
qs = {}
with utils.mock_url(url=url, response=json_get_data('q_search'), qs=qs):
resp = app.get(endpoint, params=params)
assert qs == {
'SearchableText': 'Página dentro',
'sort_on': 'title',
'sort_order': 'ascending',
'b_size': '3',
'fullobjects': 'y',
}
assert not resp.json['err']
assert len(resp.json['data']) == 3
assert [(x['id'], x['text']) for x in resp.json['data']] == [
(
'dea9d26baab944beb7e54d4024d35a33',
"Cabinet du Bourgmestre de la Commune de Braine-l'Alleud (imio.directory.Contact)",
),
(
'23a32197d6c841259963b43b24747854',
"Académie de Musique de Braine-l'Alleud (imio.directory.Contact)",
),
(
'f82d2c079131433ea6ab20f9f7f49442',
'Accueil et Orientation Volontariat (A.O.V.) (imio.directory.Contact)',
),
]
def test_search_using_id(app, connector, token):
endpoint = utils.generic_endpoint_url('plone-restapi', 'search', slug=connector.slug)
assert endpoint == '/plone-restapi/my_connector/search'
url = connector.service_url + '/braine-l-alleud/@search'
params = {
'uri': 'braine-l-alleud',
'text_template': '{{ title }} ({{ PLONE_type }})',
'id': '9fbb2afd499e465983434f974fce8404',
}
qs = {}
with utils.mock_url(url=url, response=json_get_data('id_search'), qs=qs):
resp = app.get(endpoint, params=params)
assert qs == {'UID': '9fbb2afd499e465983434f974fce8404', 'fullobjects': 'y'}
assert len(resp.json['data']) == 1
assert resp.json['data'][0]['text'] == "Académie de Musique de Braine-l'Alleud (imio.directory.Contact)"
def test_query_q(app, query, token):
endpoint = '/plone-restapi/my_connector/q/my_query/'
url = query.resource.service_url + '/braine-l-alleud/@search'
params = {
'limit': 3,
}
qs = {}
with utils.mock_url(url=url, response=json_get_data('q_search'), qs=qs):
resp = app.get(endpoint, params=params)
assert qs == {
'sort_on': 'UID',
'sort_order': 'descending',
'b_size': '3',
'portal_type': 'Document',
'review_state': 'published',
'fullobjects': 'y',
}
assert not resp.json['err']
assert len(resp.json['data']) == 3
assert resp.json['meta'] == {'label': 'demo query', 'description': "Annuaire de Braine-l'Alleud"}
def test_query_q_using_q(app, query, token):
endpoint = '/plone-restapi/my_connector/q/my_query/'
url = query.resource.service_url + '/braine-l-alleud/@search'
params = {
'q': 'Página dentro',
}
qs = {}
with utils.mock_url(url=url, response=json_get_data('q_search'), qs=qs):
resp = app.get(endpoint, params=params)
assert qs == {
'SearchableText': 'Página dentro',
'sort_on': 'UID',
'sort_order': 'descending',
'b_size': '3',
'portal_type': 'Document',
'review_state': 'published',
'fullobjects': 'y',
}
assert not resp.json['err']
assert len(resp.json['data']) == 3
assert resp.json['meta'] == {'label': 'demo query', 'description': "Annuaire de Braine-l'Alleud"}
def test_query_q_using_id(app, query, token):
endpoint = '/plone-restapi/my_connector/q/my_query/'
url = query.resource.service_url + '/braine-l-alleud/@search'
params = {
'id': '9fbb2afd499e465983434f974fce8404',
}
qs = {}
with utils.mock_url(url=url, response=json_get_data('id_search'), qs=qs):
resp = app.get(endpoint, params=params)
assert qs == {
'UID': '9fbb2afd499e465983434f974fce8404',
'fullobjects': 'y',
}
assert len(resp.json['data']) == 1
assert resp.json['data'][0]['text'] == "Académie de Musique de Braine-l'Alleud (imio.directory.Contact)"
assert resp.json['meta'] == {'label': 'demo query', 'description': "Annuaire de Braine-l'Alleud"}

View File

@ -28,7 +28,7 @@ class FakedResponse(mock.Mock):
return json_loads(self.content)
def mock_url(url=None, response='', status_code=200, headers=None, reason=None, exception=None):
def mock_url(url=None, response='', status_code=200, headers=None, reason=None, exception=None, qs=None):
urlmatch_kwargs = {}
if url:
parsed = urlparse.urlparse(url)
@ -43,6 +43,8 @@ def mock_url(url=None, response='', status_code=200, headers=None, reason=None,
@httmock.remember_called
@httmock.urlmatch(**urlmatch_kwargs)
def mocked(url, request):
if qs is not None:
qs.update(urlparse.parse_qsl(url.query))
if exception:
raise exception
return httmock.response(status_code, response, headers, reason, request=request)