Projet

Général

Profil

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

Jean-Baptiste Jaillet, 17 octobre 2016 17:17

Télécharger (14,4 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                     |  88 +++++++++++++++++
 .../cmis/templates/cmis/cmisconnector_detail.html  |  18 ++++
 passerelle/apps/cmis/urls.py                       |  22 +++++
 passerelle/apps/cmis/views.py                      |  23 +++++
 passerelle/settings.py                             |   2 +-
 tests/test_cmis.py                                 | 104 +++++++++++++++++++++
 10 files changed, 288 insertions(+), 1 deletion(-)
 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 passerelle/apps/cmis/urls.py
 create mode 100644 passerelle/apps/cmis/views.py
 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', '0003_endpointcheckping'),
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'NOTSET', 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.CharField(help_text='URL of the CMIS endpoint', max_length=250, 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
                'abstract': False,
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, UpdateConflictException, CmisException as CmisLibException
19
import json
20
import base64
21
import logging
22

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

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

  
30

  
31
class CmisConnector(BaseResource):
32
    cmis_endpoint = models.CharField(max_length=250, verbose_name=_('CMIS endpoint'),
33
                                     help_text=_('URL of the CMIS endpoint'))
34
    username = models.CharField(max_length=128, verbose_name=_('Service username'))
35
    password = models.CharField(max_length=128, verbose_name=_('Password'))
36

  
37
    category = _('Business Process Connectors')
38

  
39
    @classmethod
40
    def get_icon_class(cls):
41
        return 'ressources'
42

  
43
    @endpoint(serializer_type='json-api', methods=['post'], perm='can_upload_file')
44
    def upload_file(self, request, **kwargs):
45
        try:
46
            data = json.loads(request.body)
47
        except ValueError:
48
            raise APIError('CmisError : Invalid JSON string sent.')
49

  
50
        if not set(data.keys()) >= set(['filename', 'path', 'content', 'contentType']):
51
            raise APIError('CmisError : missing keys %s' % (set(['filename', 'path', 'content', 'contentType']) -
52
                                                     set(data.keys())))
53
        title = data['filename']
54
        path = data['path'].encode('utf-8')
55
        try:
56
            content = base64.b64decode(data['content'])
57
        except (ValueError, TypeError) as e:
58
            raise APIError('CmisError : content must be a base64 string')
59
        
60
        content_type = data['contentType']
61

  
62
        repo_name = kwargs.get('repo', False)
63
        repo = self.cmis_connection_repository(repo_name)
64
        try:
65
            folder = repo.getObjectByPath(path)
66
        except ObjectNotFoundException:
67
            raise APIError('CmisError : Path not found on platform.')
68

  
69
        try:
70
            doc = folder.createDocumentFromString(title, contentString=content, contentType=content_type)
71
        except UpdateConflictException:
72
            raise APIError('CmisError : The document already exists on platform.')
73

  
74
        return doc.properties
75

  
76
    def cmis_connection_repository(self, repo_name):
77
        try:
78
            cmis_client = CmisClient(self.cmis_endpoint, self.username, self.password)
79
        except PermissionDeniedException:
80
            raise APIError('CmisError : Wrong username or password to connect to platform.')
81
        except ObjectNotFoundException:
82
            raise APIError('CmisError : Platform endpoint not found.')
83

  
84
        if repo_name:
85
            for repo in cmis_client.getRepositories():
86
                if repo_name == repo['repositoryName']:
87
                    return cmis_client.getRepository(repo['repositoryId'])
88
        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
<p>
6
{% blocktrans %}
7
The API currently doesn't support all parameters and is limited to the JSON
8
format.
9
{% endblocktrans %}
10
</p>
11
{% endblock %}
12

  
13
{% block security %}
14
<p>
15
{% trans 'Upload is limited to the following API users:' %}
16
</p>
17
{% access_rights_table resource=object permission='can_upload_file' %}
18
{% endblock %}
passerelle/apps/cmis/urls.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
from django.conf.urls import patterns, url
17

  
18
from .views import *
19

  
20
urlpatterns = patterns('',
21
    url(r'^(?P<slug>[\w-]+)/$', CmisConnectorDetailView.as_view(),
22
        name='cmisconnector-view'))
passerelle/apps/cmis/views.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
from django.views.generic import DetailView
17

  
18
from .models import CmisConnector
19

  
20

  
21
class CmisConnectorDetailView(DetailView):
22
    model = CmisConnector
23
    template_name = 'cmis/cmisconnector_detail.html'
passerelle/settings.py
113 113
    'csvdatasource',
114 114
    'orange',
115 115
    'family',
116
    # backoffice templates and static
116
    'cmis',
117 117
    'gadjo',
118 118
)
119 119

  
tests/test_cmis.py
1
import pytest
2
import mock
3
import base64
4
import json
5

  
6
from cmis.models import CmisConnector
7
from django.contrib.contenttypes.models import ContentType
8
from passerelle.base.models import ApiUser, AccessRight
9
from django.core.urlresolvers import reverse
10

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

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

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

  
22
    return conn
23

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

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

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

  
44
class MockedCmisDocument(mock.Mock):
45

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

  
54

  
55
class MockedCmisFolder(mock.Mock):
56

  
57
    def createDocumentFromString(self, title, contentString, contentType):
58
        return MockedCmisDocument(title=title, contentString=contentString, contentType=contentType)
59

  
60

  
61
class MockedCmisRepository(mock.Mock):
62

  
63
    def getObjectByPath(self, path):
64
        return MockedCmisFolder()
65

  
66

  
67
class MockedCmisClient(mock.Mock):
68

  
69
    @property
70
    def defaultRepository(self):
71
        return MockedCmisRepository()
72

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

  
88
@mock.patch('cmis.models.CmisClient')
89
def test_cmis_upload_error_key(mock_connection, app, setup):
90
    mock_connection.return_value = MockedCmisClient()
91
    resp = app.post_json('/cmis/slug-cmis/upload_file', fake_file_error_one)
92
    assert 'err' in resp.json
93
    assert 'err_desc' in resp.json
94
    assert resp.json['err'] == 1
95
    assert 'CmisError : missing keys' in resp.json['err_desc']
96

  
97
@mock.patch('cmis.models.CmisClient')
98
def test_cmis_upload_error_base64(mock_connection, app, setup):
99
    mock_connection.return_value = MockedCmisClient()
100
    resp = app.post_json('/cmis/slug-cmis/upload_file', fake_file_error_two)
101
    assert 'err' in resp.json
102
    assert 'err_desc' in resp.json
103
    assert resp.json['err'] == 1
104
    assert 'CmisError : content must be a base64 string' in resp.json['err_desc']
0
-