Projet

Général

Profil

0001-cmis-add-cmis-connector-to-upload-file-12876.patch

Jean-Baptiste Jaillet, 09 juin 2017 12:07

Télécharger (12,6 ko)

Voir les différences:

Subject: [PATCH] cmis: add cmis connector to upload file (#12876)

 debian/control                                     |   1 +
 passerelle/apps/cmis/__init__.py                   |   0
 passerelle/apps/cmis/migrations/0001_initial.py    |  31 ++++++
 passerelle/apps/cmis/migrations/__init__.py        |   0
 passerelle/apps/cmis/models.py                     | 101 +++++++++++++++++++
 .../cmis/templates/cmis/cmisconnector_detail.html  |  27 +++++
 passerelle/settings.py                             |   1 +
 tests/test_cmis.py                                 | 110 +++++++++++++++++++++
 8 files changed, 271 insertions(+)
 create mode 100644 passerelle/apps/cmis/__init__.py
 create mode 100644 passerelle/apps/cmis/migrations/0001_initial.py
 create mode 100644 passerelle/apps/cmis/migrations/__init__.py
 create mode 100644 passerelle/apps/cmis/models.py
 create mode 100644 passerelle/apps/cmis/templates/cmis/cmisconnector_detail.html
 create mode 100644 tests/test_cmis.py
debian/control
22 22
    python-django-jsonfield,
23 23
    python-magic,
24 24
    python-suds,
25
    python-cmislib,
25 26
Recommends: python-soappy, python-phpserialize
26 27
Suggests: python-sqlalchemy, python-mako
27 28
Description: Uniform access to multiple data sources and services (Python module)
passerelle/apps/cmis/migrations/0001_initial.py
1
# -*- coding: utf-8 -*-
2
from __future__ import unicode_literals
3

  
4
from django.db import models, migrations
5

  
6

  
7
class Migration(migrations.Migration):
8

  
9
    dependencies = [
10
        ('base', '0005_resourcelog'),
11
    ]
12

  
13
    operations = [
14
        migrations.CreateModel(
15
            name='CmisConnector',
16
            fields=[
17
                ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
18
                ('title', models.CharField(max_length=50)),
19
                ('slug', models.SlugField()),
20
                ('description', models.TextField()),
21
                ('log_level', models.CharField(default=b'INFO', max_length=10, verbose_name='Log Level', choices=[(b'NOTSET', b'NOTSET'), (b'DEBUG', b'DEBUG'), (b'INFO', b'INFO'), (b'WARNING', b'WARNING'), (b'ERROR', b'ERROR'), (b'CRITICAL', b'CRITICAL'), (b'FATAL', b'FATAL')])),
22
                ('cmis_endpoint', models.URLField(help_text='URL of the CMIS endpoint', max_length=400, verbose_name='CMIS endpoint')),
23
                ('username', models.CharField(max_length=128, verbose_name='Service username')),
24
                ('password', models.CharField(max_length=128, verbose_name='Password')),
25
                ('users', models.ManyToManyField(to='base.ApiUser', blank=True)),
26
            ],
27
            options={
28
                'verbose_name': 'CMIS connector',
29
            },
30
        ),
31
    ]
passerelle/apps/cmis/models.py
1
# passerelle.apps.cmis
2
# Copyright (C) 2016  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 cmislib import CmisClient
18
from cmislib.model import (PermissionDeniedException, ObjectNotFoundException,
19
                           UpdateConflictException)
20
import json
21
import base64
22
import logging
23

  
24
from django.db import models
25
from django.utils.translation import ugettext_lazy as _
26

  
27
from passerelle.base.models import BaseResource
28
from passerelle.utils.api import endpoint
29
from passerelle.utils.jsonresponse import APIError
30

  
31

  
32
class CMISError(APIError):
33
    pass
34

  
35

  
36
class CmisConnector(BaseResource):
37
    cmis_endpoint = models.URLField(max_length=400, verbose_name=_('CMIS endpoint'),
38
                                     help_text=_('URL of the CMIS endpoint'))
39
    username = models.CharField(max_length=128, verbose_name=_('Service username'))
40
    password = models.CharField(max_length=128, verbose_name=_('Password'))
41

  
42
    category = _('Business Process Connectors')
43

  
44
    class Meta:
45
        verbose_name = _('CMIS connector')
46

  
47

  
48
    def get_description_fields(self):
49
        fields = super(CmisConnector, self).get_description_fields()
50
        return [(x[0], x[1]) for x in fields if x[0].name != 'password']
51

  
52
    @endpoint(serializer_type='json-api', methods=['post'], perm='can_upload_file')
53
    def upload_file(self, request, **kwargs):
54
        try:
55
            data = json.loads(request.body)
56
        except ValueError:
57
            raise CMISError('invalid JSON string sent.')
58

  
59
        if not set(data.keys()) >= set(['filename', 'path', 'content', 'contentType']):
60
            raise CMISError('missing keys %s' % (set(['filename', 'path', 'content', 'contentType']) -
61
                                                     set(data.keys())))
62
        title = data['filename']
63
        path = data['path'].encode('utf-8')
64
        if not path:
65
            path = '/'
66
        try:
67
            content = base64.b64decode(data['content'])
68
        except (ValueError, TypeError) as e:
69
            raise CMISError('content must be a base64 string')
70

  
71
        content_type = data['contentType']
72

  
73
        repo_name = data.get('repository_name', '')
74
        repo = self.cmis_connection_repository(repo_name)
75
        try:
76
            folder = repo.getObjectByPath(path)
77
        except ObjectNotFoundException:
78
            raise CMISError('path not found on platform.')
79

  
80
        try:
81
            doc = folder.createDocumentFromString(title, contentString=content, contentType=content_type)
82
        except UpdateConflictException:
83
            raise CMISError('the document already exists on platform.')
84

  
85
        return doc.properties
86

  
87
    def cmis_connection_repository(self, repo_name):
88
        try:
89
            cmis_client = CmisClient(self.cmis_endpoint, self.username, self.password)
90
        except PermissionDeniedException:
91
            raise CMISError('wrong username or password to connect to platform.')
92
        except ObjectNotFoundException:
93
            raise CMISError('platform endpoint not found.')
94

  
95
        if repo_name:
96
            for repo in cmis_client.getRepositories():
97
                if repo_name == repo['repositoryName']:
98
                    return cmis_client.getRepository(repo['repositoryId'])
99
            raise CMISError('repository %s not found' % repo_name)
100
        else:
101
            return cmis_client.defaultRepository
passerelle/apps/cmis/templates/cmis/cmisconnector_detail.html
1
{% extends "passerelle/manage/service_view.html" %}
2
{% load i18n passerelle %}
3

  
4
{% block endpoints %}
5
<ul>
6
  <li>
7
    <h4>{% trans 'Upload file' %}</h4>
8
    {% url "generic-endpoint" connector="cmis" slug=object.slug endpoint="upload_file" as upload_file %}
9
    <p> <strong>POST</strong> <a href="{{upload_file}}">{{upload_file}}</a></p>
10
    <pre>
11
     data_send = {
12
       'filename': 'test.txt',
13
       'path': '/a/path',
14
       'content': 'ZmlsZSBjb250ZW50' (base64),
15
       'contentType': 'text/plain'
16
    }
17
    </pre>
18
  </li>
19
</ul>
20
{% endblock %}
21

  
22
{% block security %}
23
<p>
24
{% trans 'Upload is limited to the following API users:' %}
25
</p>
26
{% access_rights_table resource=object permission='can_upload_file' %}
27
{% endblock %}
passerelle/settings.py
113 113
    'passerelle.apps.opengis',
114 114
    'passerelle.apps.airquality',
115 115
    'passerelle.apps.okina',
116
    'passerelle.apps.cmis',
116 117
    # backoffice templates and static
117 118
    'gadjo',
118 119
)
tests/test_cmis.py
1
import pytest
2
import mock
3
import base64
4
import json
5

  
6
from django.contrib.contenttypes.models import ContentType
7
from django.core.urlresolvers import reverse
8

  
9
from passerelle.base.models import ApiUser, AccessRight
10
from passerelle.apps.cmis.models import CmisConnector
11

  
12

  
13
@pytest.fixture()
14
def setup(db):
15
    api = ApiUser.objects.create(username='all', keytype='', key='')
