Преглед на файлове

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

Guénaël Muller преди 6 години
родител
ревизия
d5ade3ee45

+ 2 - 0
tracim/__init__.py Целия файл

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

+ 23 - 1
tracim/exceptions.py Целия файл

@@ -121,6 +121,25 @@ class ContentNotFoundInTracimRequest(TracimException):
121 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 143
 class ContentNotFound(TracimException):
125 144
     pass
126 145
 
@@ -141,7 +160,10 @@ class EmptyLabelNotAllowed(EmptyValueNotAllowed):
141 160
     pass
142 161
 
143 162
 
144
-class EmptyRawContentNotAllowed(EmptyValueNotAllowed):
163
+class EmptyCommentContentNotAllowed(EmptyValueNotAllowed):
164
+    pass
165
+
166
+class ParentNotFound(NotFound):
145 167
     pass
146 168
 
147 169
 

+ 21 - 10
tracim/lib/core/content.py Целия файл

@@ -23,14 +23,13 @@ from sqlalchemy import distinct
23 23
 from sqlalchemy import or_
24 24
 from sqlalchemy.sql.elements import and_
25 25
 
26
-from tracim.config import PreviewDim
27 26
 from tracim.lib.utils.utils import cmp_to_key
28 27
 from tracim.lib.core.notifications import NotifierFactory
29 28
 from tracim.exceptions import SameValueError
30 29
 from tracim.exceptions import PageOfPreviewNotFound
31 30
 from tracim.exceptions import PreviewDimNotAllowed
32
-from tracim.exceptions import EmptyRawContentNotAllowed
33 31
 from tracim.exceptions import RevisionDoesNotMatchThisContent
32
+from tracim.exceptions import EmptyCommentContentNotAllowed
34 33
 from tracim.exceptions import EmptyLabelNotAllowed
35 34
 from tracim.exceptions import ContentNotFound
36 35
 from tracim.exceptions import WorkspacesDoNotMatch
@@ -402,20 +401,28 @@ class ContentApi(object):
402 401
         return result
403 402
 
404 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 405
         assert content_type in ContentType.allowed_types()
406
+        assert not (label and filename)
406 407
 
407 408
         if content_type == ContentType.Folder and not label:
408 409
             label = self.generate_folder_label(workspace, parent)
409 410
 
410 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 415
             # set label and file_extension
416 416
             content.file_name = label
417
+        elif label:
418
+            content.label = label
417 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 427
         content.owner = self._user
421 428
         content.parent = parent
@@ -438,11 +445,11 @@ class ContentApi(object):
438 445
     def create_comment(self, workspace: Workspace=None, parent: Content=None, content:str ='', do_save=False) -> Content:
439 446
         assert parent and parent.type != ContentType.Folder
440 447
         if not content:
441
-            raise EmptyRawContentNotAllowed()
448
+            raise EmptyCommentContentNotAllowed()
442 449
         item = Content()
443 450
         item.owner = self._user
444 451
         item.parent = parent
445
-        if parent and not workspace:
452
+        if not workspace:
446 453
             workspace = item.parent.workspace
447 454
         item.workspace = workspace
448 455
         item.type = ContentType.Comment
@@ -454,7 +461,6 @@ class ContentApi(object):
454 461
             self.save(item, ActionDescription.COMMENT)
455 462
         return item
456 463
 
457
-
458 464
     def get_one_from_revision(self, content_id: int, content_type: str, workspace: Workspace=None, revision_id=None) -> Content:
459 465
         """
460 466
         This method is a hack to convert a node revision item into a node
@@ -491,6 +497,11 @@ class ContentApi(object):
491 497
         try:
492 498
             content = base_request.one()
493 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 505
             raise ContentNotFound('Content "{}" not found in database'.format(content_id)) from exc  # nopep8
495 506
         return content
496 507
 

+ 19 - 10
tracim/lib/utils/request.py Целия файл

@@ -2,7 +2,12 @@
2 2
 from pyramid.request import Request
3 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 11
 from tracim.exceptions import ContentNotFoundInTracimRequest
7 12
 from tracim.exceptions import WorkspaceNotFoundInTracimRequest
8 13
 from tracim.exceptions import UserNotFoundInTracimRequest
@@ -214,8 +219,9 @@ class TracimRequest(Request):
214 219
         comment_id = ''
215 220
         try:
216 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 225
                 comment_id = int(request.matchdict['comment_id'])
220 226
             if not comment_id:
221 227
                 raise ContentNotFoundInTracimRequest('No comment_id property found in request')  # nopep8
@@ -253,8 +259,9 @@ class TracimRequest(Request):
253 259
         content_id = ''
254 260
         try:
255 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 265
                 content_id = int(request.matchdict['content_id'])
259 266
             if not content_id:
260 267
                 raise ContentNotFoundInTracimRequest('No content_id property found in request')  # nopep8
@@ -286,8 +293,9 @@ class TracimRequest(Request):
286 293
         try:
287 294
             login = None
288 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 299
                 login = int(request.matchdict['user_id'])
292 300
             if not login:
293 301
                 raise UserNotFoundInTracimRequest('You request a candidate user but the context not permit to found one')  # nopep8
@@ -331,8 +339,9 @@ class TracimRequest(Request):
331 339
         workspace_id = ''
332 340
         try:
333 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 345
                 workspace_id = int(request.matchdict['workspace_id'])
337 346
             if not workspace_id:
338 347
                 raise WorkspaceNotFoundInTracimRequest('No workspace_id property found in request')  # nopep8
@@ -368,7 +377,7 @@ class TracimRequest(Request):
368 377
                     if workspace_id.isdecimal():
369 378
                         workspace_id = int(workspace_id)
370 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 381
             if not workspace_id:
373 382
                 raise WorkspaceNotFoundInTracimRequest('No new_workspace_id property found in body')  # nopep8
374 383
             wapi = WorkspaceApi(

+ 50 - 8
tracim/lib/webdav/dav_provider.py Целия файл

@@ -70,7 +70,12 @@ class Provider(DAVProvider):
70 70
 
71 71
         # If the requested path is the root, then we return a RootResource resource
72 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 80
         workspace_api = WorkspaceApi(
76 81
             current_user=user,
@@ -111,10 +116,24 @@ class Provider(DAVProvider):
111 116
 
112 117
         # Easy cases : path either end with /.deleted, /.archived or /.history, then we return corresponding resources
113 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 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 138
         if path.endswith(SpecialFolderExtension.History) and self._show_history:
120 139
             is_deleted_folder = re.search(r'/\.deleted/\.history$', path) is not None
@@ -124,7 +143,15 @@ class Provider(DAVProvider):
124 143
                 else HistoryType.Archived if is_archived_folder \
125 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 156
         # Now that's more complicated, we're trying to find out if the path end with /.history/file_name
130 157
         is_history_file_folder = re.search(r'/\.history/([^/]+)$', path) is not None
@@ -133,9 +160,10 @@ class Provider(DAVProvider):
133 160
             return resources.HistoryFileFolderResource(
134 161
                 path=path,
135 162
                 environ=environ,
136
-                content=content
163
+                user=user,
164
+                content=content,
165
+                session=session,
137 166
             )
138
-
139 167
         # And here next step :
140 168
         is_history_file = re.search(r'/\.history/[^/]+/\((\d+) - [a-zA-Z]+\) .+', path) is not None
141 169
 
@@ -147,9 +175,23 @@ class Provider(DAVProvider):
147 175
             content = self.get_content_from_revision(content_revision, content_api)
148 176
 
149 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 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 196
         # And if we're still going, the client is asking for a standard Folder/File/Page/Thread so we check the type7
155 197
         # and return the corresponding resource

+ 2 - 0
tracim/lib/webdav/resources.py Целия файл

@@ -516,6 +516,7 @@ class FolderResource(WorkspaceResource):
516 516
         workspace_api = WorkspaceApi(
517 517
             current_user=self.user,
518 518
             session=self.session,
519
+            config=self.provider.app_config,
519 520
         )
520 521
         workspace = self.provider.get_workspace_from_path(
521 522
             normpath(destpath), workspace_api
@@ -1308,6 +1309,7 @@ class FileResource(DAVNonCollection):
1308 1309
         workspace_api = WorkspaceApi(
1309 1310
             current_user=self.user,
1310 1311
             session=self.session,
1312
+            config=self.provider.app_config,
1311 1313
         )
1312 1314
         content_api = ContentApi(
1313 1315
             current_user=self.user,

+ 2 - 8
tracim/models/context_models.py Целия файл

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

+ 1 - 0
tracim/tests/functional/test_comments.py Целия файл

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

+ 43 - 0
tracim/tests/functional/test_workspaces.py Целия файл

@@ -834,6 +834,49 @@ class TestWorkspaceContents(FunctionalTest):
834 834
         active_contents = self.testapp.get('/api/v2/workspaces/1/contents', params=params_active, status=200).json_body  # nopep8
835 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 880
     def test_api__post_content_create_generic_content__err_400__empty_label(self) -> None:  # nopep8
838 881
         """
