Browse Source

add endpoints for html_documents

Guénaël Muller 6 years ago
parent
commit
46bf33d984

+ 3 - 0
tracim/__init__.py View File

17
 from tracim.lib.utils.authorization import TRACIM_DEFAULT_PERM
17
 from tracim.lib.utils.authorization import TRACIM_DEFAULT_PERM
18
 from tracim.lib.webdav import WebdavAppFactory
18
 from tracim.lib.webdav import WebdavAppFactory
19
 from tracim.views import BASE_API_V2
19
 from tracim.views import BASE_API_V2
20
+from tracim.views.contents_api.html_document_controller import HTMLDocumentController  # nopep8
20
 from tracim.views.core_api.session_controller import SessionController
21
 from tracim.views.core_api.session_controller import SessionController
21
 from tracim.views.core_api.system_controller import SystemController
22
 from tracim.views.core_api.system_controller import SystemController
22
 from tracim.views.core_api.user_controller import UserController
23
 from tracim.views.core_api.user_controller import UserController
73
     user_controller = UserController()
74
     user_controller = UserController()
74
     workspace_controller = WorkspaceController()
75
     workspace_controller = WorkspaceController()
75
     comment_controller = CommentController()
76
     comment_controller = CommentController()
77
+    html_document_controller = HTMLDocumentController()
76
     configurator.include(session_controller.bind, route_prefix=BASE_API_V2)
78
     configurator.include(session_controller.bind, route_prefix=BASE_API_V2)
77
     configurator.include(system_controller.bind, route_prefix=BASE_API_V2)
79
     configurator.include(system_controller.bind, route_prefix=BASE_API_V2)
78
     configurator.include(user_controller.bind, route_prefix=BASE_API_V2)
80
     configurator.include(user_controller.bind, route_prefix=BASE_API_V2)
79
     configurator.include(workspace_controller.bind, route_prefix=BASE_API_V2)
81
     configurator.include(workspace_controller.bind, route_prefix=BASE_API_V2)
80
     configurator.include(comment_controller.bind, route_prefix=BASE_API_V2)
82
     configurator.include(comment_controller.bind, route_prefix=BASE_API_V2)
83
+    configurator.include(html_document_controller.bind, route_prefix=BASE_API_V2)
81
     hapic.add_documentation_view(
84
     hapic.add_documentation_view(
82
         '/api/v2/doc',
85
         '/api/v2/doc',
83
         'Tracim v2 API',
86
         'Tracim v2 API',

+ 12 - 0
tracim/exceptions.py View File

117
     pass
117
     pass
118
 
118
 
119
 
119
 
120
+class ContentNotFoundInTracimRequest(TracimException):
121
+    pass
122
+
123
+
124
+class ContentNotFound(TracimException):
125
+    pass
126
+
127
+
128
+class ContentTypeNotAllowed(TracimException):
129
+    pass
130
+
131
+
120
 class WorkspacesDoNotMatch(TracimException):
132
 class WorkspacesDoNotMatch(TracimException):
121
     pass
133
     pass

+ 12 - 0
tracim/fixtures/content.py View File

121
             do_save=True,
121
             do_save=True,
122
             do_notify=False,
122
             do_notify=False,
123
         )
123
         )
