浏览代码

Merge pull request #88 from tracim/fix/596_move_endpoint_return_updated_content

Damien Accorsi 6 年前
父节点
当前提交
ee1624dbe8
没有帐户链接到提交者的电子邮件

+ 8 - 0
tracim/exceptions.py 查看文件

65
     pass
65
     pass
66
 
66
 
67
 
67
 
68
+class WorkspaceNotFoundInTracimRequest(NotFound):
69
+    pass
70
+
71
+
68
 class InsufficientUserWorkspaceRole(TracimException):
72
 class InsufficientUserWorkspaceRole(TracimException):
69
     pass
73
     pass
70
 
74
 
111
 
115
 
112
 class UserNotFoundInTracimRequest(TracimException):
116
 class UserNotFoundInTracimRequest(TracimException):
113
     pass
117
     pass
118
+
119
+
120
+class WorkspacesDoNotMatch(TracimException):
121
+    pass

+ 15 - 6
tracim/lib/core/content.py 查看文件

25
 from tracim.lib.utils.utils import cmp_to_key
25
 from tracim.lib.utils.utils import cmp_to_key
26
 from tracim.lib.core.notifications import NotifierFactory
26
 from tracim.lib.core.notifications import NotifierFactory
27
 from tracim.exceptions import SameValueError
27
 from tracim.exceptions import SameValueError
28
+from tracim.exceptions import WorkspacesDoNotMatch
28
 from tracim.lib.utils.utils import current_date_for_filename
29
 from tracim.lib.utils.utils import current_date_for_filename
29
 from tracim.models.revision_protection import new_revision
30
 from tracim.models.revision_protection import new_revision
30
 from tracim.models.auth import User
31
 from tracim.models.auth import User
862
         else:
863
         else:
863
             raise ValueError('The given value {} is not allowed'.format(new_status))
864
             raise ValueError('The given value {} is not allowed'.format(new_status))
864
 
865
 
865
-    def move(self, item: Content,
866
+    def move(self,
867
+             item: Content,
866
              new_parent: Content,
868
              new_parent: Content,
867
-             must_stay_in_same_workspace:bool=True,
868
-             new_workspace:Workspace=None):
869
+             must_stay_in_same_workspace: bool=True,
870
+             new_workspace: Workspace=None,
871
+    ):
869
         if must_stay_in_same_workspace:
872
         if must_stay_in_same_workspace:
870
             if new_parent and new_parent.workspace_id != item.workspace_id:
873
             if new_parent and new_parent.workspace_id != item.workspace_id:
871
                 raise ValueError('the item should stay in the same workspace')
874
                 raise ValueError('the item should stay in the same workspace')
872
 
875
 
873
         item.parent = new_parent
876
         item.parent = new_parent
874
-        if new_parent:
875
-            item.workspace = new_parent.workspace
876
-        elif new_workspace:
877
+        if new_workspace:
877
             item.workspace = new_workspace
878
             item.workspace = new_workspace
879
+            if new_parent and \
880
+                    new_parent.workspace_id != new_workspace.workspace_id:
881
+                raise WorkspacesDoNotMatch(
882
+                    'new parent workspace and new workspace should be the same.'
883
+                )
884
+        else:
885
+            if new_parent:
886
+                item.workspace = new_parent.workspace
878
 
887
 
879
         item.revision_type = ActionDescription.MOVE
888
         item.revision_type = ActionDescription.MOVE
880
 
889
 

+ 26 - 5
tracim/lib/utils/authorization.py 查看文件

8
 except ImportError:  # python3.4
8
 except ImportError:  # python3.4
9
     JSONDecodeError = ValueError
9
     JSONDecodeError = ValueError
10
 
10
 
11
-from tracim.exceptions import InsufficientUserWorkspaceRole, \
12
-    InsufficientUserProfile
13
-
11
+from tracim.exceptions import InsufficientUserWorkspaceRole
12
+from tracim.exceptions import InsufficientUserProfile
14
 if TYPE_CHECKING:
13
 if TYPE_CHECKING:
15
     from tracim import TracimRequest
14
     from tracim import TracimRequest
16
 ###
15
 ###
84
 
83
 
85
 def require_workspace_role(minimal_required_role: int):
84
 def require_workspace_role(minimal_required_role: int):
86
     """
85
     """
87
-    Decorator for view to restrict access of tracim request if role
88
-    is not high enough
86
+    Restricts access to endpoint to minimal role or raise an exception.
87
+    Check role for current_workspace.
89
     :param minimal_required_role: value from UserInWorkspace Object like
88
     :param minimal_required_role: value from UserInWorkspace Object like
90
     UserRoleInWorkspace.CONTRIBUTOR or UserRoleInWorkspace.READER
89
     UserRoleInWorkspace.CONTRIBUTOR or UserRoleInWorkspace.READER
91
     :return: decorator
90
     :return: decorator
101
 
100
 
102
         return wrapper
101
         return wrapper
103
     return decorator
102
     return decorator
103
+
104
+
105
+def require_candidate_workspace_role(minimal_required_role: int):
106
+    """
107
+    Restricts access to endpoint to minimal role or raise an exception.
108
+    Check role for candidate_workspace.
109
+    :param minimal_required_role: value from UserInWorkspace Object like
110
+    UserRoleInWorkspace.CONTRIBUTOR or UserRoleInWorkspace.READER
111
+    :return: decorator
112
+    """
113
+    def decorator(func):
114
+
115
+        def wrapper(self, context, request: 'TracimRequest'):
116
+            user = request.current_user
117
+            workspace = request.candidate_workspace
118
+
119
+            if workspace.get_user_role(user) >= minimal_required_role:
120
+                return func(self, context, request)
121
+            raise InsufficientUserWorkspaceRole()
122
+
123
+        return wrapper
124
+    return decorator

+ 53 - 4
tracim/lib/utils/request.py 查看文件

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
6
+from tracim.exceptions import WorkspaceNotFoundInTracimRequest
6
 from tracim.exceptions import UserNotFoundInTracimRequest
7
 from tracim.exceptions import UserNotFoundInTracimRequest
7
 from tracim.exceptions import UserDoesNotExist
8
 from tracim.exceptions import UserDoesNotExist
8
 from tracim.exceptions import WorkspaceNotFound
9
 from tracim.exceptions import WorkspaceNotFound
34
             decode_param_names,
35
             decode_param_names,
35
             **kw
36
             **kw
36
         )
37
         )
37
-        # Current workspace, found by request headers or content
38
+        # Current workspace, found in request path
38
         self._current_workspace = None  # type: Workspace
39
         self._current_workspace = None  # type: Workspace
39
 
40
 
41
+        # Candidate workspace found in request body
42
+        self._candidate_workspace = None  # type: Workspace
43
+
40
         # Authenticated user
44
         # Authenticated user
41
         self._current_user = None  # type: User
45
         self._current_user = None  # type: User
42
 
46
 
56
         :return: Workspace of the request
60
         :return: Workspace of the request
57
         """
61
         """
58
         if self._current_workspace is None:
62
         if self._current_workspace is None:
59
-            self.current_workspace = self._get_workspace(self.current_user, self)
63
+            self._current_workspace = self._get_current_workspace(self.current_user, self)
60
         return self._current_workspace
64
         return self._current_workspace
61
 
65
 
62
     @current_workspace.setter
66
     @current_workspace.setter
102
             self.candidate_user = self._get_candidate_user(self)
106
             self.candidate_user = self._get_candidate_user(self)
103
         return self._candidate_user
107
         return self._candidate_user
104
 
108
 
109
+    @property
110
+    def candidate_workspace(self) -> Workspace:
111
+        """
112
+        Get workspace from headers/body request. This workspace is not
113
+        the one found from path. Its the one from json body.
114
+        """
115
+        if self._candidate_workspace is None:
116
+            self._candidate_workspace = self._get_candidate_workspace(
117
+                self.current_user,
118
+                self
119
+            )
120
+        return self._candidate_workspace
121
+
105
     def _cleanup(self, request: 'TracimRequest') -> None:
122
     def _cleanup(self, request: 'TracimRequest') -> None:
106
         """
123
         """
107
         Close dbsession at the end of the request in order to avoid exception
124
         Close dbsession at the end of the request in order to avoid exception
171
             raise NotAuthenticated('User {} not found'.format(login)) from exc
188
             raise NotAuthenticated('User {} not found'.format(login)) from exc
172
         return user
189
         return user
173
 
190
 
