Projet

Général

Profil

0002-add-toulouse_smart-connector-53834.patch

Benjamin Dauvergne, 07 mai 2021 02:03

Télécharger (20,7 ko)

Voir les différences:

Subject: [PATCH 2/2] add toulouse_smart connector (#53834)

 passerelle/contrib/toulouse_smart/__init__.py |   0
 .../toulouse_smart/migrations/0001_initial.py |  92 +++++++++++++++
 .../toulouse_smart/migrations/__init__.py     |   0
 passerelle/contrib/toulouse_smart/models.py   |  94 ++++++++++++++++
 .../toulousesmartresource_detail.html         |  11 ++
 .../toulouse_smart/type-intervention.html     |  48 ++++++++
 .../templates/toulouse_smart/wcs_block.wcs    |  22 ++++
 passerelle/contrib/toulouse_smart/urls.py     |  32 ++++++
 passerelle/contrib/toulouse_smart/views.py    |  67 +++++++++++
 tests/settings.py                             |   1 +
 tests/test_toulouse_smart.py                  | 106 ++++++++++++++++++
 11 files changed, 473 insertions(+)
 create mode 100644 passerelle/contrib/toulouse_smart/__init__.py
 create mode 100644 passerelle/contrib/toulouse_smart/migrations/0001_initial.py
 create mode 100644 passerelle/contrib/toulouse_smart/migrations/__init__.py
 create mode 100644 passerelle/contrib/toulouse_smart/models.py
 create mode 100644 passerelle/contrib/toulouse_smart/templates/toulouse_smart/toulousesmartresource_detail.html
 create mode 100644 passerelle/contrib/toulouse_smart/templates/toulouse_smart/type-intervention.html
 create mode 100644 passerelle/contrib/toulouse_smart/templates/toulouse_smart/wcs_block.wcs
 create mode 100644 passerelle/contrib/toulouse_smart/urls.py
 create mode 100644 passerelle/contrib/toulouse_smart/views.py
 create mode 100644 tests/test_toulouse_smart.py
passerelle/contrib/toulouse_smart/migrations/0001_initial.py
1
# Generated by Django 2.2.19 on 2021-05-06 22:50
2

  
3
import django.contrib.postgres.fields.jsonb
4
from django.db import migrations, models
5
import django.db.models.deletion
6

  
7

  
8
class Migration(migrations.Migration):
9

  
10
    initial = True
11

  
12
    dependencies = [
13
        ('base', '0029_auto_20210202_1627'),
14
    ]
15

  
16
    operations = [
17
        migrations.CreateModel(
18
            name='ToulouseSmartResource',
19
            fields=[
20
                (
21
                    'id',
22
                    models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
23
                ),
24
                ('title', models.CharField(max_length=50, verbose_name='Title')),
25
                ('slug', models.SlugField(unique=True, verbose_name='Identifier')),
26
                ('description', models.TextField(verbose_name='Description')),
27
                (
28
                    'basic_auth_username',
29
                    models.CharField(
30
                        blank=True, max_length=128, verbose_name='Basic authentication username'
31
                    ),
32
                ),
33
                (
34
                    'basic_auth_password',
35
                    models.CharField(
36
                        blank=True, max_length=128, verbose_name='Basic authentication password'
37
                    ),
38
                ),
39
                (
40
                    'client_certificate',
41
                    models.FileField(
42
                        blank=True, null=True, upload_to='', verbose_name='TLS client certificate'
43
                    ),
44
                ),
45
                (
46
                    'trusted_certificate_authorities',
47
                    models.FileField(blank=True, null=True, upload_to='', verbose_name='TLS trusted CAs'),
48
                ),
49
                (
50
                    'verify_cert',
51
                    models.BooleanField(blank=True, default=True, verbose_name='TLS verify certificates'),
52
                ),
53
                (
54
                    'http_proxy',
55
                    models.CharField(blank=True, max_length=128, verbose_name='HTTP and HTTPS proxy'),
56
                ),
57
                ('webservice_base_url', models.URLField(verbose_name='Webservice Base URL')),
58
                (
59
                    'users',
60
                    models.ManyToManyField(
61
                        blank=True,
62
                        related_name='_toulousesmartresource_users_+',
63
                        related_query_name='+',
64
                        to='base.ApiUser',
65
                    ),
66
                ),
67
            ],
68
            options={
69
                'verbose_name': 'Toulouse Smart',
70
            },
71
        ),
72
        migrations.CreateModel(
73
            name='Cache',
74
            fields=[
75
                (
76
                    'id',
77
                    models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
78
                ),
79
                ('key', models.CharField(max_length=64, verbose_name='Key')),
80
                ('timestamp', models.DateTimeField(auto_now=True, verbose_name='Timestamp')),
81
                ('value', django.contrib.postgres.fields.jsonb.JSONField(default=dict, verbose_name='Value')),
82
                (
83
                    'resource',
84
                    models.ForeignKey(
85
                        on_delete=django.db.models.deletion.CASCADE,
86
                        to='toulouse_smart.ToulouseSmartResource',
87
                        verbose_name='Resource',
88
                    ),
89
                ),
90
            ],
91
        ),
92
    ]
passerelle/contrib/toulouse_smart/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
import datetime
18

  
19
from django.db import models
20

  
21
from django.utils.timezone import now
22
from django.utils.translation import ugettext_lazy as _
23

  
24
from django.contrib.postgres.fields import JSONField
25

  
26
import lxml.etree as ET
27

  
28
from passerelle.base.models import BaseResource, HTTPResource
29
from passerelle.utils import xml
30

  
31

  
32
class ToulouseSmartResource(BaseResource, HTTPResource):
33
    category = _('Business Process Connectors')
34

  
35
    webservice_base_url = models.URLField(_('Webservice Base URL'))
36

  
37
    log_requests_errors = False
38

  
39
    class Meta:
40
        verbose_name = _('Toulouse Smart')
41

  
42
    def get_intervention_types(self):
43
        try:
44
            return self.get('intervention-types', max_age=120)
45
        except KeyError:
46
            pass
47

  
48
        try:
49
            url = self.webservice_base_url + 'v1/type-intervention'
50
            response = self.requests.get(url)
51
            doc = ET.fromstring(response.content)
52
            intervention_types = []
53
            for xml_item in doc:
54
                item = xml.to_json(xml_item)
55
                for prop in item.get('properties', []):
56
                    prop['required'] = prop.get('required') == 'true'
57
                intervention_types.append(item)
58
            intervention_types.sort(key=lambda x: x['name'])
59
            for i, intervention_type in enumerate(intervention_types):
60
                intervention_type['order'] = i + 1
61
        except Exception:
62
            try:
63
                return self.get('intervention-types')
64
            except KeyError:
65
                raise
66
        self.set('intervention-types', intervention_types)
67
        return intervention_types
68

  
69
    def get(self, key, max_age=None):
70
        cache_entries = self.cache_entries
71
        if max_age:
72
            cache_entries = cache_entries.filter(timestamp__gt=now() - datetime.timedelta(seconds=max_age))
73
        try:
74
            return cache_entries.get(key='intervention-types').value
75
        except Cache.DoesNotExist:
76
            raise KeyError(key)
77

  
78
    def set(self, key, value):
79
        self.cache_entries.update_or_create(key=key, defaults={'value': value})
80

  
81

  
82
class Cache(models.Model):
83
    resource = models.ForeignKey(
84
        verbose_name=_('Resource'),
85
        to=ToulouseSmartResource,
86
        on_delete=models.CASCADE,
87
        related_name='cache_entries',
88
    )
89

  
90
    key = models.CharField(_('Key'), max_length=64)
91

  
92
    timestamp = models.DateTimeField(_('Timestamp'), auto_now=True)
93

  
94
    value = JSONField(_('Value'), default=dict)
passerelle/contrib/toulouse_smart/templates/toulouse_smart/toulousesmartresource_detail.html
1
{% extends "passerelle/manage/service_view.html" %}
2
{% load i18n passerelle %}
3

  
4
{% block extra-sections %}
5
    <div class="section">
6
        <h3>{% trans "Details" %}</h3>
7
        <ul>
8
            <li><a href="{% url "toulouse-smart-type-intervention" slug=object.slug %}">{% trans "Intervention types" %}</a></li>
9
        </ul>
10
    </div>
11
{% endblock %}
passerelle/contrib/toulouse_smart/templates/toulouse_smart/type-intervention.html
1
{% extends "passerelle/manage.html" %}
2
{% load i18n gadjo %}
3

  
4
{% block breadcrumb %}
5
{{ block.super }}
6
<a href="{{ toulousesmartresource.get_absolute_url }}">{{ toulousesmartresource.title }}</a>
7
<a href="#">{% trans 'Intervention types' %}</a>
8
{% endblock %}
9

  
10
{% block appbar %}
11
<h2>{% trans 'Intervention types' %}</h2>
12
<span class="actions">
13
    <a href="{% url "toulouse-smart-type-intervention-as-blocks" slug=toulousesmartresource.slug %}">{% trans "Export to blocks" %}</a>
14
</span>
15
{% endblock %}
16

  
17
{% block content %}
18
<table class="main">
19
    <thead>
20
        <tr>
21
            <th>Nom du type d'intervention</th>
22
            <th>Nom</th>
23
            <th>Type</th>
24
            <th>Requis</th>
25
            <th>Valeur par défaut</th>
26
        </tr>
27
    </thead>
28
    <tbody>
29
        {% for intervention_type in toulousesmartresource.get_intervention_types %}
30
        <tr><td colspan="4">{{ intervention_type.order }} - {{ intervention_type.name }}</td></tr>
31
            {% for property in intervention_type.properties %}
32
                <tr>
33
                    <td></td>
34
                    <td>{{ property.name }}</td>
35
                    <td>{{ property.type }}</td>
36
                    <td title="{{ property.required|lower }}">{{ property.required|yesno:"✔,✘" }}</td>
37
                    <td>{{ property.defaultValue }}</td>
38
                </tr>
39
            {% endfor %}
40
        {% endfor %}
41
    </tbody>
42
</table>
43

  
44
<!--
45
{{ toulousesmartresource.get_intervention_types|pprint }}
46
-->
47

  
48
{% endblock %}
passerelle/contrib/toulouse_smart/templates/toulouse_smart/wcs_block.wcs
1
<?xml version="1.0"?>
2
<block id="{{ id }}">
3
  <name>{{ name }}</name>
4
  <slug>{{ name|slugify }}</slug>
5
  <fields>
6
    {% for property in properties %}
7
    <field>
8
      <id>{{ property.id }}</id>
9
      <label>{{ property.name }}</label>
10
      <type>{{ property.type }}</type>
11
      <required>{{ property.required }}</required>
12
      <varname>{{ property.name|slugify }}</varname>
13
      <display_locations>
14
        <display_location>validation</display_location>
15
        <display_location>summary</display_location>
16
      </display_locations>{% if property.validation %}
17
      <validation>
18
        <type>{{ property.validation }}</type>
19
      </validation>{% endif %}
20
    </field>{% endfor %}
21
  </fields>
22
</block>
passerelle/contrib/toulouse_smart/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 .views import TypeIntervention, TypeInterventionAsBlocks
20

  
21
management_urlpatterns = [
22
    url(
23
        r'^(?P<slug>[\w,-]+)/type-intervention/as-blocks/$',
24
        TypeInterventionAsBlocks.as_view(),
25
        name='toulouse-smart-type-intervention-as-blocks',
26
    ),
27
    url(
28
        r'^(?P<slug>[\w,-]+)/type-intervention/$',
29
        TypeIntervention.as_view(),
30
        name='toulouse-smart-type-intervention',
31
    ),
32
]
passerelle/contrib/toulouse_smart/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
import hashlib
18
import uuid
19
import zipfile
20

  
21
from django.http import HttpResponse
22
from django.template.loader import render_to_string
23
from django.utils.text import slugify
24
from django.views.generic import DetailView
25

  
26
from .models import ToulouseSmartResource
27

  
28

  
29
class TypeIntervention(DetailView):
30
    model = ToulouseSmartResource
31
    template_name = 'toulouse_smart/type-intervention.html'
32

  
33

  
34
class TypeInterventionAsBlocks(DetailView):
35
    model = ToulouseSmartResource
36

  
37
    def get(self, request, *args, **kwargs):
38
        self.object = self.get_object()
39

  
40
        def make_id(s):
41
            return str(uuid.UUID(bytes=hashlib.md5(s.encode()).digest()))
42

  
43
        # generate file contents
44
        files = {}
45
        for intervention_type in self.object.get_intervention_types():
46
            slug = slugify(intervention_type['name'])
47
            for prop in intervention_type.get('properties', []):
48
                # generate a natural id for fields
49
                prop['id'] = make_id(slug + slugify(prop['name']))
50
                # adapt types
51
                prop.setdefault('type', 'string')
52
                if prop['type'] == 'boolean':
53
                    prop['type'] = 'bool'
54
                if prop['type'] == 'int':
55
                    prop['type'] = 'string'
56
                    prop['validation'] = 'digits'
57
            filename = 'block-%s.wcs' % slug
58
            files[filename] = render_to_string('toulouse_smart/wcs_block.wcs', context=intervention_type)
59

  
60
        # zip it !
61
        response = HttpResponse(content_type='application/zip')
62
        response['Content-Disposition'] = 'attachment; filename=blocks.zip'
63
        with zipfile.ZipFile(response, mode='w') as zip_file:
64
            for name in files:
65
                zip_file.writestr(name, files[name])
66

  
67
        return response
tests/settings.py
39 39
    'passerelle.contrib.teamnet_axel',
40 40
    'passerelle.contrib.tcl',
41 41
    'passerelle.contrib.toulouse_axel',
42
    'passerelle.contrib.toulouse_smart',
42 43
    'passerelle.contrib.lille_kimoce',
43 44
)
44 45

  
tests/test_toulouse_smart.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
import functools
18

  
19
import httmock
20
import pytest
21

  
22
import utils
23

  
24
from passerelle.contrib.toulouse_smart.models import ToulouseSmartResource
25

  
26

  
27
@pytest.fixture
28
def smart(db):
29
    return utils.make_resource(
30
        ToulouseSmartResource,
31
        title='Test',
32
        slug='test',
33
        description='Test',
34
        webservice_base_url='https://smart.example.com/',
35
        basic_auth_username='username',
36
        basic_auth_password='password',
37
    )
