Browse Source

Merge branch 'develop' of github.com:tracim/tracim_backend into feature/614_file_content_endpoints

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

+ 2 - 0
tracim/__init__.py View File

32
 from tracim.views.contents_api.file_controller import FileController
32
 from tracim.views.contents_api.file_controller import FileController
33
 from tracim.views.errors import ErrorSchema
33
 from tracim.views.errors import ErrorSchema
34
 from tracim.exceptions import NotAuthenticated
34
 from tracim.exceptions import NotAuthenticated
35
+from tracim.exceptions import InvalidId
35
 from tracim.exceptions import InsufficientUserProfile
36
 from tracim.exceptions import InsufficientUserProfile
36
 from tracim.exceptions import InsufficientUserRoleInWorkspace
37
 from tracim.exceptions import InsufficientUserRoleInWorkspace
37
 from tracim.exceptions import WorkspaceNotFoundInTracimRequest
38
 from tracim.exceptions import WorkspaceNotFoundInTracimRequest
93
     context.handle_exception(UserDoesNotExist, HTTPStatus.BAD_REQUEST)
94
     context.handle_exception(UserDoesNotExist, HTTPStatus.BAD_REQUEST)
94
     context.handle_exception(ContentNotFound, HTTPStatus.BAD_REQUEST)
95
     context.handle_exception(ContentNotFound, HTTPStatus.BAD_REQUEST)
95
     context.handle_exception(ContentTypeNotAllowed, HTTPStatus.BAD_REQUEST)
96
     context.handle_exception(ContentTypeNotAllowed, HTTPStatus.BAD_REQUEST)
97
+    context.handle_exception(InvalidId, HTTPStatus.BAD_REQUEST)
96
     # Auth exception
98
     # Auth exception
97
     context.handle_exception(NotAuthenticated, HTTPStatus.UNAUTHORIZED)
99
     context.handle_exception(NotAuthenticated, HTTPStatus.UNAUTHORIZED)
98
     context.handle_exception(AuthenticationFailed, HTTPStatus.FORBIDDEN)
100
     context.handle_exception(AuthenticationFailed, HTTPStatus.FORBIDDEN)

+ 23 - 1
tracim/exceptions.py View File

121
     pass
121
     pass
122
 
122
 
123
 
123
 
124
+class InvalidId(TracimException):
125
+    pass
126
+
127
+
128
+class InvalidContentId(InvalidId):
129
+    pass
130
+
131
+
132
+class InvalidCommentId(InvalidId):
133
+    pass
134
+
135
+
136
+class InvalidWorkspaceId(InvalidId):
137
+    pass
138
+
139
+
140
+class InvalidUserId(InvalidId):
141
+    pass
142
+
124
 class ContentNotFound(TracimException):
143
 class ContentNotFound(TracimException):
125
     pass
144
     pass
126
 
145
 
141
     pass
160
     pass
142
 
161
 
143
 
162
 
144
-class EmptyRawContentNotAllowed(EmptyValueNotAllowed):
163
+class EmptyCommentContentNotAllowed(EmptyValueNotAllowed):
164
+    pass
165
+
166
+class ParentNotFound(NotFound):
145
     pass
167
     pass
146
 
168
 
147
 
169
 

+ 21 - 10
tracim/lib/core/content.py View File

23
 from sqlalchemy import or_
23
 from sqlalchemy import or_
24
 from sqlalchemy.sql.elements import and_
24
 from sqlalchemy.sql.elements import and_
25
 
25
 
26
-from tracim.config import PreviewDim
27
 from tracim.lib.utils.utils import cmp_to_key
26
 from tracim.lib.utils.utils import cmp_to_key
28
 from tracim.lib.core.notifications import NotifierFactory
27
 from tracim.lib.core.notifications import NotifierFactory
29
 from tracim.exceptions import SameValueError
28
 from tracim.exceptions import SameValueError
30
 from tracim.exceptions import PageOfPreviewNotFound
29
 from tracim.exceptions import PageOfPreviewNotFound
31
 from tracim.exceptions import PreviewDimNotAllowed
30
 from tracim.exceptions import PreviewDimNotAllowed
32
-from tracim.exceptions import EmptyRawContentNotAllowed
33
 from tracim.exceptions import RevisionDoesNotMatchThisContent
31
 from tracim.exceptions import RevisionDoesNotMatchThisContent
32
+from tracim.exceptions import EmptyCommentContentNotAllowed
34
 from tracim.exceptions import EmptyLabelNotAllowed
33
 from tracim.exceptions import EmptyLabelNotAllowed
35
 from tracim.exceptions import ContentNotFound
34
 from tracim.exceptions import ContentNotFound
36
 from tracim.exceptions import WorkspacesDoNotMatch
35
 from tracim.exceptions import WorkspacesDoNotMatch
402
         return result
401
         return result
403
 
402
 
404
     def create(self, content_type: str, workspace: Workspace, parent: Content=None, label: str ='', filename: str = '', do_save=False, is_temporary: bool=False, do_notify=True) -> Content:
403
     def create(self, content_type: str, workspace: Workspace, parent: Content=None, label: str ='', filename: str = '', do_save=False, is_temporary: bool=False, do_notify=True) -> Content:
404
+        # TODO - G.M - 2018-07-16 - raise Exception instead of assert
405
         assert content_type in ContentType.allowed_types()
405
         assert content_type in ContentType.allowed_types()
406
+        assert not (label and filename)
406
 
407
 
407
         if content_type == ContentType.Folder and not label:
408
         if content_type == ContentType.Folder and not label:
408
             label = self.generate_folder_label(workspace, parent)
409
             label = self.generate_folder_label(workspace, parent)
409
 
410
 
410
         content = Content()
411
         content = Content()
411
-        if label:
412
-            content.label = label
413
-        elif filename:
414
-            # TODO - G.M - 2018-07-04 - File_name setting automatically
412
+
413
+        if filename:
414
+            # INFO - G.M - 2018-07-04 - File_name setting automatically
415
             # set label and file_extension
415
             # set label and file_extension
416
             content.file_name = label
416
             content.file_name = label
417
+        elif label:
418
+            content.label = label
417
         else:
419
         else:
418
-            raise EmptyLabelNotAllowed()
420
+            if content_type == ContentType.Comment:
421
+                # INFO - G.M - 2018-07-16 - Default label for comments is
422
+                # empty string.
423
+                content.label = ''
424
+            else:
425
+                raise EmptyLabelNotAllowed('Content of this type should have a valid label')  # nopep8
419
 
426
 
420
         content.owner = self._user
427
         content.owner = self._user
421
         content.parent = parent
428
         content.parent = parent
438
     def create_comment(self, workspace: Workspace=None, parent: Content=None, content:str ='', do_save=False) -> Content:
445
     def create_comment(self, workspace: Workspace=None, parent: Content=None, content:str ='', do_save=False) -> Content:
439
         assert parent and parent.type != ContentType.Folder
446
         assert parent and parent.type != ContentType.Folder
440
         if not content:
447
         if not content:
441
-            raise EmptyRawContentNotAllowed()
448
+            raise EmptyCommentContentNotAllowed()
442
         item = Content()
449
         item = Content()
443
         item.owner = self._user
450
         item.owner = self._user
444
         item.parent = parent
451
         item.parent = parent
445
-        if parent and not workspace:
452
+        if not workspace:
446
             workspace = item.parent.workspace
453
             workspace = item.parent.workspace
447
         item.workspace = workspace
454
         item.workspace = workspace
448
         item.type = ContentType.Comment
455
         item.type = ContentType.Comment
454
             self.save(item, ActionDescription.COMMENT)
461
             self.save(item, ActionDescription.COMMENT)
455
         return item
462
         return item
456
 
463
 
457
-
458
     def get_one_from_revision(self, content_id: int, content_type: str, workspace: Workspace=None, revision_id=None) -> Content:
464
     def get_one_from_revision(self, content_id: int, content_type: str, workspace: Workspace=None, revision_id=None) -> Content:
459
         """
465
         """
460
         This method is a hack to convert a node revision item into a node
466
         This method is a hack to convert a node revision item into a node
491
         try:
497
         try:
492
             content = base_request.one()
498
             content = base_request.one()
493
         except NoResultFound as exc:
499
         except NoResultFound as exc:
500
+            # TODO - G.M - 2018-07-16 - Add better support for all different
501
+            # error case who can happened here
502
+            # like content doesn't exist, wrong parent, wrong content_type, wrong workspace,
503
+            # wrong access to this workspace, wrong base filter according
504
+            # to content_status.
494
             raise ContentNotFound('Content "{}" not found in database'.format(content_id)) from exc  # nopep8
505
             raise ContentNotFound('Content "{}" not found in database'.format(content_id)) from exc  # nopep8
495
         return content
506
         return content
496
 
507
 

+ 19 - 10
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, ContentNotFound
5
+from tracim.exceptions import NotAuthenticated
6
+from tracim.exceptions import ContentNotFound
7
+from tracim.exceptions import InvalidUserId
8
+from tracim.exceptions import InvalidWorkspaceId
9
+from tracim.exceptions import InvalidContentId
10
+from tracim.exceptions import InvalidCommentId
6
 from tracim.exceptions import ContentNotFoundInTracimRequest
11
 from tracim.exceptions import ContentNotFoundInTracimRequest
7
 from tracim.exceptions import WorkspaceNotFoundInTracimRequest
12
 from tracim.exceptions import WorkspaceNotFoundInTracimRequest
8
 from tracim.exceptions import UserNotFoundInTracimRequest
13
 from tracim.exceptions import UserNotFoundInTracimRequest
214
         comment_id = ''
219
         comment_id = ''
215
         try:
220
         try:
216
             if 'comment_id' in request.matchdict:
221
             if 'comment_id' in request.matchdict:
217
-                if not request.matchdict['comment_id'].isdecimal():
218
-                    raise ContentNotFoundInTracimRequest('comment_id is not a correct integer')  # nopep8
222
+                comment_id_str = request.matchdict['content_id']
223
+                if not isinstance(comment_id_str, str) or not comment_id_str.isdecimal():  # nopep8
224
+                    raise InvalidCommentId('comment_id is not a correct integer')  # nopep8
219
                 comment_id = int(request.matchdict['comment_id'])
225
                 comment_id = int(request.matchdict['comment_id'])
220
             if not comment_id:
226
             if not comment_id:
221
                 raise ContentNotFoundInTracimRequest('No comment_id property found in request')  # nopep8
227
                 raise ContentNotFoundInTracimRequest('No comment_id property found in request')  # nopep8
253
         content_id = ''
259
         content_id = ''
254
         try:
260
         try:
255
             if 'content_id' in request.matchdict:
261
             if 'content_id' in request.matchdict:
256
-                if not request.matchdict['content_id'].isdecimal():
257
-                    raise ContentNotFoundInTracimRequest('content_id is not a correct integer')  # nopep8
262
+                content_id_str = request.matchdict['content_id']
263
+                if not isinstance(content_id_str, str) or not content_id_str.isdecimal():  # nopep8
264
+                    raise InvalidContentId('content_id is not a correct integer')  # nopep8
258
                 content_id = int(request.matchdict['content_id'])
265
                 content_id = int(request.matchdict['content_id'])
259
             if not content_id:
266
             if not content_id:
260
                 raise ContentNotFoundInTracimRequest('No content_id property found in request')  # nopep8
267
                 raise ContentNotFoundInTracimRequest('No content_id property found in request')  # nopep8
286
         try:
293
         try:
287
             login = None
294
             login = None
288
             if 'user_id' in request.matchdict:
295
             if 'user_id' in request.matchdict:
289
-                if not request.matchdict['user_id'].isdecimal():
290
-                    raise UserNotFoundInTracimRequest('user_id is not a correct integer')  # nopep8
296
+                user_id_str = request.matchdict['user_id']
297
+                if not isinstance(user_id_str, str) or not user_id_str.isdecimal():
298
+                    raise InvalidUserId('user_id is not a correct integer')  # nopep8
291
                 login = int(request.matchdict['user_id'])
299
                 login = int(request.matchdict['user_id'])
292
             if not login:
300
             if not login:
293
                 raise UserNotFoundInTracimRequest('You request a candidate user but the context not permit to found one')  # nopep8
301
                 raise UserNotFoundInTracimRequest('You request a candidate user but the context not permit to found one')  # nopep8
331
         workspace_id = ''
339
         workspace_id = ''
332
         try:
340
         try:
333
             if 'workspace_id' in request.matchdict:
341
             if 'workspace_id' in request.matchdict:
334
-                if not request.matchdict['workspace_id'].isdecimal():
335
-                    raise WorkspaceNotFoundInTracimRequest('workspace_id is not a correct integer')  # nopep8
342
+                workspace_id_str = request.matchdict['workspace_id']
343
+                if not isinstance(workspace_id_str, str) or not workspace_id_str.isdecimal():  # nopep8
344
+                    raise InvalidWorkspaceId('workspace_id is not a correct integer')  # nopep8
336
                 workspace_id = int(request.matchdict['workspace_id'])
345
                 workspace_id = int(request.matchdict['workspace_id'])
337
             if not workspace_id:
346
             if not workspace_id:
338
                 raise WorkspaceNotFoundInTracimRequest('No workspace_id property found in request')  # nopep8
347
                 raise WorkspaceNotFoundInTracimRequest('No workspace_id property found in request')  # nopep8
368
                     if workspace_id.isdecimal():
377
                     if workspace_id.isdecimal():
369
                         workspace_id = int(workspace_id)
378
                         workspace_id = int(workspace_id)
370
                     else:
379
                     else:
371
-                        raise WorkspaceNotFoundInTracimRequest('workspace_id is not a correct integer')  # nopep8
380
+                        raise InvalidWorkspaceId('workspace_id is not a correct integer')  # nopep8
372
             if not workspace_id:
381
             if not workspace_id:
373
                 raise WorkspaceNotFoundInTracimRequest('No new_workspace_id property found in body')  # nopep8
382
                 raise WorkspaceNotFoundInTracimRequest('No new_workspace_id property found in body')  # nopep8