174
-    def _get_workspace(
191
+    def _get_current_workspace(
175
             self,
192
             self,
176
             user: User,
193
             user: User,
177
             request: 'TracimRequest'
194
             request: 'TracimRequest'
187
             if 'workspace_id' in request.matchdict:
204
             if 'workspace_id' in request.matchdict:
188
                 workspace_id = request.matchdict['workspace_id']
205
                 workspace_id = request.matchdict['workspace_id']
189
             if not workspace_id:
206
             if not workspace_id:
190
-                raise WorkspaceNotFound('No workspace_id property found in request')
207
+                raise WorkspaceNotFoundInTracimRequest('No workspace_id property found in request')
208
+            wapi = WorkspaceApi(
209
+                current_user=user,
210
+                session=request.dbsession,
211
+                config=request.registry.settings['CFG']
212
+            )
213
+            workspace = wapi.get_one(workspace_id)
214
+        except JSONDecodeError:
215
+            raise WorkspaceNotFound('Bad json body')
216
+        except NoResultFound:
217
+            raise WorkspaceNotFound(
218
+                'Workspace {} does not exist '
219
+                'or is not visible for this user'.format(workspace_id)
220
+            )
221
+        return workspace
222
+
223
+    def _get_candidate_workspace(
224
+            self,
225
+            user: User,
226
+            request: 'TracimRequest'
227
+    ) -> Workspace:
228
+        """
229
+        Get current workspace from request
230
+        :param user: User who want to check the workspace
231
+        :param request: pyramid request
232
+        :return: current workspace
233
+        """
234
+        workspace_id = ''
235
+        try:
236
+            if 'new_workspace_id' in request.json_body:
237
+                workspace_id = request.json_body['new_workspace_id']
238
+            if not workspace_id:
239
+                raise WorkspaceNotFoundInTracimRequest('No new_workspace_id property found in body')
191
             wapi = WorkspaceApi(
240
             wapi = WorkspaceApi(
192
                 current_user=user,
241
                 current_user=user,
193
                 session=request.dbsession,
242
                 session=request.dbsession,

+ 24 - 9
tracim/models/context_models.py 查看文件

17
     """
17
     """
18
     Json body params for move action
18
     Json body params for move action
19
     """
19
     """
20
-    def __init__(self, new_parent_id: str):
20
+    def __init__(self, new_parent_id: str, new_workspace_id: str = None):
21
         self.new_parent_id = new_parent_id
21
         self.new_parent_id = new_parent_id
22
+        self.new_workspace_id = new_workspace_id
22
 
23
 
23
 
24
 
24
 class LoginCredentials(object):
25
 class LoginCredentials(object):
64
     def __init__(
65
     def __init__(
65
             self,
66
             self,
66
             label: str,
67
             label: str,
67
-            content_type_slug: str,
68
+            content_type: str,
68
     ):
69
     ):
69
         self.label = label
70
         self.label = label
70
-        self.content_type_slug = content_type_slug
71
+        self.content_type = content_type
71
 
72
 
72
 
73
 
73
 class UserInContext(object):
74
 class UserInContext(object):
91
         return self.user.user_id
92
         return self.user.user_id
92
 
93
 
93
     @property
94
     @property
95
+    def public_name(self) -> str:
96
+        return self.display_name
97
+
98
+    @property
94
     def display_name(self) -> str:
99
     def display_name(self) -> str:
95
         return self.user.display_name
100
         return self.user.display_name
96
 
101
 
108
 
113
 
109
     @property
114
     @property
110
     def profile(self) -> Profile:
115
     def profile(self) -> Profile:
111
-        return self.user.profile
116
+        return self.user.profile.name
112
 
117
 
113
     # Context related
118
     # Context related
114
 
119
 
226
         return self.user_role.role
231
         return self.user_role.role
227
 
232
 
228
     @property
233
     @property
234
+    def role(self) -> str:
235
+        return self.role_slug
236
+
237
+    @property
229
     def role_slug(self) -> str:
238
     def role_slug(self) -> str:
230
         """
239
         """
231
         simple name of the role of the user.
240
         simple name of the role of the user.
232
         can be anything from UserRoleInWorkspace SLUG, like
241
         can be anything from UserRoleInWorkspace SLUG, like
233
         'not_applicable', 'reader',
242
         'not_applicable', 'reader',
234
-        'contributor', 'content_manager', 'workspace_manager'
243
+        'contributor', 'content-manager', 'workspace-manager'
235
         :return: user workspace role as slug.
244
         :return: user workspace role as slug.
236
         """
245
         """
237
         return UserRoleInWorkspace.SLUG[self.user_role.role]
246
         return UserRoleInWorkspace.SLUG[self.user_role.role]
272
         self.config = config
281
         self.config = config
273
 
282
 
274
     # Default
283
     # Default
284
+    @property
285
+    def content_id(self) -> int:
286
+        return self.content.content_id
275
 
287
 
276
     @property
288
     @property
277
     def id(self) -> int:
289
     def id(self) -> int:
278
-        return self.content.content_id
290
+        return self.content_id
279
 
291
 
280
     @property
292
     @property
281
     def parent_id(self) -> int:
293
     def parent_id(self) -> int:
294
+        """
295
+        Return parent_id of the content
296
+        """
282
         return self.content.parent_id
297
         return self.content.parent_id
283
 
298
 
284
     @property
299
     @property
290
         return self.content.label
305
         return self.content.label
291
 
306
 
292
     @property
307
     @property
293
-    def content_type_slug(self) -> str:
308
+    def content_type(self) -> str:
294
         return self.content.type
309
         return self.content.type
295
 
310
 
296
     @property
311
     @property
297
-    def sub_content_type_slug(self) -> typing.List[str]:
312
+    def sub_content_types(self) -> typing.List[str]:
298
         return [type.slug for type in self.content.get_allowed_content_types()]
313
         return [type.slug for type in self.content.get_allowed_content_types()]
299
 
314
 
300
     @property
315
     @property
301
-    def status_slug(self) -> str:
316
+    def status(self) -> str:
302
         return self.content.status
317
         return self.content.status
303
 
318
 
304
     @property
319
     @property

+ 10 - 5
tracim/tests/functional/test_session.py 查看文件

1
 # coding=utf-8
1
 # coding=utf-8
2
+import datetime
2
 import pytest
3
 import pytest
3
 from sqlalchemy.exc import OperationalError
4
 from sqlalchemy.exc import OperationalError
4
 
5
 
45
             params=params,
46
             params=params,
46
             status=200,
47
             status=200,
47
         )
48
         )
48
-        assert res.json_body['display_name'] == 'Global manager'
49
-        assert res.json_body['email'] == 'admin@admin.admin'
50
         assert res.json_body['created']
49
         assert res.json_body['created']
50
+        datetime.datetime.strptime(
51
+            res.json_body['created'],
52
+            '%Y-%m-%dT%H:%M:%SZ'
53
+        )
54
+        assert res.json_body['public_name'] == 'Global manager'
55
+        assert res.json_body['email'] == 'admin@admin.admin'
51
         assert res.json_body['is_active']
56
         assert res.json_body['is_active']
52
         assert res.json_body['profile']
57
         assert res.json_body['profile']
53
-        assert res.json_body['profile']['slug'] == 'administrators'
58
+        assert res.json_body['profile'] == 'administrators'
54
         assert res.json_body['caldav_url'] is None
59
         assert res.json_body['caldav_url'] is None
55
         assert res.json_body['avatar_url'] is None
60
         assert res.json_body['avatar_url'] is None
56
 
61
 
103
             )
108
             )
104
         )
109
         )
105
         res = self.testapp.get('/api/v2/sessions/whoami', status=200)
110
         res = self.testapp.get('/api/v2/sessions/whoami', status=200)
106
-        assert res.json_body['display_name'] == 'Global manager'
111
+        assert res.json_body['public_name'] == 'Global manager'
107
         assert res.json_body['email'] == 'admin@admin.admin'
112
         assert res.json_body['email'] == 'admin@admin.admin'
108
         assert res.json_body['created']
113
         assert res.json_body['created']
109
         assert res.json_body['is_active']
114
         assert res.json_body['is_active']
110
         assert res.json_body['profile']
115
         assert res.json_body['profile']
111
-        assert res.json_body['profile']['slug'] == 'administrators'
116
+        assert res.json_body['profile'] == 'administrators'
112
         assert res.json_body['caldav_url'] is None
117
         assert res.json_body['caldav_url'] is None
113
         assert res.json_body['avatar_url'] is None
118
         assert res.json_body['avatar_url'] is None
114
 
119
 

+ 2 - 1
tracim/tests/functional/test_user.py 查看文件

28
         res = self.testapp.get('/api/v2/users/1/workspaces', status=200)
28
         res = self.testapp.get('/api/v2/users/1/workspaces', status=200)
29
         res = res.json_body
29
         res = res.json_body
30
         workspace = res[0]
30
         workspace = res[0]
31
-        assert workspace['id'] == 1
31
+        assert workspace['workspace_id'] == 1
32
         assert workspace['label'] == 'Business'
32
         assert workspace['label'] == 'Business'
33
+        assert workspace['slug'] == 'business'
33
         assert len(workspace['sidebar_entries']) == 7
34
         assert len(workspace['sidebar_entries']) == 7
34
 
35
 
35
         sidebar_entry = workspace['sidebar_entries'][0]
36
         sidebar_entry = workspace['sidebar_entries'][0]

+ 327 - 89
tracim/tests/functional/test_workspaces.py 查看文件

27
         )
27
         )
28
         res = self.testapp.get('/api/v2/workspaces/1', status=200)
28
         res = self.testapp.get('/api/v2/workspaces/1', status=200)
29
         workspace = res.json_body
29
         workspace = res.json_body
30
-        assert workspace['id'] == 1
30
+        assert workspace['workspace_id'] == 1
31
         assert workspace['slug'] == 'business'
31
         assert workspace['slug'] == 'business'
32
         assert workspace['label'] == 'Business'
32
         assert workspace['label'] == 'Business'
33
         assert workspace['description'] == 'All importants documents'
33
         assert workspace['description'] == 'All importants documents'
155
         res = self.testapp.get('/api/v2/workspaces/1/members', status=200).json_body   # nopep8
155
         res = self.testapp.get('/api/v2/workspaces/1/members', status=200).json_body   # nopep8
156
         assert len(res) == 1
156
         assert len(res) == 1
157
         user_role = res[0]
157
         user_role = res[0]
158
-        assert user_role['role_slug'] == 'workspace-manager'
158
+        assert user_role['role'] == 'workspace-manager'
159
         assert user_role['user_id'] == 1
159
         assert user_role['user_id'] == 1
160
         assert user_role['workspace_id'] == 1
160
         assert user_role['workspace_id'] == 1
161
-        assert user_role['user']['display_name'] == 'Global manager'
161
+        assert user_role['user']['public_name'] == 'Global manager'
162
         # TODO - G.M - 24-05-2018 - [Avatar] Replace
162
         # TODO - G.M - 24-05-2018 - [Avatar] Replace
163
         # by correct value when avatar feature will be enabled
163
         # by correct value when avatar feature will be enabled
164
         assert user_role['user']['avatar_url'] is None
164
         assert user_role['user']['avatar_url'] is None
239
         # TODO - G.M - 30-05-2018 - Check this test
239
         # TODO - G.M - 30-05-2018 - Check this test
240
         assert len(res) == 3
240
         assert len(res) == 3
241
         content = res[0]
241
         content = res[0]
242
-        assert content['id'] == 1
242
+        assert content['content_id'] == 1
243
         assert content['is_archived'] is False
243
         assert content['is_archived'] is False
244
         assert content['is_deleted'] is False
244
         assert content['is_deleted'] is False
245
         assert content['label'] == 'Tools'
245
         assert content['label'] == 'Tools'
246
         assert content['parent_id'] is None
246
         assert content['parent_id'] is None
247
         assert content['show_in_ui'] is True
247
         assert content['show_in_ui'] is True
248
         assert content['slug'] == 'tools'
248
         assert content['slug'] == 'tools'
249
-        assert content['status_slug'] == 'open'
250
-        assert set(content['sub_content_type_slug']) == {'thread', 'page', 'folder', 'file'}  # nopep8
249
+        assert content['status'] == 'open'
250
+        assert set(content['sub_content_types']) == {'thread', 'page', 'folder', 'file'}  # nopep8
251
         assert content['workspace_id'] == 1
251
         assert content['workspace_id'] == 1
252
         content = res[1]
252
         content = res[1]
253
-        assert content['id'] == 2
253
+        assert content['content_id'] == 2
254
         assert content['is_archived'] is False
254
         assert content['is_archived'] is False
255
         assert content['is_deleted'] is False
255
         assert content['is_deleted'] is False
256
         assert content['label'] == 'Menus'
256
         assert content['label'] == 'Menus'
257
         assert content['parent_id'] is None
257
         assert content['parent_id'] is None
258
         assert content['show_in_ui'] is True
258
         assert content['show_in_ui'] is True
259
         assert content['slug'] == 'menus'
259
         assert content['slug'] == 'menus'
260
-        assert content['status_slug'] == 'open'
261
-        assert set(content['sub_content_type_slug']) == {'thread', 'page', 'folder', 'file'}  # nopep8
260
+        assert content['status'] == 'open'
261
+        assert set(content['sub_content_types']) == {'thread', 'page', 'folder', 'file'}  # nopep8
262
         assert content['workspace_id'] == 1
262
         assert content['workspace_id'] == 1
263
         content = res[2]
263
         content = res[2]
264
-        assert content['id'] == 11
264
+        assert content['content_id'] == 11
265
         assert content['is_archived'] is False
265
         assert content['is_archived'] is False
266
         assert content['is_deleted'] is False
266
         assert content['is_deleted'] is False
267
         assert content['label'] == 'Current Menu'
267
         assert content['label'] == 'Current Menu'
268
         assert content['parent_id'] == 2
268
         assert content['parent_id'] == 2
269
         assert content['show_in_ui'] is True
269
         assert content['show_in_ui'] is True
270
         assert content['slug'] == 'current-menu'
270
         assert content['slug'] == 'current-menu'
271
-        assert content['status_slug'] == 'open'
272
-        assert set(content['sub_content_type_slug']) == {'thread', 'page', 'folder', 'file'}  # nopep8
271
+        assert content['status'] == 'open'
272
+        assert set(content['sub_content_types']) == {'thread', 'page', 'folder', 'file'}  # nopep8
273
         assert content['workspace_id'] == 1
273
         assert content['workspace_id'] == 1
274
 
274
 
275
     # Root related
275
     # Root related
299
         # TODO - G.M - 30-05-2018 - Check this test
299
         # TODO - G.M - 30-05-2018 - Check this test
300
         assert len(res) == 4
300
         assert len(res) == 4
301
         content = res[1]
301
         content = res[1]
302
-        assert content['content_type_slug'] == 'page'
303
-        assert content['id'] == 15
302
+        assert content['content_type'] == 'page'
303
+        assert content['content_id'] == 15
304
         assert content['is_archived'] is False
304
         assert content['is_archived'] is False
305
         assert content['is_deleted'] is False
305
         assert content['is_deleted'] is False
306
         assert content['label'] == 'New Fruit Salad'
306
         assert content['label'] == 'New Fruit Salad'
307
         assert content['parent_id'] is None
307
         assert content['parent_id'] is None
308
         assert content['show_in_ui'] is True
308
         assert content['show_in_ui'] is True
309
         assert content['slug'] == 'new-fruit-salad'
309
         assert content['slug'] == 'new-fruit-salad'
