Browse Source

Better support for move endpoint : allow to move content from one workspace to another

Guénaël Muller 6 years ago
parent
commit
7037152048

+ 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 NotSameWorkspace(TracimException):
121
+    pass

+ 13 - 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 NotSameWorkspace
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,25 @@ 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.workspace_id != new_workspace.workspace_id:
880
+                raise NotSameWorkspace(
881
+                    'new parent workspace and new workspace should be the same.'
882
+                )
883
+        else:
884
+            item.workspace = new_parent.workspace
878 885
 
879 886
         item.revision_type = ActionDescription.MOVE
880 887
 

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

@@ -9,7 +9,7 @@ except ImportError:  # python3.4
9 9
     JSONDecodeError = ValueError
10 10
 
11 11
 from tracim.exceptions import InsufficientUserWorkspaceRole, \
12
-    InsufficientUserProfile
12
+    InsufficientUserProfile, WorkspaceNotFoundInTracimRequest
13 13
 
14 14
 if TYPE_CHECKING:
15 15
     from tracim import TracimRequest
@@ -101,3 +101,28 @@ def require_workspace_role(minimal_required_role: int):
101 101
 
102 102
         return wrapper
103 103
     return decorator
104
+
105
+
106
+def require_candidate_workspace_role(minimal_required_role: int):
107
+    """
108
+    Decorator for view to restrict access of tracim request if role
109
+    is not high enough. Do nothing is candidate_workspace_role is not found.
110
+    :param minimal_required_role: value from UserInWorkspace Object like
111
+    UserRoleInWorkspace.CONTRIBUTOR or UserRoleInWorkspace.READER
112
+    :return: decorator
113
+    """
114
+    def decorator(func):
115
+
116
+        def wrapper(self, context, request: 'TracimRequest'):
117
+            user = request.current_user
118
+            try:
119
+                workspace = request.candidate_workspace
120
+            except WorkspaceNotFoundInTracimRequest:
121
+                return func(self, context, request)
122
+
123
+            if workspace.get_user_role(user) >= minimal_required_role:
124
+                return func(self, context, request)
125
+            raise InsufficientUserWorkspaceRole()
126
+
127
+        return wrapper
128
+    return decorator

+ 55 - 5
tracim/lib/utils/request.py View File

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

+ 341 - 0
tracim/lib/utils/ttest View File

