Projet

Général

Profil

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

Nicolas Roche (absent jusqu'au 3 avril), 24 septembre 2021 15:59

Télécharger (52,2 ko)

Voir les différences:

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

 passerelle/apps/plone/__init__.py             |   0
 passerelle/apps/plone/forms.py                |  28 ++
 .../apps/plone/migrations/0001_initial.py     | 150 +++++++
 passerelle/apps/plone/migrations/__init__.py  |   0
 passerelle/apps/plone/models.py               | 248 ++++++++++++
 passerelle/apps/plone/urls.py                 |  29 ++
 passerelle/apps/plone/views.py                |  44 +++
 passerelle/settings.py                        |   1 +
 passerelle/static/css/style.css               |   4 +
 tests/data/plone/id_search.json               |  89 +++++
 tests/data/plone/q_search.json                | 240 ++++++++++++
 tests/test_plone.py                           | 367 ++++++++++++++++++
 tests/utils.py                                |   4 +-
 13 files changed, 1203 insertions(+), 1 deletion(-)
 create mode 100644 passerelle/apps/plone/__init__.py
 create mode 100644 passerelle/apps/plone/forms.py
 create mode 100644 passerelle/apps/plone/migrations/0001_initial.py
 create mode 100644 passerelle/apps/plone/migrations/__init__.py
 create mode 100644 passerelle/apps/plone/models.py
 create mode 100644 passerelle/apps/plone/urls.py
 create mode 100644 passerelle/apps/plone/views.py
 create mode 100644 tests/data/plone/id_search.json
 create mode 100644 tests/data/plone/q_search.json
 create mode 100644 tests/test_plone.py
passerelle/apps/plone/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/migrations/0001_initial.py
1
# Generated by Django 2.2.19 on 2021-09-21 17:26
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='Plone',
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
                    'basic_auth_username',
30
                    models.CharField(
31
                        blank=True, max_length=128, verbose_name='Basic authentication username'
32
                    ),
33
                ),
34
                (
35
                    'basic_auth_password',
36
                    models.CharField(
37
                        blank=True, max_length=128, verbose_name='Basic authentication password'
38
                    ),
39
                ),
40
                (
41
                    'client_certificate',
42
                    models.FileField(
43
                        blank=True, null=True, upload_to='', verbose_name='TLS client certificate'
44
                    ),
45
                ),
46
                (
47
                    'trusted_certificate_authorities',
48
                    models.FileField(blank=True, null=True, upload_to='', verbose_name='TLS trusted CAs'),
49
                ),
50
                (
51
                    'verify_cert',
52
                    models.BooleanField(blank=True, default=True, verbose_name='TLS verify certificates'),
53
                ),
54
                (
55
                    'http_proxy',
56
                    models.CharField(blank=True, max_length=128, verbose_name='HTTP and HTTPS proxy'),
57
                ),
58
                (
59
                    'service_url',
60
                    models.CharField(
61
                        help_text='ex: https://demo.plone.org', max_length=256, verbose_name='Site URL'
62
                    ),
63
                ),
64
                (
65
                    'users',
66
                    models.ManyToManyField(
67
                        blank=True, related_name='_plone_users_+', related_query_name='+', to='base.ApiUser'
68
                    ),
69
                ),
70
            ],
71
            options={
72
                'verbose_name': 'Plone Web Service',
73
            },
74
        ),
75
        migrations.CreateModel(
76
            name='Query',
77
            fields=[
78
                (
79
                    'id',
80
                    models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
81
                ),
82
                ('name', models.CharField(max_length=128, verbose_name='Name')),
83
                ('slug', models.SlugField(max_length=128, verbose_name='Slug')),
84
                ('description', models.TextField(blank=True, verbose_name='Description')),
85
                (
86
                    'uri',
87
                    models.CharField(
88
                        blank=True, help_text='uri to query', max_length=128, verbose_name='Uri'
89
                    ),
90
                ),
91
                (
92
                    'text_template',
93
                    models.TextField(
94
                        blank=True,
95
                        help_text="Use Django's template syntax. Attributes can be accessed through {{ attributes.name }}",
96
                        validators=[passerelle.utils.templates.validate_template],
97
                        verbose_name='Text template',
98
                    ),
99
                ),
100
                (
101
                    'filter_expression',
102
                    models.TextField(
103
                        blank=True,
104
                        help_text='Specify refine and exclude facet expressions separated lines',
105
                        verbose_name='filter',
106
                    ),
107
                ),
108
                (
109
                    'sort',
110
                    models.CharField(
111
                        blank=True,
112
                        help_text='Sorts results by the specified field. A minus sign - may be used to perform an ascending sort.',
113
                        max_length=256,
114
                        verbose_name='Sort field',
115
                    ),
116
                ),
117
                (
118
                    'order',
119
                    models.BooleanField(
120
                        default=True,
121
                        help_text='Unset to use descending sort order',
122
                        verbose_name='Ascending sort order',
123
                    ),
124
                ),
125
                (
126
                    'limit',
127
                    models.PositiveIntegerField(
128
                        default=10,
129
                        help_text='Number of results to return in a single call',
130
                        verbose_name='Limit',
131
                    ),
132
                ),
133
                (
134
                    'resource',
135
                    models.ForeignKey(
136
                        on_delete=django.db.models.deletion.CASCADE,
137
                        related_name='queries',
138
                        to='plone.Plone',
139
                        verbose_name='Resource',
140
                    ),
141
                ),
142
            ],
143
            options={
144
                'verbose_name': 'Query',
145
                'ordering': ['name'],
146
                'abstract': False,
147
                'unique_together': {('resource', 'name'), ('resource', 'slug')},
148
            },
149
        ),
150
    ]