374
             wapi = WorkspaceApi(
383
             wapi = WorkspaceApi(

+ 50 - 8
tracim/lib/webdav/dav_provider.py View File

70
 
70
 
71
         # If the requested path is the root, then we return a RootResource resource
71
         # If the requested path is the root, then we return a RootResource resource
72
         if path == root_path:
72
         if path == root_path:
73
-            return resources.RootResource(path, environ, user=user, session=session)
73
+            return resources.RootResource(
74
+                path=path,
75
+                environ=environ,
76
+                user=user,
77
+                session=session
78
+            )
74
 
79
 
75
         workspace_api = WorkspaceApi(
80
         workspace_api = WorkspaceApi(
76
             current_user=user,
81
             current_user=user,
111
 
116
 
112
         # Easy cases : path either end with /.deleted, /.archived or /.history, then we return corresponding resources
117
         # Easy cases : path either end with /.deleted, /.archived or /.history, then we return corresponding resources
113
         if path.endswith(SpecialFolderExtension.Archived) and self._show_archive:
118
         if path.endswith(SpecialFolderExtension.Archived) and self._show_archive:
114
-            return resources.ArchivedFolderResource(path, environ, workspace, content)
119
+            return resources.ArchivedFolderResource(
120
+                path=path,
121
+                environ=environ,
122
+                workspace=workspace,
123
+                user=user,
124
+                content=content,
125
+                session=session,
126
+            )
115
 
127
 
116
         if path.endswith(SpecialFolderExtension.Deleted) and self._show_delete:
128
         if path.endswith(SpecialFolderExtension.Deleted) and self._show_delete:
117
-            return resources.DeletedFolderResource(path, environ, workspace, content)
129
+            return resources.DeletedFolderResource(
130
+                path=path,
131
+                environ=environ,
132
+                workspace=workspace,
133
+                user=user,
134
+                content=content,
135
+                session=session,
136
+            )
118
 
137
 
119
         if path.endswith(SpecialFolderExtension.History) and self._show_history:
138
         if path.endswith(SpecialFolderExtension.History) and self._show_history:
120
             is_deleted_folder = re.search(r'/\.deleted/\.history$', path) is not None
139
             is_deleted_folder = re.search(r'/\.deleted/\.history$', path) is not None
124
                 else HistoryType.Archived if is_archived_folder \
143
                 else HistoryType.Archived if is_archived_folder \
125
                 else HistoryType.Standard
144
                 else HistoryType.Standard
126
 
145
 
127
-            return resources.HistoryFolderResource(path, environ, workspace, content, type)
146
+            return resources.HistoryFolderResource(
147
+                path=path,
148
+                environ=environ,
149
+                workspace=workspace,
150
+                user=user,
151
+                content=content,
152
+                session=session,
153
+                type=type
154
+            )
128
 
155
 
129
         # Now that's more complicated, we're trying to find out if the path end with /.history/file_name
156
         # Now that's more complicated, we're trying to find out if the path end with /.history/file_name
130
         is_history_file_folder = re.search(r'/\.history/([^/]+)$', path) is not None
157
         is_history_file_folder = re.search(r'/\.history/([^/]+)$', path) is not None
133
             return resources.HistoryFileFolderResource(
160
             return resources.HistoryFileFolderResource(
134
                 path=path,
161
                 path=path,
135
                 environ=environ,
162
                 environ=environ,
136
-                content=content
163
+                user=user,
164
+                content=content,
165
+                session=session,
137
             )
166
             )
138
-
139
         # And here next step :
167
         # And here next step :
140
         is_history_file = re.search(r'/\.history/[^/]+/\((\d+) - [a-zA-Z]+\) .+', path) is not None
168
         is_history_file = re.search(r'/\.history/[^/]+/\((\d+) - [a-zA-Z]+\) .+', path) is not None
141
 
169
 
147
             content = self.get_content_from_revision(content_revision, content_api)
175
             content = self.get_content_from_revision(content_revision, content_api)
148
 
176
 
149
             if content.type == ContentType.File:
177
             if content.type == ContentType.File:
150
-                return resources.HistoryFileResource(path, environ, content, content_revision)
178
+                return resources.HistoryFileResource(
179
+                    path=path,
180
+                    environ=environ,
181
+                    user=user,
182
+                    content=content,
183
+                    content_revision=content_revision,
184
+                    session=session,
185
+                )
151
             else:
186
             else:
152
-                return resources.HistoryOtherFile(path, environ, content, content_revision)
187
+                return resources.HistoryOtherFile(
188
+                    path=path,
189
+                    environ=environ,
190
+                    user=user,
191
+                    content=content,
192
+                    content_revision=content_revision,
193
+                    session=session,
194
+                )
153
 
195
 
154
         # And if we're still going, the client is asking for a standard Folder/File/Page/Thread so we check the type7
196
         # And if we're still going, the client is asking for a standard Folder/File/Page/Thread so we check the type7
155
         # and return the corresponding resource
197
         # and return the corresponding resource

+ 2 - 0
tracim/lib/webdav/resources.py View File

516
         workspace_api = WorkspaceApi(
516
         workspace_api = WorkspaceApi(
517
             current_user=self.user,
517
             current_user=self.user,
518
             session=self.session,
518
             session=self.session,
519
+            config=self.provider.app_config,
519
         )
520
         )
520
         workspace = self.provider.get_workspace_from_path(
521
         workspace = self.provider.get_workspace_from_path(
521
             normpath(destpath), workspace_api
522
             normpath(destpath), workspace_api
1308
         workspace_api = WorkspaceApi(
1309
         workspace_api = WorkspaceApi(
1309
             current_user=self.user,
1310
             current_user=self.user,
1310
             session=self.session,
1311
             session=self.session,
1312
+            config=self.provider.app_config,
1311
         )
1313
         )
1312
         content_api = ContentApi(
1314
         content_api = ContentApi(
1313
             current_user=self.user,
1315
             current_user=self.user,

+ 2 - 8
tracim/models/context_models.py View File

139
             self,
139
             self,
140
             label: str,
140
             label: str,
141
             content_type: str,
141
             content_type: str,
142
+            parent_id: typing.Optional[int] = None,
142
     ) -> None:
143
     ) -> None:
143
         self.label = label
144
         self.label = label
144
         self.content_type = content_type
145
         self.content_type = content_type
146
+        self.parent_id = parent_id
145
 
147
 
146
 
148
 
147
 class CommentCreation(object):
149
 class CommentCreation(object):
394
         return self.content.content_id
396
         return self.content.content_id
395
 
397
 
396
     @property
398
     @property
397
-    def id(self) -> int:
398
-        return self.content_id
399
-
400
-    @property
401
     def parent_id(self) -> int:
399
     def parent_id(self) -> int:
402
         """
400
         """
403
         Return parent_id of the content
401
         Return parent_id of the content
501
         return self.revision.content_id
499
         return self.revision.content_id
502
 
500
 
503
     @property
501
     @property
504
-    def id(self) -> int:
505
-        return self.content_id
506
-
507
-    @property
508
     def parent_id(self) -> int:
502
     def parent_id(self) -> int:
509
         """
503
         """
510
         Return parent_id of the content
504
         Return parent_id of the content

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

113
             params=params,
113
             params=params,
114
             status=400
114
             status=400
115
         )
115
         )
116
+
116
     def test_api__delete_content_comment__ok_200__user_is_owner_and_workspace_manager(self) -> None:  # nopep8
117
     def test_api__delete_content_comment__ok_200__user_is_owner_and_workspace_manager(self) -> None:  # nopep8
117
         """
118
         """
118
         delete comment (user is workspace_manager and owner)
119
         delete comment (user is workspace_manager and owner)

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

834
         active_contents = self.testapp.get('/api/v2/workspaces/1/contents', params=params_active, status=200).json_body  # nopep8
834
         active_contents = self.testapp.get('/api/v2/workspaces/1/contents', params=params_active, status=200).json_body  # nopep8
835
         assert res.json_body in active_contents
835
         assert res.json_body in active_contents
836
 
836
 
837
+    def test_api__post_content_create_generic_content__ok_200__in_folder(self) -> None:  # nopep8
838
+        """
839
+        Create generic content in folder
840
+        """
841
+        self.testapp.authorization = (
842
+            'Basic',
843
+            (
844
+                'admin@admin.admin',
845
+                'admin@admin.admin'
846
+            )
847
+        )
848
+        params = {
849
+            'label': 'GenericCreatedContent',
850
+            'content_type': 'markdownpage',
851
+            'parent_id': 10,
852
+        }
853
+        res = self.testapp.post_json(
854
+            '/api/v2/workspaces/1/contents',
855
+            params=params,
856
+            status=200
857
+        )
858
+        assert res
859
+        assert res.json_body
860
+        assert res.json_body['status'] == 'open'
861
+        assert res.json_body['content_id']
862
+        assert res.json_body['content_type'] == 'markdownpage'
863
+        assert res.json_body['is_archived'] is False
864
+        assert res.json_body['is_deleted'] is False
865
+        assert res.json_body['workspace_id'] == 1
866
+        assert res.json_body['slug'] == 'genericcreatedcontent'
867
+        assert res.json_body['parent_id'] == 10
868
+        assert res.json_body['show_in_ui'] is True
869
+        assert res.json_body['sub_content_types']
870
+        params_active = {
871
+            'parent_id': 10,
872
+            'show_archived': 0,
873
+            'show_deleted': 0,
874
+            'show_active': 1,
875
+        }
876
+        # INFO - G.M - 2018-06-165 - Verify if new content is correctly created
877
+        active_contents = self.testapp.get('/api/v2/workspaces/1/contents', params=params_active, status=200).json_body  # nopep8
878
+        assert res.json_body in active_contents
879
+
837
     def test_api__post_content_create_generic_content__err_400__empty_label(self) -> None:  # nopep8
880
     def test_api__post_content_create_generic_content__err_400__empty_label(self) -> None:  # nopep8
838
         """
881
         """
839
         Create generic content
882
         Create generic content

+ 11 - 6
tracim/tests/library/test_content_api.py View File

506
             session=self.session,
506
             session=self.session,
507
             config=self.app_config,
507
             config=self.app_config,
508
         )
508
         )
509
-        c = api.create(ContentType.Folder, workspace, None, 'parent', True)
509
+        c = api.create(ContentType.Folder, workspace, None, 'parent', '', True)
510
         with new_revision(
510
         with new_revision(
511
             session=self.session,
511
             session=self.session,
512
             tm=transaction.manager,
512
             tm=transaction.manager,
546
             session=self.session,
546
             session=self.session,
547
             config=self.app_config,
547
             config=self.app_config,
548
         )
548
         )
549
-        c = api.create(ContentType.Folder, workspace, None, 'parent', True)
549
+        c = api.create(ContentType.Folder, workspace, None, 'parent', '', True)
550
         with new_revision(
550
         with new_revision(
551
             session=self.session,
551
             session=self.session,
552
             tm=transaction.manager,
552
             tm=transaction.manager,
656
             workspace,
656
             workspace,
657
             None,
657
             None,
658
             'folder a',
658
             'folder a',
659
+            '',
659
             True
660
             True
660
         )
661
         )
661
         with self.session.no_autoflush:
662
         with self.session.no_autoflush:
692
             workspace2,
693
             workspace2,
693
             None,
694
             None,
694
             'folder b',
695
             'folder b',
696
+            '',
695
             True
697
             True
696
         )
698
         )