@@ -0,0 +1,341 @@
1
+
2
+definitions:
3
+  CommentSchema:
4
+    properties:
5
+      content_id:
6
+        example: 6
7
+        format: int32
8
+        type: integer
9
+      parent_id:
10
+        example: 34
11
+        format: int32
12
+        type: integer
13
+        x-nullable: true
14
+      content:
15
+        type: string
16
+        example: "Coucou !"
17
+      author:
18
+        $ref: '#/definitions/UserDigestSchema'
19
+  UserDigestSchema:
20
+    properties:
21
+      user_id:
22
+        example: 3
23
+        format: int32
24
+        readOnly: true
25
+        type: integer
26
+      avatar_url:
27
+        description: avatar_url is the url to the image file. If no avatar, then set
28
+          it to null (and frontend will interpret this with a default avatar)
29
+        example: /api/v2/assets/avatars/suri-cate.jpg
30
+        format: url
31
+        type: string
32
+        x-nullable: true
33
+      public_name:
34
+        example: Suri Cate
35
+        type: string
36
+  HtmlPageContentSchema:
37
+    properties:
38
+      content_type:
39
+        enum:
40
+        - thread
41
+        - file
42
+        - markdownpage
43
+        - page
44
+        - folder
45
+        example: htmlpage
46
+        type: string
47
+      content_id:
48
+        example: 6
49
+        format: int32
50
+        type: integer
51
+      is_archived:
52
+        example: false
53
+        type: boolean
54
+      is_deleted:
55
+        example: false
56
+        type: boolean
57
+      label:
58
+        example: Intervention Report 12
59
+        type: string
60
+      parent_id:
61
+        example: 34
62
+        format: int32
63
+        type: integer
64
+        x-nullable: true
65
+      show_in_ui:
66
+        description: if false, then do not show content in the treeview. This may
67
+          his maybe used for specific contents or for sub-contents. Default is True.
68
+          In first version of the API, this field is always True
69
+        example: true
70
+        type: boolean
71
+      slug:
72
+        example: intervention-report-12
73
+        type: string
74
+      status_slug:
75
+        description: this slug is found in content_type available statuses
76
+        enum:
77
+        - open
78
+        - closed-validated
79
+        - closed-unvalidated
80
+        - closed-deprecated
81
+        example: closed-deprecated
82
+        type: string
83
+      sub_content_types:
84
+        description: list of content types allowed as sub contents. This field is
85
+          required for folder contents, set it to empty list in other cases
86
+        items:
87
+          type: string
88
+        type: array
89
+      workspace_id:
90
+        example: 19
91
+        format: int32
92
+        type: integer
93
+      current_revision_id:
94
+        type: integer
95
+        example: 74
96
+      created:
97
+        format: date-time
98
+        type: string
99
+      author:
100
+        $ref: '#/definitions/UserDigestSchema'
101
+      modified:
102
+        format: date-time
103
+        type: string
104
+      last_modifier:
105
+        $ref: '#/definitions/UserDigestSchema'
106
+      content:
107
+        example: '<p> Coucou </p>'
108
+        type: string
109
+    type: object
110
+  HtmlPageRevisionSchema:
111
+    properties:
112
+      content_type:
113
+        enum:
114
+        - thread
115
+        - file
116
+        - markdownpage
117
+        - page
118
+        - folder
119
+        example: htmlpage
120
+        type: string
121
+      content_id:
122
+        example: 6
123
+        format: int32
124
+        type: integer
125
+      is_archived:
126
+        example: false
127
+        type: boolean
128
+      is_deleted:
129
+        example: false
130
+        type: boolean
131
+      label:
132
+        example: Intervention Report 12
133
+        type: string
134
+      parent_id:
135
+        example: 34
136
+        format: int32
137
+        type: integer
138
+        x-nullable: true
139
+      show_in_ui:
140
+        description: if false, then do not show content in the treeview. This may
141
+          his maybe used for specific contents or for sub-contents. Default is True.
142
+          In first version of the API, this field is always True
143
+        example: true
144
+        type: boolean
145
+      slug:
146
+        example: intervention-report-12
147
+        type: string
148
+      status_slug:
149
+        description: this slug is found in content_type available statuses
150
+        enum:
151
+        - open
152
+        - closed-validated
153
+        - closed-unvalidated
154
+        - closed-deprecated
155
+        example: closed-deprecated
156
+        type: string
157
+      sub_content_types:
158
+        description: list of content types allowed as sub contents. This field is
159
+          required for folder contents, set it to empty list in other cases
160
+        items:
161
+          type: string
162
+        type: array
163
+      workspace_id:
164
+        example: 19
165
+        format: int32
166
+        type: integer
167
+      revision_id:
168
+        type: integer
169
+        example: 74
170
+      created:
171
+        format: date-time
172
+        type: string
173
+      author:
174
+        $ref: '#/definitions/UserDigestSchema'
175
+      content:
176
+        example: '<p> Coucou </p>'
177
+        type: string
178
+    type: object
179
+  HtmlPageRevisionListSchema:
180
+    properties:
181
+      revisions:
182
+        type: array
183
+        items:
184
+          $ref: '#/definitions/HtmlPageRevisionSchema'
185
+      revision_nb:
186
+        type: integer
187
+        example: 40
188
+  HtmlPageModifySchema:
189
+    type: object
190
+    properties:
191
+      label:
192
+        example: "My Page"
193
+        type: string
194
+      content:
195
+        example: '<p> Coucou </p>'
196
+        type: string
197
+  ContentSetStatusSchema:
198
+    type: object
199
+    properties:
200
+      status:
201
+        example: "open-workinprogress"
202
+        type: string
203
+  NoContentSchema:
204
+    type: object
205
+info:
206
+  description: API of Tracim v2
207
+  title: Tracim v2 API
208
+  version: 1.0.0
209
+parameters: {}
210
+paths:
211
+  "/api/v2/workspaces/{workspace_id}/htmlpages/{htmlpage_id}":
212
+    get:
213
+      description: "get htmlpage content"
214
+      parameters:
215
+        - name: "workspace_id"
216
+          in: path
217
+          required: true
218
+          type: integer
219
+          description: id of the current workspace.
220
+        - name: "htmlpage_id"
221
+          in: path
222
+          required: true
223
+          type: integer
224
+          description: content id of htmlpage.
225
+      responses:
226
+        '200':
227
+          description: "nominal case"
228
+          schema:
229
+              $ref: '#/definitions/HtmlPageContentSchema'
230
+    put:
231
+      description: "modify htmlpage label or/and content"
232
+      parameters:
233
+        - in: body
234
+          name: "body"
235
+          schema:
236
+            $ref: '#/definitions/HtmlPageModifySchema'
237
+        - name: "workspace_id"
238
+          in: path
239
+          required: true
240
+          type: integer
241
+          description: id of the current workspace.
242
+        - name: "htmlpage_id"
243
+          in: path
244
+          required: true
245
+          type: integer
246
+          description: content id of htmlpage.
247
+      responses:
248
+        '200':
249
+          description: "nominal case"
250
+          schema:
251
+            $ref: '#/definitions/HtmlPageContentSchema'
252
+  "/api/v2/workspaces/{workspace_id}/htmlpages/{htmlpage_id}/revisions":
253
+    get:
254
+      description: "gets all htmlpages revisions (sorted by"
255
+      parameters:
256
+        - name: "workspace_id"
257
+          in: path
258
+          required: true
259
+          type: integer
260
+          description: id of the current workspace.
261
+        - name: "htmlpage_id"
262
+          in: path
263
+          required: true
264
+          type: integer
265
+          description: content id of htmlpage.
266
+      responses:
267
+        '200':
268
+          description: "nominal case"
269
+          schema:
270
+            $ref: '#/definitions/HtmlPageRevisionListSchema'
271
+  "/api/v2/workspaces/{workspace_id}/htmlpages/{htmlpage_id}/status":
272
+    put:
273
+      description: "set htmlpage content status"
274
+      parameters:
275
+        - in: body
276
+          name: "body"
277
+          schema:
278
+            $ref: '#/definitions/ContentSetStatusSchema'
279
+        - name: "workspace_id"
280
+          in: path
281
+          required: true
282
+          type: integer
283
+          description: id of the current workspace.
284
+        - name: "htmlpage_id"
285
+          in: path
286
+          required: true
287
+          type: integer
288
+          description: content id of htmlpage.
289
+      responses:
290
+        '200':
291
+          description: "nominal case"
292
+          schema:
293
+            $ref: '#/definitions/NoContentSchema'
294
+  "/api/v2/workspaces/{workspace_id}/contents/{content_id}/comments":
295
+    get:
296
+      description: "get all comments related to a content"
297
+      parameters:
298
+        - name: "workspace_id"
299
+          in: path
300
+          required: true
301
+          type: integer
302
+          description: id of the current workspace.
303
+        - name: "content_id"
304
+          in: path
305
+          required: true
306
+          type: integer
307
+          description: content id.
308
+      responses:
309
+        '200':
310
+          description: "nominal case"
311
+          schema:
312
+            type: array
313
+            items:
314
+              $ref: '#/definitions/CommentSchema'
315
+
316
+  "/api/v2/workspaces/{workspace_id}/contents/{content_id}/comments/{comments_id}":
317
+    delete:
318
+      description: "delete one comment"
319
+      parameters:
320
+        - name: "workspace_id"
321
+          in: path
322
+          required: true
323
+          type: integer
324
+          description: id of the current workspace.
325
+        - name: "content_id"
326
+          in: path
327
+          required: true
328
+          type: integer
329
+          description: content id.
330
+        - name: "comments_id"
331
+          in: path
332
+          required: true
333
+          type: integer
334
+          description: id of a comment related to content content_id.
335
+      responses:
336
+        '204':
337
+          description: "nominal case"
338
+          schema:
339
+            $ref: '#/definitions/NoContentSchema'
340
+swagger: '2.0'
341
+tags: []