passerelle/apps/plone/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 django.db import models
18
from django.shortcuts import get_object_or_404
19
from django.urls import reverse
20
from django.utils.six.moves.urllib import parse as urlparse
21
from django.utils.translation import ugettext_lazy as _
22
from requests import RequestException
23

  
24
from passerelle.base.models import BaseQuery, BaseResource, HTTPResource
25
from passerelle.utils.api import endpoint
26
from passerelle.utils.jsonresponse import APIError
27
from passerelle.utils.templates import render_to_string, validate_template
28

  
29

  
30
class Plone(BaseResource, HTTPResource):
31
    service_url = models.CharField(
32
        _('Site URL'),
33
        max_length=256,
34
        blank=False,
35
        help_text=_('ex: https://demo.plone.org'),
36
    )
37

  
38
    category = _('Data Sources')
39

  
40
    class Meta:
41
        verbose_name = _('Plone Web Service')
42

  
43
    def export_json(self):
44
        data = super(Plone, self).export_json()
45
        data['queries'] = [query.export_json() for query in self.queries.all()]
46
        return data
47

  
48
    @classmethod
49
    def import_json_real(cls, overwrite, instance, data, **kwargs):
50
        data_queries = data.pop('queries', [])
51
        instance = super(Plone, cls).import_json_real(overwrite, instance, data, **kwargs)
52
        queries = []
53
        if instance and overwrite:
54
            Query.objects.filter(resource=instance).delete()
55
        for data_query in data_queries:
56
            query = Query.import_json(data_query)
57
            query.resource = instance
58
            queries.append(query)
59
        Query.objects.bulk_create(queries)
60
        return instance
61

  
62
    def call_search(
63
        self,
64
        uri='',
65
        text_template='',
66
        filter_expression='',
67
        sort=None,
68
        order=True,
69
        limit=None,
70
        id=None,
71
        q=None,
72
    ):
73
        scheme, netloc, path, query, fragment = urlparse.urlsplit(self.service_url)
74
        path = '/'.join(x for x in [path.rstrip('/'), uri.strip('/'), '@search'] if x)
75
        url = urlparse.urlunsplit((scheme, netloc, path, '', fragment))
76
        params = dict(urlparse.parse_qsl(query))
77

  
78
        if id:
79
            params['UID'] = id
80
        else:
81
            if q is not None:
82
                params['SearchableText'] = q
83
            if sort:
84
                params['sort_on'] = sort
85
                if order:
86
                    params['sort_order'] = 'ascending'
87
                else:
88
                    params['sort_order'] = 'descending'
89
            if limit:
90
                params['b_size'] = limit
91
            params.update(urlparse.parse_qsl(filter_expression))
92
        params['fullobjects'] = 'y'
93

  
94
        headers = {'Accept': 'application/json'}
95
        try:
96
            response = self.requests.get(url, headers=headers, params=params)
97
        except RequestException as e:
98
            raise APIError('Plone error: %s' % e)
99
        try:
100
            json_response = response.json()
101
        except ValueError as e:
102
            raise APIError('Plone error: bad JSON response')
103
        try:
104
            response.raise_for_status()
105
        except RequestException as e:
106
            raise APIError('Plone error: %s "%s"' % (e, json_response))
107

  
108
        def replace_arobase_keys(data):
109
            if isinstance(data, list):
110
                for value in list(data):
111
                    replace_arobase_keys(value)
112
            elif isinstance(data, dict):
113
                for key, value in list(data.items()):
114
                    replace_arobase_keys(value)
115
                    if key[0] == '@':
116
                        data['portal_%s' % key[1:]] = value
117
                        del data[key]
118

  
119
        replace_arobase_keys(json_response)
120
        result = []
121
        for record in json_response.get('items'):
122
            data = {}
123
            for key, value in record.items():
124
                if key in ('id', 'text'):
125
                    key = 'original_%s' % key
126
                data[key] = value
127
            data['id'] = record.get('UID')
128
            data['text'] = render_to_string(text_template, data).strip()
129
            result.append(data)
130

  
131
        return result
132

  
133
    @endpoint(
134
        perm='can_access',
135
        description=_('Search'),
136
        parameters={
137
            'uri': {'description': _('Uri')},
138
            'text_template': {'description': _('Text template')},
139
            'sort': {'description': _('Sort field')},
140
            'order': {'description': _('Ascending sort order'), 'type': 'bool'},
141
            'limit': {'description': _('Maximum items')},
142
            'id': {'description': _('Record identifier')},
143
            'q': {'description': _('Full text query')},
144
        },
145
    )
146
    def search(
147
        self,
148
        request,
149
        uri='',
150
        text_template='',
151
        sort=None,
152
        order=True,
153
        limit=None,
154
        id=None,
155
        q=None,
156
        **kwargs,
157
    ):
158
        result = self.call_search(uri, text_template, '', sort, order, limit, id, q)
159
        return {'data': result}
160

  
161
    @endpoint(
162
        name='q',
163
        description=_('Query'),
164
        pattern=r'^(?P<query_slug>[\w:_-]+)/$',
165
        perm='can_access',
166
        show=False,
167
    )
168
    def q(self, request, query_slug, **kwargs):
169
        query = get_object_or_404(Query, resource=self, slug=query_slug)
170
        result = query.q(request, **kwargs)
171
        meta = {'label': query.name, 'description': query.description}
172
        return {'data': result, 'meta': meta}
173

  
174
    def create_query_url(self):
175
        return reverse('plone-query-new', kwargs={'slug': self.slug})
176

  
177

  
178
class Query(BaseQuery):
179
    resource = models.ForeignKey(
180
        to=Plone, related_name='queries', verbose_name=_('Resource'), on_delete=models.CASCADE
181
    )