310
-        assert content['status_slug'] == 'open'
311
-        assert set(content['sub_content_type_slug']) == {'thread', 'page', 'folder', 'file'}  # nopep8
310
+        assert content['status'] == 'open'
311
+        assert set(content['sub_content_types']) == {'thread', 'page', 'folder', 'file'}  # nopep8
312
         assert content['workspace_id'] == 3
312
         assert content['workspace_id'] == 3
313
 
313
 
314
         content = res[2]
314
         content = res[2]
315
-        assert content['content_type_slug'] == 'page'
316
-        assert content['id'] == 16
315
+        assert content['content_type'] == 'page'
316
+        assert content['content_id'] == 16
317
         assert content['is_archived'] is True
317
         assert content['is_archived'] is True
318
         assert content['is_deleted'] is False
318
         assert content['is_deleted'] is False
319
         assert content['label'].startswith('Fruit Salad')
319
         assert content['label'].startswith('Fruit Salad')
320
         assert content['parent_id'] is None
320
         assert content['parent_id'] is None
321
         assert content['show_in_ui'] is True
321
         assert content['show_in_ui'] is True
322
         assert content['slug'].startswith('fruit-salad')
322
         assert content['slug'].startswith('fruit-salad')
323
-        assert content['status_slug'] == 'open'
324
-        assert set(content['sub_content_type_slug']) == {'thread', 'page', 'folder', 'file'}  # nopep8
323
+        assert content['status'] == 'open'
324
+        assert set(content['sub_content_types']) == {'thread', 'page', 'folder', 'file'}  # nopep8
325
         assert content['workspace_id'] == 3
325
         assert content['workspace_id'] == 3
326
 
326
 
327
         content = res[3]
327
         content = res[3]
328
-        assert content['content_type_slug'] == 'page'
329
-        assert content['id'] == 17
328
+        assert content['content_type'] == 'page'
329
+        assert content['content_id'] == 17
330
         assert content['is_archived'] is False
330
         assert content['is_archived'] is False
331
         assert content['is_deleted'] is True
331
         assert content['is_deleted'] is True
332
         assert content['label'].startswith('Bad Fruit Salad')
332
         assert content['label'].startswith('Bad Fruit Salad')
333
         assert content['parent_id'] is None
333
         assert content['parent_id'] is None
334
         assert content['show_in_ui'] is True
334
         assert content['show_in_ui'] is True
335
         assert content['slug'].startswith('bad-fruit-salad')
335
         assert content['slug'].startswith('bad-fruit-salad')
336
-        assert content['status_slug'] == 'open'
337
-        assert set(content['sub_content_type_slug']) == {'thread', 'page', 'folder', 'file'}  # nopep8
336
+        assert content['status'] == 'open'
337
+        assert set(content['sub_content_types']) == {'thread', 'page', 'folder', 'file'}  # nopep8
338
         assert content['workspace_id'] == 3
338
         assert content['workspace_id'] == 3
339
 
339
 
340
     def test_api__get_workspace_content__ok_200__get_only_active_root_content(self):  # nopep8
340
     def test_api__get_workspace_content__ok_200__get_only_active_root_content(self):  # nopep8
362
         # TODO - G.M - 30-05-2018 - Check this test
362
         # TODO - G.M - 30-05-2018 - Check this test
363
         assert len(res) == 2
363
         assert len(res) == 2
364
         content = res[1]
364
         content = res[1]
365
-        assert content['content_type_slug'] == 'page'
366
-        assert content['id'] == 15
365
+        assert content['content_type'] == 'page'
366
+        assert content['content_id'] == 15
367
         assert content['is_archived'] is False
367
         assert content['is_archived'] is False
368
         assert content['is_deleted'] is False
368
         assert content['is_deleted'] is False
369
         assert content['label'] == 'New Fruit Salad'
369
         assert content['label'] == 'New Fruit Salad'
370
         assert content['parent_id'] is None
370
         assert content['parent_id'] is None
371
         assert content['show_in_ui'] is True
371
         assert content['show_in_ui'] is True
372
         assert content['slug'] == 'new-fruit-salad'
372
         assert content['slug'] == 'new-fruit-salad'
373
-        assert content['status_slug'] == 'open'
374
-        assert set(content['sub_content_type_slug']) == {'thread', 'page', 'folder', 'file'}  # nopep8
373
+        assert content['status'] == 'open'
374
+        assert set(content['sub_content_types']) == {'thread', 'page', 'folder', 'file'}  # nopep8
375
         assert content['workspace_id'] == 3
375
         assert content['workspace_id'] == 3
376
 
376
 
377
     def test_api__get_workspace_content__ok_200__get_only_archived_root_content(self):  # nopep8
377
     def test_api__get_workspace_content__ok_200__get_only_archived_root_content(self):  # nopep8
398
         ).json_body   # nopep8
398
         ).json_body   # nopep8
399
         assert len(res) == 1
399
         assert len(res) == 1
400
         content = res[0]
400
         content = res[0]
401
-        assert content['content_type_slug'] == 'page'
402
-        assert content['id'] == 16
401
+        assert content['content_type'] == 'page'
402
+        assert content['content_id'] == 16
403
         assert content['is_archived'] is True
403
         assert content['is_archived'] is True
404
         assert content['is_deleted'] is False
404
         assert content['is_deleted'] is False
405
         assert content['label'].startswith('Fruit Salad')
405
         assert content['label'].startswith('Fruit Salad')
406
         assert content['parent_id'] is None
406
         assert content['parent_id'] is None
407
         assert content['show_in_ui'] is True
407
         assert content['show_in_ui'] is True
408
         assert content['slug'].startswith('fruit-salad')
408
         assert content['slug'].startswith('fruit-salad')
409
-        assert content['status_slug'] == 'open'
410
-        assert set(content['sub_content_type_slug']) == {'thread', 'page', 'folder', 'file'}  # nopep8
409
+        assert content['status'] == 'open'
410
+        assert set(content['sub_content_types']) == {'thread', 'page', 'folder', 'file'}  # nopep8
411
         assert content['workspace_id'] == 3
411
         assert content['workspace_id'] == 3
412
 
412
 
413
     def test_api__get_workspace_content__ok_200__get_only_deleted_root_content(self):  # nopep8
413
     def test_api__get_workspace_content__ok_200__get_only_deleted_root_content(self):  # nopep8
436
 
436
 
437
         assert len(res) == 1
437
         assert len(res) == 1
438
         content = res[0]
438
         content = res[0]
439
-        assert content['content_type_slug'] == 'page'
440
-        assert content['id'] == 17
439
+        assert content['content_type'] == 'page'
440
+        assert content['content_id'] == 17
441
         assert content['is_archived'] is False
441
         assert content['is_archived'] is False
442
         assert content['is_deleted'] is True
442
         assert content['is_deleted'] is True
443
         assert content['label'].startswith('Bad Fruit Salad')
443
         assert content['label'].startswith('Bad Fruit Salad')
444
         assert content['parent_id'] is None
444
         assert content['parent_id'] is None
445
         assert content['show_in_ui'] is True
445
         assert content['show_in_ui'] is True
446
         assert content['slug'].startswith('bad-fruit-salad')
446
         assert content['slug'].startswith('bad-fruit-salad')
447
-        assert content['status_slug'] == 'open'
448
-        assert set(content['sub_content_type_slug']) == {'thread', 'page', 'folder', 'file'}  # nopep8
447
+        assert content['status'] == 'open'
448
+        assert set(content['sub_content_types']) == {'thread', 'page', 'folder', 'file'}  # nopep8
449
         assert content['workspace_id'] == 3
449
         assert content['workspace_id'] == 3
450
 
450
 
451
     def test_api__get_workspace_content__ok_200__get_nothing_root_content(self):
451
     def test_api__get_workspace_content__ok_200__get_nothing_root_content(self):
500
         ).json_body   # nopep8
500
         ).json_body   # nopep8
501
         assert len(res) == 3
501
         assert len(res) == 3
502
         content = res[0]
502
         content = res[0]
503
-        assert content['content_type_slug'] == 'page'
504
-        assert content['id'] == 12
503
+        assert content['content_type'] == 'page'
504
+        assert content['content_id'] == 12
505
         assert content['is_archived'] is False
505
         assert content['is_archived'] is False
506
         assert content['is_deleted'] is False
506
         assert content['is_deleted'] is False
507
         assert content['label'] == 'New Fruit Salad'
507
         assert content['label'] == 'New Fruit Salad'
508
         assert content['parent_id'] == 10
508
         assert content['parent_id'] == 10
509
         assert content['show_in_ui'] is True
509
         assert content['show_in_ui'] is True
510
         assert content['slug'] == 'new-fruit-salad'
510
         assert content['slug'] == 'new-fruit-salad'
511
-        assert content['status_slug'] == 'open'
512
-        assert set(content['sub_content_type_slug']) == {'thread', 'page', 'folder', 'file'}  # nopep8
511
+        assert content['status'] == 'open'
512
+        assert set(content['sub_content_types']) == {'thread', 'page', 'folder', 'file'}  # nopep8
513
         assert content['workspace_id'] == 2
513
         assert content['workspace_id'] == 2
514
 
514
 
515
         content = res[1]
515
         content = res[1]
516
-        assert content['content_type_slug'] == 'page'
517
-        assert content['id'] == 13
516
+        assert content['content_type'] == 'page'
517
+        assert content['content_id'] == 13
518
         assert content['is_archived'] is True
518
         assert content['is_archived'] is True
519
         assert content['is_deleted'] is False
519
         assert content['is_deleted'] is False
520
         assert content['label'].startswith('Fruit Salad')
520
         assert content['label'].startswith('Fruit Salad')
521
         assert content['parent_id'] == 10
521
         assert content['parent_id'] == 10
522
         assert content['show_in_ui'] is True
522
         assert content['show_in_ui'] is True
523
         assert content['slug'].startswith('fruit-salad')
523
         assert content['slug'].startswith('fruit-salad')
524
-        assert content['status_slug'] == 'open'
525
-        assert set(content['sub_content_type_slug']) == {'thread', 'page', 'folder', 'file'}  # nopep8
524
+        assert content['status'] == 'open'
525
+        assert set(content['sub_content_types']) == {'thread', 'page', 'folder', 'file'}  # nopep8
526
         assert content['workspace_id'] == 2
526
         assert content['workspace_id'] == 2
527
 
527
 
528
         content = res[2]
528
         content = res[2]
529
-        assert content['content_type_slug'] == 'page'
530
-        assert content['id'] == 14
529
+        assert content['content_type'] == 'page'
530
+        assert content['content_id'] == 14
531
         assert content['is_archived'] is False
531
         assert content['is_archived'] is False
532
         assert content['is_deleted'] is True
532
         assert content['is_deleted'] is True
533
         assert content['label'].startswith('Bad Fruit Salad')
533
         assert content['label'].startswith('Bad Fruit Salad')
534
         assert content['parent_id'] == 10
534
         assert content['parent_id'] == 10
535
         assert content['show_in_ui'] is True
535
         assert content['show_in_ui'] is True
536
         assert content['slug'].startswith('bad-fruit-salad')
536
         assert content['slug'].startswith('bad-fruit-salad')
537
-        assert content['status_slug'] == 'open'
538
-        assert set(content['sub_content_type_slug']) == {'thread', 'page', 'folder', 'file'}  # nopep8
537
+        assert content['status'] == 'open'
538
+        assert set(content['sub_content_types']) == {'thread', 'page', 'folder', 'file'}  # nopep8
539
         assert content['workspace_id'] == 2
539
         assert content['workspace_id'] == 2
540
 
540
 
541
     def test_api__get_workspace_content__ok_200__get_only_active_folder_content(self):  # nopep8
541
     def test_api__get_workspace_content__ok_200__get_only_active_folder_content(self):  # nopep8
562
         ).json_body   # nopep8
562
         ).json_body   # nopep8
563
         assert len(res) == 1
563
         assert len(res) == 1
564
         content = res[0]
564
         content = res[0]
565
-        assert content['content_type_slug']
566
-        assert content['id'] == 12
565
+        assert content['content_type']
566
+        assert content['content_id'] == 12
567
         assert content['is_archived'] is False
567
         assert content['is_archived'] is False
568
         assert content['is_deleted'] is False