16

  
17
    conn = CmisConnector.objects.create(cmis_endpoint='http://example.com/cmisatom', username='admin',
18
                                        password='admin', slug='slug-cmis')
19
    obj_type = ContentType.objects.get_for_model(conn)
20

  
21
    AccessRight.objects.create(codename='can_upload_file', apiuser=api,
22
                               resource_type=obj_type, resource_pk=conn.pk)
23

  
24
    return conn
25

  
26
fake_file = {
27
    'filename': 'test.txt',
28
    'path': '/a/path',
29
    'content': base64.b64encode('fake file content.'),
30
    'contentType': 'text/plain'
31
}
32

  
33
fake_file_error_one = {
34
    'path': '/a/path',
35
    'content': base64.b64encode('fake file content.'),
36
    'contentType': 'text/plain'
37
}
38

  
39
fake_file_error_two = {
40
    'filename': 'test.txt',
41
    'path': '/a/path',
42
    'content': 'fake file content.',
43
    'contentType': 'text/plain'
44
}
45

  
46

  
47
class MockedCmisDocument(mock.Mock):
48

  
49
    @property
50
    def properties(self):
51
        return {
52
            'cmis:baseTypeId': 'cmis:document',
53
            'cmis:contentStreamFileName': self.title,
54
            'cmis:contentStreamMimeType': self.contentType,
55
        }