124
+        with new_revision(
125
+                session=self._session,
126
+                tm=transaction.manager,
127
+                content=tiramisu_page,
128
+        ):
129
+            content_api.update_content(
130
+                item=tiramisu_page,
131
+                new_content='<p>To cook a great Tiramisu, you need many ingredients.</p>',  # nopep8
132
+                new_label='Tiramisu Recipe',
133
+            )
134
+            content_api.save(tiramisu_page)
135
+
124
         best_cake_thread = content_api.create(
136
         best_cake_thread = content_api.create(
125
             content_type=ContentType.Thread,
137
             content_type=ContentType.Thread,
126
             workspace=recipe_workspace,
138
             workspace=recipe_workspace,

+ 12 - 2
tracim/lib/core/content.py View File

16
 from sqlalchemy.orm import aliased
16
 from sqlalchemy.orm import aliased
17
 from sqlalchemy.orm import joinedload
17
 from sqlalchemy.orm import joinedload
18
 from sqlalchemy.orm.attributes import get_history
18
 from sqlalchemy.orm.attributes import get_history
19
+from sqlalchemy.orm.exc import NoResultFound
19
 from sqlalchemy.orm.session import Session
20
 from sqlalchemy.orm.session import Session
20
 from sqlalchemy import desc
21
 from sqlalchemy import desc
21
 from sqlalchemy import distinct
22
 from sqlalchemy import distinct
25
 from tracim.lib.utils.utils import cmp_to_key
26
 from tracim.lib.utils.utils import cmp_to_key
26
 from tracim.lib.core.notifications import NotifierFactory
27
 from tracim.lib.core.notifications import NotifierFactory
27
 from tracim.exceptions import SameValueError
28
 from tracim.exceptions import SameValueError
29
+from tracim.exceptions import ContentNotFound
28
 from tracim.exceptions import WorkspacesDoNotMatch
30
 from tracim.exceptions import WorkspacesDoNotMatch
29
 from tracim.lib.utils.utils import current_date_for_filename
31
 from tracim.lib.utils.utils import current_date_for_filename
30
 from tracim.models.revision_protection import new_revision
32
 from tracim.models.revision_protection import new_revision
39
 from tracim.models.data import UserRoleInWorkspace
41
 from tracim.models.data import UserRoleInWorkspace
40
 from tracim.models.data import Workspace
42
 from tracim.models.data import Workspace
41
 from tracim.lib.utils.translation import fake_translator as _
43
 from tracim.lib.utils.translation import fake_translator as _
44
+from tracim.models.context_models import RevisionInContext
42
 from tracim.models.context_models import ContentInContext
45
 from tracim.models.context_models import ContentInContext
43
 
46
 
44
-
45
 __author__ = 'damien'
47
 __author__ = 'damien'
46
 
48
 
47
 
49
 
161
     def get_content_in_context(self, content: Content):
163
     def get_content_in_context(self, content: Content):
162
         return ContentInContext(content, self._session, self._config)
164
         return ContentInContext(content, self._session, self._config)
163
 
165
 
166
+    def get_revision_in_context(self, revision: ContentRevisionRO):
167
+        # TODO - G.M - 2018-06-173 - create revision in context object
168
+        return RevisionInContext(revision, self._session, self._config)
169
+    
164
     def get_revision_join(self) -> sqlalchemy.sql.elements.BooleanClauseList:
170
     def get_revision_join(self) -> sqlalchemy.sql.elements.BooleanClauseList:
165
         """
171
         """
166
         Return the Content/ContentRevision query join condition
172
         Return the Content/ContentRevision query join condition
464
         if parent:
470
         if parent:
465
             base_request = base_request.filter(Content.parent_id==parent.content_id)  # nopep8
471
             base_request = base_request.filter(Content.parent_id==parent.content_id)  # nopep8
466
 
472
 
467
-        return base_request.one()
473
+        try:
474
+            content = base_request.one()
475
+        except NoResultFound as exc:
476
+            raise ContentNotFound('Content "{}" not found in database'.format(content_id)) from exc  # nopep8
477
+        return content
468
 
478
 
469
     def get_one_revision(self, revision_id: int = None) -> ContentRevisionRO:
479
     def get_one_revision(self, revision_id: int = None) -> ContentRevisionRO:
470
         """
480
         """

+ 24 - 0
tracim/lib/utils/authorization.py View File

1
 # -*- coding: utf-8 -*-
1
 # -*- coding: utf-8 -*-
2
+import typing
2
 from typing import TYPE_CHECKING
3
 from typing import TYPE_CHECKING
3
 
4
 
5
+import functools
4
 from pyramid.interfaces import IAuthorizationPolicy
6
 from pyramid.interfaces import IAuthorizationPolicy
5
 from zope.interface import implementer
7
 from zope.interface import implementer
8
+
9
+from tracim.models.contents import NewContentType
10
+
6
 try:
11
 try:
7
     from json.decoder import JSONDecodeError
12
     from json.decoder import JSONDecodeError
8
 except ImportError:  # python3.4
13
 except ImportError:  # python3.4
9
     JSONDecodeError = ValueError
14
     JSONDecodeError = ValueError
10
 
15
 
11
 from tracim.exceptions import InsufficientUserWorkspaceRole
16
 from tracim.exceptions import InsufficientUserWorkspaceRole
17
+from tracim.exceptions import ContentTypeNotAllowed
12
 from tracim.exceptions import InsufficientUserProfile
18
 from tracim.exceptions import InsufficientUserProfile
13
 if TYPE_CHECKING:
19
 if TYPE_CHECKING:
14
     from tracim import TracimRequest
20
     from tracim import TracimRequest
122
 
128
 
123
         return wrapper
129
         return wrapper
124
     return decorator
130
     return decorator
131
+
132
+
133
+def require_content_types(content_types: typing.List['NewContentType']):
134
+    """
135
+    Restricts access to specific file type or raise an exception.
136
+    Check role for candidate_workspace.
137
+    :param content_types: list of NewContentType object
138
+    :return: decorator
139
+    """
140
+    def decorator(func):
141
+        @functools.wraps(func)
142
+        def wrapper(self, context, request: 'TracimRequest'):
143
+            content = request.current_content
144
+            if content.type in [content.slug for content in content_types]:
145
+                return func(self, context, request)
146
+            raise ContentTypeNotAllowed()
147
+        return wrapper
148
+    return decorator

+ 62 - 3
tracim/lib/utils/request.py View File

2
 from pyramid.request import Request
2
 from pyramid.request import Request
3
 from sqlalchemy.orm.exc import NoResultFound
3
 from sqlalchemy.orm.exc import NoResultFound
4
 
4
 
5
-from tracim.exceptions import NotAuthenticated
5
+from tracim.exceptions import NotAuthenticated, ContentNotFound
6
+from tracim.exceptions import ContentNotFoundInTracimRequest
6
 from tracim.exceptions import WorkspaceNotFoundInTracimRequest
7
 from tracim.exceptions import WorkspaceNotFoundInTracimRequest
7
 from tracim.exceptions import UserNotFoundInTracimRequest
8
 from tracim.exceptions import UserNotFoundInTracimRequest
8
 from tracim.exceptions import UserDoesNotExist
9
 from tracim.exceptions import UserDoesNotExist
9
 from tracim.exceptions import WorkspaceNotFound
10
 from tracim.exceptions import WorkspaceNotFound
10
 from tracim.exceptions import ImmutableAttribute
11
 from tracim.exceptions import ImmutableAttribute
12
+from tracim.models.contents import ContentTypeLegacy as ContentType
13
+from tracim.lib.core.content import ContentApi
11
 from tracim.lib.core.user import UserApi
14
 from tracim.lib.core.user import UserApi
12
 from tracim.lib.core.workspace import WorkspaceApi
15
 from tracim.lib.core.workspace import WorkspaceApi
13
 from tracim.lib.utils.authorization import JSONDecodeError
16
 from tracim.lib.utils.authorization import JSONDecodeError
14
 
17
 
15
 from tracim.models import User
18
 from tracim.models import User
16
-from tracim.models.data import Workspace
19
+from tracim.models.data import Workspace, Content
17
 
20
 
18
 
21
 
19
 class TracimRequest(Request):
22
 class TracimRequest(Request):
35
             decode_param_names,
38
             decode_param_names,
36
             **kw
39
             **kw
37
         )
40
         )
