Projet

Général

Profil

0007-toulouse_smart-add-update-intervention-endpoint-5523.patch

Nicolas Roche, 02 août 2021 16:18

Télécharger (19,7 ko)

Voir les différences:

Subject: [PATCH 7/7] toulouse_smart: add update-intervention endpoint (#55230)

 .../migrations/0003_smartrequest.py           |  48 +++++
 passerelle/contrib/toulouse_smart/models.py   | 107 +++++++++++
 passerelle/contrib/toulouse_smart/schemas.py  |  45 +++++
 tests/test_toulouse_smart.py                  | 168 +++++++++++++++++-
 4 files changed, 367 insertions(+), 1 deletion(-)
 create mode 100644 passerelle/contrib/toulouse_smart/migrations/0003_smartrequest.py
passerelle/contrib/toulouse_smart/migrations/0003_smartrequest.py
1
# -*- coding: utf-8 -*-
2
# Generated by Django 1.11.29 on 2021-07-30 22:37
3
from __future__ import unicode_literals
4

  
5
import django.contrib.postgres.fields.jsonb
6
import django.db.models.deletion
7
from django.db import migrations, models
8

  
9

  
10
class Migration(migrations.Migration):
11

  
12
    dependencies = [
13
        ('toulouse_smart', '0002_auto_20210731_0031'),
14
    ]
15

  
16
    operations = [
17
        migrations.CreateModel(
18
            name='SmartRequest',
19
            fields=[
20
                (
21
                    'id',
22
                    models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
23
                ),
24
                ('payload', django.contrib.postgres.fields.jsonb.JSONField(default=dict)),
25
                ('result', django.contrib.postgres.fields.jsonb.JSONField(default=dict)),
26
                (
27
                    'status',
28
                    models.CharField(
29
                        choices=[('registered', 'Registered'), ('sent', 'Sent')],
30
                        default='registered',
31
                        max_length=20,
32
                    ),
33
                ),
34
                (
35
                    'resource',
36
                    models.ForeignKey(
37
                        on_delete=django.db.models.deletion.CASCADE,
38
                        related_name='smart_requests',
39
                        to='toulouse_smart.WcsRequest',
40
                        verbose_name='SmartRequest',
41
                    ),
42
                ),
43
            ],
44
            options={
45
                'ordering': ['pk', 'status'],
46
            },
47
        ),
48
    ]
passerelle/contrib/toulouse_smart/models.py
10 10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12 12
# GNU Affero General Public License for more details.
13 13
#
14 14
# You should have received a copy of the GNU Affero General Public License
15 15
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
16 16

  
17 17
import datetime
18
import json
18 19
from uuid import uuid4
19 20

  
20 21
import lxml.etree as ET
22
from django.conf import settings
21 23
from django.contrib.postgres.fields import JSONField
22 24
from django.db import models
23 25
from django.db.transaction import atomic
26
from django.utils.six.moves.urllib import parse as urlparse
24 27
from django.utils.text import slugify
25 28
from django.utils.timezone import now
26 29
from django.utils.translation import ugettext_lazy as _
27 30
from requests import RequestException
28 31

  
29 32
from passerelle.base.models import BaseResource, HTTPResource, SkipJob
30 33
from passerelle.utils import xml
31 34
from passerelle.utils.api import endpoint
32 35
from passerelle.utils.jsonresponse import APIError
36
from passerelle.utils.wcs import WcsApi, WcsApiError
33 37

  
34 38
from . import schemas
35 39

  
36 40

  
37 41
class ToulouseSmartResource(BaseResource, HTTPResource):
38 42
    category = _('Business Process Connectors')
39 43

  
40 44
    webservice_base_url = models.URLField(_('Webservice Base URL'))
......
236 240
            }
237 241
        }
238 242

  
239 243
    def create_intervention_job(self, *args, **kwargs):
240 244
        wcs_request = self.wcs_requests.get(pk=kwargs['pk'])
241 245
        if not wcs_request.push():
242 246
            raise SkipJob()
243 247

  
248
    @atomic
249
    @endpoint(
250
        name='update-intervention',
251
        methods=['post'],
252
        description=_('Update an intervention status'),
253
        parameters={
254
            'id': {'description': _('Intervention identifier')},
255
        },
256
        post={'request_body': {'schema': {'application/json': schemas.UPDATE_SCHEMA}}},
257
    )