+ 2 - 1
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):

+ 126 - 0
tracim/tests/functional/test_workspaces.py View File

@@ -814,6 +814,132 @@ class TestWorkspaceContents(FunctionalTest):
814 814
         assert not [content for content in new_folder1_contents if content['id'] == 8]  # nopep8
815 815
         assert [content for content in new_folder2_contents if content['id'] == 8]  # nopep8
816 816
 
817
+    def test_api_put_move_content__ok_200__with_workspace_id(self):
818
+        """
819
+        Move content
820
+        move Apple_Pie (content_id: 8)
821
+        from Desserts folder(content_id: 3) to Salads subfolder (content_id: 4)
822
+        of workspace Recipes.
823
+        """
824
+        self.testapp.authorization = (
825
+            'Basic',
826
+            (
827
+                'admin@admin.admin',
828
+                'admin@admin.admin'
829
+            )
830
+        )
831
+        params = {
832
+            'new_parent_id': '4',  # Salads
833
+            'new_workspace_id': '2',
834
+        }
835
+        params_folder1 = {
836
+            'parent_id': 3,
837
+            'show_archived': 0,
838
+            'show_deleted': 0,
839
+            'show_active': 1,
840
+        }
841
+        params_folder2 = {
842
+            'parent_id': 4,
843
+            'show_archived': 0,
844
+            'show_deleted': 0,
845
+            'show_active': 1,
846
+        }
847
+        folder1_contents = self.testapp.get('/api/v2/workspaces/2/contents', params=params_folder1, status=200).json_body  # nopep8
848
+        folder2_contents = self.testapp.get('/api/v2/workspaces/2/contents', params=params_folder2, status=200).json_body  # nopep8
849
+        assert [content for content in folder1_contents if content['id'] == 8]  # nopep8
850
+        assert not [content for content in folder2_contents if content['id'] == 8]  # nopep8
851
+        # TODO - G.M - 2018-06-163 - Check content
852
+        res = self.testapp.put_json(
853
+            '/api/v2/workspaces/2/contents/8/move',
854
+            params=params,
855
+            status=200
856
+        )
857
+        new_folder1_contents = self.testapp.get('/api/v2/workspaces/2/contents', params=params_folder1, status=200).json_body  # nopep8
858
+        new_folder2_contents = self.testapp.get('/api/v2/workspaces/2/contents', params=params_folder2, status=200).json_body  # nopep8
859
+        assert not [content for content in new_folder1_contents if content['id'] == 8]  # nopep8
860
+        assert [content for content in new_folder2_contents if content['id'] == 8]  # nopep8
861
+
862
+    def test_api_put_move_content__ok_200__to_another_workspace(self):
863
+        """
864
+        Move content
865
+        move Apple_Pie (content_id: 8)
866
+        from Desserts folder(content_id: 3) to Menus subfolder (content_id: 2)
867
+        of workspace Business.
868
+        """
869
+        self.testapp.authorization = (
870
+            'Basic',
871
+            (
872
+                'admin@admin.admin',
873
+                'admin@admin.admin'
874
+            )
875
+        )
876
+        params = {
877
+            'new_parent_id': '2',  # Menus
878
+        }
879
+        params_folder1 = {
880
+            'parent_id': 3,
881
+            'show_archived': 0,
882
+            'show_deleted': 0,
883
+            'show_active': 1,
884
+        }
885
+        params_folder2 = {
886
+            'parent_id': 2,
887
+            'show_archived': 0,
888
+            'show_deleted': 0,
889
+            'show_active': 1,
890
+        }
891
+        folder1_contents = self.testapp.get('/api/v2/workspaces/2/contents', params=params_folder1, status=200).json_body  # nopep8
892
+        folder2_contents = self.testapp.get('/api/v2/workspaces/1/contents', params=params_folder2, status=200).json_body  # nopep8
893
+        assert [content for content in folder1_contents if content['id'] == 8]  # nopep8
894
+        assert not [content for content in folder2_contents if content['id'] == 8]  # nopep8
895
+        # TODO - G.M - 2018-06-163 - Check content
896
+        res = self.testapp.put_json(
897
+            '/api/v2/workspaces/2/contents/8/move',
898
+            params=params,
899
+            status=200
900
+        )
901
+        new_folder1_contents = self.testapp.get('/api/v2/workspaces/2/contents', params=params_folder1, status=200).json_body  # nopep8
902
+        new_folder2_contents = self.testapp.get('/api/v2/workspaces/1/contents', params=params_folder2, status=200).json_body  # nopep8
903
+        assert not [content for content in new_folder1_contents if content['id'] == 8]  # nopep8
904
+        assert [content for content in new_folder2_contents if content['id'] == 8]  # nopep8
905
+
906
+    def test_api_put_move_content__err_400__wrong_workspace_id(self):
907
+        """
908
+        Move content
909
+        move Apple_Pie (content_id: 8)
910
+        from Desserts folder(content_id: 3) to Salads subfolder (content_id: 4)
911
+        of workspace Recipes.
912
+        Workspace_id of parent_id don't match with workspace_id of workspace
913
+        """
914
+        self.testapp.authorization = (
915
+            'Basic',
916
+            (
917
+                'admin@admin.admin',
918
+                'admin@admin.admin'
919
+            )
920
+        )
921
+        params = {
922
+            'new_parent_id': '4',  # Salads
923
+            'new_workspace_id': '1',
924
+        }
925
+        params_folder1 = {
926
+            'parent_id': 3,
927
+            'show_archived': 0,
928
+            'show_deleted': 0,
929
+            'show_active': 1,
930
+        }
931
+        params_folder2 = {
932
+            'parent_id': 4,
933
+            'show_archived': 0,
934
+            'show_deleted': 0,
935
+            'show_active': 1,
936
+        }
937
+        res = self.testapp.put_json(
938
+            '/api/v2/workspaces/2/contents/8/move',
939
+            params=params,
940
+            status=400,
941
+        )
942
+
817 943
     def test_api_put_delete_content__ok_200__nominal_case(self):
