Browse Source

Merge branch 'develop' of github.com:tracim/tracim_backend into fix/create_content_in_folder

Guénaël Muller 6 years ago
parent
commit
5dc606fb07

+ 2 - 0
tracim/__init__.py View File

@@ -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 View File

@@ -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 View File

@@ -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
 class ParentNotFound(NotFound):

+ 22 - 9
tracim/lib/core/content.py View File

@@ -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 View File

@@ -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 View File

@@ -340,10 +340,6 @@ class ContentInContext(object):
340 340
         return self.content.content_id
341 341
 
342 342
     @property
343
-    def id(self) -> int:
344
-        return self.content_id
345
-
346
-    @property
347 343
     def parent_id(self) -> int:
348 344
         """
349 345
         Return parent_id of the content
@@ -447,10 +443,6 @@ class RevisionInContext(object):
447 443
         return self.revision.content_id
448 444
 
449 445
     @property
450
-    def id(self) -> int:
451
-        return self.content_id
452
-
453
-    @property
454 446
     def parent_id(self) -> int:
455 447
         """
456 448
         Return parent_id of the content

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

@@ -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 View File

@@ -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 View File

@@ -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 View File

@@ -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
@@ -79,15 +80,30 @@ class UserSchema(UserDigestSchema):
79 80
 
80 81
 
81 82
 class UserIdPathSchema(marshmallow.Schema):
82
-    user_id = marshmallow.fields.Int(example=3, required=True)
83
+    user_id = marshmallow.fields.Int(
84
+        example=3,
85
+        required=True,
86
+        description='id of a valid user',
87
+        validate=Range(min=1, error="Value must be greater than 0"),
88
+    )
83 89
 
84 90
 
85 91
 class WorkspaceIdPathSchema(marshmallow.Schema):
86
-    workspace_id = marshmallow.fields.Int(example=4, required=True)
92
+    workspace_id = marshmallow.fields.Int(
93
+        example=4,
94
+        required=True,
95
+        description='id of a valid workspace',
96
+        validate=Range(min=1, error="Value must be greater than 0"),
97
+    )
87 98
 
88 99
 
89 100
 class ContentIdPathSchema(marshmallow.Schema):
90
-    content_id = marshmallow.fields.Int(example=6, required=True)
101
+    content_id = marshmallow.fields.Int(
102
+        example=6,
103
+        required=True,
104
+        description='id of a valid content',
105
+        validate=Range(min=1, error="Value must be greater than 0"),
106
+    )
91 107
 
92 108
 
93 109
 class WorkspaceAndContentIdPathSchema(
@@ -102,8 +118,9 @@ class WorkspaceAndContentIdPathSchema(
102 118
 class CommentsPathSchema(WorkspaceAndContentIdPathSchema):
103 119
     comment_id = marshmallow.fields.Int(
104 120
         example=6,
105
-        description='id of a comment related to content content_id',
106
-        required=True
121
+        description='id of a valid comment related to content content_id',
122
+        required=True,
123
+        validate=Range(min=1, error="Value must be greater than 0"),
107 124
     )
108 125
     @post_load
109 126
     def make_path_object(self, data):
@@ -118,19 +135,22 @@ class FilterContentQuerySchema(marshmallow.Schema):
118 135
                     ' If not set, then return all contents.'
119 136
                     ' If set to 0, then return root contents.'
120 137
                     ' If set to another value, return all contents'
121
-                    ' directly included in the folder parent_id'
138
+                    ' directly included in the folder parent_id',
139
+        validate=Range(min=0, error="Value must be positive or 0"),
122 140
     )
123 141
     show_archived = marshmallow.fields.Int(
124 142
         example=0,
125 143
         default=0,
126 144
         description='if set to 1, then show archived contents.'
127
-                    ' Default is 0 - hide archived content'
145
+                    ' Default is 0 - hide archived content',
146
+        validate=Range(min=0, max=1, error="Value must be 0 or 1"),
128 147
     )
129 148
     show_deleted = marshmallow.fields.Int(
130 149
         example=0,
131 150
         default=0,
132 151
         description='if set to 1, then show deleted contents.'
133
-                    ' Default is 0 - hide deleted content'
152
+                    ' Default is 0 - hide deleted content',
153
+        validate=Range(min=0, max=1, error="Value must be 0 or 1"),
134 154
     )
135 155
     show_active = marshmallow.fields.Int(
136 156
         example=1,
@@ -140,7 +160,8 @@ class FilterContentQuerySchema(marshmallow.Schema):
140 160
                     ' Note: active content are content '
141 161
                     'that is neither archived nor deleted. '
142 162
                     'The reason for this parameter to exist is for example '
143
-                    'to allow to show only archived documents'
163
+                    'to allow to show only archived documents',
164
+        validate=Range(min=0, max=1, error="Value must be 0 or 1"),
144 165
     )
145 166
 
146 167
     @post_load
@@ -204,7 +225,10 @@ class WorkspaceMenuEntrySchema(marshmallow.Schema):
204 225
 
205 226
 
206 227
 class WorkspaceDigestSchema(marshmallow.Schema):
207
-    workspace_id = marshmallow.fields.Int(example=4)
228
+    workspace_id = marshmallow.fields.Int(
229
+        example=4,
230
+        validate=Range(min=1, error="Value must be greater than 0"),
231
+    )
208 232
     slug = marshmallow.fields.String(example='intranet')
209 233
     label = marshmallow.fields.String(example='Intranet')
210 234
     sidebar_entries = marshmallow.fields.Nested(
@@ -228,8 +252,14 @@ class WorkspaceMemberSchema(marshmallow.Schema):
228 252
         example='contributor',
229 253
         validate=OneOf(UserRoleInWorkspace.get_all_role_slug())
230 254
     )
231
-    user_id = marshmallow.fields.Int(example=3)
232
-    workspace_id = marshmallow.fields.Int(example=4)
255
+    user_id = marshmallow.fields.Int(
256
+        example=3,
257
+        validate=Range(min=1, error="Value must be greater than 0"),
258
+    )
259
+    workspace_id = marshmallow.fields.Int(
260
+        example=4,
261
+        validate=Range(min=1, error="Value must be greater than 0"),
262
+    )
233 263
     user = marshmallow.fields.Nested(
234 264
         UserSchema(only=('public_name', 'avatar_url'))
235 265
     )
@@ -318,11 +348,13 @@ class ContentMoveSchema(marshmallow.Schema):
318 348
         description='id of the new parent content id.',
319 349
         allow_none=True,
320 350
         required=True,
351
+        validate=Range(min=0, error="Value must be positive or 0"),
321 352
     )