258
    def update_intervention(self, request, uuid, post_data):
259
        try:
260
            wcs_request = self.wcs_requests.get(uuid=uuid)
261
        except WcsRequest.DoesNotExist:
262
            raise APIError("Cannot find intervention '%s'" % uuid, http_status=400)
263
        smart_request = wcs_request.smart_requests.create(payload=post_data)
264
        smart_request.status = 'registered'
265
        smart_request.save()
266
        if smart_request.push():
267
            smart_request = SmartRequest.objects.get(id=smart_request.id)
268
            if smart_request.status != 'sent':
269
                raise APIError(smart_request.result)
270
        else:
271
            self.add_job(
272
                'update_intervention_job',
273
                id=smart_request.id,
274
                natural_id='smart-request-%s' % smart_request.id,
275
            )
276
        smart_request = SmartRequest.objects.get(id=smart_request.id)
277
        return {
278
            'data': {
279
                'wcs_form_api_url': wcs_request.wcs_form_api_url,
280
                'wcs_form_number': wcs_request.wcs_form_number,
281
                'uuid': wcs_request.uuid,
282
                'payload': smart_request.payload,
283
                'result': smart_request.result,
284
                'status': smart_request.status,
285
            }
286
        }
287

  
288
    def update_intervention_job(self, *args, **kwargs):
289
        smart_request = SmartRequest.objects.get(id=kwargs['id'])
290
        if not smart_request.push():
291
            raise SkipJob()
292

  
244 293

  
245 294
class Cache(models.Model):
246 295
    resource = models.ForeignKey(
247 296
        verbose_name=_('Resource'),
248 297
        to=ToulouseSmartResource,
249 298
        on_delete=models.CASCADE,
250 299
        related_name='cache_entries',
251 300
    )
......
291 340
        except ValueError:
292 341
            err_desc = 'invalid json, got: %s' % response.text
293 342
            self.result = err_desc
294 343
            self.save()
295 344
            return False
296 345
        self.status = 'sent'
297 346
        self.save()
298 347
        return True
348

  
349

  
350
class SmartRequest(models.Model):
351
    resource = models.ForeignKey(
352
        verbose_name=_('SmartRequest'),
353
        to=WcsRequest,
354
        on_delete=models.CASCADE,
355
        related_name='smart_requests',
356
    )
357
    payload = JSONField(default=dict)
358
    result = JSONField(default=dict)
359
    status = models.CharField(
360
        max_length=20,
361
        default='registered',
362
        choices=(
363
            ('registered', _('Registered')),
364
            ('sent', _('Sent')),
365
        ),
366
    )
367

  
368
    class Meta:
369
        ordering = ['pk', 'status']
370

  
371
    def push(self):
372
        base_url = '%shooks/update-intervention/' % (self.resource.wcs_form_api_url)
373
        scheme, netloc, path, params, query, fragment = urlparse.urlparse(base_url)
374
        services = settings.KNOWN_SERVICES.get('wcs', {})
375
        service = None
376
        for service in services.values():
377
            remote_url = service.get('url')
378
            r_scheme, r_netloc, r_path, r_params, r_query, r_fragment = urlparse.urlparse(remote_url)
379
            if r_scheme == scheme and r_netloc == netloc:
380
                break
381
        else:
382
            err_desc = 'Cannot find wcs service for %s' % base_url
383
            self.result = err_desc
384
            self.save()
385
            return True
386

  
387
        wcs_api = WcsApi(base_url, orig=service.get('orig'), key=service.get('secret'))
388
        headers = {
389
            'Content-Type': 'application/json',
390
            'Accept': 'application/json',
391
        }
392
        try:
393
            result = wcs_api.post_json(self.payload, [], headers=headers)
394
        except WcsApiError as e:
395
            try:
396
                result = json.loads(e.args[3])
397
            except (ValueError):
398
                return False
399
        self.result = result
400
        if result['err']:
401
            self.save()
402
            return False
403
        self.status = 'sent'
404
        self.save()
405
        return True
passerelle/contrib/toulouse_smart/schemas.py
119 119
        'submitterType',
120 120
        'external_number',
121 121
        'external_status',
122 122
        'address',
123 123
        'form_api_url',
124 124
    ],