182
    uri = models.CharField(
183
        verbose_name=_('Uri'),
184
        max_length=128,
185
        help_text=_('uri to query'),
186
        blank=True,
187
    )
188
    text_template = models.TextField(
189
        verbose_name=_('Text template'),
190
        help_text=_("Use Django's template syntax. Attributes can be accessed through {{ attributes.name }}"),
191
        validators=[validate_template],
192
        blank=True,
193
    )
194
    filter_expression = models.TextField(
195
        verbose_name=_('filter'),
196
        help_text=_('Specify refine and exclude facet expressions separated lines'),
197
        blank=True,
198
    )
199
    sort = models.CharField(
200
        verbose_name=_('Sort field'),
201
        help_text=_(
202
            "Sorts results by the specified field. A minus sign - may be used to perform an ascending sort."
203
        ),
204
        max_length=256,
205
        blank=True,
206
    )
207
    order = models.BooleanField(
208
        verbose_name=_('Ascending sort order'),
209
        help_text=_("Unset to use descending sort order"),
210
        default=True,
211
    )
212
    limit = models.PositiveIntegerField(
213
        default=10,
214
        verbose_name='Limit',
215
        help_text=_('Number of results to return in a single call'),
216
    )
217

  
218
    delete_view = 'plone-query-delete'
219
    edit_view = 'plone-query-edit'
220

  
221
    def q(self, request, **kwargs):
222
        return self.resource.call_search(
223
            uri=self.uri,
224
            text_template=self.text_template,
225
            filter_expression='&'.join(
226
                [x.strip() for x in str(self.filter_expression).splitlines() if x.strip()]
227
            ),
228
            sort=self.sort,
229
            order=self.order,
230
            limit=self.limit,
231
            id=kwargs.get('id'),
232
            q=kwargs.get('q'),
233
        )
234

  
235
    def as_endpoint(self):
236
        endpoint = super(Query, self).as_endpoint(path=self.resource.q.endpoint_info.name)
237

  
238
        search_endpoint = self.resource.search.endpoint_info
239
        endpoint.func = search_endpoint.func
240
        endpoint.show_undocumented_params = False
241

  
242
        # Copy generic params descriptions from original endpoint
243
        # if they are not overloaded by the query
244
        for param in search_endpoint.parameters:
245
            if param in ('uri', 'text_template') and getattr(self, param):
246
                continue
247
            endpoint.parameters[param] = search_endpoint.parameters[param]
248
        return endpoint
passerelle/apps/plone/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-query-new'),
23
    url(r'^(?P<slug>[\w,-]+)/query/(?P<pk>\d+)/$', views.QueryEdit.as_view(), name='plone-query-edit'),
24
    url(
25
        r'^(?P<slug>[\w,-]+)/query/(?P<pk>\d+)/delete/$',
26
        views.QueryDelete.as_view(),
27
        name='plone-query-delete',
28
    ),
