Browse Source

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

Damien Accorsi 6 years ago
parent
commit
ee1624dbe8
No account linked to committer's email

+ 8 - 0
tracim/exceptions.py View File

@@ -65,6 +65,10 @@ class WorkspaceNotFound(NotFound):
65 65
     pass
66 66
 
67 67
 
68
+class WorkspaceNotFoundInTracimRequest(NotFound):
69
+    pass
70
+
71
+
68 72
 class InsufficientUserWorkspaceRole(TracimException):
69 73
     pass
70 74
 
@@ -111,3 +115,7 @@ class UserDoesNotExist(TracimException):
111 115
 
112 116
 class UserNotFoundInTracimRequest(TracimException):
113 117
     pass
118
+
119
+
120
+class WorkspacesDoNotMatch(TracimException):
121
+    pass

+ 15 - 6
tracim/lib/core/content.py View File

@@ -25,6 +25,7 @@ from sqlalchemy.sql.elements import and_
25 25
 from tracim.lib.utils.utils import cmp_to_key
26 26
 from tracim.lib.core.notifications import NotifierFactory
27 27
 from tracim.exceptions import SameValueError
28
+from tracim.exceptions import WorkspacesDoNotMatch
28 29
 from tracim.lib.utils.utils import current_date_for_filename
29 30
 from tracim.models.revision_protection import new_revision
30 31
 from tracim.models.auth import User
@@ -862,19 +863,27 @@ class ContentApi(object):
862 863
         else:
863 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 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 872
         if must_stay_in_same_workspace:
870 873
             if new_parent and new_parent.workspace_id != item.workspace_id:
871 874
                 raise ValueError('the item should stay in the same workspace')
872 875
 
873 876
         item.parent = new_parent
874
-        if new_parent:
875
-            item.workspace = new_parent.workspace
876
-        elif new_workspace:
877
+        if new_workspace:
877 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 888
         item.revision_type = ActionDescription.MOVE
880 889
 

+ 26 - 5
tracim/lib/utils/authorization.py View File

@@ -8,9 +8,8 @@ try:
8 8
 except ImportError:  # python3.4
9 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 13
 if TYPE_CHECKING:
15 14
     from tracim import TracimRequest
16 15
 ###
@@ -84,8 +83,8 @@ def require_profile(group: int):
84 83
 
85 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 88
     :param minimal_required_role: value from UserInWorkspace Object like
90 89
     UserRoleInWorkspace.CONTRIBUTOR or UserRoleInWorkspace.READER
91 90
     :return: decorator
@@ -101,3 +100,25 @@ def require_workspace_role(minimal_required_role: int):
101 100
 
102 101
         return wrapper
103 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 View File

@@ -3,6 +3,7 @@ from pyramid.request import Request
3 3
 from sqlalchemy.orm.exc import NoResultFound
4 4
 
5 5
 from tracim.exceptions import NotAuthenticated
6
+from tracim.exceptions import WorkspaceNotFoundInTracimRequest
6 7
 from tracim.exceptions import UserNotFoundInTracimRequest
7 8
 from tracim.exceptions import UserDoesNotExist
8 9
 from tracim.exceptions import WorkspaceNotFound
@@ -34,9 +35,12 @@ class TracimRequest(Request):
34 35
             decode_param_names,
35 36
             **kw
36 37
         )
37
-        # Current workspace, found by request headers or content
38
+        # Current workspace, found in request path
38 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 44
         # Authenticated user
41 45
         self._current_user = None  # type: User
42 46
 
@@ -56,7 +60,7 @@ class TracimRequest(Request):
56 60
         :return: Workspace of the request
57 61
         """
58 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 64
         return self._current_workspace
61 65
 
62 66
     @current_workspace.setter
@@ -102,6 +106,19 @@ class TracimRequest(Request):
102 106
             self.candidate_user = self._get_candidate_user(self)
103 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 122
     def _cleanup(self, request: 'TracimRequest') -> None:
106 123
         """
107 124
         Close dbsession at the end of the request in order to avoid exception
@@ -171,7 +188,7 @@ class TracimRequest(Request):
171 188
             raise NotAuthenticated('User {} not found'.format(login)) from exc
172 189
         return user
173 190
 