125 125
    'merge_extra': True,
126 126
}
127

  
128
UPDATE_SCHEMA = {
129
    '$schema': 'http://json-schema.org/draft-04/schema#',
130
    'type': 'object',
131
    'properties': {
132
        'data': {
133
            'type': 'object',
134
            'properties': {
135
                'status': {
136
                    'description': "Nouveau Statut de l'intervention",
137
                    'type': 'string',
138
                    'maxLength': 20,
139
                },
140
                'date_planification': {
141
                    'description': "Date de planification de l'intervention",
142
                    'type': 'string',
143
                    'pattern': '[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}',
144
                },
145
                'date_mise_en_securite': {
146
                    'description': "Date de mise en securite de l'intervention",
147
                    'type': 'string',
148
                    'pattern': '[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}',
149
                },
150
                'type_retour': {
151
                    'description': "Type du retour",
152
                    'type': 'string',
153
                },
154
                'type_retour_cloture': {
155
                    'description': "Type du retour de la clôture",
156
                    'type': 'string',
157
                },
158
                'libelle_cloture': {
159
                    'description': "Libellé de la clôture",
160
                    'type': 'string',
161
                },
162
                'commentaire_cloture': {
163
                    'description': "Commentaire de la clôture",
164
                    'type': 'string',
165
                },
166
            },
167
            'required': ['status'],
168
        },
169
    },
170
    'required': ['data'],
171
}
tests/test_toulouse_smart.py
25 25
import httmock
26 26
import lxml.etree as ET
27 27
import mock
28 28
import pytest
29 29
import utils
30 30
from test_manager import login
31 31

  
32 32
from passerelle.base.models import Job
33
from passerelle.contrib.toulouse_smart.models import ToulouseSmartResource, WcsRequest
33
from passerelle.contrib.toulouse_smart.models import SmartRequest, ToulouseSmartResource, WcsRequest
34 34

  
35 35
TEST_BASE_DIR = os.path.join(os.path.dirname(__file__), 'data', 'toulouse_smart')
36 36

  
37 37

  
38 38
@pytest.fixture
39 39
def smart(db):
40 40
    return utils.make_resource(
41 41
        ToulouseSmartResource,
......
43 43
        slug='test',
44 44
        description='Test',
45 45
        webservice_base_url='https://smart.example.com/',
46 46
        basic_auth_username='username',
47 47
        basic_auth_password='password',
48 48
    )
49 49

  
50 50

  
51
@pytest.fixture
52
def wcs_service(settings):
53
    wcs_service = {
54
        'default': {
55
            'title': 'test',
56
            'url': 'https://wcs.example.com',
57
            'secret': 'xxx',
58
            'orig': 'passerelle',
59
        },
60
    }
61
    settings.KNOWN_SERVICES = {'wcs': wcs_service}
62
    return wcs_service
63

  
64

  
51 65
def mock_response(*path_contents):
52 66
    def decorator(func):
53 67
        @httmock.urlmatch()
54 68
        def error(url, request):
55 69
            assert False, 'request to %s' % url.geturl()
56 70

  
57 71
        def register(path, payload, content, status_code=200):
58 72
            @httmock.urlmatch(path=path)
......
519 533
)
520 534
@mock.patch("django.db.models.fields.UUIDField.get_default", return_value=UUID)
521 535
def test_create_intervention_content_error(mocked_uuid, app, freezer, smart):
522 536
    freezer.move_to('2021-07-08 00:00:00')
523 537
    resp = app.post_json(URL + 'create-intervention/', params=CREATE_INTERVENTION_PAYLOAD)
524 538
    wcs_request = smart.wcs_requests.get(uuid=UUID)
525 539
    assert wcs_request.status == 'registered'
526 540
    assert 'invalid json' in wcs_request.result