29
]
passerelle/apps/plone/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',
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.plone 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/id_search.json
1
{
2
  "@id": "https://demo.plone.org/es/@search?UID=19f0cd24fec847c09744fbc85aace167",
3
  "items": [
4
    {
5
      "@components": {
6
        "actions": {
7
          "@id": "https://demo.plone.org/es/frontpage/@actions"
8
        },
9
        "breadcrumbs": {
10
          "@id": "https://demo.plone.org/es/frontpage/@breadcrumbs"
11
        },
12
        "contextnavigation": {
13
          "@id": "https://demo.plone.org/es/frontpage/@contextnavigation"
14
        },
15
        "navigation": {
16
          "@id": "https://demo.plone.org/es/frontpage/@navigation"
17
        },
18
        "translations": {
19
          "@id": "https://demo.plone.org/es/frontpage/@translations"
20
        },
21
        "types": {
22
          "@id": "https://demo.plone.org/es/frontpage/@types"
23
        },
24
        "workflow": {
25
          "@id": "https://demo.plone.org/es/frontpage/@workflow"
26
        }
27
      },
28
      "@id": "https://demo.plone.org/es/frontpage",
29
      "@type": "Document",
30
      "UID": "19f0cd24fec847c09744fbc85aace167",
31
      "allow_discussion": false,
32
      "blocks": {},
33
      "blocks_layout": {
34
        "items": []
35
      },
36
      "changeNote": "",
37
      "contributors": [],
38
      "created": "2021-09-23T20:05:00+00:00",
39
      "creators": [
40
        "admin"
41
      ],
42
      "description": "El Sistema de Gesti\u00f3n de Contenido de Fuentes Abiertas",
43
      "effective": null,
44
      "exclude_from_nav": false,
45
      "expires": null,
46
      "id": "frontpage",
47
      "is_folderish": false,
48
      "language": {
49
        "title": "Espa\u00f1ol",
50
        "token": "es"
51
      },
52
      "layout": "document_view",
53
      "modified": "2021-09-23T20:05:00+00:00",
54
      "next_item": {
55
        "@id": "https://demo.plone.org/es/demo",
56
        "@type": "Folder",
57
        "description": "Vestibulum dignissim erat id eros mollis vitae tempus leo ultricies. Cras dapibus suscipit consectetur. Integer tincidunt feugiat tristique. Sed et arcu risus. Nam venenatis, tortor ac tincidunt amet.",
58
        "title": "Demo"
59
      },
60
      "parent": {
61
        "@id": "https://demo.plone.org/es",
62
        "@type": "LRF",
63
        "description": "",
64
        "review_state": "published",
65
        "title": "Espa\u00f1ol"
66
      },
67
      "previous_item": {
68
        "@id": "https://demo.plone.org/es/recursos",
69
        "@type": "LIF",
70
        "description": "",
71
        "title": "Recursos"
72
      },
73
      "relatedItems": [],
74
      "review_state": "published",
75
      "rights": "",
76
      "subjects": [],
77
      "table_of_contents": null,
78
      "text": {
79
        "content-type": "text/html",
80
        "data": "<p>\u00a1Edita esta p\u00e1gina y prueba Plone ahora!</p>",
81
        "encoding": "utf-8"
82
      },
83
      "title": "Bienvenido a Plone",
84
      "version": "current",
85
      "versioning_enabled": true
86
    }
87
  ],
88
  "items_total": 1
89
}
tests/data/plone/q_search.json
1
{
2
  "@id": "https://demo.plone.org/es/@search?bsize=3&portal_type=Document&review_state=published",
3
  "items": [
4
    {
5
      "@components": {
6
        "actions": {
7
          "@id": "https://demo.plone.org/es/demo/una-carpeta/una-pagina-dentro-de-una-carpeta/@actions"
8
        },
9
        "breadcrumbs": {
10
          "@id": "https://demo.plone.org/es/demo/una-carpeta/una-pagina-dentro-de-una-carpeta/@breadcrumbs"
11
        },
12
        "contextnavigation": {
13
          "@id": "https://demo.plone.org/es/demo/una-carpeta/una-pagina-dentro-de-una-carpeta/@contextnavigation"
14
        },
15
        "navigation": {
16
          "@id": "https://demo.plone.org/es/demo/una-carpeta/una-pagina-dentro-de-una-carpeta/@navigation"
17
        },
18
        "translations": {
19
          "@id": "https://demo.plone.org/es/demo/una-carpeta/una-pagina-dentro-de-una-carpeta/@translations"
20
        },
21
        "types": {
22
          "@id": "https://demo.plone.org/es/demo/una-carpeta/una-pagina-dentro-de-una-carpeta/@types"
23
        },
24
        "workflow": {
25
          "@id": "https://demo.plone.org/es/demo/una-carpeta/una-pagina-dentro-de-una-carpeta/@workflow"
26
        }
27
      },
28
      "@id": "https://demo.plone.org/es/demo/una-carpeta/una-pagina-dentro-de-una-carpeta",
29
      "@type": "Document",
30
      "UID": "593cf5489fce493a95b59bb4f3ef9ee1",
31
      "allow_discussion": false,
32
      "blocks": {},
33
      "blocks_layout": {
34
        "items": []
35
      },
36
      "changeNote": "",
37
      "contributors": [],
38
      "created": "2018-08-27T11:20:58+00:00",
39
      "creators": [
40
        "admin"
41
      ],
42
      "description": "Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Aenean eu leo quam. Pellentesque ornare sem lacinia quam venenatis vestibulum.",
43
      "effective": "2018-08-27T13:21:10",
44
      "exclude_from_nav": false,
45
      "expires": null,
46
      "id": "una-pagina-dentro-de-una-carpeta",
47
      "is_folderish": false,
48
      "language": {
49
        "title": "Espa\u00f1ol",
50
        "token": "es"
51
      },
52
      "layout": "document_view",
53
      "modified": "2021-09-23T20:05:01+00:00",
54
      "next_item": {},
55
      "parent": {
56
        "@id": "https://demo.plone.org/es/demo/una-carpeta",
57
        "@type": "Folder",
58
        "description": "",
59
        "review_state": "published",
60
        "title": "Una carpeta"
61
      },
62
      "previous_item": {},
63
      "relatedItems": [],
64
      "review_state": "published",
65
      "rights": null,
66
      "subjects": [],
67
      "table_of_contents": false,
68
      "text": {
69
        "content-type": "text/html",
70
        "data": "Aenean eu leo quam. Pellentesque ornare sem lacinia quam venenatis vestibulum. Cras mattis consectetur purus sit amet fermentum. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Cras justo odio, dapibus ac facilisis in, egestas eget quam. Integer posuere erat a ante venenatis dapibus posuere velit aliquet. Praesent commodo cursus magna, vel scelerisque nisl consectetur et. Maecenas faucibus mollis interdum.&#13;\n&#13;\nPraesent commodo cursus magna, vel scelerisque nisl consectetur et. Nulla vitae elit libero, a pharetra augue. Maecenas sed diam eget risus varius blandit sit amet non magna. Donec ullamcorper nulla non metus auctor fringilla. Vestibulum id ligula porta felis euismod semper. Cras mattis consectetur purus sit amet fermentum.&#13;\n&#13;\nNullam quis risus eget urna mollis ornare vel eu leo. Donec ullamcorper nulla non metus auctor fringilla. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla vitae elit libero, a pharetra augue. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Nullam id dolor id nibh ultricies vehicula ut id elit. Morbi leo risus, porta ac consectetur ac, vestibulum at eros.",
71
        "encoding": "utf-8"
72
      },
73
      "title": "Una P\u00e1gina dentro de una carpeta",
74
      "version": "current",
75
      "versioning_enabled": true
76
    },
77
    {
78
      "@components": {
79
        "actions": {
80
          "@id": "https://demo.plone.org/es/frontpage/@actions"
81
        },
82
        "breadcrumbs": {
83
          "@id": "https://demo.plone.org/es/frontpage/@breadcrumbs"
84
        },
85
        "contextnavigation": {
86
          "@id": "https://demo.plone.org/es/frontpage/@contextnavigation"
87
        },
88
        "navigation": {
89
          "@id": "https://demo.plone.org/es/frontpage/@navigation"
90
        },
91
        "translations": {
92
          "@id": "https://demo.plone.org/es/frontpage/@translations"
93
        },
94
        "types": {
95
          "@id": "https://demo.plone.org/es/frontpage/@types"
96
        },
97
        "workflow": {
98
          "@id": "https://demo.plone.org/es/frontpage/@workflow"
99
        }
100
      },
101
      "@id": "https://demo.plone.org/es/frontpage",
102
      "@type": "Document",
103
      "UID": "19f0cd24fec847c09744fbc85aace167",
104
      "allow_discussion": false,
105
      "blocks": {},
106
      "blocks_layout": {
107
        "items": []
108
      },
109
      "changeNote": "",
110
      "contributors": [],
111
      "created": "2021-09-23T20:05:00+00:00",
112
      "creators": [
113
        "admin"
114
      ],
115
      "description": "El Sistema de Gesti\u00f3n de Contenido de Fuentes Abiertas",
116
      "effective": null,
117
      "exclude_from_nav": false,
118
      "expires": null,
119
      "id": "frontpage",
120
      "is_folderish": false,
121
      "language": {
122
        "title": "Espa\u00f1ol",
123
        "token": "es"
124
      },
125
      "layout": "document_view",
126
      "modified": "2021-09-23T20:05:00+00:00",
127
      "next_item": {
128
        "@id": "https://demo.plone.org/es/demo",
129
        "@type": "Folder",
130
        "description": "Vestibulum dignissim erat id eros mollis vitae tempus leo ultricies. Cras dapibus suscipit consectetur. Integer tincidunt feugiat tristique. Sed et arcu risus. Nam venenatis, tortor ac tincidunt amet.",
131
        "title": "Demo"
132
      },
133
      "parent": {
134
        "@id": "https://demo.plone.org/es",
135
        "@type": "LRF",
136
        "description": "",
137
        "review_state": "published",
138
        "title": "Espa\u00f1ol"
139
      },
140
      "previous_item": {
141
        "@id": "https://demo.plone.org/es/recursos",
142
        "@type": "LIF",
143
        "description": "",
144
        "title": "Recursos"
145
      },
146
      "relatedItems": [],
147
      "review_state": "published",
148
      "rights": "",
149
      "subjects": [],
150
      "table_of_contents": null,
151
      "text": {
152
        "content-type": "text/html",
153
        "data": "<p>\u00a1Edita esta p\u00e1gina y prueba Plone ahora!</p>",
154
        "encoding": "utf-8"
155
      },
156
      "title": "Bienvenido a Plone",
157
      "version": "current",
158
      "versioning_enabled": true
159
    },
160
    {
161
      "@components": {
162
        "actions": {
163
          "@id": "https://demo.plone.org/es/demo/una-pagina/@actions"
164
        },
165
        "breadcrumbs": {
166
          "@id": "https://demo.plone.org/es/demo/una-pagina/@breadcrumbs"
167
        },
168
        "contextnavigation": {
169
          "@id": "https://demo.plone.org/es/demo/una-pagina/@contextnavigation"
170
        },
171
        "navigation": {
172
          "@id": "https://demo.plone.org/es/demo/una-pagina/@navigation"
173
        },
174
        "translations": {
175
          "@id": "https://demo.plone.org/es/demo/una-pagina/@translations"
176
        },
177
        "types": {
178
          "@id": "https://demo.plone.org/es/demo/una-pagina/@types"
179
        },
180
        "workflow": {
181
          "@id": "https://demo.plone.org/es/demo/una-pagina/@workflow"
182
        }
183
      },
184
      "@id": "https://demo.plone.org/es/demo/una-pagina",
185
      "@type": "Document",
186
      "UID": "13f909b522a94443823e187ea9ebab0b",
187
      "allow_discussion": false,
188
      "blocks": {},
189
      "blocks_layout": {
190
        "items": []
191
      },
192
      "changeNote": "",
193
      "contributors": [],
194
      "created": "2018-08-27T11:12:41+00:00",
195
      "creators": [
196
        "admin"
197
      ],
198
      "description": "Aenean dictum auctor elit, in volutpat ipsum venenatis at. Quisque lobortis augue et enim venenatis interdum. In egestas, est at condimentum ultrices, tortor enim malesuada nulla; vel sagittis nullam.",
199
      "effective": "2018-08-27T13:16:45",
200
      "exclude_from_nav": false,
201
      "expires": null,
202
      "id": "una-pagina",
203
      "is_folderish": false,
204
      "language": {
205
        "title": "Espa\u00f1ol",
206
        "token": "es"
207
      },
208
      "layout": "document_view",
209
      "modified": "2021-09-23T20:05:00+00:00",
210
      "next_item": {
211
        "@id": "https://demo.plone.org/es/demo/un-evento",
212
        "@type": "Event",
213
        "description": "Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Maecenas sed diam eget risus varius blandit sit amet non magna.",
214
        "title": "Un Evento"
215
      },
216
      "parent": {
217
        "@id": "https://demo.plone.org/es/demo",
218
        "@type": "Folder",
219
        "description": "Vestibulum dignissim erat id eros mollis vitae tempus leo ultricies. Cras dapibus suscipit consectetur. Integer tincidunt feugiat tristique. Sed et arcu risus. Nam venenatis, tortor ac tincidunt amet.",
220
        "review_state": "published",
221
        "title": "Demo"
222
      },
223
      "previous_item": {},
224
      "relatedItems": [],
225
      "review_state": "published",
226
      "rights": null,
227
      "subjects": [],
228
      "table_of_contents": false,
229
      "text": {
230
        "content-type": "text/html",
231
        "data": "Fusce vel ante vel dolor feugiat vulputate? Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque luctus pretium tellus, id porttitor erat volutpat et. Donec vehicula accumsan ornare. Mauris quis vulputate dui. Duis convallis congue ornare. Vivamus cursus vestibulum neque at fermentum. Mauris sed velit in enim scelerisque luctus ac vel mauris. Etiam imperdiet tempor lorem, quis ultrices quam blandit ultricies. Ut sodales lacinia purus hendrerit lobortis. Pellentesque blandit; sem at aliquam pulvinar, felis diam tincidunt nisi, ac varius ligula eros eget tortor.&#13;\n&#13;\nVivamus leo ipsum&#13;\nDictum sed luctus elementum, ornare quis justo? Nam sagittis mattis turpis, eu varius sapien pulvinar non. Etiam in enim eget odio cursus condimentum! Nullam porta, quam ut sagittis auctor, dui urna ullamcorper urna, semper facilisis purus velit nec leo. Nam libero sem, auctor vitae pretium sit amet, posuere eget arcu.&#13;\n&#13;\nMaecenas ultrices&#13;\nNeque in porttitor scelerisque, nunc nunc semper libero, ac dapibus leo lectus et dui. Praesent sem urna, malesuada in volutpat ac, tincidunt sit amet dolor. Ut dignissim ante vel sem semper venenatis? Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. In hac habitasse platea dictumst. Aenean vitae lectus leo; non bibendum purus.&#13;\n&#13;\nQuisque sit amet aliquam elit&#13;\nAenean odio urna, congue eu sollicitudin ac&#13;\nInterdum ac lorem&#13;\nPellentesque habitant morbi tristique&#13;\nAliquam mattis purus vel nunc tempor sed tempus turpis imperdiet. Pellentesque tincidunt gravida eros at adipiscing. Sed ut tempus nibh. Suspendisse euismod, metus sed lobortis luctus, odio nulla malesuada turpis, in aliquam elit lorem id ante? Pellentesque a elementum dui! Morbi id tellus eget lacus sollicitudin dignissim. Praesent venenatis pellentesque dolor, nec vestibulum felis consectetur non? Sed facilisis, velit vel auctor aliquam, lorem mauris euismod libero, non pellentesque urna mauris vel sem. Aenean eget diam at sem auctor lacinia nec non nisl. Integer sodales fringilla vulputate. Duis massa ante, aliquet id interdum at; placerat nec magna. Ut eleifend sem ut mi elementum eget pellentesque urna dignissim!",
232
        "encoding": "utf-8"
233
      },
234
      "title": "Una p\u00e1gina",
235
      "version": "current",
236
      "versioning_enabled": true
237
    }
238
  ],
239
  "items_total": 3
240
}
tests/test_plone.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 mock
22
import pytest
23
import utils
24
from requests.exceptions import ConnectionError
25
from test_manager import login
26

  
27
from passerelle.apps.plone.models import Plone, Query
28
from passerelle.utils import import_site
29

  
30
pytestmark = pytest.mark.django_db
31

  
32
TEST_BASE_DIR = os.path.join(os.path.dirname(__file__), 'data', 'plone')
33

  
34

  
35
# test data comes from https://demo.plone.org/en
36
# TODO : gérer les accents
37
# $ curl 'https://demo.plone.org/es/@search?b_size=5' -H "Accept: application/json
38
def json_get_data(filename):
39
    with open(os.path.join(TEST_BASE_DIR, "%s.json" % filename)) as fd:
40
        return json.load(fd)
41

  
42

  
43
@pytest.fixture
44
def connector():
45
    return utils.setup_access_rights(
46
        Plone.objects.create(
47
            slug='my_connector',
48
            service_url='http://www.example.net',
49
        )
50
    )
51

  
52

  
53
@pytest.fixture
54
def query(connector):
55
    return Query.objects.create(
56
        resource=connector,
57
        name='demo query',
58
        slug='my_query',
59
        description='Spanish published documents',
60
        uri='es',
61
        text_template='{{ title }} ({{ portal_type }})',
62
        filter_expression='''
63
portal_type=Document
64
review_state=published
65
''',
66
        sort='UID',
67
        order=False,
68
        limit=3,
69
    )
70

  
71

  
72
def test_views(db, admin_user, app, connector):
73
    app = login(app)
74
    resp = app.get('/plone/my_connector/', status=200)
75
    resp = resp.click('New Query')
76
    resp.form['name'] = 'my query'
77
    resp.form['slug'] = 'my-query'
78
    resp.form['uri'] = 'my-uri'
79
    resp = resp.form.submit()
80
    resp = resp.follow()
81
    assert resp.html.find('div', {'id': 'queries'}).ul.li.a.text == 'my query'
82

  
83

  
84
def test_views_query_unicity(admin_user, app, connector, query):
85
    connector2 = Plone.objects.create(
86
        slug='my_connector2',
87
    )