56

  
57

  
58
class MockedCmisFolder(mock.Mock):
59

  
60
    def createDocumentFromString(self, title, contentString, contentType):
61
        return MockedCmisDocument(title=title, contentString=contentString, contentType=contentType)
62

  
63

  
64
class MockedCmisRepository(mock.Mock):
65

  
66
    def getObjectByPath(self, path):
67
        return MockedCmisFolder()
68

  
69

  
70
class MockedCmisClient(mock.Mock):
71

  
72
    @property
73
    def defaultRepository(self):
74
        return MockedCmisRepository()
75

  
76

  
77
@mock.patch('passerelle.apps.cmis.models.CmisClient')
78
def test_cmis_upload(mock_connection, app, setup):
79
    mock_connection.return_value = MockedCmisClient()
80
    response = app.post_json('/cmis/slug-cmis/upload_file', fake_file)
81
    body = json.loads(response.body)
82
    assert 'data' in body
83
    assert body['err'] == 0
84
    data = body['data']
85
    assert 'cmis:baseTypeId' in data
86
    assert 'cmis:contentStreamFileName' in data
87
    assert 'cmis:contentStreamMimeType' in data
88
    assert data['cmis:baseTypeId'] == 'cmis:document'
89
    assert data['cmis:contentStreamFileName'] == fake_file['filename']
90
    assert data['cmis:contentStreamMimeType'] == fake_file['contentType']
91

  
92

  
93
@mock.patch('passerelle.apps.cmis.models.CmisClient')
94
def test_cmis_upload_error_key(mock_connection, app, setup):
95
    mock_connection.return_value = MockedCmisClient()
96
    resp = app.post_json('/cmis/slug-cmis/upload_file', fake_file_error_one)
97
    assert 'err' in resp.json
98
    assert 'err_desc' in resp.json
99
    assert resp.json['err'] == 1
100
    assert 'CmisError : missing keys' in resp.json['err_desc']
101

  
102

  
103
@mock.patch('passerelle.apps.cmis.models.CmisClient')
104
def test_cmis_upload_error_base64(mock_connection, app, setup):
105
    mock_connection.return_value = MockedCmisClient()
106
    resp = app.post_json('/cmis/slug-cmis/upload_file', fake_file_error_two)
107
    assert 'err' in resp.json
108
    assert 'err_desc' in resp.json
109
    assert resp.json['err'] == 1
110
    assert 'CmisError : content must be a base64 string' in resp.json['err_desc']
0
-