541

  
542

  
543
UPDATE_INTERVENTION_PAYLOAD = {
544
    'data': {
545
        'status': 'close manque info',
546
        'type_retour_cloture': 'Smart non Fait',
547
        'libelle_cloture': "rien à l'adresse indiquée",
548
        'commentaire_cloture': 'le commentaire',
549
    }
550
}
551
UPDATE_INTERVENTION_QUERY = UPDATE_INTERVENTION_PAYLOAD
552
WCS_RESPONSE_SUCCESS = '{"err": 0, "url": null}'
553
WCS_RESPONSE_ERROR = '{"err": 1, "err_class": "Access denied", "err_desc": null}'
554

  
555

  
556
@mock_response(
557
    ['/v1/type-intervention', None, INTERVENTION_TYPES],
558
    ['/v1/intervention', CREATE_INTERVENTION_QUERY, get_json_file('create_intervention')],
559
    ['/api/forms/foo/2/hooks/update-intervention/', UPDATE_INTERVENTION_QUERY, WCS_RESPONSE_SUCCESS],
560
)
561
@mock.patch("django.db.models.fields.UUIDField.get_default", return_value=UUID)
562
def test_update_intervention(mocked_uuid, app, smart, wcs_service):
563
    resp = app.post_json(URL + 'create-intervention/', params=CREATE_INTERVENTION_PAYLOAD)
564
    assert not resp.json['err']
565
    assert CREATE_INTERVENTION_QUERY['notificationUrl'] == 'update-intervention?uuid=%s' % str(UUID)
566
    wcs_request = smart.wcs_requests.get(uuid=UUID)
567
    assert wcs_request.status == 'sent'
568

  
569
    url = URL + 'update-intervention?uuid=%s' % str(UUID)
570
    resp = app.post_json(url, params=UPDATE_INTERVENTION_PAYLOAD)
571
    assert not resp.json['err']
572
    assert resp.json['data']['uuid'] == str(UUID)
573
    assert resp.json['data']['payload']['data']['type_retour_cloture'] == 'Smart non Fait'
574
    smart_request = smart.wcs_requests.get(uuid=UUID).smart_requests.get()
575
    assert smart_request.status == 'sent'
576
    assert smart_request.result == {'err': 0, 'url': None}
577

  
578

  
579
@mock_response(
580
    ['/v1/type-intervention', None, INTERVENTION_TYPES],
581
    ['/v1/intervention', CREATE_INTERVENTION_QUERY, get_json_file('create_intervention')],
582
    ['/api/forms/foo/2/hooks/update-intervention/', UPDATE_INTERVENTION_QUERY, WCS_RESPONSE_SUCCESS],
583
)
584
@mock.patch("django.db.models.fields.UUIDField.get_default", return_value=UUID)
585
def test_update_intervention_async(mocked_uuid, app, smart, wcs_service):
586
    resp = app.post_json(URL + 'create-intervention/', params=CREATE_INTERVENTION_PAYLOAD)
587
    assert not resp.json['err']
588
    assert CREATE_INTERVENTION_QUERY['notificationUrl'] == 'update-intervention?uuid=%s' % str(UUID)
589
    wcs_request = smart.wcs_requests.get(uuid=UUID)
590
    assert wcs_request.status == 'sent'
591

  
592
    mocked_push = mock.patch(
593
        "passerelle.contrib.toulouse_smart.models.SmartRequest.push",
594
        return_value=False,
595
    )
596
    mocked_push.start()
597

  
598
    assert Job.objects.count() == 0
599
    url = URL + 'update-intervention?uuid=%s' % str(UUID)
600
    resp = app.post_json(url, params=UPDATE_INTERVENTION_PAYLOAD)
601
    assert not resp.json['err']
602
    assert resp.json['data']['uuid'] == str(UUID)
603
    assert resp.json['data']['payload']['data']['type_retour_cloture'] == 'Smart non Fait'
604
    smart_request = smart.wcs_requests.get(uuid=UUID).smart_requests.get()
605
    assert smart_request.status == 'registered'
606

  
607
    mocked_push.stop()
608
    assert Job.objects.count() == 1
609
    job = Job.objects.get(method_name='update_intervention_job')
610
    assert job.status == 'registered'
611
    smart.jobs()
612
    job = Job.objects.get(method_name='update_intervention_job')
613
    assert job.status == 'completed'
614
    smart_request = smart.wcs_requests.get(uuid=UUID).smart_requests.get()
615
    assert smart_request.status == 'sent'
616
    assert smart_request.result == {'err': 0, 'url': None}
617

  
618

  
619
def test_update_intervention_wrong_uuid(app, smart):
620
    with pytest.raises(WcsRequest.DoesNotExist):
621
        smart.wcs_requests.get(uuid=UUID)
622

  
623
    url = URL + 'update-intervention?uuid=%s' % str(UUID)