174
-    def _get_workspace(
191
+    def _get_current_workspace(
175 192
             self,
176 193
             user: User,
177 194
             request: 'TracimRequest'
@@ -187,7 +204,39 @@ class TracimRequest(Request):
187 204
             if 'workspace_id' in request.matchdict:
188 205
                 workspace_id = request.matchdict['workspace_id']
189 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 240
             wapi = WorkspaceApi(
192 241
                 current_user=user,
193 242
                 session=request.dbsession,

+ 24 - 9
tracim/models/context_models.py View File

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

+ 10 - 5
tracim/tests/functional/test_session.py View File

@@ -1,4 +1,5 @@
1 1
 # coding=utf-8
2
+import datetime
2 3
 import pytest
3 4
 from sqlalchemy.exc import OperationalError
4 5
 
@@ -45,12 +46,16 @@ class TestLoginEndpoint(FunctionalTest):
45 46
             params=params,
46 47
             status=200,
47 48
         )
48
-        assert res.json_body['display_name'] == 'Global manager'
49
-        assert res.json_body['email'] == 'admin@admin.admin'
50 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 56
         assert res.json_body['is_active']
52 57
         assert res.json_body['profile']
53
-        assert res.json_body['profile']['slug'] == 'administrators'
58
+        assert res.json_body['profile'] == 'administrators'
54 59
         assert res.json_body['caldav_url'] is None
55 60
         assert res.json_body['avatar_url'] is None
56 61
 
@@ -103,12 +108,12 @@ class TestWhoamiEndpoint(FunctionalTest):
103 108
             )
104 109
         )
105 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 112
         assert res.json_body['email'] == 'admin@admin.admin'
108 113
         assert res.json_body['created']
109 114
         assert res.json_body['is_active']
110 115
         assert res.json_body['profile']
111
-        assert res.json_body['profile']['slug'] == 'administrators'
116
+        assert res.json_body['profile'] == 'administrators'
112 117
         assert res.json_body['caldav_url'] is None
113 118
         assert res.json_body['avatar_url'] is None
114 119
 

+ 2 - 1
tracim/tests/functional/test_user.py View File

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

+ 327 - 89
tracim/tests/functional/test_workspaces.py View File

@@ -27,7 +27,7 @@ class TestWorkspaceEndpoint(FunctionalTest):
27 27
         )
28 28
         res = self.testapp.get('/api/v2/workspaces/1', status=200)
29 29
         workspace = res.json_body
30
-        assert workspace['id'] == 1
30
+        assert workspace['workspace_id'] == 1
31 31
         assert workspace['slug'] == 'business'
32 32
         assert workspace['label'] == 'Business'
33 33
         assert workspace['description'] == 'All importants documents'
@@ -155,10 +155,10 @@ class TestWorkspaceMembersEndpoint(FunctionalTest):
155 155
         res = self.testapp.get('/api/v2/workspaces/1/members', status=200).json_body   # nopep8
156 156
         assert len(res) == 1
157 157
         user_role = res[0]
158
-        assert user_role['role_slug'] == 'workspace-manager'
158
+        assert user_role['role'] == 'workspace-manager'
159 159
         assert user_role['user_id'] == 1
160 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 162
         # TODO - G.M - 24-05-2018 - [Avatar] Replace
163 163
         # by correct value when avatar feature will be enabled
164 164
         assert user_role['user']['avatar_url'] is None
@@ -239,37 +239,37 @@ class TestWorkspaceContents(FunctionalTest):
239 239
         # TODO - G.M - 30-05-2018 - Check this test
240 240
         assert len(res) == 3
241 241
         content = res[0]
242
-        assert content['id'] == 1
242
+        assert content['content_id'] == 1
243 243
         assert content['is_archived'] is False
244 244
         assert content['is_deleted'] is False
245 245
         assert content['label'] == 'Tools'
246 246
         assert content['parent_id'] is None
247 247
         assert content['show_in_ui'] is True
248 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 251
         assert content['workspace_id'] == 1
252 252
         content = res[1]
253
-        assert content['id'] == 2
253
+        assert content['content_id'] == 2
254 254
         assert content['is_archived'] is False
255 255
         assert content['is_deleted'] is False
256 256
         assert content['label'] == 'Menus'
257 257
         assert content['parent_id'] is None
258 258
         assert content['show_in_ui'] is True
259 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 262
         assert content['workspace_id'] == 1
263 263
         content = res[2]
264
-        assert content['id'] == 11
264
+        assert content['content_id'] == 11
265 265
         assert content['is_archived'] is False