88
    Query.objects.create(
89
        resource=connector2,
90
        slug='foo-bar',
91
        name='Foo Bar',
92
    )
93

  
94
    # create
95
    app = login(app)
96
    resp = app.get('/manage/plone/%s/query/new/' % connector.slug)
97
    resp.form['slug'] = query.slug
98
    resp.form['name'] = 'Foo Bar'
99
    resp = resp.form.submit()
100
    assert resp.status_code == 200
101
    assert 'A query with this slug already exists' in resp.text
102
    assert Query.objects.filter(resource=connector).count() == 1
103
    resp.form['slug'] = 'foo-bar'
104
    resp.form['name'] = query.name
105
    resp = resp.form.submit()
106
    assert resp.status_code == 200
107
    assert 'A query with this name already exists' in resp.text
108
    assert Query.objects.filter(resource=connector).count() == 1
109
    resp.form['slug'] = 'foo-bar'
110
    resp.form['name'] = 'Foo Bar'
111
    resp = resp.form.submit()
112
    assert resp.status_code == 302
113
    assert Query.objects.filter(resource=connector).count() == 2
114
    new_query = Query.objects.latest('pk')
115
    assert new_query.resource == connector
116
    assert new_query.slug == 'foo-bar'
117
    assert new_query.name == 'Foo Bar'
118

  
119
    # update
