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
     pass
65
     pass
66
 
66
 
67
 
67
 
68
+class WorkspaceNotFoundInTracimRequest(NotFound):
69
+    pass
70
+
71
+
68
 class InsufficientUserWorkspaceRole(TracimException):
72
 class InsufficientUserWorkspaceRole(TracimException):
69
     pass
73
     pass
70
 
74
 
111
 
115
 
112
 class UserNotFoundInTracimRequest(TracimException):
116
 class UserNotFoundInTracimRequest(TracimException):
113
     pass
117
     pass
118
+
119
+
120
+class NotSameWorkspace(TracimException):
121
+    pass

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

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

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

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

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

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
     """
17
     """
18
     Json body params for move action
18
     Json body params for move action
19
     """
19
     """
20
-    def __init__(self, new_parent_id: str):
20
+    def __init__(self, new_parent_id: str, new_workspace_id: str = None):
21
         self.new_parent_id = new_parent_id
21
         self.new_parent_id = new_parent_id
22
+        self.new_workspace_id = new_workspace_id
22
 
23
 
23
 
24
 
24
 class LoginCredentials(object):
25
 class LoginCredentials(object):

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

814
         assert not [content for content in new_folder1_contents if content['id'] == 8]  # 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 [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
     def test_api_put_delete_content__ok_200__nominal_case(self):
943
     def test_api_put_delete_content__ok_200__nominal_case(self):
818
         """
944
         """
819
         delete content
945
         delete content

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

300
         example=42,
300
         example=42,
301
         description='id of the new parent content id.'
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
     @post_load
310
     @post_load
305
     def make_move_params(self, data):
311
     def make_move_params(self, data):

+ 25 - 6
tracim/views/core_api/workspace_controller.py View File

1
 import typing
1
 import typing
2
-
3
 import transaction
2
 import transaction
4
 from pyramid.config import Configurator
3
 from pyramid.config import Configurator
5
 try:  # Python 3.5+
4
 try:  # Python 3.5+
7
 except ImportError:
6
 except ImportError:
8
     from http import client as HTTPStatus
7
     from http import client as HTTPStatus
9
 
8
 
10
-from tracim import hapic, TracimRequest
9
+from tracim import hapic
10
+from tracim import TracimRequest
11
 from tracim.lib.core.workspace import WorkspaceApi
11
 from tracim.lib.core.workspace import WorkspaceApi
12
 from tracim.lib.core.content import ContentApi
12
 from tracim.lib.core.content import ContentApi
13
 from tracim.lib.core.userworkspace import RoleApi
13
 from tracim.lib.core.userworkspace import RoleApi
14
-from tracim.lib.utils.authorization import require_workspace_role
15
-from tracim.models.data import UserRoleInWorkspace, ActionDescription
14
+from tracim.lib.utils.authorization import require_workspace_role, \
15
+    require_candidate_workspace_role
16
+from tracim.models.data import UserRoleInWorkspace
17
+from tracim.models.data import ActionDescription
16
 from tracim.models.context_models import UserRoleWorkspaceInContext
18
 from tracim.models.context_models import UserRoleWorkspaceInContext
17
 from tracim.models.context_models import ContentInContext
19
 from tracim.models.context_models import ContentInContext
18
-from tracim.exceptions import NotAuthenticated
20
+from tracim.exceptions import NotAuthenticated, InsufficientUserWorkspaceRole
21
+from tracim.exceptions import WorkspaceNotFoundInTracimRequest
22
+from tracim.exceptions import NotSameWorkspace
19
 from tracim.exceptions import InsufficientUserProfile
23
 from tracim.exceptions import InsufficientUserProfile
20
 from tracim.exceptions import WorkspaceNotFound
24
 from tracim.exceptions import WorkspaceNotFound
21
 from tracim.views.controllers import Controller
25
 from tracim.views.controllers import Controller
156
     @hapic.handle_exception(NotAuthenticated, HTTPStatus.UNAUTHORIZED)
160
     @hapic.handle_exception(NotAuthenticated, HTTPStatus.UNAUTHORIZED)
157
     @hapic.handle_exception(InsufficientUserProfile, HTTPStatus.FORBIDDEN)
161
     @hapic.handle_exception(InsufficientUserProfile, HTTPStatus.FORBIDDEN)
158
     @hapic.handle_exception(WorkspaceNotFound, HTTPStatus.FORBIDDEN)
162
     @hapic.handle_exception(WorkspaceNotFound, HTTPStatus.FORBIDDEN)
163
+    @hapic.handle_exception(InsufficientUserWorkspaceRole, HTTPStatus.FORBIDDEN)
164
+    @hapic.handle_exception(NotSameWorkspace, HTTPStatus.BAD_REQUEST)
159
     @require_workspace_role(UserRoleInWorkspace.CONTRIBUTOR)
165
     @require_workspace_role(UserRoleInWorkspace.CONTRIBUTOR)
166
+    @require_candidate_workspace_role(UserRoleInWorkspace.CONTRIBUTOR)
160
     @hapic.input_path(WorkspaceAndContentIdPathSchema())
167
     @hapic.input_path(WorkspaceAndContentIdPathSchema())
161
     @hapic.input_body(ContentMoveSchema())
168
     @hapic.input_body(ContentMoveSchema())
162
     @hapic.output_body(NoContentSchema())
169
     @hapic.output_body(NoContentSchema())
172
         app_config = request.registry.settings['CFG']
179
         app_config = request.registry.settings['CFG']
173
         path_data = hapic_data.path
180
         path_data = hapic_data.path
174
         move_data = hapic_data.body
181
         move_data = hapic_data.body
182
+
175
         api = ContentApi(
183
         api = ContentApi(
176
             current_user=request.current_user,
184
             current_user=request.current_user,
177
             session=request.dbsession,
185
             session=request.dbsession,
184
         new_parent = api.get_one(
192
         new_parent = api.get_one(
185
             move_data.new_parent_id, content_type=ContentType.Any
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
         with new_revision(
201
         with new_revision(
188
                 session=request.dbsession,
202
                 session=request.dbsession,
189
                 tm=transaction.manager,
203
                 tm=transaction.manager,
190
                 content=content
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
         return
212
         return
194
 
213
 
195
     @hapic.with_api_doc()
214
     @hapic.with_api_doc()