266 266
         assert content['is_deleted'] is False
267 267
         assert content['label'] == 'Current Menu'
268 268
         assert content['parent_id'] == 2
269 269
         assert content['show_in_ui'] is True
270 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 273
         assert content['workspace_id'] == 1
274 274
 
275 275
     # Root related
@@ -299,42 +299,42 @@ class TestWorkspaceContents(FunctionalTest):
299 299
         # TODO - G.M - 30-05-2018 - Check this test
300 300
         assert len(res) == 4
301 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 304
         assert content['is_archived'] is False
305 305
         assert content['is_deleted'] is False
306 306
         assert content['label'] == 'New Fruit Salad'
307 307
         assert content['parent_id'] is None
308 308
         assert content['show_in_ui'] is True
309 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 312
         assert content['workspace_id'] == 3
313 313
 
314 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 317
         assert content['is_archived'] is True
318 318
         assert content['is_deleted'] is False
319 319
         assert content['label'].startswith('Fruit Salad')
320 320
         assert content['parent_id'] is None
321 321
         assert content['show_in_ui'] is True
322 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 325
         assert content['workspace_id'] == 3
326 326
 
327 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 330
         assert content['is_archived'] is False
331 331
         assert content['is_deleted'] is True
332 332
         assert content['label'].startswith('Bad Fruit Salad')
333 333
         assert content['parent_id'] is None
334 334
         assert content['show_in_ui'] is True
335 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 338
         assert content['workspace_id'] == 3
339 339
 
340 340
     def test_api__get_workspace_content__ok_200__get_only_active_root_content(self):  # nopep8
@@ -362,16 +362,16 @@ class TestWorkspaceContents(FunctionalTest):
362 362
         # TODO - G.M - 30-05-2018 - Check this test
363 363
         assert len(res) == 2
364 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 367
         assert content['is_archived'] is False
368 368
         assert content['is_deleted'] is False
369 369
         assert content['label'] == 'New Fruit Salad'
370 370
         assert content['parent_id'] is None
371 371
         assert content['show_in_ui'] is True
372 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 375
         assert content['workspace_id'] == 3
376 376
 
377 377
     def test_api__get_workspace_content__ok_200__get_only_archived_root_content(self):  # nopep8
@@ -398,16 +398,16 @@ class TestWorkspaceContents(FunctionalTest):
398 398
         ).json_body   # nopep8
399 399
         assert len(res) == 1
400 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 403
         assert content['is_archived'] is True
404 404
         assert content['is_deleted'] is False
405 405
         assert content['label'].startswith('Fruit Salad')
406 406
         assert content['parent_id'] is None
407 407
         assert content['show_in_ui'] is True
408 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 411
         assert content['workspace_id'] == 3
412 412
 
413 413
     def test_api__get_workspace_content__ok_200__get_only_deleted_root_content(self):  # nopep8
@@ -436,16 +436,16 @@ class TestWorkspaceContents(FunctionalTest):
436 436
 
437 437
         assert len(res) == 1
438 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 441
         assert content['is_archived'] is False
442 442
         assert content['is_deleted'] is True
443 443
         assert content['label'].startswith('Bad Fruit Salad')
444 444
         assert content['parent_id'] is None
445 445
         assert content['show_in_ui'] is True
446 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 449
         assert content['workspace_id'] == 3
450 450
 
451 451
     def test_api__get_workspace_content__ok_200__get_nothing_root_content(self):
@@ -500,42 +500,42 @@ class TestWorkspaceContents(FunctionalTest):
500 500
         ).json_body   # nopep8
501 501
         assert len(res) == 3
502 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 505
         assert content['is_archived'] is False
506 506
         assert content['is_deleted'] is False
507 507
         assert content['label'] == 'New Fruit Salad'
508 508
         assert content['parent_id'] == 10
509 509
         assert content['show_in_ui'] is True
510 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 513
         assert content['workspace_id'] == 2
514 514
 
515 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 518
         assert content['is_archived'] is True
519 519
         assert content['is_deleted'] is False
520 520
         assert content['label'].startswith('Fruit Salad')
521 521
         assert content['parent_id'] == 10
522 522
         assert content['show_in_ui'] is True
523 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 526
         assert content['workspace_id'] == 2
527 527
 
528 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 531
         assert content['is_archived'] is False