41
+        # Current content, found in request path
42
+        self._current_content = None  # type: Content
43
+
38
         # Current workspace, found in request path
44
         # Current workspace, found in request path
39
         self._current_workspace = None  # type: Workspace
45
         self._current_workspace = None  # type: Workspace
40
 
46
 
93
             )
99
             )
94
         self._current_user = user
100
         self._current_user = user
95
 
101
 
102
+    @property
103
+    def current_content(self) -> User:
104
+        """
105
+        Get current  content from path
106
+        """
107
+        if self._current_content is None:
108
+            self._current_content = self._get_current_content(
109
+                self.current_user,
110
+                self.current_workspace,
111
+                self
112
+                )
113
+        return self._current_content
114
+
115
+    @current_content.setter
116
+    def current_content(self, content: Content) -> None:
117
+        if self._current_content is not None:
118
+            raise ImmutableAttribute(
119
+                "Can't modify already setted current_content"
120
+            )
121
+        self._current_content = content
122
+
96
     # TODO - G.M - 24-05-2018 - Find a better naming for this ?
123
     # TODO - G.M - 24-05-2018 - Find a better naming for this ?
97
     @property
124
     @property
98
     def candidate_user(self) -> User:
125
     def candidate_user(self) -> User:
132
         self._current_workspace = None
159
         self._current_workspace = None
133
         self.dbsession.close()
160
         self.dbsession.close()
134
 
161
 
135
-
136
     @candidate_user.setter
162
     @candidate_user.setter
137
     def candidate_user(self, user: User) -> None:
163
     def candidate_user(self, user: User) -> None:
138
         if self._candidate_user is not None:
164
         if self._candidate_user is not None:
145
     # Utils for TracimRequest
171
     # Utils for TracimRequest
146
     ###
172
     ###
147
 
173
 
174
+    def _get_current_content(
175
+            self,
176
+            user: User,
177
+            workspace: Workspace,
178
+            request: 'TracimRequest'
179
+    ):
180
+        """
181
+        Get current content from request
182
+        :param user: User who want to check the workspace
183
+        :param request: pyramid request
184
+        :return: current content
185
+        """
186
+        content_id = ''
187
+        try:
188
+            if 'content_id' in request.matchdict:
189
+                content_id = int(request.matchdict['content_id'])
190
+            if not content_id:
191
+                raise ContentNotFoundInTracimRequest('No content_id property found in request')  # nopep8
192
+            api = ContentApi(
193
+                current_user=user,
194
+                session=request.dbsession,
195
+                config=request.registry.settings['CFG']
196
+            )
197
+            content = api.get_one(content_id=content_id, workspace=workspace, content_type=ContentType.Any)  # nopep8
198
+        except JSONDecodeError:
199
+            raise ContentNotFound('Bad json body')
200
+        except NoResultFound:
201
+            raise ContentNotFound(
202
+                'Content {} does not exist '
203
+                'or is not visible for this user'.format(content_id)
204
+            )
205
+        return content
206
+
148
     def _get_candidate_user(
207
     def _get_candidate_user(
149
             self,
208
             self,
150
             request: 'TracimRequest',
209
             request: 'TracimRequest',

+ 168 - 3
tracim/models/context_models.py View File

8
 from tracim.models import User
8
 from tracim.models import User
9
 from tracim.models.auth import Profile
9
 from tracim.models.auth import Profile
10
 from tracim.models.data import Content
10
 from tracim.models.data import Content
11
+from tracim.models.data import ContentRevisionRO
11
 from tracim.models.data import Workspace, UserRoleInWorkspace
12
 from tracim.models.data import Workspace, UserRoleInWorkspace
12
 from tracim.models.workspace_menu_entries import default_workspace_menu_entry
13
 from tracim.models.workspace_menu_entries import default_workspace_menu_entry
13
 from tracim.models.workspace_menu_entries import WorkspaceMenuEntry
14
 from tracim.models.workspace_menu_entries import WorkspaceMenuEntry
89
             self,
90
             self,
90
             raw_content: str,
91
             raw_content: str,
91
     ):
92
     ):
92
-        self.raw_content=raw_content
93
+        self.raw_content = raw_content
94
+
95
+
96
+class SetContentStatus(object):
97
+    """
98
+    Set content status
99
+    """
100
+    def __init__(
101
+            self,
102
+            status: str,
103
+    ):
104
+        self.status = status
105
+
106
+
107
+class HTMLDocumentUpdate(object):
108
+    """
109
+    Comment creation model
110
+    """
111
+    def __init__(
112
+            self,
113
+            label: str,
114
+            raw_content: str,
115
+    ):
116
+        self.label = label
117
+        self.raw_content = raw_content
118
+
119
+
120
+class ThreadUpdate(object):
121
+    """
122
+    Comment creation model
123
+    """
124
+    def __init__(
125
+            self,
126
+            label: str,
127
+            raw_content: str,
128
+    ):
129
+        self.label = label
130
+        self.raw_content = raw_content
93
 