697
 
699
 
775
             workspace,
777
             workspace,
776
             None,
778
             None,
777
             'folder a',
779
             'folder a',
780
+            '',
778
             True
781
             True
779
         )
782
         )
780
         with self.session.no_autoflush:
783
         with self.session.no_autoflush:
811
             workspace2,
814
             workspace2,
812
             None,
815
             None,
813
             'folder b',
816
             'folder b',
817
+            '',
814
             True
818
             True
815
         )
819
         )
816
         api2.copy(
820
         api2.copy(
891
             workspace,
895
             workspace,
892
             None,
896
             None,
893
             'folder a',
897
             'folder a',
898
+            '',
894
             True
899
             True
895
         )
900
         )
896
         with self.session.no_autoflush:
901
         with self.session.no_autoflush:
2008
             config=self.app_config,
2013
             config=self.app_config,
2009
         )
2014
         )
2010
         a = api.create(ContentType.Folder, workspace, None,
2015
         a = api.create(ContentType.Folder, workspace, None,
2011
-                       'this is randomized folder', True)
2016
+                       'this is randomized folder', '', True)
2012
         p = api.create(ContentType.Page, workspace, a,
2017
         p = api.create(ContentType.Page, workspace, a,
2013
-                       'this is randomized label content', True)
2018
+                       'this is randomized label content', '', True)
2014
 
2019
 