532 532
         assert content['is_deleted'] is True
533 533
         assert content['label'].startswith('Bad Fruit Salad')
534 534
         assert content['parent_id'] == 10
535 535
         assert content['show_in_ui'] is True
536 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 539
         assert content['workspace_id'] == 2
540 540
 
541 541
     def test_api__get_workspace_content__ok_200__get_only_active_folder_content(self):  # nopep8
@@ -562,16 +562,16 @@ class TestWorkspaceContents(FunctionalTest):
562 562
         ).json_body   # nopep8
563 563
         assert len(res) == 1
564 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 567
         assert content['is_archived'] is False
568 568
         assert content['is_deleted'] is False
569 569
         assert content['label'] == 'New Fruit Salad'
570 570
         assert content['parent_id'] == 10
571 571
         assert content['show_in_ui'] is True
572 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 575
         assert content['workspace_id'] == 2
576 576
 
577 577
     def test_api__get_workspace_content__ok_200__get_only_archived_folder_content(self):  # nopep8
@@ -598,16 +598,16 @@ class TestWorkspaceContents(FunctionalTest):
598 598
         ).json_body   # nopep8
599 599
         assert len(res) == 1
600 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 603
         assert content['is_archived'] is True
604 604
         assert content['is_deleted'] is False
605 605
         assert content['label'].startswith('Fruit Salad')
606 606
         assert content['parent_id'] == 10
607 607
         assert content['show_in_ui'] is True
608 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 611
         assert content['workspace_id'] == 2
612 612
 
613 613
     def test_api__get_workspace_content__ok_200__get_only_deleted_folder_content(self):  # nopep8
@@ -635,16 +635,16 @@ class TestWorkspaceContents(FunctionalTest):
635 635
 
636 636
         assert len(res) == 1
637 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 640
         assert content['is_archived'] is False
641 641
         assert content['is_deleted'] is True
642 642
         assert content['label'].startswith('Bad Fruit Salad')
643 643
         assert content['parent_id'] == 10
644 644
         assert content['show_in_ui'] is True
645 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 648
         assert content['workspace_id'] == 2
649 649
 
650 650
     def test_api__get_workspace_content__ok_200__get_nothing_folder_content(self):  # nopep8
@@ -741,7 +741,7 @@ class TestWorkspaceContents(FunctionalTest):
741 741
         )
742 742
         params = {
743 743
             'label': 'GenericCreatedContent',
744
-            'content_type_slug': 'markdownpage',
744
+            'content_type': 'markdownpage',
745 745
         }
746 746
         res = self.testapp.post_json(
747 747
             '/api/v2/workspaces/1/contents',
@@ -750,16 +750,16 @@ class TestWorkspaceContents(FunctionalTest):
750 750
         )
751 751
         assert res
752 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 756
         assert res.json_body['is_archived'] is False
757 757
         assert res.json_body['is_deleted'] is False
758 758
         assert res.json_body['workspace_id'] == 1
759 759
         assert res.json_body['slug'] == 'genericcreatedcontent'
760 760
         assert res.json_body['parent_id'] is None
761 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 763
         params_active = {
764 764
             'parent_id': 0,
765 765
             'show_archived': 0,
@@ -786,6 +786,7 @@ class TestWorkspaceContents(FunctionalTest):
786 786
         )
787 787
         params = {
788 788
             'new_parent_id': '4',  # Salads
789
+            'new_workspace_id': '2',
789 790
         }
790 791
         params_folder1 = {
791 792
             'parent_id': 3,
@@ -801,8 +802,8 @@ class TestWorkspaceContents(FunctionalTest):
801 802
         }
802 803
         folder1_contents = self.testapp.get('/api/v2/workspaces/2/contents', params=params_folder1, status=200).json_body  # nopep8
803 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 807
         # TODO - G.M - 2018-06-163 - Check content
807 808
         res = self.testapp.put_json(
808 809
             '/api/v2/workspaces/2/contents/8/move',
@@ -811,8 +812,245 @@ class TestWorkspaceContents(FunctionalTest):
811 812
         )
812 813
         new_folder1_contents = self.testapp.get('/api/v2/workspaces/2/contents', params=params_folder1, status=200).json_body  # nopep8
813 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 1055
     def test_api_put_delete_content__ok_200__nominal_case(self):