38

  
39

  
40
def mock_response(*path_contents):
41
    def decorator(func):
42
        @httmock.urlmatch()
43
        def error(url, request):
44
            assert False, 'request to %s' % (url,)
45

  
46
        @functools.wraps(func)
47
        def wrapper(*args, **kwargs):
48
            handlers = []
49
            for row in path_contents:
50
                path, content = row
51

  
52
                @httmock.urlmatch(path=path)
53
                def handler(url, request):
54
                    return content
55

  
56
                handlers.append(handler)
57
            handlers.append(error)
58

  
59
            with httmock.HTTMock(*handlers):
60
                return func(*args, **kwargs)
61

  
62
        return wrapper
63

  
64
    return decorator
65

  
66

  
67
@mock_response(['/v1/type-intervention', b'<List></List>'])
68
def test_empty_intervention_types(smart):
69
    assert smart.get_intervention_types() == []
70

  
71

  
72
INTERVENTION_TYPES = (
73
    b'''<List>
74
   <item>
75
       <id>1234</id>
76
       <name>coin</name>
77
       <properties>
78
           <properties>
79
              <name>FIELD1</name>
80
              <type>string</type>
81
              <required>false</required>
82
           </properties>
83
           <properties>
84
              <name>FIELD2</name>
85
              <type>int</type>
86
              <required>true</required>
87
           </properties>
88
       </properties>
89
   </item>
90
</List>''',
91
)
92

  
93

  
94
@mock_response(['/v1/type-intervention', INTERVENTION_TYPES])
95
def test_simple_intervention_types(smart):
96
    assert smart.get_intervention_types() == [
97
        {
98
            'id': '1234',
99
            'name': 'coin',
100
            'order': 1,
101
            'properties': [
102
                {'name': 'FIELD1', 'required': False, 'type': 'string'},
103
                {'name': 'FIELD2', 'required': True, 'type': 'int'},
104
            ],
105
        },
106
    ]
0
-