322 353
     new_workspace_id = marshmallow.fields.Int(
323 354
         example=2,
324 355
         description='id of the new workspace id.',
325
-        required=True
356
+        required=True,
357
+        validate=Range(min=1, error="Value must be greater than 0"),
326 358
     )
327 359
 
328 360
     @post_load
@@ -351,15 +383,20 @@ class ContentCreationSchema(marshmallow.Schema):
351 383
 
352 384
 
353 385
 class ContentDigestSchema(marshmallow.Schema):
354
-    content_id = marshmallow.fields.Int(example=6)
386
+    content_id = marshmallow.fields.Int(
387
+        example=6,
388
+        validate=Range(min=1, error="Value must be greater than 0"),
389
+    )
355 390
     slug = marshmallow.fields.Str(example='intervention-report-12')
356 391
     parent_id = marshmallow.fields.Int(
357 392
         example=34,
358 393
         allow_none=True,
359
-        default=None
394
+        default=None,
395
+        validate=Range(min=0, error="Value must be positive or 0"),
360 396
     )
361 397
     workspace_id = marshmallow.fields.Int(
362 398
         example=19,
399
+        validate=Range(min=1, error="Value must be greater than 0"),
363 400
     )
364 401
     label = marshmallow.fields.Str(example='Intervention Report 12')
365 402
     content_type = marshmallow.fields.Str(
@@ -426,8 +463,16 @@ class TextBasedContentSchema(ContentSchema, TextBasedDataAbstractSchema):
426 463
 
427 464
 
428 465
 class RevisionSchema(ContentDigestSchema):
429
-    comment_ids = marshmallow.fields.List(marshmallow.fields.Int(example=4))
430
-    revision_id = marshmallow.fields.Int(example=12)
466
+    comment_ids = marshmallow.fields.List(
467
+        marshmallow.fields.Int(
468
+            example=4,
469
+            validate=Range(min=1, error="Value must be greater than 0"),
470
+        )
471
+    )
472
+    revision_id = marshmallow.fields.Int(
473
+        example=12,
474
+        validate=Range(min=1, error="Value must be greater than 0"),
475
+    )
431 476
     created = marshmallow.fields.DateTime(
432 477
         format=DATETIME_FORMAT,
433 478
         description='Content creation date',
@@ -440,8 +485,14 @@ class TextBasedRevisionSchema(RevisionSchema, TextBasedDataAbstractSchema):
440 485
 
441 486
 
442 487
 class CommentSchema(marshmallow.Schema):
443
-    content_id = marshmallow.fields.Int(example=6)
444
-    parent_id = marshmallow.fields.Int(example=34)
488
+    content_id = marshmallow.fields.Int(
489
+        example=6,
490
+        validate=Range(min=1, error="Value must be greater than 0"),
491
+    )
492
+    parent_id = marshmallow.fields.Int(
493
+        example=34,
494
+        validate=Range(min=0, error="Value must be positive or 0"),
495
+    )
445 496
     raw_content = marshmallow.fields.String(
446 497
         example='<p>This is just an html comment !</p>'
447 498
     )