131
 
94
 
132
 
95
 class UserInContext(object):
133
 class UserInContext(object):
331
 
369
 
332
     @property
370
     @property
333
     def sub_content_types(self) -> typing.List[str]:
371
     def sub_content_types(self) -> typing.List[str]:
334
-        return [type.slug for type in self.content.get_allowed_content_types()]
372
+        return [_type.slug for _type in self.content.get_allowed_content_types()]
335
 
373
 
336
     @property
374
     @property
337
     def status(self) -> str:
375
     def status(self) -> str:
357
             user=self.content.owner
395
             user=self.content.owner
358
         )
396
         )
359
 
397
 
360
-    # Context-related
398
+    @property
399
+    def current_revision_id(self):
400
+        return self.content.revision_id
401
+
402
+    @property
403
+    def created(self):
404
+        return self.content.created
405
+
406
+    @property
407
+    def modified(self):
408
+        return self.updated
409
+
410
+    @property
411
+    def updated(self):
412
+        return self.content.updated
361
 
413
 
362
     @property
414
     @property
415
+    def last_modifier(self):
416
+        # TODO - G.M - 2018-06-173 - Repair owner/last modifier
417
+        return self.author
418
+    # Context-related
419
+    @property
363
     def show_in_ui(self):
420
     def show_in_ui(self):
364
         # TODO - G.M - 31-05-2018 - Enable Show_in_ui params
421
         # TODO - G.M - 31-05-2018 - Enable Show_in_ui params
365
         # if false, then do not show content in the treeview.
422
         # if false, then do not show content in the treeview.
371
     @property
428
     @property
372
     def slug(self):
429
     def slug(self):
373
         return slugify(self.content.label)
430
         return slugify(self.content.label)
431
+
432
+
433
+class RevisionInContext(object):
434
+    """
435
+    Interface to get Content data and Content data related to context.
436
+    """
437
+
438
+    def __init__(self, content: ContentRevisionRO, dbsession: Session, config: CFG):
439
+        self.revision = content
440
+        self.dbsession = dbsession
441
+        self.config = config
442
+
443
+    # Default
444
+    @property
445
+    def content_id(self) -> int:
446
+        return self.revision.content_id
447
+
448
+    @property
449
+    def id(self) -> int:
450
+        return self.content_id
451
+
452
+    @property
453
+    def parent_id(self) -> int:
454
+        """
455
+        Return parent_id of the content
456
+        """
457
+        return self.revision.parent_id
458
+
459
+    @property
460
+    def workspace_id(self) -> int:
461
+        return self.revision.workspace_id
462
+
463
+    @property
464
+    def label(self) -> str:
465
+        return self.revision.label
466
+
467
+    @property
468
+    def content_type(self) -> str:
469
+        return self.revision.type
470
+
471
+    @property
472
+    def sub_content_types(self) -> typing.List[str]:
473
+        return [_type.slug for _type
474
+                in self.revision.node.get_allowed_content_types()]
475
+
476
+    @property
477
+    def status(self) -> str:
478
+        return self.revision.status
479
+
480
+    @property
481
+    def is_archived(self):
482
+        return self.revision.is_archived
483
+
484
+    @property
485
+    def is_deleted(self):
486
+        return self.revision.is_deleted
487
+
488
+    @property
489
+    def raw_content(self):
490
+        return self.revision.description
491
+
492
+    @property
493
+    def author(self):
494
+        return UserInContext(
495
+            dbsession=self.dbsession,
496
+            config=self.config,
497
+            user=self.revision.owner
498
+        )
499
+
500
+    @property
501
+    def revision_id(self):
502
+        return self.revision.revision_id
503
+
504
+    @property
505
+    def created(self):
506
+        return self.revision.created
507
+
508
+    @property
509
+    def modified(self):
510
+        return self.updated
511
+
512
+    @property
513
+    def updated(self):
514
+        return self.revision.updated
515
+
516
+    @property
517
+    def last_modifier(self):
518
+        # TODO - G.M - 2018-06-173 - Repair owner/last modifier
519
+        return self.author
520
+    
521
+    @property
522
+    def comments_ids(self):
523
+        # TODO - G.M - 2018-06-173 - Return comments related to this revision
524
+        return []
525
+
526
+    # Context-related
527
+    @property
528
+    def show_in_ui(self):
529
+        # TODO - G.M - 31-05-2018 - Enable Show_in_ui params
530
+        # if false, then do not show content in the treeview.
531
+        # This may his maybe used for specific contents or for sub-contents.
532
+        # Default is True.
533
+        # In first version of the API, this field is always True
534
+        return True
535
+
536
+    @property
537
+    def slug(self):
538
+        return slugify(self.revision.label)

+ 0 - 3
tracim/tests/functional/test_comments.py View File

1
 # -*- coding: utf-8 -*-
1
 # -*- coding: utf-8 -*-
2
-"""
3
-Tests for /api/v2/workspaces subpath endpoints.
4
-"""
5
 from tracim.tests import FunctionalTest
2
 from tracim.tests import FunctionalTest
6
 from tracim.fixtures.content import Content as ContentFixtures
3
 from tracim.fixtures.content import Content as ContentFixtures
7
 from tracim.fixtures.users_and_groups import Base as BaseFixture
4
 from tracim.fixtures.users_and_groups import Base as BaseFixture

+ 218 - 0
tracim/tests/functional/test_contents.py View File