2015
         with new_revision(
2020
         with new_revision(
2016
             session=self.session,
2021
             session=self.session,
2064
             config=self.app_config,
2069
             config=self.app_config,
2065
         )
2070
         )
2066
         a = api.create(ContentType.Folder, workspace, None,
2071
         a = api.create(ContentType.Folder, workspace, None,
2067
-                       'this is randomized folder', True)
2072
+                       'this is randomized folder', '', True)
2068
         p = api.create(ContentType.Page, workspace, a,
2073
         p = api.create(ContentType.Page, workspace, a,
2069
-                       'this is dummy label content', True)
2074
+                       'this is dummy label content', '', True)
2070
 
2075
 
2071
         with new_revision(
2076
         with new_revision(
2072
             tm=transaction.manager,
2077
             tm=transaction.manager,

+ 2 - 5
tracim/views/contents_api/comment_controller.py View File

19
 from tracim.views.core_api.schemas import SetCommentSchema
19
 from tracim.views.core_api.schemas import SetCommentSchema
20
 from tracim.views.core_api.schemas import WorkspaceAndContentIdPathSchema
20
 from tracim.views.core_api.schemas import WorkspaceAndContentIdPathSchema
21
 from tracim.views.core_api.schemas import NoContentSchema
21
 from tracim.views.core_api.schemas import NoContentSchema
22
-from tracim.exceptions import WorkspaceNotFound, EmptyRawContentNotAllowed
23
-from tracim.exceptions import InsufficientUserRoleInWorkspace
24
-from tracim.exceptions import NotAuthenticated
25
-from tracim.exceptions import AuthenticationFailed
22
+from tracim.exceptions import EmptyCommentContentNotAllowed
26
 from tracim.models.contents import ContentTypeLegacy as ContentType
23
 from tracim.models.contents import ContentTypeLegacy as ContentType
27
 from tracim.models.revision_protection import new_revision
24
 from tracim.models.revision_protection import new_revision
28
 from tracim.models.data import UserRoleInWorkspace
25
 from tracim.models.data import UserRoleInWorkspace
59
         ]
56
         ]
60
 
57
 
61
     @hapic.with_api_doc(tags=[COMMENT_ENDPOINTS_TAG])
58
     @hapic.with_api_doc(tags=[COMMENT_ENDPOINTS_TAG])
62
-    @hapic.handle_exception(EmptyRawContentNotAllowed, HTTPStatus.BAD_REQUEST)
59
+    @hapic.handle_exception(EmptyCommentContentNotAllowed, HTTPStatus.BAD_REQUEST)  # nopep8
63
     @require_workspace_role(UserRoleInWorkspace.CONTRIBUTOR)
60
     @require_workspace_role(UserRoleInWorkspace.CONTRIBUTOR)
64
     @hapic.input_path(WorkspaceAndContentIdPathSchema())
61
     @hapic.input_path(WorkspaceAndContentIdPathSchema())
65
     @hapic.input_body(SetCommentSchema())
62
     @hapic.input_body(SetCommentSchema())

+ 76 - 20
tracim/views/core_api/schemas.py View File

1
 # coding=utf-8
1
 # coding=utf-8
2
 import marshmallow
2
 import marshmallow
3
 from marshmallow import post_load
3
 from marshmallow import post_load
4
-from marshmallow.validate import OneOf, Range
4
+from marshmallow.validate import OneOf
5
+from marshmallow.validate import Range
5
 
6
 
6
 from tracim.lib.utils.utils import DATETIME_FORMAT
7
 from tracim.lib.utils.utils import DATETIME_FORMAT
7
 from tracim.models.auth import Profile
8
 from tracim.models.auth import Profile
83
 
84
 
84
 
85
 
85
 class UserIdPathSchema(marshmallow.Schema):
86
 class UserIdPathSchema(marshmallow.Schema):
86
-    user_id = marshmallow.fields.Int(example=3, required=True)
87
+    user_id = marshmallow.fields.Int(
88
+        example=3,
89
+        required=True,
90
+        description='id of a valid user',
91
+        validate=Range(min=1, error="Value must be greater than 0"),
92
+    )
87
 
93
 
88
 
94
 
89
 class WorkspaceIdPathSchema(marshmallow.Schema):
95
 class WorkspaceIdPathSchema(marshmallow.Schema):
90
-    workspace_id = marshmallow.fields.Int(example=4, required=True)
96
+    workspace_id = marshmallow.fields.Int(
97
+        example=4,
98
+        required=True,
99
+        description='id of a valid workspace',
100
+        validate=Range(min=1, error="Value must be greater than 0"),
101
+    )
91
 
102
 
92
 
103
 
93
 class ContentIdPathSchema(marshmallow.Schema):
104
 class ContentIdPathSchema(marshmallow.Schema):
94
-    content_id = marshmallow.fields.Int(example=6, required=True)
105
+    content_id = marshmallow.fields.Int(
106
+        example=6,
107
+        required=True,
108
+        description='id of a valid content',
109
+        validate=Range(min=1, error="Value must be greater than 0"),
110
+    )
95
 
111
 
96
 
112
 
97
 class RevisionIdPathSchema(marshmallow.Schema):
113
 class RevisionIdPathSchema(marshmallow.Schema):
156
 class CommentsPathSchema(WorkspaceAndContentIdPathSchema):
172
 class CommentsPathSchema(WorkspaceAndContentIdPathSchema):