568
         assert content['is_deleted'] is False
569
         assert content['label'] == 'New Fruit Salad'
569
         assert content['label'] == 'New Fruit Salad'
570
         assert content['parent_id'] == 10
570
         assert content['parent_id'] == 10
571
         assert content['show_in_ui'] is True
571
         assert content['show_in_ui'] is True
572
         assert content['slug'] == 'new-fruit-salad'
572
         assert content['slug'] == 'new-fruit-salad'
573
-        assert content['status_slug'] == 'open'
574
-        assert set(content['sub_content_type_slug']) == {'thread', 'page', 'folder', 'file'}  # nopep8
573
+        assert content['status'] == 'open'
574
+        assert set(content['sub_content_types']) == {'thread', 'page', 'folder', 'file'}  # nopep8
575
         assert content['workspace_id'] == 2
575
         assert content['workspace_id'] == 2
576
 
576
 
577
     def test_api__get_workspace_content__ok_200__get_only_archived_folder_content(self):  # nopep8
577
     def test_api__get_workspace_content__ok_200__get_only_archived_folder_content(self):  # nopep8
598
         ).json_body   # nopep8
598
         ).json_body   # nopep8
599
         assert len(res) == 1
599
         assert len(res) == 1
600
         content = res[0]
600
         content = res[0]
601
-        assert content['content_type_slug'] == 'page'
602
-        assert content['id'] == 13
601
+        assert content['content_type'] == 'page'
602
+        assert content['content_id'] == 13
603
         assert content['is_archived'] is True
603
         assert content['is_archived'] is True
604
         assert content['is_deleted'] is False
604
         assert content['is_deleted'] is False
605
         assert content['label'].startswith('Fruit Salad')
605
         assert content['label'].startswith('Fruit Salad')
606
         assert content['parent_id'] == 10
606
         assert content['parent_id'] == 10
607
         assert content['show_in_ui'] is True
607
         assert content['show_in_ui'] is True
608
         assert content['slug'].startswith('fruit-salad')
608
         assert content['slug'].startswith('fruit-salad')
609
-        assert content['status_slug'] == 'open'
610
-        assert set(content['sub_content_type_slug']) == {'thread', 'page', 'folder', 'file'}  # nopep8
609
+        assert content['status'] == 'open'
610
+        assert set(content['sub_content_types']) == {'thread', 'page', 'folder', 'file'}  # nopep8
611
         assert content['workspace_id'] == 2
611
         assert content['workspace_id'] == 2
612
 
612
 
613
     def test_api__get_workspace_content__ok_200__get_only_deleted_folder_content(self):  # nopep8
613
     def test_api__get_workspace_content__ok_200__get_only_deleted_folder_content(self):  # nopep8
635
 
635
 
636
         assert len(res) == 1
636
         assert len(res) == 1
637
         content = res[0]
637
         content = res[0]
638
-        assert content['content_type_slug'] == 'page'
639
-        assert content['id'] == 14
638
+        assert content['content_type'] == 'page'
639
+        assert content['content_id'] == 14
640
         assert content['is_archived'] is False
640
         assert content['is_archived'] is False
641
         assert content['is_deleted'] is True
641
         assert content['is_deleted'] is True
642
         assert content['label'].startswith('Bad Fruit Salad')
642
         assert content['label'].startswith('Bad Fruit Salad')
643
         assert content['parent_id'] == 10
643
         assert content['parent_id'] == 10
644
         assert content['show_in_ui'] is True
644
         assert content['show_in_ui'] is True
645
         assert content['slug'].startswith('bad-fruit-salad')
645
         assert content['slug'].startswith('bad-fruit-salad')
646
-        assert content['status_slug'] == 'open'
647
-        assert set(content['sub_content_type_slug']) == {'thread', 'page', 'folder', 'file'}  # nopep8
646
+        assert content['status'] == 'open'
647
+        assert set(content['sub_content_types']) == {'thread', 'page', 'folder', 'file'}  # nopep8
648
         assert content['workspace_id'] == 2
648
         assert content['workspace_id'] == 2
649
 
649
 
650
     def test_api__get_workspace_content__ok_200__get_nothing_folder_content(self):  # nopep8
650
     def test_api__get_workspace_content__ok_200__get_nothing_folder_content(self):  # nopep8
741
         )
741
         )
742
         params = {
742
         params = {
743
             'label': 'GenericCreatedContent',
743
             'label': 'GenericCreatedContent',
744
-            'content_type_slug': 'markdownpage',
744
+            'content_type': 'markdownpage',
745
         }
745
         }
746
         res = self.testapp.post_json(
746
         res = self.testapp.post_json(
747
             '/api/v2/workspaces/1/contents',
747
             '/api/v2/workspaces/1/contents',
750
         )
750
         )
751
         assert res
751
         assert res
752
         assert res.json_body
752
         assert res.json_body
753
-        assert res.json_body['status_slug'] == 'open'
754
-        assert res.json_body['id']
755
-        assert res.json_body['content_type_slug'] == 'markdownpage'
753
+        assert res.json_body['status'] == 'open'
754
+        assert res.json_body['content_id']
755
+        assert res.json_body['content_type'] == 'markdownpage'
756
         assert res.json_body['is_archived'] is False
756
         assert res.json_body['is_archived'] is False
757
         assert res.json_body['is_deleted'] is False
757
         assert res.json_body['is_deleted'] is False
758
         assert res.json_body['workspace_id'] == 1
758
         assert res.json_body['workspace_id'] == 1
759
         assert res.json_body['slug'] == 'genericcreatedcontent'
759
         assert res.json_body['slug'] == 'genericcreatedcontent'
760
         assert res.json_body['parent_id'] is None
760
         assert res.json_body['parent_id'] is None
761
         assert res.json_body['show_in_ui'] is True
761
         assert res.json_body['show_in_ui'] is True
762
-        assert res.json_body['sub_content_type_slug']
762
+        assert res.json_body['sub_content_types']
763
         params_active = {
763
         params_active = {
764
             'parent_id': 0,
764
             'parent_id': 0,
765
             'show_archived': 0,
765
             'show_archived': 0,
786
         )
786
         )
787
         params = {
787
         params = {
788
             'new_parent_id': '4',  # Salads
788
             'new_parent_id': '4',  # Salads
789
+            'new_workspace_id': '2',
789
         }
790
         }
790
         params_folder1 = {
791
         params_folder1 = {
791
             'parent_id': 3,
792
             'parent_id': 3,
801
         }
802
         }
802
         folder1_contents = self.testapp.get('/api/v2/workspaces/2/contents', params=params_folder1, status=200).json_body  # nopep8
803
         folder1_contents = self.testapp.get('/api/v2/workspaces/2/contents', params=params_folder1, status=200).json_body  # nopep8
803
         folder2_contents = self.testapp.get('/api/v2/workspaces/2/contents', params=params_folder2, status=200).json_body  # nopep8
804
         folder2_contents = self.testapp.get('/api/v2/workspaces/2/contents', params=params_folder2, status=200).json_body  # nopep8
804
-        assert [content for content in folder1_contents if content['id'] == 8]  # nopep8
805
-        assert not [content for content in folder2_contents if content['id'] == 8]  # nopep8
805
+        assert [content for content in folder1_contents if content['content_id'] == 8]  # nopep8
806
+        assert not [content for content in folder2_contents if content['content_id'] == 8]  # nopep8
806
         # TODO - G.M - 2018-06-163 - Check content
807
         # TODO - G.M - 2018-06-163 - Check content
807
         res = self.testapp.put_json(
808
         res = self.testapp.put_json(
808
             '/api/v2/workspaces/2/contents/8/move',
809
             '/api/v2/workspaces/2/contents/8/move',
811
         )
812
         )
812
         new_folder1_contents = self.testapp.get('/api/v2/workspaces/2/contents', params=params_folder1, status=200).json_body  # nopep8
813
         new_folder1_contents = self.testapp.get('/api/v2/workspaces/2/contents', params=params_folder1, status=200).json_body  # nopep8
813
         new_folder2_contents = self.testapp.get('/api/v2/workspaces/2/contents', params=params_folder2, status=200).json_body  # nopep8
814
         new_folder2_contents = self.testapp.get('/api/v2/workspaces/2/contents', params=params_folder2, status=200).json_body  # nopep8