1
+# -*- coding: utf-8 -*-
2
+from tracim.tests import FunctionalTest
3
+from tracim.fixtures.content import Content as ContentFixtures
4
+from tracim.fixtures.users_and_groups import Base as BaseFixture
5
+
6
+
7
+class TestHtmlDocuments(FunctionalTest):
8
+    """
9
+    Tests for /api/v2/workspaces/{workspace_id}/html-documents/{content_id}
10
+    endpoint
11
+    """
12
+
13
+    fixtures = [BaseFixture, ContentFixtures]
14
+
15
+    def test_api__get_html_document__err_400__wrong_content_type(self) -> None:
16
+        """
17
+        Get one html document of a content
18
+        """
19
+        self.testapp.authorization = (
20
+            'Basic',
21
+            (
22
+                'admin@admin.admin',
23
+                'admin@admin.admin'
24
+            )
25
+        )
26
+        res = self.testapp.get(
27
+            '/api/v2/workspaces/2/html-documents/7',
28
+            status=400
29
+        )   # nopep8
30
+
31
+    def test_api__get_html_document__ok_200__nominal_case(self) -> None:
32
+        """
33
+        Get one html document of a content
34
+        """
35
+        self.testapp.authorization = (
36
+            'Basic',
37
+            (
38
+                'admin@admin.admin',
39
+                'admin@admin.admin'
40
+            )
41
+        )
42
+        res = self.testapp.get(
43
+            '/api/v2/workspaces/2/html-documents/6',
44
+            status=200
45
+        )   # nopep8
46
+        content = res.json_body
47
+        assert content['content_type'] == 'page'
48
+        assert content['content_id'] == 6
49
+        assert content['is_archived'] is False
50
+        assert content['is_deleted'] is False
51
+        assert content['label'] == 'Tiramisu Recipe'
52
+        assert content['parent_id'] == 3
53
+        assert content['show_in_ui'] is True
54
+        assert content['slug'] == 'tiramisu-recipe'
55
+        assert content['status'] == 'open'
56
+        assert content['workspace_id'] == 2
57
+        assert content['current_revision_id'] == 7
58
+        # TODO - G.M - 2018-06-173 - check date format
59
+        assert content['created']
60
+        assert content['author']
61
+        assert content['author']['user_id'] == 1
62
+        assert content['author']['avatar_url'] is None
63
+        assert content['author']['public_name'] == 'Global manager'
64
+        # TODO - G.M - 2018-06-173 - check date format
65
+        assert content['modified']
66
+        assert content['last_modifier'] == content['author']
67
+        assert content['raw_content'] == '<p>To cook a great Tiramisu, you need many ingredients.</p>'  # nopep8
68
+
69
+    def test_api__update_html_document__ok_200__nominal_case(self) -> None:
70
+        """
71
+        Update(put) one html document of a content
72
+        """
73
+        self.testapp.authorization = (
74
+            'Basic',
75
+            (
76
+                'admin@admin.admin',
77
+                'admin@admin.admin'
78
+            )
79
+        )
80
+        params = {
81
+            'label' : 'My New label',
82
+            'raw_content': '<p> Le nouveau contenu </p>',
83
+        }
84
+        res = self.testapp.put_json(
85
+            '/api/v2/workspaces/2/html-documents/6',
86
+            params=params,
87
+            status=200
88
+        )
89
+        content = res.json_body
90
+        assert content['content_type'] == 'page'
91
+        assert content['content_id'] == 6
92
+        assert content['is_archived'] is False
93
+        assert content['is_deleted'] is False
94
+        assert content['label'] == 'My New label'
95
+        assert content['parent_id'] == 3
96
+        assert content['show_in_ui'] is True
97
+        assert content['slug'] == 'my-new-label'
98
+        assert content['status'] == 'open'
99
+        assert content['workspace_id'] == 2
100
+        assert content['current_revision_id'] == 26
101
+        # TODO - G.M - 2018-06-173 - check date format
102
+        assert content['created']
103
+        assert content['author']
104
+        assert content['author']['user_id'] == 1
105
+        assert content['author']['avatar_url'] is None
106
+        assert content['author']['public_name'] == 'Global manager'
107
+        # TODO - G.M - 2018-06-173 - check date format
108
+        assert content['modified']
109
+        assert content['last_modifier'] == content['author']
110
+        assert content['raw_content'] == '<p> Le nouveau contenu </p>'
111
+
112
+        res = self.testapp.get(
113
+            '/api/v2/workspaces/2/html-documents/6',
114
+            status=200
115
+        )   # nopep8
116
+        content = res.json_body
117
+        assert content['content_type'] == 'page'
118
+        assert content['content_id'] == 6
119
+        assert content['is_archived'] is False
120
+        assert content['is_deleted'] is False
121
+        assert content['label'] == 'My New label'
122
+        assert content['parent_id'] == 3
123
+        assert content['show_in_ui'] is True
124
+        assert content['slug'] == 'my-new-label'
125
+        assert content['status'] == 'open'
126
+        assert content['workspace_id'] == 2
127
+        assert content['current_revision_id'] == 26
128
+        # TODO - G.M - 2018-06-173 - check date format
129
+        assert content['created']
130
+        assert content['author']
131
+        assert content['author']['user_id'] == 1
132
+        assert content['author']['avatar_url'] is None
133
+        assert content['author']['public_name'] == 'Global manager'
134
+        # TODO - G.M - 2018-06-173 - check date format
135
+        assert content['modified']
136
+        assert content['last_modifier'] == content['author']
137
+        assert content['raw_content'] == '<p> Le nouveau contenu </p>'
138
+
139
+    def test_api__get_html_document_revisions__ok_200__nominal_case(self) -> None:
140
+        """
141
+        Get one html document of a content
142
+        """
143
+        self.testapp.authorization = (
144
+            'Basic',
145
+            (
146
+                'admin@admin.admin',
147
+                'admin@admin.admin'
148
+            )
149
+        )
150
+        res = self.testapp.get(
151
+            '/api/v2/workspaces/2/html-documents/6/revisions',
152
+            status=200
153
+        )
154
+        revisions = res.json_body
155
+        assert len(revisions) == 2
156
+        revision = revisions[0]
157
+        assert revision['content_type'] == 'page'
158
+        assert revision['content_id'] == 6
159
+        assert revision['is_archived'] is False
160
+        assert revision['is_deleted'] is False
161
+        assert revision['label'] == 'Tiramisu Recipe'
162
+        assert revision['parent_id'] == 3
163
+        assert revision['show_in_ui'] is True
164
+        assert revision['slug'] == 'tiramisu-recipe'
165
+        assert revision['status'] == 'open'
166
+        assert revision['workspace_id'] == 2
167
+        assert revision['revision_id'] == 6
168
+        assert revision['sub_content_types']
169
+        # TODO - G.M - 2018-06-173 - Test with real comments
170
+        assert revision['comments_ids'] == []
171
+        # TODO - G.M - 2018-06-173 - check date format
172
+        assert revision['created']
173
+        assert revision['author']
174
+        assert revision['author']['user_id'] == 1
175
+        assert revision['author']['avatar_url'] is None
176
+        assert revision['author']['public_name'] == 'Global manager'
177
+
178
+    def test_api__set_html_document_status__ok_200__nominal_case(self) -> None:
179
+        """
180
+        Get one html document of a content
181
+        """
182
+        self.testapp.authorization = (
183
+            'Basic',
184
+            (
185
+                'admin@admin.admin',
186
+                'admin@admin.admin'
187
+            )
188
+        )
189
+        params = {
190
+            'status': 'closed-deprecated',
191
+        }
192
+
193
+        # before
194
+        res = self.testapp.get(
195
+            '/api/v2/workspaces/2/html-documents/6',
196
+            status=200
197
+        )   # nopep8
198
+        content = res.json_body
199
+        assert content['content_type'] == 'page'
200
+        assert content['content_id'] == 6
201
+        assert content['status'] == 'open'
202
+
203
+        # set status
204
+        res = self.testapp.put_json(
205
+            '/api/v2/workspaces/2/html-documents/6/status',
206
+            params=params,
207
+            status=204
208
+        )
209
+
210
+        # after
211
+        res = self.testapp.get(
212
+            '/api/v2/workspaces/2/html-documents/6',
213
+            status=200
214
+        )   # nopep8
215
+        content = res.json_body
216
+        assert content['content_type'] == 'page'
217
+        assert content['content_id'] == 6
218
+        assert content['status'] == 'closed-deprecated'