157
     comment_id = marshmallow.fields.Int(
173
     comment_id = marshmallow.fields.Int(
158
         example=6,
174
         example=6,
159
-        description='id of a comment related to content content_id',
160
-        required=True
175
+        description='id of a valid comment related to content content_id',
176
+        required=True,
177
+        validate=Range(min=1, error="Value must be greater than 0"),
161
     )
178
     )
162
     @post_load
179
     @post_load
163
     def make_path_object(self, data):
180
     def make_path_object(self, data):
185
                     ' If not set, then return all contents.'
202
                     ' If not set, then return all contents.'
186
                     ' If set to 0, then return root contents.'
203
                     ' If set to 0, then return root contents.'
187
                     ' If set to another value, return all contents'
204
                     ' If set to another value, return all contents'
188
-                    ' directly included in the folder parent_id'
205
+                    ' directly included in the folder parent_id',
206
+        validate=Range(min=0, error="Value must be positive or 0"),
189
     )
207
     )
190
     show_archived = marshmallow.fields.Int(
208
     show_archived = marshmallow.fields.Int(
191
         example=0,
209
         example=0,
192
         default=0,
210
         default=0,
193
         description='if set to 1, then show archived contents.'
211
         description='if set to 1, then show archived contents.'
194
-                    ' Default is 0 - hide archived content'
212
+                    ' Default is 0 - hide archived content',
213
+        validate=Range(min=0, max=1, error="Value must be 0 or 1"),
195
     )
214
     )
196
     show_deleted = marshmallow.fields.Int(
215
     show_deleted = marshmallow.fields.Int(
197
         example=0,
216
         example=0,
198
         default=0,
217
         default=0,
199
         description='if set to 1, then show deleted contents.'
218
         description='if set to 1, then show deleted contents.'
200
-                    ' Default is 0 - hide deleted content'
219
+                    ' Default is 0 - hide deleted content',
220
+        validate=Range(min=0, max=1, error="Value must be 0 or 1"),
201
     )
221
     )
202
     show_active = marshmallow.fields.Int(
222
     show_active = marshmallow.fields.Int(
203
         example=1,
223
         example=1,
207
                     ' Note: active content are content '
227
                     ' Note: active content are content '
208
                     'that is neither archived nor deleted. '
228
                     'that is neither archived nor deleted. '
209
                     'The reason for this parameter to exist is for example '
229
                     'The reason for this parameter to exist is for example '
210
-                    'to allow to show only archived documents'
230
+                    'to allow to show only archived documents',
231
+        validate=Range(min=0, max=1, error="Value must be 0 or 1"),
211
     )
232
     )
212
 
233
 
213
     @post_load
234
     @post_load
271
 
292
 
272
 
293
 
273
 class WorkspaceDigestSchema(marshmallow.Schema):
294
 class WorkspaceDigestSchema(marshmallow.Schema):
274
-    workspace_id = marshmallow.fields.Int(example=4)
295
+    workspace_id = marshmallow.fields.Int(
296
+        example=4,
297
+        validate=Range(min=1, error="Value must be greater than 0"),
298
+    )
275
     slug = marshmallow.fields.String(example='intranet')
299
     slug = marshmallow.fields.String(example='intranet')
276
     label = marshmallow.fields.String(example='Intranet')
300
     label = marshmallow.fields.String(example='Intranet')
277
     sidebar_entries = marshmallow.fields.Nested(
301
     sidebar_entries = marshmallow.fields.Nested(
295
         example='contributor',
319
         example='contributor',
296
         validate=OneOf(UserRoleInWorkspace.get_all_role_slug())
320
         validate=OneOf(UserRoleInWorkspace.get_all_role_slug())
297
     )
321
     )
298
-    user_id = marshmallow.fields.Int(example=3)
299
-    workspace_id = marshmallow.fields.Int(example=4)
322
+    user_id = marshmallow.fields.Int(
323
+        example=3,
324
+        validate=Range(min=1, error="Value must be greater than 0"),
325
+    )
326
+    workspace_id = marshmallow.fields.Int(
327
+        example=4,
328
+        validate=Range(min=1, error="Value must be greater than 0"),
329
+    )
300
     user = marshmallow.fields.Nested(
330
     user = marshmallow.fields.Nested(
301
         UserSchema(only=('public_name', 'avatar_url'))
331
         UserSchema(only=('public_name', 'avatar_url'))
302
     )
332
     )
385
         description='id of the new parent content id.',
415
         description='id of the new parent content id.',
386
         allow_none=True,
416
         allow_none=True,
387
         required=True,
417
         required=True,
418
+        validate=Range(min=0, error="Value must be positive or 0"),
388
     )
419
     )
389
     new_workspace_id = marshmallow.fields.Int(
420
     new_workspace_id = marshmallow.fields.Int(
390
         example=2,
421
         example=2,
391
         description='id of the new workspace id.',
422
         description='id of the new workspace id.',
392
-        required=True
423
+        required=True,
424
+        validate=Range(min=1, error="Value must be greater than 0"),
393
     )
425
     )
394
 
426
 
395
     @post_load
427
     @post_load
406
         example='html-documents',
438
         example='html-documents',
407
         validate=OneOf(ContentType.allowed_types_for_folding()),  # nopep8
439
         validate=OneOf(ContentType.allowed_types_for_folding()),  # nopep8
408
     )
440
     )
441
+    parent_id = marshmallow.fields.Integer(
442
+        example=35,
443
+        description='content_id of parent content, if content should be placed in a folder, this should be folder content_id.'
444
+    )
445
+
409
 
446
 
410
     @post_load
447
     @post_load
411
     def make_content_filter(self, data):