839 882
         Create generic content

+ 11 - 6
tracim/tests/library/test_content_api.py Целия файл

@@ -506,7 +506,7 @@ class TestContentApi(DefaultTest):
506 506
             session=self.session,
507 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 510
         with new_revision(
511 511
             session=self.session,
512 512
             tm=transaction.manager,
@@ -546,7 +546,7 @@ class TestContentApi(DefaultTest):
546 546
             session=self.session,
547 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 550
         with new_revision(
551 551
             session=self.session,
552 552
             tm=transaction.manager,
@@ -656,6 +656,7 @@ class TestContentApi(DefaultTest):
656 656
             workspace,
657 657
             None,
658 658
             'folder a',
659
+            '',
659 660
             True
660 661
         )
661 662
         with self.session.no_autoflush:
@@ -692,6 +693,7 @@ class TestContentApi(DefaultTest):
692 693
             workspace2,
693 694
             None,
694 695
             'folder b',
696
+            '',
695 697
             True
696 698
         )
697 699
 
@@ -775,6 +777,7 @@ class TestContentApi(DefaultTest):
775 777
             workspace,
776 778
             None,
777 779
             'folder a',
780
+            '',
778 781
             True
779 782
         )
780 783
         with self.session.no_autoflush:
@@ -811,6 +814,7 @@ class TestContentApi(DefaultTest):
811 814
             workspace2,
812 815
             None,
813 816
             'folder b',
817
+            '',
814 818
             True
815 819
         )
816 820
         api2.copy(
@@ -891,6 +895,7 @@ class TestContentApi(DefaultTest):
891 895
             workspace,
892 896
             None,
893 897
             'folder a',
898
+            '',
894 899
             True
895 900
         )
896 901
         with self.session.no_autoflush:
@@ -2008,9 +2013,9 @@ class TestContentApi(DefaultTest):
2008 2013
             config=self.app_config,
2009 2014
         )
2010 2015
         a = api.create(ContentType.Folder, workspace, None,
2011
-                       'this is randomized folder', True)
2016
+                       'this is randomized folder', '', True)
2012 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 2020
         with new_revision(
2016 2021
             session=self.session,
@@ -2064,9 +2069,9 @@ class TestContentApi(DefaultTest):
2064 2069
             config=self.app_config,
2065 2070
         )
2066 2071
         a = api.create(ContentType.Folder, workspace, None,
2067
-                       'this is randomized folder', True)
2072
+                       'this is randomized folder', '', True)
2068 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 2076
         with new_revision(
2072 2077
             tm=transaction.manager,

+ 2 - 5
tracim/views/contents_api/comment_controller.py Целия файл

@@ -19,10 +19,7 @@ from tracim.views.core_api.schemas import CommentsPathSchema
19 19
 from tracim.views.core_api.schemas import SetCommentSchema
20 20
 from tracim.views.core_api.schemas import WorkspaceAndContentIdPathSchema
21 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 23
 from tracim.models.contents import ContentTypeLegacy as ContentType
27 24
 from tracim.models.revision_protection import new_revision
28 25
 from tracim.models.data import UserRoleInWorkspace
@@ -59,7 +56,7 @@ class CommentController(Controller):
59 56
         ]
60 57
 
61 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 60
     @require_workspace_role(UserRoleInWorkspace.CONTRIBUTOR)
64 61
     @hapic.input_path(WorkspaceAndContentIdPathSchema())
65 62
     @hapic.input_body(SetCommentSchema())

+ 76 - 20
tracim/views/core_api/schemas.py Целия файл

@@ -1,7 +1,8 @@
1 1
 # coding=utf-8
2 2
 import marshmallow
3 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 7
 from tracim.lib.utils.utils import DATETIME_FORMAT
7 8
 from tracim.models.auth import Profile
@@ -83,15 +84,30 @@ class UserSchema(UserDigestSchema):
83 84
 
84 85
 
85 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 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 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 113
 class RevisionIdPathSchema(marshmallow.Schema):