+ 196 - 0
tracim/views/contents_api/html_document_controller.py View File

1
+# coding=utf-8
2
+import transaction
3
+from pyramid.config import Configurator
4
+
5
+from tracim.models.data import UserRoleInWorkspace
6
+
7
+try:  # Python 3.5+
8
+    from http import HTTPStatus
9
+except ImportError:
10
+    from http import client as HTTPStatus
11
+
12
+from tracim import TracimRequest
13
+from tracim.extensions import hapic
14
+from tracim.lib.core.content import ContentApi
15
+from tracim.views.controllers import Controller
16
+from tracim.views.core_api.schemas import HtmlDocumentContentSchema
17
+from tracim.views.core_api.schemas import HtmlDocumentRevisionSchema
18
+from tracim.views.core_api.schemas import SetContentStatusSchema
19
+from tracim.views.core_api.schemas import HtmlDocumentModifySchema
20
+from tracim.views.core_api.schemas import WorkspaceAndContentIdPathSchema
21
+from tracim.views.core_api.schemas import NoContentSchema
22
+from tracim.lib.utils.authorization import require_content_types
23
+from tracim.lib.utils.authorization import require_workspace_role
24
+from tracim.exceptions import WorkspaceNotFound, ContentTypeNotAllowed
25
+from tracim.exceptions import InsufficientUserProfile
26
+from tracim.exceptions import NotAuthenticated
27
+from tracim.exceptions import AuthenticationFailed
28
+from tracim.models.contents import ContentTypeLegacy as ContentType
29
+from tracim.models.contents import htmlpage_type
30
+from tracim.models.revision_protection import new_revision
31
+
32
+HTML_DOCUMENT_ENDPOINTS_TAG = 'HTML documents'
33
+
34
+
35
+class HTMLDocumentController(Controller):
36
+
37
+    @hapic.with_api_doc(tags=[HTML_DOCUMENT_ENDPOINTS_TAG])
38
+    @hapic.handle_exception(NotAuthenticated, HTTPStatus.UNAUTHORIZED)
39
+    @hapic.handle_exception(InsufficientUserProfile, HTTPStatus.FORBIDDEN)
40
+    @hapic.handle_exception(WorkspaceNotFound, HTTPStatus.FORBIDDEN)
41
+    @hapic.handle_exception(AuthenticationFailed, HTTPStatus.BAD_REQUEST)
42
+    @hapic.handle_exception(ContentTypeNotAllowed, HTTPStatus.BAD_REQUEST)
43
+    @require_workspace_role(UserRoleInWorkspace.READER)
44
+    @require_content_types([htmlpage_type])
45
+    @hapic.input_path(WorkspaceAndContentIdPathSchema())
46
+    @hapic.output_body(HtmlDocumentContentSchema())
47
+    def get_html_document(self, context, request: TracimRequest, hapic_data=None):  # nopep8
48
+        """
49
+        Get html document content
50
+        """
51
+        app_config = request.registry.settings['CFG']
52
+        api = ContentApi(
53
+            current_user=request.current_user,
54
+            session=request.dbsession,
55
+            config=app_config,
56
+        )
57
+        content = api.get_one(
58
+            hapic_data.path.content_id,
59
+            content_type=ContentType.Any
60
+        )
61
+        return api.get_content_in_context(content)
62
+
63
+    @hapic.with_api_doc(tags=[HTML_DOCUMENT_ENDPOINTS_TAG])
64
+    @hapic.handle_exception(NotAuthenticated, HTTPStatus.UNAUTHORIZED)
65
+    @hapic.handle_exception(InsufficientUserProfile, HTTPStatus.FORBIDDEN)
66
+    @hapic.handle_exception(WorkspaceNotFound, HTTPStatus.FORBIDDEN)
67
+    @hapic.handle_exception(AuthenticationFailed, HTTPStatus.BAD_REQUEST)
68
+    @require_workspace_role(UserRoleInWorkspace.CONTRIBUTOR)
69
+    @require_content_types([htmlpage_type])
70
+    @hapic.input_path(WorkspaceAndContentIdPathSchema())
71
+    @hapic.input_body(HtmlDocumentModifySchema())
72
+    @hapic.output_body(HtmlDocumentContentSchema())
73
+    def update_html_document(self, context, request: TracimRequest, hapic_data=None):
74
+        """
75
+        update_html_document
76
+        """
77
+        app_config = request.registry.settings['CFG']
78
+        api = ContentApi(
79
+            current_user=request.current_user,
80
+            session=request.dbsession,
81
+            config=app_config,
82
+        )
83
+        content = api.get_one(
84
+            hapic_data.path.content_id,
85
+            content_type=ContentType.Any
86
+        )
87
+        with new_revision(
88
+                session=request.dbsession,
89
+                tm=transaction.manager,
90
+                content=content
91
+        ):
92
+            api.update_content(
93
+                item=content,
94
+                new_label=hapic_data.body.label,
95
+                new_content=hapic_data.body.raw_content,
96
+
97
+            )
98
+            api.save(content)
99
+        return api.get_content_in_context(content)
100
+
101
+    @hapic.with_api_doc(tags=[HTML_DOCUMENT_ENDPOINTS_TAG])
102
+    @hapic.handle_exception(NotAuthenticated, HTTPStatus.UNAUTHORIZED)
103
+    @hapic.handle_exception(InsufficientUserProfile, HTTPStatus.FORBIDDEN)
104
+    @hapic.handle_exception(WorkspaceNotFound, HTTPStatus.FORBIDDEN)
105
+    @hapic.handle_exception(AuthenticationFailed, HTTPStatus.BAD_REQUEST)
106
+    @require_workspace_role(UserRoleInWorkspace.READER)
107
+    @require_content_types([htmlpage_type])
108
+    @hapic.input_path(WorkspaceAndContentIdPathSchema())
109
+    @hapic.output_body(HtmlDocumentRevisionSchema(many=True))
110
+    def get_html_document_revisions(self, context, request: TracimRequest, hapic_data=None):  # nopep8
111
+        """
112
+        get html_document revisions
113
+        """
114
+        app_config = request.registry.settings['CFG']
115
+        api = ContentApi(
116
+            current_user=request.current_user,
117
+            session=request.dbsession,
118
+            config=app_config,
119
+        )
120
+        content = api.get_one(
121
+            hapic_data.path.content_id,
122
+            content_type=ContentType.Any
123
+        )
124
+        revisions = content.revisions
125
+        return [
126
+            api.get_revision_in_context(revision)
127
+            for revision in revisions
128
+        ]
129
+
130
+    @hapic.with_api_doc(tags=[HTML_DOCUMENT_ENDPOINTS_TAG])
131
+    @hapic.handle_exception(NotAuthenticated, HTTPStatus.UNAUTHORIZED)
132
+    @hapic.handle_exception(InsufficientUserProfile, HTTPStatus.FORBIDDEN)
133
+    @hapic.handle_exception(WorkspaceNotFound, HTTPStatus.FORBIDDEN)
134
+    @hapic.handle_exception(AuthenticationFailed, HTTPStatus.BAD_REQUEST)
135
+    @require_content_types([htmlpage_type])
136
+    @hapic.input_path(WorkspaceAndContentIdPathSchema())
137
+    @hapic.input_body(SetContentStatusSchema())
138
+    @hapic.output_body(NoContentSchema(), default_http_code=HTTPStatus.NO_CONTENT)  # nopep8
139
+    def set_html_document_status(self, context, request: TracimRequest, hapic_data=None):  # nopep8
140
+        """
141
+        set html_document status
142
+        """
143
+        app_config = request.registry.settings['CFG']
144
+        api = ContentApi(
145
+            current_user=request.current_user,
146
+            session=request.dbsession,
147
+            config=app_config,
148
+        )
149
+        content = api.get_one(
150
+            hapic_data.path.content_id,
151
+            content_type=ContentType.Any
152
+        )
153
+        with new_revision(
154
+                session=request.dbsession,
155
+                tm=transaction.manager,
156
+                content=content
157
+        ):
158
+            api.set_status(
159
+                content,
160
+                hapic_data.body.status,
161
+            )
162
+            api.save(content)
163
+        return
164
+
165
+    def bind(self, configurator: Configurator):
166
+        # Get html-document
167
+        configurator.add_route(
168
+            'html_document',
169
+            '/workspaces/{workspace_id}/html-documents/{content_id}',
170
+            request_method='GET'
171
+        )
172
+        configurator.add_view(self.get_html_document, route_name='html_document')  # nopep8
173
+
174
+        # update html-document
175
+        configurator.add_route(
176
+            'update_html_document',
177
+            '/workspaces/{workspace_id}/html-documents/{content_id}',
178
+            request_method='PUT'
179
+        )  # nopep8
180
+        configurator.add_view(self.update_html_document, route_name='update_html_document')  # nopep8
181
+
182
+        # get html document revisions
183
+        configurator.add_route(
184
+            'html_document_revisions',
185
+            '/workspaces/{workspace_id}/html-documents/{content_id}/revisions',  # nopep8
186
+            request_method='GET'
187
+        )
188
+        configurator.add_view(self.get_html_document_revisions, route_name='html_document_revisions')  # nopep8
189
+
190
+        # get html document revisions
191
+        configurator.add_route(
192
+            'set_html_document_status',
193
+            '/workspaces/{workspace_id}/html-documents/{content_id}/status',  # nopep8
194
+            request_method='PUT'
195
+        )
196
+        configurator.add_view(self.set_html_document_status, route_name='set_html_document_status')  # nopep8