814
-        assert not [content for content in new_folder1_contents if content['id'] == 8]  # nopep8
815
-        assert [content for content in new_folder2_contents if content['id'] == 8]  # nopep8
815
+        assert not [content for content in new_folder1_contents if content['content_id'] == 8]  # nopep8
816
+        assert [content for content in new_folder2_contents if content['content_id'] == 8]  # nopep8
817
+        assert res.json_body
818
+        assert res.json_body['parent_id'] == 4
819
+        assert res.json_body['content_id'] == 8
820
+        assert res.json_body['workspace_id'] == 2
821
+
822
+    def test_api_put_move_content__ok_200__to_root(self):
823
+        """
824
+        Move content
825
+        move Apple_Pie (content_id: 8)
826
+        from Desserts folder(content_id: 3) to root (content_id: 0)
827
+        of workspace Recipes.
828
+        """
829
+        self.testapp.authorization = (
830
+            'Basic',
831
+            (
832
+                'admin@admin.admin',
833
+                'admin@admin.admin'
834
+            )
835
+        )
836
+        params = {
837
+            'new_parent_id': None,  # root
838
+            'new_workspace_id': 2,
839
+        }
840
+        params_folder1 = {
841
+            'parent_id': 3,
842
+            'show_archived': 0,
843
+            'show_deleted': 0,
844
+            'show_active': 1,
845
+        }
846
+        params_folder2 = {
847
+            'parent_id': 0,
848
+            'show_archived': 0,
849
+            'show_deleted': 0,
850
+            'show_active': 1,
851
+        }
852
+        folder1_contents = self.testapp.get('/api/v2/workspaces/2/contents', params=params_folder1, status=200).json_body  # nopep8
853
+        folder2_contents = self.testapp.get('/api/v2/workspaces/2/contents', params=params_folder2, status=200).json_body  # nopep8
854
+        assert [content for content in folder1_contents if content['content_id'] == 8]  # nopep8
855
+        assert not [content for content in folder2_contents if content['content_id'] == 8]  # nopep8
856
+        # TODO - G.M - 2018-06-163 - Check content
857
+        res = self.testapp.put_json(
858
+            '/api/v2/workspaces/2/contents/8/move',
859
+            params=params,
860
+            status=200
861
+        )
862
+        new_folder1_contents = self.testapp.get('/api/v2/workspaces/2/contents', params=params_folder1, status=200).json_body  # nopep8
863
+        new_folder2_contents = self.testapp.get('/api/v2/workspaces/2/contents', params=params_folder2, status=200).json_body  # nopep8
864
+        assert not [content for content in new_folder1_contents if content['content_id'] == 8]  # nopep8
865
+        assert [content for content in new_folder2_contents if content['content_id'] == 8]  # nopep8
866
+        assert res.json_body
867
+        assert res.json_body['parent_id'] is None
868
+        assert res.json_body['content_id'] == 8
869
+        assert res.json_body['workspace_id'] == 2
870
+
871
+    def test_api_put_move_content__ok_200__with_workspace_id(self):
872
+        """
873
+        Move content
874
+        move Apple_Pie (content_id: 8)
875
+        from Desserts folder(content_id: 3) to Salads subfolder (content_id: 4)
876
+        of workspace Recipes.
877
+        """
878
+        self.testapp.authorization = (
879
+            'Basic',
880
+            (
881
+                'admin@admin.admin',
882
+                'admin@admin.admin'
883
+            )
884
+        )
885
+        params = {
886
+            'new_parent_id': '4',  # Salads
887
+            'new_workspace_id': '2',
888
+        }
889
+        params_folder1 = {
890
+            'parent_id': 3,
891
+            'show_archived': 0,
892
+            'show_deleted': 0,
893
+            'show_active': 1,
894
+        }
895
+        params_folder2 = {
896
+            'parent_id': 4,
897
+            'show_archived': 0,
898
+            'show_deleted': 0,
899
+            'show_active': 1,
900
+        }
901
+        folder1_contents = self.testapp.get('/api/v2/workspaces/2/contents', params=params_folder1, status=200).json_body  # nopep8
902
+        folder2_contents = self.testapp.get('/api/v2/workspaces/2/contents', params=params_folder2, status=200).json_body  # nopep8
903
+        assert [content for content in folder1_contents if content['content_id'] == 8]  # nopep8
904
+        assert not [content for content in folder2_contents if content['content_id'] == 8]  # nopep8
905
+        # TODO - G.M - 2018-06-163 - Check content
906
+        res = self.testapp.put_json(
907
+            '/api/v2/workspaces/2/contents/8/move',
908
+            params=params,
909
+            status=200
910
+        )
911
+        new_folder1_contents = self.testapp.get('/api/v2/workspaces/2/contents', params=params_folder1, status=200).json_body  # nopep8
912
+        new_folder2_contents = self.testapp.get('/api/v2/workspaces/2/contents', params=params_folder2, status=200).json_body  # nopep8
913
+        assert not [content for content in new_folder1_contents if content['content_id'] == 8]  # nopep8
914
+        assert [content for content in new_folder2_contents if content['content_id'] == 8]  # nopep8
915
+        assert res.json_body
916
+        assert res.json_body['parent_id'] == 4
917
+        assert res.json_body['content_id'] == 8
918
+        assert res.json_body['workspace_id'] == 2
919
+
920
+    def test_api_put_move_content__ok_200__to_another_workspace(self):
921
+        """
922
+        Move content
923
+        move Apple_Pie (content_id: 8)
924
+        from Desserts folder(content_id: 3) to Menus subfolder (content_id: 2)
925
+        of workspace Business.
926
+        """
927
+        self.testapp.authorization = (
928
+            'Basic',
929
+            (
930
+                'admin@admin.admin',
931
+                'admin@admin.admin'
932
+            )
933
+        )
934
+        params = {
935
+            'new_parent_id': '2',  # Menus
936
+            'new_workspace_id': '1',
937
+        }
938
+        params_folder1 = {
939
+            'parent_id': 3,
940
+            'show_archived': 0,
941
+            'show_deleted': 0,
942
+            'show_active': 1,
943
+        }
944
+        params_folder2 = {
945
+            'parent_id': 2,
946
+            'show_archived': 0,
947
+            'show_deleted': 0,
948
+            'show_active': 1,
949
+        }
950
+        folder1_contents = self.testapp.get('/api/v2/workspaces/2/contents', params=params_folder1, status=200).json_body  # nopep8
951
+        folder2_contents = self.testapp.get('/api/v2/workspaces/1/contents', params=params_folder2, status=200).json_body  # nopep8
952
+        assert [content for content in folder1_contents if content['content_id'] == 8]  # nopep8
953
+        assert not [content for content in folder2_contents if content['content_id'] == 8]  # nopep8
954
+        # TODO - G.M - 2018-06-163 - Check content
955
+        res = self.testapp.put_json(
956
+            '/api/v2/workspaces/2/contents/8/move',
957
+            params=params,
958
+            status=200
959
+        )
960
+        new_folder1_contents = self.testapp.get('/api/v2/workspaces/2/contents', params=params_folder1, status=200).json_body  # nopep8
961
+        new_folder2_contents = self.testapp.get('/api/v2/workspaces/1/contents', params=params_folder2, status=200).json_body  # nopep8
962
+        assert not [content for content in new_folder1_contents if content['content_id'] == 8]  # nopep8
963
+        assert [content for content in new_folder2_contents if content['content_id'] == 8]  # nopep8
964
+        assert res.json_body
965
+        assert res.json_body['parent_id'] == 2
966
+        assert res.json_body['content_id'] == 8
967
+        assert res.json_body['workspace_id'] == 1
968
+
969
+    def test_api_put_move_content__ok_200__to_another_workspace_root(self):
970
+        """
971
+        Move content
972
+        move Apple_Pie (content_id: 8)
973
+        from Desserts folder(content_id: 3) to root (content_id: 0)
974
+        of workspace Business.
975
+        """
976
+        self.testapp.authorization = (
977
+            'Basic',
978
+            (
979
+                'admin@admin.admin',
980
+                'admin@admin.admin'
981
+            )
982
+        )
983
+        params = {
984
+            'new_parent_id': None,  # root
985
+            'new_workspace_id': '1',
986
+        }
987
+        params_folder1 = {
988
+            'parent_id': 3,
989
+            'show_archived': 0,
990
+            'show_deleted': 0,
991
+            'show_active': 1,
992
+        }
993
+        params_folder2 = {
994
+            'parent_id': 0,
995
+            'show_archived': 0,
996
+            'show_deleted': 0,
997
+            'show_active': 1,
998
+        }
999
+        folder1_contents = self.testapp.get('/api/v2/workspaces/2/contents', params=params_folder1, status=200).json_body  # nopep8
1000
+        folder2_contents = self.testapp.get('/api/v2/workspaces/1/contents', params=params_folder2, status=200).json_body  # nopep8
1001
+        assert [content for content in folder1_contents if content['content_id'] == 8]  # nopep8
1002
+        assert not [content for content in folder2_contents if content['content_id'] == 8]  # nopep8
1003
+        # TODO - G.M - 2018-06-163 - Check content
1004
+        res = self.testapp.put_json(
1005
+            '/api/v2/workspaces/2/contents/8/move',
1006
+            params=params,
1007
+            status=200
1008
+        )
1009
+        new_folder1_contents = self.testapp.get('/api/v2/workspaces/2/contents', params=params_folder1, status=200).json_body  # nopep8
1010
+        new_folder2_contents = self.testapp.get('/api/v2/workspaces/1/contents', params=params_folder2, status=200).json_body  # nopep8
1011
+        assert not [content for content in new_folder1_contents if content['content_id'] == 8]  # nopep8
1012
+        assert [content for content in new_folder2_contents if content['content_id'] == 8]  # nopep8
1013
+        assert res.json_body
1014
+        assert res.json_body['parent_id'] is None
1015
+        assert res.json_body['content_id'] == 8
1016
+        assert res.json_body['workspace_id'] == 1
1017
+
1018
+    def test_api_put_move_content__err_400__wrong_workspace_id(self):
1019
+        """
1020
+        Move content
1021
+        move Apple_Pie (content_id: 8)
1022
+        from Desserts folder(content_id: 3) to Salads subfolder (content_id: 4)
1023
+        of workspace Recipes.
1024
+        Workspace_id of parent_id don't match with workspace_id of workspace
1025
+        """
1026
+        self.testapp.authorization = (
1027
+            'Basic',
1028
+            (
1029
+                'admin@admin.admin',
1030
+                'admin@admin.admin'
1031
+            )
1032
+        )
1033
+        params = {
1034
+            'new_parent_id': '4',  # Salads
1035
+            'new_workspace_id': '1',
1036
+        }
1037
+        params_folder1 = {
1038
+            'parent_id': 3,
1039
+            'show_archived': 0,
1040
+            'show_deleted': 0,
1041
+            'show_active': 1,
1042
+        }
1043
+        params_folder2 = {
1044
+            'parent_id': 4,
1045
+            'show_archived': 0,
1046
+            'show_deleted': 0,
1047
+            'show_active': 1,
1048
+        }
1049
+        res = self.testapp.put_json(
1050
+            '/api/v2/workspaces/2/contents/8/move',
1051
+            params=params,
1052
+            status=400,
1053
+        )
816
 
1054
 
817
     def test_api_put_delete_content__ok_200__nominal_case(self):
1055
     def test_api_put_delete_content__ok_200__nominal_case(self):
818
         """
1056
         """
840
         }
1078
         }
841
         active_contents = self.testapp.get('/api/v2/workspaces/2/contents', params=params_active, status=200).json_body  # nopep8
1079
         active_contents = self.testapp.get('/api/v2/workspaces/2/contents', params=params_active, status=200).json_body  # nopep8
842
         deleted_contents = self.testapp.get('/api/v2/workspaces/2/contents', params=params_deleted, status=200).json_body  # nopep8
1080
         deleted_contents = self.testapp.get('/api/v2/workspaces/2/contents', params=params_deleted, status=200).json_body  # nopep8
843
-        assert [content for content in active_contents if content['id'] == 8]  # nopep8
844
-        assert not [content for content in deleted_contents if content['id'] == 8]  # nopep8
1081
+        assert [content for content in active_contents if content['content_id'] == 8]  # nopep8
1082
+        assert not [content for content in deleted_contents if content['content_id'] == 8]  # nopep8
845
         # TODO - G.M - 2018-06-163 - Check content
1083
         # TODO - G.M - 2018-06-163 - Check content
846
         res = self.testapp.put_json(
1084
         res = self.testapp.put_json(
847
             # INFO - G.M - 2018-06-163 - delete Apple_Pie
1085
             # INFO - G.M - 2018-06-163 - delete Apple_Pie
848
             '/api/v2/workspaces/2/contents/8/delete',
1086
             '/api/v2/workspaces/2/contents/8/delete',
849
-            status=200
1087
+            status=204
850
         )
1088
         )
851
         new_active_contents = self.testapp.get('/api/v2/workspaces/2/contents', params=params_active, status=200).json_body  # nopep8
1089
         new_active_contents = self.testapp.get('/api/v2/workspaces/2/contents', params=params_active, status=200).json_body  # nopep8
852
         new_deleted_contents = self.testapp.get('/api/v2/workspaces/2/contents', params=params_deleted, status=200).json_body  # nopep8
1090
         new_deleted_contents = self.testapp.get('/api/v2/workspaces/2/contents', params=params_deleted, status=200).json_body  # nopep8
853
-        assert not [content for content in new_active_contents if content['id'] == 8]  # nopep8
854
-        assert [content for content in new_deleted_contents if content['id'] == 8]  # nopep8
1091
+        assert not [content for content in new_active_contents if content['content_id'] == 8]  # nopep8
1092
+        assert [content for content in new_deleted_contents if content['content_id'] == 8]  # nopep8
855
 
1093
 
856
     def test_api_put_archive_content__ok_200__nominal_case(self):
1094
     def test_api_put_archive_content__ok_200__nominal_case(self):
857
         """
1095
         """
879
         }
1117
         }
880
         active_contents = self.testapp.get('/api/v2/workspaces/2/contents', params=params_active, status=200).json_body  # nopep8
1118
         active_contents = self.testapp.get('/api/v2/workspaces/2/contents', params=params_active, status=200).json_body  # nopep8
881
         archived_contents = self.testapp.get('/api/v2/workspaces/2/contents', params=params_archived, status=200).json_body  # nopep8
1119
         archived_contents = self.testapp.get('/api/v2/workspaces/2/contents', params=params_archived, status=200).json_body  # nopep8
882
-        assert [content for content in active_contents if content['id'] == 8]  # nopep8
883
-        assert not [content for content in archived_contents if content['id'] == 8]  # nopep8
1120
+        assert [content for content in active_contents if content['content_id'] == 8]  # nopep8
1121
+        assert not [content for content in archived_contents if content['content_id'] == 8]  # nopep8
884
         res = self.testapp.put_json(
1122
         res = self.testapp.put_json(
885
             '/api/v2/workspaces/2/contents/8/archive',
1123
             '/api/v2/workspaces/2/contents/8/archive',
886
-            status=200
1124
+            status=204
887
         )
1125
         )
888
         new_active_contents = self.testapp.get('/api/v2/workspaces/2/contents', params=params_active, status=200).json_body  # nopep8
1126
         new_active_contents = self.testapp.get('/api/v2/workspaces/2/contents', params=params_active, status=200).json_body  # nopep8
889
         new_archived_contents = self.testapp.get('/api/v2/workspaces/2/contents', params=params_archived, status=200).json_body  # nopep8
1127
         new_archived_contents = self.testapp.get('/api/v2/workspaces/2/contents', params=params_archived, status=200).json_body  # nopep8