624
    resp = app.post_json(url, params=UPDATE_INTERVENTION_PAYLOAD, status=400)
625
    assert resp.json['err']
626
    assert 'Cannot find intervention' in resp.json['err_desc']
627
    assert SmartRequest.objects.count() == 0
628

  
629

  
630
@mock_response(
631
    ['/v1/type-intervention', None, INTERVENTION_TYPES],
632
    ['/v1/intervention', CREATE_INTERVENTION_QUERY, get_json_file('create_intervention')],
633
)
634
@mock.patch("django.db.models.fields.UUIDField.get_default", return_value=UUID)
635
def test_update_intervention_job_wrong_service(mocked_uuid, app, smart, wcs_service):
636
    resp = app.post_json(URL + 'create-intervention/', params=CREATE_INTERVENTION_PAYLOAD)
637
    assert not resp.json['err']
638

  
639
    wcs_service['default']['url'] = 'http://wrong.example.com'
640
    url = URL + 'update-intervention?uuid=%s' % str(UUID)
641
    resp = app.post_json(url, params=UPDATE_INTERVENTION_PAYLOAD)
642
    assert resp.json['err']
643
    assert 'Cannot find wcs service' in resp.json['err_desc']
644
    assert SmartRequest.objects.count() == 0
645

  
646

  
647
@mock_response(
648
    ['/v1/type-intervention', None, INTERVENTION_TYPES],
649
    ['/v1/intervention', CREATE_INTERVENTION_QUERY, get_json_file('create_intervention')],
650
    ['/api/forms/foo/2/hooks/update-intervention/', UPDATE_INTERVENTION_QUERY, WCS_RESPONSE_ERROR, 403],
651
)
652
@mock.patch("django.db.models.fields.UUIDField.get_default", return_value=UUID)
653
def test_update_intervention_job_wcs_error(mocked_uuid, app, smart, wcs_service):
654
    resp = app.post_json(URL + 'create-intervention/', params=CREATE_INTERVENTION_PAYLOAD)
655
    assert not resp.json['err']
656

  
657
    url = URL + 'update-intervention?uuid=%s' % str(UUID)
658
    resp = app.post_json(url, params=UPDATE_INTERVENTION_PAYLOAD)
659
    assert not resp.json['err']
660
    smart_request = smart.wcs_requests.get(uuid=UUID).smart_requests.get()
661
    assert smart_request.status == 'registered'
662
    assert smart_request.result == {'err': 1, 'err_class': 'Access denied', 'err_desc': None}
663

  
664

  
665
@mock_response(
666
    ['/v1/type-intervention', None, INTERVENTION_TYPES],
667
    ['/v1/intervention', CREATE_INTERVENTION_QUERY, get_json_file('create_intervention')],
668
    ['/api/forms/foo/2/hooks/update-intervention/', UPDATE_INTERVENTION_QUERY, 'bla', 500],
669
)
670
@mock.patch("django.db.models.fields.UUIDField.get_default", return_value=UUID)
671
def test_update_intervention_job_transport_error(mocked_uuid, app, freezer, smart, wcs_service):
672
    freezer.move_to('2021-07-08 00:00:00')
673
    resp = app.post_json(URL + 'create-intervention/', params=CREATE_INTERVENTION_PAYLOAD)
674
    assert not resp.json['err']
675

  
676
    url = URL + 'update-intervention?uuid=%s' % str(UUID)
677
    resp = app.post_json(url, params=UPDATE_INTERVENTION_PAYLOAD)
678
    assert not resp.json['err']
679
    job = Job.objects.get(method_name='update_intervention_job')
680
    assert job.status == 'registered'
681
    smart_request = smart.wcs_requests.get(uuid=UUID).smart_requests.get()
682
    assert smart_request.status == 'registered'
683
    assert smart_request.result == {}
684

  
685
    freezer.move_to('2021-07-08 00:00:03')
686
    smart.jobs()
687
    job = Job.objects.get(method_name='update_intervention_job')
688
    assert job.status == 'registered'
689
    assert job.update_timestamp > job.creation_timestamp
690
    smart_request = smart.wcs_requests.get(uuid=UUID).smart_requests.get()
691
    assert smart_request.status == 'registered'
692
    assert smart_request.result == {}
527
-