+ 19 - 3
tracim/views/core_api/schemas.py View File

9
 from tracim.models.contents import GlobalStatus
9
 from tracim.models.contents import GlobalStatus
10
 from tracim.models.contents import open_status
10
 from tracim.models.contents import open_status
11
 from tracim.models.context_models import ContentCreation
11
 from tracim.models.context_models import ContentCreation
12
+from tracim.models.context_models import SetContentStatus
12
 from tracim.models.context_models import CommentCreation
13
 from tracim.models.context_models import CommentCreation
13
 from tracim.models.context_models import CommentPath
14
 from tracim.models.context_models import CommentPath
14
 from tracim.models.context_models import MoveParams
15
 from tracim.models.context_models import MoveParams
15
 from tracim.models.context_models import WorkspaceAndContentPath
16
 from tracim.models.context_models import WorkspaceAndContentPath
16
 from tracim.models.context_models import ContentFilter
17
 from tracim.models.context_models import ContentFilter
17
 from tracim.models.context_models import LoginCredentials
18
 from tracim.models.context_models import LoginCredentials
19
+from tracim.models.context_models import HTMLDocumentUpdate
20
+from tracim.models.context_models import ThreadUpdate
18
 from tracim.models.data import UserRoleInWorkspace
21
 from tracim.models.data import UserRoleInWorkspace