890
-        assert not [content for content in new_active_contents if content['id'] == 8]  # nopep8
891
-        assert [content for content in new_archived_contents if content['id'] == 8]  # nopep8
1128
+        assert not [content for content in new_active_contents if content['content_id'] == 8]  # nopep8
1129
+        assert [content for content in new_archived_contents if content['content_id'] == 8]  # nopep8
892
 
1130
 
893
     def test_api_put_undelete_content__ok_200__nominal_case(self):
1131
     def test_api_put_undelete_content__ok_200__nominal_case(self):
894
         """
1132
         """
916
         }
1154
         }
917
         active_contents = self.testapp.get('/api/v2/workspaces/2/contents', params=params_active, status=200).json_body  # nopep8
1155
         active_contents = self.testapp.get('/api/v2/workspaces/2/contents', params=params_active, status=200).json_body  # nopep8
918
         deleted_contents = self.testapp.get('/api/v2/workspaces/2/contents', params=params_deleted, status=200).json_body  # nopep8
1156
         deleted_contents = self.testapp.get('/api/v2/workspaces/2/contents', params=params_deleted, status=200).json_body  # nopep8
919
-        assert not [content for content in active_contents if content['id'] == 14]  # nopep8
920
-        assert [content for content in deleted_contents if content['id'] == 14]  # nopep8
1157
+        assert not [content for content in active_contents if content['content_id'] == 14]  # nopep8
1158
+        assert [content for content in deleted_contents if content['content_id'] == 14]  # nopep8
921
         res = self.testapp.put_json(
1159
         res = self.testapp.put_json(
922
             '/api/v2/workspaces/2/contents/14/undelete',
1160
             '/api/v2/workspaces/2/contents/14/undelete',
923
-            status=200
1161
+            status=204
924
         )
1162
         )
925
         new_active_contents = self.testapp.get('/api/v2/workspaces/2/contents', params=params_active, status=200).json_body  # nopep8
1163
         new_active_contents = self.testapp.get('/api/v2/workspaces/2/contents', params=params_active, status=200).json_body  # nopep8
926
         new_deleted_contents = self.testapp.get('/api/v2/workspaces/2/contents', params=params_deleted, status=200).json_body  # nopep8
1164
         new_deleted_contents = self.testapp.get('/api/v2/workspaces/2/contents', params=params_deleted, status=200).json_body  # nopep8
927
-        assert [content for content in new_active_contents if content['id'] == 14]  # nopep8
928
-        assert not [content for content in new_deleted_contents if content['id'] == 14]  # nopep8
1165
+        assert [content for content in new_active_contents if content['content_id'] == 14]  # nopep8
1166
+        assert not [content for content in new_deleted_contents if content['content_id'] == 14]  # nopep8
929
 
1167
 
930
     def test_api_put_unarchive_content__ok_200__nominal_case(self):
1168
     def test_api_put_unarchive_content__ok_200__nominal_case(self):
931
         """
1169
         """
953
         }
1191
         }
954
         active_contents = self.testapp.get('/api/v2/workspaces/2/contents', params=params_active, status=200).json_body  # nopep8
1192
         active_contents = self.testapp.get('/api/v2/workspaces/2/contents', params=params_active, status=200).json_body  # nopep8
955
         archived_contents = self.testapp.get('/api/v2/workspaces/2/contents', params=params_archived, status=200).json_body  # nopep8
1193
         archived_contents = self.testapp.get('/api/v2/workspaces/2/contents', params=params_archived, status=200).json_body  # nopep8
956
-        assert not [content for content in active_contents if content['id'] == 13]  # nopep8
957
-        assert [content for content in archived_contents if content['id'] == 13]  # nopep8
1194
+        assert not [content for content in active_contents if content['content_id'] == 13]  # nopep8
1195
+        assert [content for content in archived_contents if content['content_id'] == 13]  # nopep8
958
         res = self.testapp.put_json(
1196
         res = self.testapp.put_json(
959
             '/api/v2/workspaces/2/contents/13/unarchive',
1197
             '/api/v2/workspaces/2/contents/13/unarchive',
960
-            status=200
1198
+            status=204
961
         )
1199
         )
962
         new_active_contents = self.testapp.get('/api/v2/workspaces/2/contents', params=params_active, status=200).json_body  # nopep8
1200
         new_active_contents = self.testapp.get('/api/v2/workspaces/2/contents', params=params_active, status=200).json_body  # nopep8
963
         new_archived_contents = self.testapp.get('/api/v2/workspaces/2/contents', params=params_archived, status=200).json_body  # nopep8
1201
         new_archived_contents = self.testapp.get('/api/v2/workspaces/2/contents', params=params_archived, status=200).json_body  # nopep8
964
-        assert [content for content in new_active_contents if content['id'] == 13]  # nopep8
965
-        assert not [content for content in new_archived_contents if content['id'] == 13]  # nopep8
1202
+        assert [content for content in new_active_contents if content['content_id'] == 13]  # nopep8
1203
+        assert not [content for content in new_archived_contents if content['content_id'] == 13]  # nopep8

+ 1 - 1
tracim/tests/library/test_user_api.py 查看文件

131
         new_user = api.get_user_with_context(user)
131
         new_user = api.get_user_with_context(user)
132
         assert isinstance(new_user, UserInContext)
132
         assert isinstance(new_user, UserInContext)
133
         assert new_user.user == user
133
         assert new_user.user == user
134
-        assert new_user.profile.name == 'nobody'
134
+        assert new_user.profile == 'nobody'
135
         assert new_user.user_id == user.user_id
135
         assert new_user.user_id == user.user_id
136
         assert new_user.email == 'admin@tracim.tracim'
136
         assert new_user.email == 'admin@tracim.tracim'
137
         assert new_user.display_name == 'Admin'
137
         assert new_user.display_name == 'Admin'

+ 27 - 30
tracim/views/core_api/schemas.py 查看文件

16
 from tracim.models.data import UserRoleInWorkspace
16
 from tracim.models.data import UserRoleInWorkspace
17
 
17
 
18
 
18
 
19
-class ProfileSchema(marshmallow.Schema):
20
-    slug = marshmallow.fields.String(
21
-        attribute='name',
22
-        validate=OneOf(Profile._NAME),
23
-        example='managers',
24
-    )
25
-
26
-    class Meta:
27
-        description = 'User Profile, give user right on whole Tracim instance.'
28
-
29
-
30
 class UserSchema(marshmallow.Schema):
19
 class UserSchema(marshmallow.Schema):
31
 
20
 
32
     user_id = marshmallow.fields.Int(dump_only=True, example=3)
21
     user_id = marshmallow.fields.Int(dump_only=True, example=3)
34
         required=True,
23
         required=True,
35
         example='suri.cate@algoo.fr'
24
         example='suri.cate@algoo.fr'
36
     )
25
     )
37
-    display_name = marshmallow.fields.String(
26
+    public_name = marshmallow.fields.String(
38
         example='Suri Cate',
27
         example='Suri Cate',
39
     )
28
     )
40
     created = marshmallow.fields.DateTime(
29
     created = marshmallow.fields.DateTime(
41
-        format='iso8601',
42
-        description='User account creation date (iso8601 format).',
30
+        format='%Y-%m-%dT%H:%M:%SZ',
31
+        description='User account creation date',
43
     )
32
     )
44
     is_active = marshmallow.fields.Bool(
33
     is_active = marshmallow.fields.Bool(
45
         example=True,
34
         example=True,
64
                     "If no avatar, then set it to null "
53
                     "If no avatar, then set it to null "
65
                     "(and frontend will interpret this with a default avatar)",
54
                     "(and frontend will interpret this with a default avatar)",
66
     )
55
     )
67
-    profile = marshmallow.fields.Nested(
68
-        ProfileSchema,
69
-        many=False,
56
+    profile = marshmallow.fields.String(
57
+        attribute='profile',
58
+        validate=OneOf(Profile._NAME),
59
+        example='managers',
70
     )
60
     )
71
 
61
 
72
     class Meta:
62
     class Meta:
95
 
85
 
96
 
86
 
97
 class FilterContentQuerySchema(marshmallow.Schema):
87
 class FilterContentQuerySchema(marshmallow.Schema):
98
-    parent_id = workspace_id = marshmallow.fields.Int(
88
+    parent_id = marshmallow.fields.Int(
99
         example=2,
89
         example=2,
100
-        default=None,
90
+        default=0,
101
         description='allow to filter items in a folder.'
91
         description='allow to filter items in a folder.'
102
                     ' If not set, then return all contents.'
92
                     ' If not set, then return all contents.'
103
                     ' If set to 0, then return root contents.'
93
                     ' If set to 0, then return root contents.'
187
 
177
 
188
 
178
 
189
 class WorkspaceDigestSchema(marshmallow.Schema):
179
 class WorkspaceDigestSchema(marshmallow.Schema):
190
-    id = marshmallow.fields.Int(example=4)
180
+    workspace_id = marshmallow.fields.Int(example=4)
181
+    slug = marshmallow.fields.String(example='intranet')
191
     label = marshmallow.fields.String(example='Intranet')
182
     label = marshmallow.fields.String(example='Intranet')
192
     sidebar_entries = marshmallow.fields.Nested(
183
     sidebar_entries = marshmallow.fields.Nested(
193
         WorkspaceMenuEntrySchema,
184
         WorkspaceMenuEntrySchema,
199
 
190
 
200
 
191
 
201
 class WorkspaceSchema(WorkspaceDigestSchema):
192
 class WorkspaceSchema(WorkspaceDigestSchema):
202
-    slug = marshmallow.fields.String(example='intranet')
203
     description = marshmallow.fields.String(example='All intranet data.')
193
     description = marshmallow.fields.String(example='All intranet data.')
204
 
194
 
205
     class Meta:
195
     class Meta:
207
 
197
 
208
 
198
 
209
 class WorkspaceMemberSchema(marshmallow.Schema):
199
 class WorkspaceMemberSchema(marshmallow.Schema):
210
-    role_slug = marshmallow.fields.String(
200
+    role = marshmallow.fields.String(
211
         example='contributor',
201
         example='contributor',
212
         validate=OneOf(UserRoleInWorkspace.get_all_role_slug())
202
         validate=OneOf(UserRoleInWorkspace.get_all_role_slug())
213
     )
203
     )
214
     user_id = marshmallow.fields.Int(example=3)
204
     user_id = marshmallow.fields.Int(example=3)
215
     workspace_id = marshmallow.fields.Int(example=4)
205
     workspace_id = marshmallow.fields.Int(example=4)
216
     user = marshmallow.fields.Nested(
206
     user = marshmallow.fields.Nested(
217
-        UserSchema(only=('display_name', 'avatar_url'))
207
+        UserSchema(only=('public_name', 'avatar_url'))
218
     )
208
     )
219
 
209
 
220
     class Meta:
210
     class Meta:
256
                     'Statuses are open, closed-validated, closed-invalidated, closed-deprecated'  # nopep8
246
                     'Statuses are open, closed-validated, closed-invalidated, closed-deprecated'  # nopep8
257
     )
247
     )
258
     global_status = marshmallow.fields.String(
248
     global_status = marshmallow.fields.String(
259
-        example='Open',
249
+        example='open',
260
         description='global_status: open, closed',
250
         description='global_status: open, closed',
261
         validate=OneOf([status.value for status in GlobalStatus]),
251
         validate=OneOf([status.value for status in GlobalStatus]),
262
     )
252
     )
298
     # (the user must be content manager of both workspaces)
288
     # (the user must be content manager of both workspaces)
299
     new_parent_id = marshmallow.fields.Int(
289
     new_parent_id = marshmallow.fields.Int(
300
         example=42,
290
         example=42,
301
-        description='id of the new parent content id.'
291
+        description='id of the new parent content id.',
292
+        allow_none=True,
293
+        required=True,
294
+    )
295
+    new_workspace_id = marshmallow.fields.Int(
296
+        example=2,
297
+        description='id of the new workspace id.',
298
+        required=True
302
     )
299
     )
303
 
300
 
304
     @post_load
301
     @post_load
311
         example='contract for client XXX',
308
         example='contract for client XXX',
312
         description='Title of the content to create'
309
         description='Title of the content to create'
313
     )
310
     )