120
    resp = app.get('/manage/plone/%s/query/%s/' % (connector.slug, new_query.pk))
121
    resp.form['slug'] = query.slug
122
    resp.form['name'] = 'Foo Bar'
123
    resp = resp.form.submit()
124
    assert resp.status_code == 200
125
    assert 'A query with this slug already exists' in resp.text
126
    resp.form['slug'] = 'foo-bar'
127
    resp.form['name'] = query.name
128
    resp = resp.form.submit()
129
    assert resp.status_code == 200
130
    assert 'A query with this name already exists' in resp.text
131
    resp.form['slug'] = 'foo-bar'
132
    resp.form['name'] = 'Foo Bar'
133
    resp.form['uri'] = 'fr'
134
    resp = resp.form.submit()
135
    assert resp.status_code == 302
136
    query = Query.objects.get(resource=connector, slug='foo-bar')
137
    assert query.uri == 'fr'
138

  
139

  
140
def test_export_import(query):
141
    assert Plone.objects.count() == 1
142
    assert Query.objects.count() == 1
143
    serialization = {'resources': [query.resource.export_json()]}
144
    Plone.objects.all().delete()
145
    assert Plone.objects.count() == 0
146
    assert Query.objects.count() == 0
147
    import_site(serialization)
148
    assert Plone.objects.count() == 1
149
    assert Query.objects.count() == 1
150

  
151

  
152
def test_call_search_errors(app, connector):
153
    endpoint = utils.generic_endpoint_url('plone', 'search', slug=connector.slug)
154
    assert endpoint == '/plone/my_connector/search'
155
    url = connector.service_url + '/@search'
156

  
157
    # Connection error
158
    exception = ConnectionError('Remote end closed connection without response')
159
    with utils.mock_url(url=url, exception=exception):
160
        resp = app.get(endpoint)
161
    assert resp.json['err']
162
    assert resp.json['err_desc'] == 'Plone error: Remote end closed connection without response'
163

  
164
    # HTTP error
165
    json_response = {"message": "Resource not found: https://demo.plone.org/es/@searh", "type": "NotFound"}
166
    with utils.mock_url(url=url, response=json_response, status_code=404):
167
        resp = app.get(endpoint)
168
    assert resp.json['err']
169
    assert 'Plone error: 404 Client Error' in resp.json['err_desc']
170
    assert 'Resource not found' in resp.json['err_desc']
171

  
172
    # bad JSON response
173
    with utils.mock_url(url=url, response='not a json content', status_code=200):
174
        resp = app.get(endpoint)
175
    assert resp.json['err']
176
    assert resp.json['err_desc'] == 'Plone error: bad JSON response'
177

  
178

  
179
def test_call_search_normalize_keys(app, connector):
180
    endpoint = utils.generic_endpoint_url('plone', 'search', slug=connector.slug)
181
    assert endpoint == '/plone/my_connector/search'
182
    url = connector.service_url + '/@search'
183

  
184
    plone_response = json_get_data('q_search')
185
    assert [x['id'] for x in plone_response['items']] == [
186
        'una-pagina-dentro-de-una-carpeta',
187
        'frontpage',
188
        'una-pagina',
189
    ]
190
    assert plone_response['items'][1]['text'] == {
191
        'content-type': 'text/html',
192
        'data': '<p>¡Edita esta página y prueba Plone ahora!</p>',
193
        'encoding': 'utf-8',
194
    }
195
    assert plone_response['items'][1]['parent'] == {
196
        '@id': 'https://demo.plone.org/es',
197
        '@type': 'LRF',
198
        'description': '',
199
        'review_state': 'published',
200
        'title': 'Español',
201
    }
202

  
203
    with utils.mock_url(url=url, response=plone_response):
204
        resp = app.get(
205
            endpoint,
206
            params={
207
                'text_template': '{{ original_id }} {{ original_text.encoding }} {{ parent.portal_type }}',
208
            },
209
        )
210
    assert not resp.json['err']
211
    assert resp.json['data'][1]['text'] == 'frontpage utf-8 LRF'
212

  
213
    # original 'id' and 'text' keys are renamed
214
    assert [x['original_id'] for x in resp.json['data']] == [x['id'] for x in plone_response['items']]
215
    assert resp.json['data'][1]['original_text'] == plone_response['items'][1]['text']
216
    # key prefixed with '@' are renamed
217
    assert resp.json['data'][1]['parent'] == {
218
        'description': '',
219
        'review_state': 'published',
220
        'title': 'Español',
221
        'portal_id': 'https://demo.plone.org/es',
222
        'portal_type': 'LRF',
223
    }
224

  
225

  
226
def test_search(app, connector):
227
    endpoint = utils.generic_endpoint_url('plone', 'search', slug=connector.slug)
228
    assert endpoint == '/plone/my_connector/search'
229
    url = connector.service_url + '/es/@search'
230
    params = {
231
        'uri': 'es',
232
        'text_template': '{{ title }} ({{ portal_type }})',
233
        'sort': 'UID',
234
        'order': False,
235
        'limit': 3,
236
    }
237
    qs = {}
238
    with utils.mock_url(url=url, response=json_get_data('q_search'), qs=qs):
239
        resp = app.get(endpoint, params=params)
240
    assert qs == {'sort_on': 'UID', 'sort_order': 'descending', 'b_size': '3', 'fullobjects': 'y'}
241
    assert not resp.json['err']
242
    assert len(resp.json['data']) == 3
243
    assert [(x['id'], x['text']) for x in resp.json['data']] == [
244
        ('593cf5489fce493a95b59bb4f3ef9ee1', 'Una Página dentro de una carpeta (Document)'),
245
        ('19f0cd24fec847c09744fbc85aace167', 'Bienvenido a Plone (Document)'),
246
        ('13f909b522a94443823e187ea9ebab0b', 'Una página (Document)'),
247
    ]