818 1056
         """
@@ -840,18 +1078,18 @@ class TestWorkspaceContents(FunctionalTest):
840 1078
         }
841 1079
         active_contents = self.testapp.get('/api/v2/workspaces/2/contents', params=params_active, status=200).json_body  # nopep8
842 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 1083
         # TODO - G.M - 2018-06-163 - Check content
846 1084
         res = self.testapp.put_json(
847 1085
             # INFO - G.M - 2018-06-163 - delete Apple_Pie
848 1086
             '/api/v2/workspaces/2/contents/8/delete',
849
-            status=200
1087
+            status=204
850 1088
         )
851 1089
         new_active_contents = self.testapp.get('/api/v2/workspaces/2/contents', params=params_active, status=200).json_body  # nopep8
852 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 1094
     def test_api_put_archive_content__ok_200__nominal_case(self):
857 1095
         """
@@ -879,16 +1117,16 @@ class TestWorkspaceContents(FunctionalTest):
879 1117
         }
880 1118
         active_contents = self.testapp.get('/api/v2/workspaces/2/contents', params=params_active, status=200).json_body  # nopep8
881 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 1122
         res = self.testapp.put_json(
885 1123
             '/api/v2/workspaces/2/contents/8/archive',
886
-            status=200
1124
+            status=204
887 1125
         )
888 1126
         new_active_contents = self.testapp.get('/api/v2/workspaces/2/contents', params=params_active, status=200).json_body  # nopep8
889 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 1131
     def test_api_put_undelete_content__ok_200__nominal_case(self):
894 1132
         """
@@ -916,16 +1154,16 @@ class TestWorkspaceContents(FunctionalTest):
916 1154
         }
917 1155
         active_contents = self.testapp.get('/api/v2/workspaces/2/contents', params=params_active, status=200).json_body  # nopep8
918 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 1159
         res = self.testapp.put_json(
922 1160
             '/api/v2/workspaces/2/contents/14/undelete',
923
-            status=200
1161
+            status=204
924 1162
         )
925 1163
         new_active_contents = self.testapp.get('/api/v2/workspaces/2/contents', params=params_active, status=200).json_body  # nopep8
926 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 1168
     def test_api_put_unarchive_content__ok_200__nominal_case(self):
931 1169
         """
@@ -953,13 +1191,13 @@ class TestWorkspaceContents(FunctionalTest):
953 1191
         }
954 1192
         active_contents = self.testapp.get('/api/v2/workspaces/2/contents', params=params_active, status=200).json_body  # nopep8
955 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 1196
         res = self.testapp.put_json(
959 1197
             '/api/v2/workspaces/2/contents/13/unarchive',
960
-            status=200
1198
+            status=204
961 1199
         )
962 1200
         new_active_contents = self.testapp.get('/api/v2/workspaces/2/contents', params=params_active, status=200).json_body  # nopep8
963 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 View File

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

+ 27 - 30
tracim/views/core_api/schemas.py View File

@@ -16,17 +16,6 @@ from tracim.models.context_models import LoginCredentials
16 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 19
 class UserSchema(marshmallow.Schema):
31 20
 
32 21
     user_id = marshmallow.fields.Int(dump_only=True, example=3)
@@ -34,12 +23,12 @@ class UserSchema(marshmallow.Schema):
34 23
         required=True,
35 24
         example='suri.cate@algoo.fr'
36 25
     )