448
     def make_content_filter(self, data):
413
 
450
 
414
 
451
 
415
 class ContentDigestSchema(marshmallow.Schema):
452
 class ContentDigestSchema(marshmallow.Schema):
416
-    content_id = marshmallow.fields.Int(example=6)
453
+    content_id = marshmallow.fields.Int(
454
+        example=6,
455
+        validate=Range(min=1, error="Value must be greater than 0"),
456
+    )
417
     slug = marshmallow.fields.Str(example='intervention-report-12')
457
     slug = marshmallow.fields.Str(example='intervention-report-12')
418
     parent_id = marshmallow.fields.Int(
458
     parent_id = marshmallow.fields.Int(
419
         example=34,
459
         example=34,
420
         allow_none=True,
460
         allow_none=True,
421
-        default=None
461
+        default=None,
462
+        validate=Range(min=0, error="Value must be positive or 0"),
422
     )
463
     )
423
     workspace_id = marshmallow.fields.Int(
464
     workspace_id = marshmallow.fields.Int(
424
         example=19,
465
         example=19,
466
+        validate=Range(min=1, error="Value must be greater than 0"),
425
     )
467
     )
426
     label = marshmallow.fields.Str(example='Intervention Report 12')
468
     label = marshmallow.fields.Str(example='Intervention Report 12')
427
     content_type = marshmallow.fields.Str(
469
     content_type = marshmallow.fields.Str(
497
 
539
 
498
 
540
 
499
 class RevisionSchema(ContentDigestSchema):
541
 class RevisionSchema(ContentDigestSchema):
500
-    comment_ids = marshmallow.fields.List(marshmallow.fields.Int(example=4))
501
-    revision_id = marshmallow.fields.Int(example=12)
542
+    comment_ids = marshmallow.fields.List(
543
+        marshmallow.fields.Int(
544
+            example=4,
545
+            validate=Range(min=1, error="Value must be greater than 0"),
546
+        )
547
+    )
548
+    revision_id = marshmallow.fields.Int(
549
+        example=12,
550
+        validate=Range(min=1, error="Value must be greater than 0"),
551
+    )
502
     created = marshmallow.fields.DateTime(
552
     created = marshmallow.fields.DateTime(
503
         format=DATETIME_FORMAT,
553
         format=DATETIME_FORMAT,
504
         description='Content creation date',
554
         description='Content creation date',
515
 
565
 
516
 
566
 
517
 class CommentSchema(marshmallow.Schema):
567
 class CommentSchema(marshmallow.Schema):
518
-    content_id = marshmallow.fields.Int(example=6)
519
-    parent_id = marshmallow.fields.Int(example=34)
568
+    content_id = marshmallow.fields.Int(
569
+        example=6,
570
+        validate=Range(min=1, error="Value must be greater than 0"),
571
+    )
572
+    parent_id = marshmallow.fields.Int(
573
+        example=34,
574
+        validate=Range(min=0, error="Value must be positive or 0"),
575
+    )
520
     raw_content = marshmallow.fields.String(
576
     raw_content = marshmallow.fields.String(
521
         example='<p>This is just an html comment !</p>'
577
         example='<p>This is just an html comment !</p>'
522
     )
578
     )

+ 12 - 1
tracim/views/core_api/workspace_controller.py View File

18
 from tracim.models.context_models import UserRoleWorkspaceInContext
18
 from tracim.models.context_models import UserRoleWorkspaceInContext
19
 from tracim.models.context_models import ContentInContext
19
 from tracim.models.context_models import ContentInContext
20
 from tracim.exceptions import EmptyLabelNotAllowed
20
 from tracim.exceptions import EmptyLabelNotAllowed
21
+from tracim.exceptions import ContentNotFound
21
 from tracim.exceptions import WorkspacesDoNotMatch
22
 from tracim.exceptions import WorkspacesDoNotMatch
23
+from tracim.exceptions import ParentNotFound
22
 from tracim.views.controllers import Controller
24
 from tracim.views.controllers import Controller
23
 from tracim.views.core_api.schemas import FilterContentQuerySchema
25
 from tracim.views.core_api.schemas import FilterContentQuerySchema
24
 from tracim.views.core_api.schemas import ContentMoveSchema
26
 from tracim.views.core_api.schemas import ContentMoveSchema
133
         api = ContentApi(
135
         api = ContentApi(
134
             current_user=request.current_user,
136
             current_user=request.current_user,
135
             session=request.dbsession,
137
             session=request.dbsession,
136
-            config=app_config,
138
+            config=app_config
137
         )
139
         )
140
+        parent = None
141
+        if creation_data.parent_id:
142
+            try:
143
+                parent = api.get_one(content_id=creation_data.parent_id, content_type=ContentType.Any)  # nopep8
144
+            except ContentNotFound as exc:
145
+                raise ParentNotFound(
146
+                    'Parent with content_id {} not found'.format(creation_data.parent_id)
147
+                ) from exc
138
         content = api.create(
148
         content = api.create(
139
             label=creation_data.label,
149
             label=creation_data.label,
140
             content_type=creation_data.content_type,
150
             content_type=creation_data.content_type,
141
             workspace=request.current_workspace,
151
             workspace=request.current_workspace,
152
+            parent=parent,
142
         )
153
         )
143
         api.save(content, ActionDescription.CREATION)
154
         api.save(content, ActionDescription.CREATION)
144
         content = api.get_content_in_context(content)
155
         content = api.get_content_in_context(content)