浏览代码

Merge branch 'develop' of github.com:tracim/tracim_backend into feature/611_workspace_and_workspace_member_action_endpoints

Guénaël Muller 6 年前
父节点
当前提交
8871ca51d4

+ 2 - 0
tracim/__init__.py 查看文件

@@ -29,6 +29,7 @@ from tracim.views.core_api.workspace_controller import WorkspaceController
29 29
 from tracim.views.contents_api.comment_controller import CommentController
30 30
 from tracim.views.errors import ErrorSchema
31 31
 from tracim.exceptions import NotAuthenticated
32
+from tracim.exceptions import InvalidId
32 33
 from tracim.exceptions import InsufficientUserProfile
33 34
 from tracim.exceptions import InsufficientUserRoleInWorkspace
34 35
 from tracim.exceptions import WorkspaceNotFoundInTracimRequest
@@ -90,6 +91,7 @@ def web(global_config, **local_settings):
90 91
     context.handle_exception(UserDoesNotExist, HTTPStatus.BAD_REQUEST)
91 92
     context.handle_exception(ContentNotFound, HTTPStatus.BAD_REQUEST)
92 93
     context.handle_exception(ContentTypeNotAllowed, HTTPStatus.BAD_REQUEST)
94
+    context.handle_exception(InvalidId, HTTPStatus.BAD_REQUEST)
93 95
     # Auth exception
94 96
     context.handle_exception(NotAuthenticated, HTTPStatus.UNAUTHORIZED)
95 97
     context.handle_exception(AuthenticationFailed, HTTPStatus.FORBIDDEN)

+ 5 - 0
tracim/command/database.py 查看文件

@@ -46,6 +46,10 @@ class InitializeDBCommand(AppContextCommand):
46 46
         config_uri = parsed_args.config_file
47 47
         setup_logging(config_uri)
48 48
         settings = get_appsettings(config_uri)
49
+        # INFO - G.M - 2018-06-178 - We need to add info from [DEFAULT]
50
+        # section of config file in order to have both global and
51
+        # web app specific param.
52
+        settings.update(settings.global_conf)
49 53
         self._create_schema(settings)
50 54
         self._populate_database(settings, add_test_data=parsed_args.test_data)
51 55
 
@@ -113,6 +117,7 @@ class DeleteDBCommand(AppContextCommand):
113 117
         config_uri = parsed_args.config_file
114 118
         setup_logging(config_uri)
115 119
         settings = get_appsettings(config_uri)
120
+        settings.update(settings.global_conf)
116 121
         engine = get_engine(settings)
117 122
         app_config = CFG(settings)
118 123
         app_config.configure_filedepot()

+ 20 - 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,7 @@ class EmptyLabelNotAllowed(EmptyValueNotAllowed):
141 160
     pass
142 161
 
143 162
 
144
-class EmptyRawContentNotAllowed(EmptyValueNotAllowed):
163
+class EmptyCommentContentNotAllowed(EmptyValueNotAllowed):
145 164
     pass
146 165
 
147 166
 

+ 22 - 9
tracim/lib/core/content.py 查看文件

@@ -24,7 +24,8 @@ from sqlalchemy.sql.elements import and_
24 24
 
25 25
 from tracim.lib.utils.utils import cmp_to_key
26 26
 from tracim.lib.core.notifications import NotifierFactory
27
-from tracim.exceptions import SameValueError, EmptyRawContentNotAllowed
27
+from tracim.exceptions import SameValueError
28
+from tracim.exceptions import EmptyCommentContentNotAllowed
28 29
 from tracim.exceptions import EmptyLabelNotAllowed
29 30
 from tracim.exceptions import ContentNotFound
30 31
 from tracim.exceptions import WorkspacesDoNotMatch
@@ -394,20 +395,28 @@ class ContentApi(object):
394 395
         return result
395 396
 
396 397
     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:
398
+        # TODO - G.M - 2018-07-16 - raise Exception instead of assert
397 399
         assert content_type in ContentType.allowed_types()
400
+        assert not (label and filename)
398 401
 
399 402
         if content_type == ContentType.Folder and not label:
400 403
             label = self.generate_folder_label(workspace, parent)
401 404
 
402 405
         content = Content()
403
-        if label:
404
-            content.label = label
405
-        elif filename:
406
-            # TODO - G.M - 2018-07-04 - File_name setting automatically
406
+
407
+        if filename:
408
+            # INFO - G.M - 2018-07-04 - File_name setting automatically
407 409
             # set label and file_extension
408 410
             content.file_name = label
411
+        elif label:
412
+            content.label = label
409 413
         else:
410
-            raise EmptyLabelNotAllowed()
414
+            if content_type == ContentType.Comment:
415
+                # INFO - G.M - 2018-07-16 - Default label for comments is
416
+                # empty string.
417
+                content.label = ''
418
+            else:
419
+                raise EmptyLabelNotAllowed('Content of this type should have a valid label')  # nopep8
411 420
 