314
-    content_type_slug = marshmallow.fields.String(
311
+    content_type = marshmallow.fields.String(
315
         example='htmlpage',
312
         example='htmlpage',
316
         validate=OneOf([content.slug for content in CONTENT_DEFAULT_TYPE]),
313
         validate=OneOf([content.slug for content in CONTENT_DEFAULT_TYPE]),
317
     )
314
     )
322
 
319
 
323
 
320
 
324
 class ContentDigestSchema(marshmallow.Schema):
321
 class ContentDigestSchema(marshmallow.Schema):
325
-    id = marshmallow.fields.Int(example=6)
322
+    content_id = marshmallow.fields.Int(example=6)
326
     slug = marshmallow.fields.Str(example='intervention-report-12')
323
     slug = marshmallow.fields.Str(example='intervention-report-12')
327
     parent_id = marshmallow.fields.Int(
324
     parent_id = marshmallow.fields.Int(
328
         example=34,
325
         example=34,
333
         example=19,
330
         example=19,
334
     )
331
     )
335
     label = marshmallow.fields.Str(example='Intervention Report 12')
332
     label = marshmallow.fields.Str(example='Intervention Report 12')
336
-    content_type_slug = marshmallow.fields.Str(
333
+    content_type = marshmallow.fields.Str(
337
         example='htmlpage',
334
         example='htmlpage',
338
         validate=OneOf([content.slug for content in CONTENT_DEFAULT_TYPE]),
335
         validate=OneOf([content.slug for content in CONTENT_DEFAULT_TYPE]),
339
     )
336
     )
340
-    sub_content_type_slug = marshmallow.fields.List(
337
+    sub_content_types = marshmallow.fields.List(
341
         marshmallow.fields.Str,
338
         marshmallow.fields.Str,
342
         description='list of content types allowed as sub contents. '
339
         description='list of content types allowed as sub contents. '
343
                     'This field is required for folder contents, '
340
                     'This field is required for folder contents, '
344
                     'set it to empty list in other cases'
341
                     'set it to empty list in other cases'
345
     )
342
     )
346
-    status_slug = marshmallow.fields.Str(
343
+    status = marshmallow.fields.Str(
347
         example='closed-deprecated',
344
         example='closed-deprecated',
348
         validate=OneOf([status.slug for status in CONTENT_DEFAULT_STATUS]),
345
         validate=OneOf([status.slug for status in CONTENT_DEFAULT_STATUS]),
349
         description='this slug is found in content_type available statuses',
346
         description='this slug is found in content_type available statuses',

+ 5 - 3
tracim/views/core_api/session_controller.py 查看文件

16
 from tracim.exceptions import NotAuthenticated
16
 from tracim.exceptions import NotAuthenticated
17
 from tracim.exceptions import AuthenticationFailed
17
 from tracim.exceptions import AuthenticationFailed
18
 
18
 
19
+SESSION_ENDPOINTS_TAG = 'Session'
20
+
19
 
21
 
20
 class SessionController(Controller):
22
 class SessionController(Controller):
21
 
23
 
22
-    @hapic.with_api_doc()
24
+    @hapic.with_api_doc(tags=[SESSION_ENDPOINTS_TAG])
23
     @hapic.input_headers(LoginOutputHeaders())
25
     @hapic.input_headers(LoginOutputHeaders())
24
     @hapic.input_body(BasicAuthSchema())
26
     @hapic.input_body(BasicAuthSchema())
25
     @hapic.handle_exception(AuthenticationFailed, HTTPStatus.BAD_REQUEST)
27
     @hapic.handle_exception(AuthenticationFailed, HTTPStatus.BAD_REQUEST)
42
         user = uapi.authenticate_user(login.email, login.password)
44
         user = uapi.authenticate_user(login.email, login.password)
43
         return uapi.get_user_with_context(user)
45
         return uapi.get_user_with_context(user)
44
 
46
 
45
-    @hapic.with_api_doc()
47
+    @hapic.with_api_doc(tags=[SESSION_ENDPOINTS_TAG])
46
     @hapic.output_body(NoContentSchema(), default_http_code=HTTPStatus.NO_CONTENT)  # nopep8
48
     @hapic.output_body(NoContentSchema(), default_http_code=HTTPStatus.NO_CONTENT)  # nopep8
47
     def logout(self, context, request: TracimRequest, hapic_data=None):
49
     def logout(self, context, request: TracimRequest, hapic_data=None):
48
         """
50
         """
51
 
53
 
52
         return
54
         return
53
 
55
 
54
-    @hapic.with_api_doc()
56
+    @hapic.with_api_doc(tags=[SESSION_ENDPOINTS_TAG])
55
     @hapic.handle_exception(NotAuthenticated, HTTPStatus.UNAUTHORIZED)
57
     @hapic.handle_exception(NotAuthenticated, HTTPStatus.UNAUTHORIZED)
56
     @hapic.output_body(UserSchema(),)
58
     @hapic.output_body(UserSchema(),)
57
     def whoami(self, context, request: TracimRequest, hapic_data=None):
59
     def whoami(self, context, request: TracimRequest, hapic_data=None):

+ 3 - 2
tracim/views/core_api/system_controller.py 查看文件

18
 from tracim.views.core_api.schemas import ApplicationSchema
18
 from tracim.views.core_api.schemas import ApplicationSchema
19
 from tracim.views.core_api.schemas import ContentTypeSchema
19
 from tracim.views.core_api.schemas import ContentTypeSchema
20
 
20
 
21
+SYSTEM_ENDPOINTS_TAG = 'System'
21
 
22
 
22
 class SystemController(Controller):
23
 class SystemController(Controller):
23
 
24
 
24
-    @hapic.with_api_doc()
25
+    @hapic.with_api_doc(tags=[SYSTEM_ENDPOINTS_TAG])
25
     @hapic.handle_exception(NotAuthenticated, HTTPStatus.UNAUTHORIZED)
26
     @hapic.handle_exception(NotAuthenticated, HTTPStatus.UNAUTHORIZED)
26
     @hapic.handle_exception(InsufficientUserProfile, HTTPStatus.FORBIDDEN)
27
     @hapic.handle_exception(InsufficientUserProfile, HTTPStatus.FORBIDDEN)
27
     @require_profile(Group.TIM_USER)
28
     @require_profile(Group.TIM_USER)
32
         """
33
         """
33
         return applications
34
         return applications
34
 
35
 
35
-    @hapic.with_api_doc()
36
+    @hapic.with_api_doc(tags=[SYSTEM_ENDPOINTS_TAG])
36
     @hapic.handle_exception(NotAuthenticated, HTTPStatus.UNAUTHORIZED)
37
     @hapic.handle_exception(NotAuthenticated, HTTPStatus.UNAUTHORIZED)
37
     @hapic.handle_exception(InsufficientUserProfile, HTTPStatus.FORBIDDEN)
38
     @hapic.handle_exception(InsufficientUserProfile, HTTPStatus.FORBIDDEN)
38
     @require_profile(Group.TIM_USER)
39
     @require_profile(Group.TIM_USER)

+ 3 - 1
tracim/views/core_api/user_controller.py 查看文件

20
 from tracim.views.core_api.schemas import UserIdPathSchema
20
 from tracim.views.core_api.schemas import UserIdPathSchema
21
 from tracim.views.core_api.schemas import WorkspaceDigestSchema
21
 from tracim.views.core_api.schemas import WorkspaceDigestSchema
22
 
22
 
23
+USER_ENDPOINTS_TAG = 'Users'
24
+
23
 
25
 
24
 class UserController(Controller):
26
 class UserController(Controller):
25
 
27
 
26
-    @hapic.with_api_doc()
28
+    @hapic.with_api_doc(tags=[USER_ENDPOINTS_TAG])
27
     @hapic.handle_exception(NotAuthenticated, HTTPStatus.UNAUTHORIZED)
29
     @hapic.handle_exception(NotAuthenticated, HTTPStatus.UNAUTHORIZED)
28
     @hapic.handle_exception(InsufficientUserProfile, HTTPStatus.FORBIDDEN)
30
     @hapic.handle_exception(InsufficientUserProfile, HTTPStatus.FORBIDDEN)
29
     @hapic.handle_exception(UserDoesNotExist, HTTPStatus.NOT_FOUND)
31
     @hapic.handle_exception(UserDoesNotExist, HTTPStatus.NOT_FOUND)

+ 44 - 22
tracim/views/core_api/workspace_controller.py 查看文件

1
 import typing
1
 import typing
2
-
3
 import transaction
2
 import transaction
4
 from pyramid.config import Configurator
3
 from pyramid.config import Configurator
5
 try:  # Python 3.5+
4
 try:  # Python 3.5+
7
 except ImportError:
6
 except ImportError:
8
     from http import client as HTTPStatus
7
     from http import client as HTTPStatus
9
 
8
 
10
-from tracim import hapic, TracimRequest
9
+from tracim import hapic
10
+from tracim import TracimRequest
11
 from tracim.lib.core.workspace import WorkspaceApi
11
 from tracim.lib.core.workspace import WorkspaceApi
12
 from tracim.lib.core.content import ContentApi
12
 from tracim.lib.core.content import ContentApi
13
 from tracim.lib.core.userworkspace import RoleApi
13
 from tracim.lib.core.userworkspace import RoleApi
14
-from tracim.lib.utils.authorization import require_workspace_role
15
-from tracim.models.data import UserRoleInWorkspace, ActionDescription
14
+from tracim.lib.utils.authorization import require_workspace_role, \
15
+    require_candidate_workspace_role
16
+from tracim.models.data import UserRoleInWorkspace
17
+from tracim.models.data import ActionDescription
16
 from tracim.models.context_models import UserRoleWorkspaceInContext
18
 from tracim.models.context_models import UserRoleWorkspaceInContext
17
 from tracim.models.context_models import ContentInContext
19
 from tracim.models.context_models import ContentInContext
18
-from tracim.exceptions import NotAuthenticated
20
+from tracim.exceptions import NotAuthenticated, InsufficientUserWorkspaceRole
21
+from tracim.exceptions import WorkspaceNotFoundInTracimRequest
22
+from tracim.exceptions import WorkspacesDoNotMatch
19
 from tracim.exceptions import InsufficientUserProfile
23
 from tracim.exceptions import InsufficientUserProfile
20
 from tracim.exceptions import WorkspaceNotFound
24
 from tracim.exceptions import WorkspaceNotFound
21
 from tracim.views.controllers import Controller
25
 from tracim.views.controllers import Controller
31
 from tracim.models.contents import ContentTypeLegacy as ContentType
35
 from tracim.models.contents import ContentTypeLegacy as ContentType
32
 from tracim.models.revision_protection import new_revision
36
 from tracim.models.revision_protection import new_revision
33
 
37
 
38
+WORKSPACE_ENDPOINTS_TAG = 'Workspaces'
39
+
34
 
40
 
35
 class WorkspaceController(Controller):
41
 class WorkspaceController(Controller):
36
 
42
 
37
-    @hapic.with_api_doc()
43
+    @hapic.with_api_doc(tags=[WORKSPACE_ENDPOINTS_TAG])
38
     @hapic.handle_exception(NotAuthenticated, HTTPStatus.UNAUTHORIZED)
44
     @hapic.handle_exception(NotAuthenticated, HTTPStatus.UNAUTHORIZED)
39
     @hapic.handle_exception(InsufficientUserProfile, HTTPStatus.FORBIDDEN)
45
     @hapic.handle_exception(InsufficientUserProfile, HTTPStatus.FORBIDDEN)
40
     @hapic.handle_exception(WorkspaceNotFound, HTTPStatus.FORBIDDEN)
46
     @hapic.handle_exception(WorkspaceNotFound, HTTPStatus.FORBIDDEN)
54
         )
60
         )
55
         return wapi.get_workspace_with_context(request.current_workspace)
61
         return wapi.get_workspace_with_context(request.current_workspace)
56
 
62
 
57
-    @hapic.with_api_doc()
63
+    @hapic.with_api_doc(tags=[WORKSPACE_ENDPOINTS_TAG])
58
     @hapic.handle_exception(NotAuthenticated, HTTPStatus.UNAUTHORIZED)