248

  
249

  
250
def test_search_using_q(app, connector):
251
    endpoint = utils.generic_endpoint_url('plone', 'search', slug=connector.slug)
252
    assert endpoint == '/plone/my_connector/search'
253
    url = connector.service_url + '/es/@search'
254
    params = {
255
        'uri': 'es',
256
        'text_template': '{{ title }} ({{ portal_type }})',
257
        'sort': 'title',
258
        'order': False,
259
        'limit': '3',
260
        'q': 'Página dentro',
261
    }
262
    qs = {}
263
    with utils.mock_url(url=url, response=json_get_data('q_search'), qs=qs):
264
        resp = app.get(endpoint, params=params)
265
    assert qs == {
266
        'SearchableText': 'Página dentro',
267
        'sort_on': 'title',
268
        'sort_order': 'descending',
269
        'b_size': '3',
270
        'fullobjects': 'y',
271
    }
272
    assert not resp.json['err']
273
    assert len(resp.json['data']) == 3
274
    assert [(x['id'], x['text']) for x in resp.json['data']] == [
275
        ('593cf5489fce493a95b59bb4f3ef9ee1', 'Una Página dentro de una carpeta (Document)'),
276
        ('19f0cd24fec847c09744fbc85aace167', 'Bienvenido a Plone (Document)'),
277
        ('13f909b522a94443823e187ea9ebab0b', 'Una página (Document)'),
278
    ]
279

  
280

  
281
def test_search_using_id(app, connector):
282
    endpoint = utils.generic_endpoint_url('plone', 'search', slug=connector.slug)
283
    assert endpoint == '/plone/my_connector/search'
284
    url = connector.service_url + '/es/@search'
285
    params = {
286
        'uri': 'es',
287
        'text_template': '{{ title }} ({{ portal_type }})',
288
        'id': '19f0cd24fec847c09744fbc85aace167',
289
    }
290
    qs = {}
291
    with utils.mock_url(url=url, response=json_get_data('id_search'), qs=qs):
292
        resp = app.get(endpoint, params=params)
293
    assert qs == {'UID': '19f0cd24fec847c09744fbc85aace167', 'fullobjects': 'y'}
294
    assert len(resp.json['data']) == 1
295
    assert resp.json['data'][0]['text'] == 'Bienvenido a Plone (Document)'
296

  
297

  
298
def test_query_q(app, query):
299
    endpoint = '/plone/my_connector/q/my_query/'
300
    url = query.resource.service_url + '/es/@search'
301
    params = {
302
        'limit': 3,
303
    }
304
    qs = {}
305
    with utils.mock_url(url=url, response=json_get_data('q_search'), qs=qs):
306
        resp = app.get(endpoint, params=params)
307
        assert qs == {
308
            'sort_on': 'UID',
309
            'sort_order': 'descending',
310
            'b_size': '3',
311
            'portal_type': 'Document',
312
            'review_state': 'published',
313
            'fullobjects': 'y',
314
        }
315
    assert not resp.json['err']
316
    assert len(resp.json['data']) == 3
317
    assert [(x['id'], x['text']) for x in resp.json['data']] == [
318
        ('593cf5489fce493a95b59bb4f3ef9ee1', 'Una Página dentro de una carpeta (Document)'),
319
        ('19f0cd24fec847c09744fbc85aace167', 'Bienvenido a Plone (Document)'),
320
        ('13f909b522a94443823e187ea9ebab0b', 'Una página (Document)'),
321
    ]
322
    assert resp.json['meta'] == {'label': 'demo query', 'description': 'Spanish published documents'}
323

  
324

  
325
def test_query_q_using_q(app, query):
326
    endpoint = '/plone/my_connector/q/my_query/'
327
    url = query.resource.service_url + '/es/@search'
328
    params = {
329
        'q': 'Página dentro',
330
    }
331
    qs = {}
332
    with utils.mock_url(url=url, response=json_get_data('q_search'), qs=qs):
333
        resp = app.get(endpoint, params=params)
334
        assert qs == {
335
            'SearchableText': 'Página dentro',
336
            'sort_on': 'UID',
337
            'sort_order': 'descending',
338
            'b_size': '3',
339
            'portal_type': 'Document',
340
            'review_state': 'published',
341
            'fullobjects': 'y',
342
        }
343
    assert not resp.json['err']
344
    assert [(x['id'], x['text']) for x in resp.json['data']] == [
345
        ('593cf5489fce493a95b59bb4f3ef9ee1', 'Una Página dentro de una carpeta (Document)'),
346
        ('19f0cd24fec847c09744fbc85aace167', 'Bienvenido a Plone (Document)'),
347
        ('13f909b522a94443823e187ea9ebab0b', 'Una página (Document)'),
348
    ]
349
    assert resp.json['meta'] == {'label': 'demo query', 'description': 'Spanish published documents'}
350

  
351

  
352
def test_query_q_using_id(app, query):
353
    endpoint = '/plone/my_connector/q/my_query/'
354
    url = query.resource.service_url + '/es/@search'
355
    params = {
356
        'id': '19f0cd24fec847c09744fbc85aace167',
357
    }
358
    qs = {}
359
    with utils.mock_url(url=url, response=json_get_data('id_search'), qs=qs):
360
        resp = app.get(endpoint, params=params)
361
    assert qs == {
362
        'UID': '19f0cd24fec847c09744fbc85aace167',
363
        'fullobjects': 'y',
364
    }
365
    assert len(resp.json['data']) == 1
366
    assert resp.json['data'][0]['text'] == 'Bienvenido a Plone (Document)'
367
    assert resp.json['meta'] == {'label': 'demo query', 'description': 'Spanish published documents'}
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
-