37
-    display_name = marshmallow.fields.String(
26
+    public_name = marshmallow.fields.String(
38 27
         example='Suri Cate',
39 28
     )
40 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 33
     is_active = marshmallow.fields.Bool(
45 34
         example=True,
@@ -64,9 +53,10 @@ class UserSchema(marshmallow.Schema):
64 53
                     "If no avatar, then set it to null "
65 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 62
     class Meta:
@@ -95,9 +85,9 @@ class WorkspaceAndContentIdPathSchema(WorkspaceIdPathSchema, ContentIdPathSchema
95 85
 
96 86
 
97 87
 class FilterContentQuerySchema(marshmallow.Schema):
98
-    parent_id = workspace_id = marshmallow.fields.Int(
88
+    parent_id = marshmallow.fields.Int(
99 89
         example=2,
100
-        default=None,
90
+        default=0,
101 91
         description='allow to filter items in a folder.'
102 92
                     ' If not set, then return all contents.'
103 93
                     ' If set to 0, then return root contents.'
@@ -187,7 +177,8 @@ class WorkspaceMenuEntrySchema(marshmallow.Schema):
187 177
 
188 178
 
189 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 182
     label = marshmallow.fields.String(example='Intranet')
192 183
     sidebar_entries = marshmallow.fields.Nested(
193 184
         WorkspaceMenuEntrySchema,
@@ -199,7 +190,6 @@ class WorkspaceDigestSchema(marshmallow.Schema):
199 190
 
200 191
 
201 192
 class WorkspaceSchema(WorkspaceDigestSchema):
202
-    slug = marshmallow.fields.String(example='intranet')
203 193
     description = marshmallow.fields.String(example='All intranet data.')
204 194
 
205 195
     class Meta:
@@ -207,14 +197,14 @@ class WorkspaceSchema(WorkspaceDigestSchema):
207 197
 
208 198
 
209 199
 class WorkspaceMemberSchema(marshmallow.Schema):
210
-    role_slug = marshmallow.fields.String(
200
+    role = marshmallow.fields.String(
211 201
         example='contributor',
212 202
         validate=OneOf(UserRoleInWorkspace.get_all_role_slug())
213 203
     )
214 204
     user_id = marshmallow.fields.Int(example=3)
215 205
     workspace_id = marshmallow.fields.Int(example=4)
216 206
     user = marshmallow.fields.Nested(
217
-        UserSchema(only=('display_name', 'avatar_url'))
207
+        UserSchema(only=('public_name', 'avatar_url'))
218 208
     )
219 209
 
220 210
     class Meta:
@@ -256,7 +246,7 @@ class StatusSchema(marshmallow.Schema):
256 246
                     'Statuses are open, closed-validated, closed-invalidated, closed-deprecated'  # nopep8
257 247
     )
258 248
     global_status = marshmallow.fields.String(
259
-        example='Open',
249
+        example='open',
260 250
         description='global_status: open, closed',
261 251
         validate=OneOf([status.value for status in GlobalStatus]),
262 252
     )
@@ -298,7 +288,14 @@ class ContentMoveSchema(marshmallow.Schema):
298 288
     # (the user must be content manager of both workspaces)
299 289
     new_parent_id = marshmallow.fields.Int(
300 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 301
     @post_load
@@ -311,7 +308,7 @@ class ContentCreationSchema(marshmallow.Schema):
311 308
         example='contract for client XXX',
312 309
         description='Title of the content to create'
313 310
     )
314
-    content_type_slug = marshmallow.fields.String(
311
+    content_type = marshmallow.fields.String(
315 312
         example='htmlpage',
316 313
         validate=OneOf([content.slug for content in CONTENT_DEFAULT_TYPE]),
317 314
     )
@@ -322,7 +319,7 @@ class ContentCreationSchema(marshmallow.Schema):
322 319
 
323 320
 
324 321
 class ContentDigestSchema(marshmallow.Schema):
325
-    id = marshmallow.fields.Int(example=6)
322
+    content_id = marshmallow.fields.Int(example=6)
326 323
     slug = marshmallow.fields.Str(example='intervention-report-12')
327 324
     parent_id = marshmallow.fields.Int(
328 325
         example=34,
@@ -333,17 +330,17 @@ class ContentDigestSchema(marshmallow.Schema):
333 330
         example=19,
334 331
     )
335 332
     label = marshmallow.fields.Str(example='Intervention Report 12')
336
-    content_type_slug = marshmallow.fields.Str(
333
+    content_type = marshmallow.fields.Str(
337 334
         example='htmlpage',
338 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 338
         marshmallow.fields.Str,
342 339
         description='list of content types allowed as sub contents. '
343 340
                     'This field is required for folder contents, '
344 341
                     'set it to empty list in other cases'
345 342
     )
346
-    status_slug = marshmallow.fields.Str(
343
+    status = marshmallow.fields.Str(
347 344
         example='closed-deprecated',
348 345
         validate=OneOf([status.slug for status in CONTENT_DEFAULT_STATUS]),
349 346
         description='this slug is found in content_type available statuses',

+ 5 - 3
tracim/views/core_api/session_controller.py View File

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

+ 3 - 2
tracim/views/core_api/system_controller.py View File

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

+ 3 - 1
tracim/views/core_api/user_controller.py View File

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

+ 44 - 22
tracim/views/core_api/workspace_controller.py View File

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