@@ -156,8 +172,9 @@ class RevisionPreviewSizedPathSchema(
156 172
 class CommentsPathSchema(WorkspaceAndContentIdPathSchema):
157 173
     comment_id = marshmallow.fields.Int(
158 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 179
     @post_load
163 180
     def make_path_object(self, data):
@@ -185,19 +202,22 @@ class FilterContentQuerySchema(marshmallow.Schema):
185 202
                     ' If not set, then return all contents.'
186 203
                     ' If set to 0, then return root contents.'
187 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 208
     show_archived = marshmallow.fields.Int(
191 209
         example=0,
192 210
         default=0,
193 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 215
     show_deleted = marshmallow.fields.Int(
197 216
         example=0,
198 217
         default=0,
199 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 222
     show_active = marshmallow.fields.Int(
203 223
         example=1,
@@ -207,7 +227,8 @@ class FilterContentQuerySchema(marshmallow.Schema):
207 227
                     ' Note: active content are content '
208 228
                     'that is neither archived nor deleted. '
209 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 234
     @post_load
@@ -271,7 +292,10 @@ class WorkspaceMenuEntrySchema(marshmallow.Schema):
271 292
 
272 293
 
273 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 299
     slug = marshmallow.fields.String(example='intranet')
276 300
     label = marshmallow.fields.String(example='Intranet')
277 301
     sidebar_entries = marshmallow.fields.Nested(
@@ -295,8 +319,14 @@ class WorkspaceMemberSchema(marshmallow.Schema):
295 319
         example='contributor',
296 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 330
     user = marshmallow.fields.Nested(
301 331
         UserSchema(only=('public_name', 'avatar_url'))
302 332
     )
@@ -385,11 +415,13 @@ class ContentMoveSchema(marshmallow.Schema):
385 415
         description='id of the new parent content id.',
386 416
         allow_none=True,
387 417
         required=True,
418
+        validate=Range(min=0, error="Value must be positive or 0"),
388 419
     )
389 420
     new_workspace_id = marshmallow.fields.Int(
390 421
         example=2,
391 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 427
     @post_load
@@ -406,6 +438,11 @@ class ContentCreationSchema(marshmallow.Schema):
406 438
         example='html-documents',
407 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 447
     @post_load
411 448
     def make_content_filter(self, data):
@@ -413,15 +450,20 @@ class ContentCreationSchema(marshmallow.Schema):
413 450
 
414 451
 
415 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 457
     slug = marshmallow.fields.Str(example='intervention-report-12')
418 458
     parent_id = marshmallow.fields.Int(
419 459
         example=34,
420 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 464
     workspace_id = marshmallow.fields.Int(
424 465
         example=19,
466
+        validate=Range(min=1, error="Value must be greater than 0"),
425 467
     )
426 468
     label = marshmallow.fields.Str(example='Intervention Report 12')
427 469
     content_type = marshmallow.fields.Str(
@@ -497,8 +539,16 @@ class FileContentSchema(ContentSchema, FileInfoAbstractSchema):
497 539
 
498 540
 
499 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 552
     created = marshmallow.fields.DateTime(
503 553
         format=DATETIME_FORMAT,
504 554
         description='Content creation date',
@@ -515,8 +565,14 @@ class FileRevisionSchema(RevisionSchema, FileInfoAbstractSchema):
515 565
 
516 566
 
517 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 576
     raw_content = marshmallow.fields.String(
521 577
         example='<p>This is just an html comment !</p>'
522 578
     )

+ 12 - 1
tracim/views/core_api/workspace_controller.py Целия файл

@@ -18,7 +18,9 @@ from tracim.models.data import ActionDescription
18 18
 from tracim.models.context_models import UserRoleWorkspaceInContext
19 19
 from tracim.models.context_models import ContentInContext
20 20
 from tracim.exceptions import EmptyLabelNotAllowed
21
+from tracim.exceptions import ContentNotFound
21 22
 from tracim.exceptions import WorkspacesDoNotMatch
23
+from tracim.exceptions import ParentNotFound
22 24
 from tracim.views.controllers import Controller
23 25
 from tracim.views.core_api.schemas import FilterContentQuerySchema
24 26
 from tracim.views.core_api.schemas import ContentMoveSchema
@@ -133,12 +135,21 @@ class WorkspaceController(Controller):
133 135
         api = ContentApi(
134 136
             current_user=request.current_user,
135 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 148
         content = api.create(
139 149
             label=creation_data.label,
140 150
             content_type=creation_data.content_type,
141 151
             workspace=request.current_workspace,
152
+            parent=parent,
142 153
         )
143 154
         api.save(content, ActionDescription.CREATION)
144 155
         content = api.get_content_in_context(content)