64
     @hapic.handle_exception(NotAuthenticated, HTTPStatus.UNAUTHORIZED)
59
     @hapic.handle_exception(InsufficientUserProfile, HTTPStatus.FORBIDDEN)
65
     @hapic.handle_exception(InsufficientUserProfile, HTTPStatus.FORBIDDEN)
60
     @hapic.handle_exception(WorkspaceNotFound, HTTPStatus.FORBIDDEN)
66
     @hapic.handle_exception(WorkspaceNotFound, HTTPStatus.FORBIDDEN)
83
             for user_role in roles
89
             for user_role in roles
84
         ]
90
         ]
85
 
91
 
86
-    @hapic.with_api_doc()
92
+    @hapic.with_api_doc(tags=[WORKSPACE_ENDPOINTS_TAG])
87
     @hapic.handle_exception(NotAuthenticated, HTTPStatus.UNAUTHORIZED)
93
     @hapic.handle_exception(NotAuthenticated, HTTPStatus.UNAUTHORIZED)
88
     @hapic.handle_exception(InsufficientUserProfile, HTTPStatus.FORBIDDEN)
94
     @hapic.handle_exception(InsufficientUserProfile, HTTPStatus.FORBIDDEN)
89
     @hapic.handle_exception(WorkspaceNotFound, HTTPStatus.FORBIDDEN)
95
     @hapic.handle_exception(WorkspaceNotFound, HTTPStatus.FORBIDDEN)
119
         ]
125
         ]
120
         return contents
126
         return contents
121
 
127
 
122
-    @hapic.with_api_doc()
128
+    @hapic.with_api_doc(tags=[WORKSPACE_ENDPOINTS_TAG])
123
     @hapic.handle_exception(NotAuthenticated, HTTPStatus.UNAUTHORIZED)
129
     @hapic.handle_exception(NotAuthenticated, HTTPStatus.UNAUTHORIZED)
124
     @hapic.handle_exception(InsufficientUserProfile, HTTPStatus.FORBIDDEN)
130
     @hapic.handle_exception(InsufficientUserProfile, HTTPStatus.FORBIDDEN)
125
     @hapic.handle_exception(WorkspaceNotFound, HTTPStatus.FORBIDDEN)
131
     @hapic.handle_exception(WorkspaceNotFound, HTTPStatus.FORBIDDEN)
145
         )
151
         )
146
         content = api.create(
152
         content = api.create(
147
             label=creation_data.label,
153
             label=creation_data.label,
148
-            content_type=creation_data.content_type_slug,
154
+            content_type=creation_data.content_type,
149
             workspace=request.current_workspace,
155
             workspace=request.current_workspace,
150
         )
156
         )
151
         api.save(content, ActionDescription.CREATION)
157
         api.save(content, ActionDescription.CREATION)
152
         content = api.get_content_in_context(content)
158
         content = api.get_content_in_context(content)
153
         return content
159
         return content
154
 
160
 
155
-    @hapic.with_api_doc()
161
+    @hapic.with_api_doc(tags=[WORKSPACE_ENDPOINTS_TAG])
156
     @hapic.handle_exception(NotAuthenticated, HTTPStatus.UNAUTHORIZED)
162
     @hapic.handle_exception(NotAuthenticated, HTTPStatus.UNAUTHORIZED)
157
     @hapic.handle_exception(InsufficientUserProfile, HTTPStatus.FORBIDDEN)
163
     @hapic.handle_exception(InsufficientUserProfile, HTTPStatus.FORBIDDEN)
158
     @hapic.handle_exception(WorkspaceNotFound, HTTPStatus.FORBIDDEN)
164
     @hapic.handle_exception(WorkspaceNotFound, HTTPStatus.FORBIDDEN)
165
+    @hapic.handle_exception(InsufficientUserWorkspaceRole, HTTPStatus.FORBIDDEN)
166
+    @hapic.handle_exception(WorkspacesDoNotMatch, HTTPStatus.BAD_REQUEST)
159
     @require_workspace_role(UserRoleInWorkspace.CONTRIBUTOR)
167
     @require_workspace_role(UserRoleInWorkspace.CONTRIBUTOR)
168
+    @require_candidate_workspace_role(UserRoleInWorkspace.CONTRIBUTOR)
160
     @hapic.input_path(WorkspaceAndContentIdPathSchema())
169
     @hapic.input_path(WorkspaceAndContentIdPathSchema())
161
     @hapic.input_body(ContentMoveSchema())
170
     @hapic.input_body(ContentMoveSchema())
162
-    @hapic.output_body(NoContentSchema())
171
+    @hapic.output_body(ContentDigestSchema())
163
     def move_content(
172
     def move_content(
164
             self,
173
             self,
165
             context,
174
             context,
172
         app_config = request.registry.settings['CFG']
181
         app_config = request.registry.settings['CFG']
173
         path_data = hapic_data.path
182
         path_data = hapic_data.path
174
         move_data = hapic_data.body
183
         move_data = hapic_data.body
184
+
175
         api = ContentApi(
185
         api = ContentApi(
176
             current_user=request.current_user,
186
             current_user=request.current_user,
177
             session=request.dbsession,
187
             session=request.dbsession,
184
         new_parent = api.get_one(
194
         new_parent = api.get_one(
185
             move_data.new_parent_id, content_type=ContentType.Any
195
             move_data.new_parent_id, content_type=ContentType.Any
186
         )
196
         )
197
+
198
+        new_workspace = request.candidate_workspace
199
+
187
         with new_revision(
200
         with new_revision(
188
                 session=request.dbsession,
201
                 session=request.dbsession,
189
                 tm=transaction.manager,
202
                 tm=transaction.manager,
190
                 content=content
203
                 content=content
191
         ):
204
         ):
192
-            api.move(content, new_parent=new_parent)
193
-        return
205
+            api.move(
206
+                content,
207
+                new_parent=new_parent,
208
+                new_workspace=new_workspace,
209
+                must_stay_in_same_workspace=False,
210
+            )
211
+        updated_content = api.get_one(
212
+            path_data.content_id,
213
+            content_type=ContentType.Any
214
+        )
215
+        return api.get_content_in_context(updated_content)
194
 
216
 
195
-    @hapic.with_api_doc()
217
+    @hapic.with_api_doc(tags=[WORKSPACE_ENDPOINTS_TAG])
196
     @hapic.handle_exception(NotAuthenticated, HTTPStatus.UNAUTHORIZED)
218
     @hapic.handle_exception(NotAuthenticated, HTTPStatus.UNAUTHORIZED)
197
     @hapic.handle_exception(InsufficientUserProfile, HTTPStatus.FORBIDDEN)
219
     @hapic.handle_exception(InsufficientUserProfile, HTTPStatus.FORBIDDEN)
198
     @hapic.handle_exception(WorkspaceNotFound, HTTPStatus.FORBIDDEN)
220
     @hapic.handle_exception(WorkspaceNotFound, HTTPStatus.FORBIDDEN)
199
     @require_workspace_role(UserRoleInWorkspace.CONTRIBUTOR)
221
     @require_workspace_role(UserRoleInWorkspace.CONTRIBUTOR)
200
     @hapic.input_path(WorkspaceAndContentIdPathSchema())
222
     @hapic.input_path(WorkspaceAndContentIdPathSchema())
201
-    @hapic.output_body(NoContentSchema())
223
+    @hapic.output_body(NoContentSchema(), default_http_code=HTTPStatus.NO_CONTENT)  # nopep8
202
     def delete_content(
224
     def delete_content(
203
             self,
225
             self,
204
             context,
226
             context,
227
             api.delete(content)
249
             api.delete(content)
228
         return
250
         return
229
 
251
 
230
-    @hapic.with_api_doc()
252
+    @hapic.with_api_doc(tags=[WORKSPACE_ENDPOINTS_TAG])
231
     @hapic.handle_exception(NotAuthenticated, HTTPStatus.UNAUTHORIZED)
253
     @hapic.handle_exception(NotAuthenticated, HTTPStatus.UNAUTHORIZED)
232
     @hapic.handle_exception(InsufficientUserProfile, HTTPStatus.FORBIDDEN)
254
     @hapic.handle_exception(InsufficientUserProfile, HTTPStatus.FORBIDDEN)
233
     @hapic.handle_exception(WorkspaceNotFound, HTTPStatus.FORBIDDEN)
255
     @hapic.handle_exception(WorkspaceNotFound, HTTPStatus.FORBIDDEN)
234
     @require_workspace_role(UserRoleInWorkspace.CONTRIBUTOR)
256
     @require_workspace_role(UserRoleInWorkspace.CONTRIBUTOR)
235
     @hapic.input_path(WorkspaceAndContentIdPathSchema())
257
     @hapic.input_path(WorkspaceAndContentIdPathSchema())
236
-    @hapic.output_body(NoContentSchema())
258
+    @hapic.output_body(NoContentSchema(), default_http_code=HTTPStatus.NO_CONTENT)  # nopep8
237
     def undelete_content(
259
     def undelete_content(
238
             self,
260
             self,
239
             context,
261
             context,
263
             api.undelete(content)
285
             api.undelete(content)
264
         return
286
         return
265
 
287
 
266
-    @hapic.with_api_doc()
288
+    @hapic.with_api_doc(tags=[WORKSPACE_ENDPOINTS_TAG])
267
     @hapic.handle_exception(NotAuthenticated, HTTPStatus.UNAUTHORIZED)
289
     @hapic.handle_exception(NotAuthenticated, HTTPStatus.UNAUTHORIZED)
268
     @hapic.handle_exception(InsufficientUserProfile, HTTPStatus.FORBIDDEN)
290
     @hapic.handle_exception(InsufficientUserProfile, HTTPStatus.FORBIDDEN)
269
     @hapic.handle_exception(WorkspaceNotFound, HTTPStatus.FORBIDDEN)
291
     @hapic.handle_exception(WorkspaceNotFound, HTTPStatus.FORBIDDEN)
270
     @require_workspace_role(UserRoleInWorkspace.CONTRIBUTOR)
292
     @require_workspace_role(UserRoleInWorkspace.CONTRIBUTOR)
271
     @hapic.input_path(WorkspaceAndContentIdPathSchema())
293
     @hapic.input_path(WorkspaceAndContentIdPathSchema())
272
-    @hapic.output_body(NoContentSchema())
294
+    @hapic.output_body(NoContentSchema(), default_http_code=HTTPStatus.NO_CONTENT)  # nopep8
273
     def archive_content(
295
     def archive_content(
274
             self,
296
             self,
275
             context,
297
             context,
295
             api.archive(content)
317
             api.archive(content)
296
         return
318
         return
297
 
319
 
298
-    @hapic.with_api_doc()
320
+    @hapic.with_api_doc(tags=[WORKSPACE_ENDPOINTS_TAG])
299
     @hapic.handle_exception(NotAuthenticated, HTTPStatus.UNAUTHORIZED)
321
     @hapic.handle_exception(NotAuthenticated, HTTPStatus.UNAUTHORIZED)
300
     @hapic.handle_exception(InsufficientUserProfile, HTTPStatus.FORBIDDEN)
322
     @hapic.handle_exception(InsufficientUserProfile, HTTPStatus.FORBIDDEN)
301
     @hapic.handle_exception(WorkspaceNotFound, HTTPStatus.FORBIDDEN)
323
     @hapic.handle_exception(WorkspaceNotFound, HTTPStatus.FORBIDDEN)
302
     @require_workspace_role(UserRoleInWorkspace.CONTRIBUTOR)
324
     @require_workspace_role(UserRoleInWorkspace.CONTRIBUTOR)
303
     @hapic.input_path(WorkspaceAndContentIdPathSchema())
325
     @hapic.input_path(WorkspaceAndContentIdPathSchema())
304
-    @hapic.output_body(NoContentSchema())
326
+    @hapic.output_body(NoContentSchema(), default_http_code=HTTPStatus.NO_CONTENT)  # nopep8
305
     def unarchive_content(
327
     def unarchive_content(
306
             self,
328
             self,
307
             context,
329
             context,