19
 
22
 
20
 
23
 
412
 
415
 
413
 
416
 
414
 class RevisionSchema(ContentDigestSchema):
417
 class RevisionSchema(ContentDigestSchema):
415
-    comments_id = marshmallow.fields.List(marshmallow.fields.Int(example=4))
418
+    comments_ids = marshmallow.fields.List(marshmallow.fields.Int(example=4))
416
     revision_id = marshmallow.fields.Int(example=12)
419
     revision_id = marshmallow.fields.Int(example=12)
417
     created = marshmallow.fields.DateTime(
420
     created = marshmallow.fields.DateTime(
418
         format='%Y-%m-%dT%H:%M:%SZ',
421
         format='%Y-%m-%dT%H:%M:%SZ',
450
 class HtmlDocumentModifySchema(ContentModifySchema):
453
 class HtmlDocumentModifySchema(ContentModifySchema):
451
     raw_content = marshmallow.fields.String('<p>Html page Content !</p>')
454
     raw_content = marshmallow.fields.String('<p>Html page Content !</p>')
452
 
455
 
456
+    @post_load
457
+    def html_document_update(self, data):
458
+        return HTMLDocumentUpdate(**data)
459
+
453
 
460
 
454
 class ThreadModifySchema(ContentModifySchema):
461
 class ThreadModifySchema(ContentModifySchema):
455
     raw_content = marshmallow.fields.String('Description of Thread')
462
     raw_content = marshmallow.fields.String('Description of Thread')
456
 
463
 
464
+    @post_load
465
+    def thread_update(self, data):
466
+        return ThreadUpdate(**data)
467
+
457
 
468
 
458
 class SetCommentSchema(marshmallow.Schema):
469
 class SetCommentSchema(marshmallow.Schema):
459
     raw_content = marshmallow.fields.String(
470
     raw_content = marshmallow.fields.String(
470
         example='closed-deprecated',
481
         example='closed-deprecated',
471
         validate=OneOf([status.slug for status in CONTENT_DEFAULT_STATUS]),
482
         validate=OneOf([status.slug for status in CONTENT_DEFAULT_STATUS]),
472
         description='this slug is found in content_type available statuses',
483
         description='this slug is found in content_type available statuses',
473
-        default=open_status
474
-    )
484
+        default=open_status,
485
+        required=True,
486
+    )
487
+
488
+    @post_load
489
+    def set_status(self, data):
490
+        return SetContentStatus(**data)