818 944
         """
819 945
         delete content

+ 6 - 0
tracim/views/core_api/schemas.py View File

@@ -300,6 +300,12 @@ class ContentMoveSchema(marshmallow.Schema):
300 300
         example=42,
301 301
         description='id of the new parent content id.'
302 302
     )
303
+    new_workspace_id = marshmallow.fields.Int(
304
+        example=2,
305
+        description='id of the new workspace id.',
306
+        allow_none=True,
307
+        default=None,
308
+    )
303 309
 
304 310
     @post_load
305 311
     def make_move_params(self, data):

+ 25 - 6
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 NotSameWorkspace
19 23
 from tracim.exceptions import InsufficientUserProfile
20 24
 from tracim.exceptions import WorkspaceNotFound
21 25
 from tracim.views.controllers import Controller
@@ -156,7 +160,10 @@ class WorkspaceController(Controller):
156 160
     @hapic.handle_exception(NotAuthenticated, HTTPStatus.UNAUTHORIZED)
157 161
     @hapic.handle_exception(InsufficientUserProfile, HTTPStatus.FORBIDDEN)
158 162
     @hapic.handle_exception(WorkspaceNotFound, HTTPStatus.FORBIDDEN)
163
+    @hapic.handle_exception(InsufficientUserWorkspaceRole, HTTPStatus.FORBIDDEN)
164
+    @hapic.handle_exception(NotSameWorkspace, HTTPStatus.BAD_REQUEST)
159 165
     @require_workspace_role(UserRoleInWorkspace.CONTRIBUTOR)
166
+    @require_candidate_workspace_role(UserRoleInWorkspace.CONTRIBUTOR)
160 167
     @hapic.input_path(WorkspaceAndContentIdPathSchema())
161 168
     @hapic.input_body(ContentMoveSchema())
162 169
     @hapic.output_body(NoContentSchema())
@@ -172,6 +179,7 @@ class WorkspaceController(Controller):
172 179
         app_config = request.registry.settings['CFG']
173 180
         path_data = hapic_data.path
174 181
         move_data = hapic_data.body
182
+
175 183
         api = ContentApi(
176 184
             current_user=request.current_user,
177 185
             session=request.dbsession,
@@ -184,12 +192,23 @@ class WorkspaceController(Controller):
184 192
         new_parent = api.get_one(
185 193
             move_data.new_parent_id, content_type=ContentType.Any
186 194
         )
195
+
196
+        try:
197
+            new_workspace = request.candidate_workspace
198
+        except WorkspaceNotFoundInTracimRequest:
199
+            new_workspace = None
200
+
187 201
         with new_revision(
188 202
                 session=request.dbsession,
189 203
                 tm=transaction.manager,
190 204
                 content=content
191 205
         ):
192
-            api.move(content, new_parent=new_parent)
206
+            api.move(
207
+                content,
208
+                new_parent=new_parent,
209
+                new_workspace=new_workspace,
210
+                must_stay_in_same_workspace=False,
211
+            )
193 212
         return
194 213
 
195 214
     @hapic.with_api_doc()