412 421
         content.owner = self._user
413 422
         content.parent = parent
@@ -430,11 +439,11 @@ class ContentApi(object):
430 439
     def create_comment(self, workspace: Workspace=None, parent: Content=None, content:str ='', do_save=False) -> Content:
431 440
         assert parent and parent.type != ContentType.Folder
432 441
         if not content:
433
-            raise EmptyRawContentNotAllowed()
442
+            raise EmptyCommentContentNotAllowed()
434 443
         item = Content()
435 444
         item.owner = self._user
436 445
         item.parent = parent
437
-        if parent and not workspace:
446
+        if not workspace:
438 447
             workspace = item.parent.workspace
439 448
         item.workspace = workspace
440 449
         item.type = ContentType.Comment
@@ -446,7 +455,6 @@ class ContentApi(object):
446 455
             self.save(item, ActionDescription.COMMENT)
447 456
         return item
448 457
 
449
-
450 458
     def get_one_from_revision(self, content_id: int, content_type: str, workspace: Workspace=None, revision_id=None) -> Content:
451 459
         """
452 460
         This method is a hack to convert a node revision item into a node
@@ -483,6 +491,11 @@ class ContentApi(object):
483 491
         try:
484 492
             content = base_request.one()
485 493
         except NoResultFound as exc:
494
+            # TODO - G.M - 2018-07-16 - Add better support for all different
495
+            # error case who can happened here
496
+            # like content doesn't exist, wrong parent, wrong content_type, wrong workspace,
497
+            # wrong access to this workspace, wrong base filter according
498
+            # to content_status.
486 499
             raise ContentNotFound('Content "{}" not found in database'.format(content_id)) from exc  # nopep8
487 500
         return content
488 501
 

+ 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(

+ 0 - 8
tracim/models/context_models.py 查看文件

@@ -406,10 +406,6 @@ class ContentInContext(object):
406 406
         return self.content.content_id
407 407
 
408 408
     @property
409
-    def id(self) -> int:
410
-        return self.content_id
411
-
412
-    @property
413 409
     def parent_id(self) -> int:
414 410
         """
415 411
         Return parent_id of the content
@@ -513,10 +509,6 @@ class RevisionInContext(object):
513 509
         return self.revision.content_id
514 510
 
515 511
     @property
516
-    def id(self) -> int:
517
-        return self.content_id
518
-
519
-    @property
520 512
     def parent_id(self) -> int:
521 513
         """
522 514
         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)

+ 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())

+ 70 - 19
tracim/views/core_api/schemas.py 查看文件

@@ -2,6 +2,7 @@
2 2
 import marshmallow
3 3
 from marshmallow import post_load
4 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 WorkspaceAndUserIdPathSchema(
@@ -115,8 +131,9 @@ class WorkspaceAndContentIdPathSchema(
115 131
 class CommentsPathSchema(WorkspaceAndContentIdPathSchema):
116 132
     comment_id = marshmallow.fields.Int(
117 133
         example=6,
118
-        description='id of a comment related to content content_id',
119
-        required=True
134
+        description='id of a valid comment related to content content_id',
135
+        required=True,
136
+        validate=Range(min=1, error="Value must be greater than 0"),
120 137
     )
121 138
 
122 139
     @post_load
@@ -132,19 +149,22 @@ class FilterContentQuerySchema(marshmallow.Schema):
132 149
                     ' If not set, then return all contents.'
133 150
                     ' If set to 0, then return root contents.'
134 151
                     ' If set to another value, return all contents'
135
-                    ' directly included in the folder parent_id'
152
+                    ' directly included in the folder parent_id',
153
+        validate=Range(min=0, error="Value must be positive or 0"),
136 154
     )
137 155
     show_archived = marshmallow.fields.Int(
138 156
         example=0,
139 157
         default=0,
140 158
         description='if set to 1, then show archived contents.'
141
-                    ' Default is 0 - hide archived content'
159
+                    ' Default is 0 - hide archived content',
160
+        validate=Range(min=0, max=1, error="Value must be 0 or 1"),
142 161
     )
143 162
     show_deleted = marshmallow.fields.Int(
144 163
         example=0,
145 164
         default=0,
146 165
         description='if set to 1, then show deleted contents.'
147
-                    ' Default is 0 - hide deleted content'
166
+                    ' Default is 0 - hide deleted content',
167
+        validate=Range(min=0, max=1, error="Value must be 0 or 1"),
148 168
     )
149 169
     show_active = marshmallow.fields.Int(
150 170
         example=1,
@@ -154,7 +174,8 @@ class FilterContentQuerySchema(marshmallow.Schema):
154 174
                     ' Note: active content are content '
155 175
                     'that is neither archived nor deleted. '
156 176
                     'The reason for this parameter to exist is for example '
157
-                    'to allow to show only archived documents'
177
+                    'to allow to show only archived documents',
178
+        validate=Range(min=0, max=1, error="Value must be 0 or 1"),
158 179
     )
159 180
 
160 181
     @post_load
@@ -263,7 +284,10 @@ class WorkspaceMenuEntrySchema(marshmallow.Schema):
263 284
 
264 285
 
265 286
 class WorkspaceDigestSchema(marshmallow.Schema):
266
-    workspace_id = marshmallow.fields.Int(example=4)
287
+    workspace_id = marshmallow.fields.Int(
288
+        example=4,
289
+        validate=Range(min=1, error="Value must be greater than 0"),
290
+    )
267 291
     slug = marshmallow.fields.String(example='intranet')
268 292
     label = marshmallow.fields.String(example='Intranet')
269 293
     sidebar_entries = marshmallow.fields.Nested(
@@ -287,8 +311,14 @@ class WorkspaceMemberSchema(marshmallow.Schema):
287 311
         example='contributor',
288 312
         validate=OneOf(UserRoleInWorkspace.get_all_role_slug())
289 313
     )
290
-    user_id = marshmallow.fields.Int(example=3)
291
-    workspace_id = marshmallow.fields.Int(example=4)
314
+    user_id = marshmallow.fields.Int(
315
+        example=3,
316
+        validate=Range(min=1, error="Value must be greater than 0"),
317
+    )
318
+    workspace_id = marshmallow.fields.Int(
319
+        example=4,
320
+        validate=Range(min=1, error="Value must be greater than 0"),
321
+    )
292 322
     user = marshmallow.fields.Nested(
293 323
         UserDigestSchema()
294 324
     )
@@ -395,11 +425,13 @@ class ContentMoveSchema(marshmallow.Schema):
395 425
         description='id of the new parent content id.',
396 426
         allow_none=True,
397 427
         required=True,
428
+        validate=Range(min=0, error="Value must be positive or 0"),
398 429
     )
399 430
     new_workspace_id = marshmallow.fields.Int(
400 431
         example=2,
401 432
         description='id of the new workspace id.',
402
-        required=True
433
+        required=True,
434
+        validate=Range(min=1, error="Value must be greater than 0"),
403 435
     )
404 436
 
405 437
     @post_load
@@ -423,15 +455,20 @@ class ContentCreationSchema(marshmallow.Schema):
423 455
 
424 456
 
425 457
 class ContentDigestSchema(marshmallow.Schema):
426
-    content_id = marshmallow.fields.Int(example=6)
458
+    content_id = marshmallow.fields.Int(
459
+        example=6,
460
+        validate=Range(min=1, error="Value must be greater than 0"),
461
+    )
427 462
     slug = marshmallow.fields.Str(example='intervention-report-12')
428 463
     parent_id = marshmallow.fields.Int(
429 464
         example=34,
430 465
         allow_none=True,
431
-        default=None
466
+        default=None,
467
+        validate=Range(min=0, error="Value must be positive or 0"),
432 468
     )
433 469
     workspace_id = marshmallow.fields.Int(
434 470
         example=19,
471
+        validate=Range(min=1, error="Value must be greater than 0"),
435 472
     )
436 473
     label = marshmallow.fields.Str(example='Intervention Report 12')
437 474
     content_type = marshmallow.fields.Str(
@@ -498,8 +535,16 @@ class TextBasedContentSchema(ContentSchema, TextBasedDataAbstractSchema):
498 535
 
499 536
 
500 537
 class RevisionSchema(ContentDigestSchema):
501
-    comment_ids = marshmallow.fields.List(marshmallow.fields.Int(example=4))
502
-    revision_id = marshmallow.fields.Int(example=12)
538
+    comment_ids = marshmallow.fields.List(
539
+        marshmallow.fields.Int(
540
+            example=4,
541
+            validate=Range(min=1, error="Value must be greater than 0"),
542
+        )
543
+    )
544
+    revision_id = marshmallow.fields.Int(
545
+        example=12,
546
+        validate=Range(min=1, error="Value must be greater than 0"),
547
+    )
503 548
     created = marshmallow.fields.DateTime(
504 549
         format=DATETIME_FORMAT,
505 550
         description='Content creation date',
@@ -512,8 +557,14 @@ class TextBasedRevisionSchema(RevisionSchema, TextBasedDataAbstractSchema):
512 557
 
513 558
 
514 559
 class CommentSchema(marshmallow.Schema):
515
-    content_id = marshmallow.fields.Int(example=6)
516
-    parent_id = marshmallow.fields.Int(example=34)
560
+    content_id = marshmallow.fields.Int(
561
+        example=6,
562
+        validate=Range(min=1, error="Value must be greater than 0"),
563
+    )
564
+    parent_id = marshmallow.fields.Int(
565
+        example=34,
566
+        validate=Range(min=0, error="Value must be positive or 0"),
567
+    )
517 568
     raw_content = marshmallow.fields.String(
518 569
         example='<p>This is just an html comment !</p>'
519 570
     )