Browse Source

Merge pull request #93 from tracim/fix/better_content_types_and_status_code

Damien Accorsi 6 years ago
parent
commit
60e88c581c
No account linked to committer's email

+ 38 - 6
tracim/__init__.py View File

@@ -1,6 +1,8 @@
1 1
 # -*- coding: utf-8 -*-
2
-import json
3
-import time
2
+try:  # Python 3.5+
3
+    from http import HTTPStatus
4
+except ImportError:
5
+    from http import client as HTTPStatus
4 6
 
5 7
 from pyramid.config import Configurator
6 8
 from pyramid.authentication import BasicAuthAuthenticationPolicy
@@ -15,6 +17,7 @@ from tracim.lib.utils.authentification import basic_auth_check_credentials
15 17
 from tracim.lib.utils.authentification import BASIC_AUTH_WEBUI_REALM
16 18
 from tracim.lib.utils.authorization import AcceptAllAuthorizationPolicy
17 19
 from tracim.lib.utils.authorization import TRACIM_DEFAULT_PERM
20
+from tracim.lib.utils.cors import add_cors_support
18 21
 from tracim.lib.webdav import WebdavAppFactory
19 22
 from tracim.views import BASE_API_V2
20 23
 from tracim.views.contents_api.html_document_controller import HTMLDocumentController  # nopep8
@@ -25,7 +28,18 @@ from tracim.views.core_api.user_controller import UserController
25 28
 from tracim.views.core_api.workspace_controller import WorkspaceController
26 29
 from tracim.views.contents_api.comment_controller import CommentController
27 30
 from tracim.views.errors import ErrorSchema
28
-from tracim.lib.utils.cors import add_cors_support
31
+from tracim.exceptions import NotAuthenticated
32
+from tracim.exceptions import InvalidId
33
+from tracim.exceptions import InsufficientUserProfile
34
+from tracim.exceptions import InsufficientUserRoleInWorkspace
35
+from tracim.exceptions import WorkspaceNotFoundInTracimRequest
36
+from tracim.exceptions import UserNotFoundInTracimRequest
37
+from tracim.exceptions import ContentNotFoundInTracimRequest
38
+from tracim.exceptions import WorkspaceNotFound
39
+from tracim.exceptions import ContentNotFound
40
+from tracim.exceptions import UserDoesNotExist
41
+from tracim.exceptions import AuthenticationFailed
42
+from tracim.exceptions import ContentTypeNotAllowed
29 43
 
30 44
 
31 45
 def web(global_config, **local_settings):
@@ -66,9 +80,27 @@ def web(global_config, **local_settings):
66 80
         debug=app_config.DEBUG,
67 81
     )
68 82
     hapic.set_context(context)
69
-    context.handle_exception(NotFound, 404)
70
-    context.handle_exception(OperationalError, 500)
71
-    context.handle_exception(Exception, 500)
83
+    # INFO - G.M - 2018-07-04 - global-context exceptions
84
+    # Not found
85
+    context.handle_exception(NotFound, HTTPStatus.NOT_FOUND)
86
+    # Bad request
87
+    context.handle_exception(WorkspaceNotFoundInTracimRequest, HTTPStatus.BAD_REQUEST)  # nopep8
88
+    context.handle_exception(UserNotFoundInTracimRequest, HTTPStatus.BAD_REQUEST)  # nopep8
89
+    context.handle_exception(ContentNotFoundInTracimRequest, HTTPStatus.BAD_REQUEST)  # nopep8
90
+    context.handle_exception(WorkspaceNotFound, HTTPStatus.BAD_REQUEST)
91
+    context.handle_exception(UserDoesNotExist, HTTPStatus.BAD_REQUEST)
92
+    context.handle_exception(ContentNotFound, HTTPStatus.BAD_REQUEST)
93
+    context.handle_exception(ContentTypeNotAllowed, HTTPStatus.BAD_REQUEST)
94
+    context.handle_exception(InvalidId, HTTPStatus.BAD_REQUEST)
95
+    # Auth exception
96
+    context.handle_exception(NotAuthenticated, HTTPStatus.UNAUTHORIZED)
97
+    context.handle_exception(AuthenticationFailed, HTTPStatus.FORBIDDEN)
98
+    context.handle_exception(InsufficientUserRoleInWorkspace, HTTPStatus.FORBIDDEN)  # nopep8
99
+    context.handle_exception(InsufficientUserProfile, HTTPStatus.FORBIDDEN)
100
+    # Internal server error
101
+    context.handle_exception(OperationalError, HTTPStatus.INTERNAL_SERVER_ERROR)
102
+    context.handle_exception(Exception, HTTPStatus.INTERNAL_SERVER_ERROR)
103
+
72 104
     # Add controllers
73 105
     session_controller = SessionController()
74 106
     system_controller = SystemController()

+ 31 - 0
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
 
@@ -131,3 +150,15 @@ class ContentTypeNotAllowed(TracimException):
131 150
 
132 151
 class WorkspacesDoNotMatch(TracimException):
133 152
     pass
153
+
154
+
155
+class EmptyValueNotAllowed(TracimException):
156
+    pass
157
+
158
+
159
+class EmptyLabelNotAllowed(EmptyValueNotAllowed):
160
+    pass
161
+
162
+
163
+class EmptyCommentContentNotAllowed(EmptyValueNotAllowed):
164
+    pass

+ 31 - 7
tracim/lib/core/content.py View File

@@ -5,7 +5,6 @@ import datetime
5 5
 import re
6 6
 import typing
7 7
 from operator import itemgetter
8
-from operator import not_
9 8
 
10 9
 import transaction
11 10
 from sqlalchemy import func
@@ -26,6 +25,8 @@ from sqlalchemy.sql.elements import and_
26 25
 from tracim.lib.utils.utils import cmp_to_key
27 26
 from tracim.lib.core.notifications import NotifierFactory
28 27
 from tracim.exceptions import SameValueError
28
+from tracim.exceptions import EmptyCommentContentNotAllowed
29
+from tracim.exceptions import EmptyLabelNotAllowed
29 30
 from tracim.exceptions import ContentNotFound
30 31
 from tracim.exceptions import WorkspacesDoNotMatch
31 32
 from tracim.lib.utils.utils import current_date_for_filename
@@ -393,18 +394,34 @@ class ContentApi(object):
393 394
 
394 395
         return result
395 396
 
396
-    def create(self, content_type: str, workspace: Workspace, parent: Content=None, label:str ='', do_save=False, is_temporary: bool=False, do_notify=True) -> Content:
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()
406
+
407
+        if filename:
408
+            # INFO - G.M - 2018-07-04 - File_name setting automatically
409
+            # set label and file_extension
410
+            content.file_name = label
411
+        elif label:
412
+            content.label = label
413
+        else:
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
420
+
403 421
         content.owner = self._user
404 422
         content.parent = parent
405 423
         content.workspace = workspace
406 424
         content.type = content_type
407
-        content.label = label
408 425
         content.is_temporary = is_temporary
409 426
         content.revision_type = ActionDescription.CREATION
410 427
 
@@ -419,13 +436,14 @@ class ContentApi(object):
419 436
             self.save(content, ActionDescription.CREATION, do_notify=do_notify)
420 437
         return content
421 438
 
422
-
423 439
     def create_comment(self, workspace: Workspace=None, parent: Content=None, content:str ='', do_save=False) -> Content:
424
-        assert parent  and parent.type!=ContentType.Folder
440
+        assert parent and parent.type != ContentType.Folder
441
+        if not content:
442
+            raise EmptyCommentContentNotAllowed()
425 443
         item = Content()
426 444
         item.owner = self._user
427 445
         item.parent = parent
428
-        if parent and not workspace:
446
+        if not workspace:
429 447
             workspace = item.parent.workspace
430 448
         item.workspace = workspace
431 449
         item.type = ContentType.Comment
@@ -437,7 +455,6 @@ class ContentApi(object):
437 455
             self.save(item, ActionDescription.COMMENT)
438 456
         return item
439 457
 
440
-
441 458
     def get_one_from_revision(self, content_id: int, content_type: str, workspace: Workspace=None, revision_id=None) -> Content:
442 459
         """
443 460
         This method is a hack to convert a node revision item into a node
@@ -474,6 +491,11 @@ class ContentApi(object):
474 491
         try:
475 492
             content = base_request.one()
476 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.
477 499
             raise ContentNotFound('Content "{}" not found in database'.format(content_id)) from exc  # nopep8
478 500
         return content
479 501
 
@@ -972,6 +994,8 @@ class ContentApi(object):
972 994
             # TODO - G.M - 20-03-2018 - Fix internatization for webdav access.
973 995
             # Internatization disabled in libcontent for now.
974 996
             raise SameValueError('The content did not changed')
997
+        if not new_label:
998
+            raise EmptyLabelNotAllowed()
975 999
         item.owner = self._user
976 1000
         item.label = new_label
977 1001
         item.description = new_content if new_content else item.description # TODO: convert urls into links

+ 25 - 9
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,6 +219,9 @@ class TracimRequest(Request):
214 219
         comment_id = ''
215 220
         try:
216 221
             if 'comment_id' in request.matchdict:
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
217 225
                 comment_id = int(request.matchdict['comment_id'])
218 226
             if not comment_id:
219 227
                 raise ContentNotFoundInTracimRequest('No comment_id property found in request')  # nopep8
@@ -228,8 +236,6 @@ class TracimRequest(Request):
228 236
                 workspace=workspace,
229 237
                 parent=content,
230 238
             )
231
-        except JSONDecodeError as exc:
232
-            raise ContentNotFound('Invalid JSON content') from exc
233 239
         except NoResultFound as exc:
234 240
             raise ContentNotFound(
235 241
                 'Comment {} does not exist '
@@ -253,6 +259,9 @@ class TracimRequest(Request):
253 259
         content_id = ''
254 260
         try:
255 261
             if 'content_id' in request.matchdict:
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
256 265
                 content_id = int(request.matchdict['content_id'])
257 266
             if not content_id:
258 267
                 raise ContentNotFoundInTracimRequest('No content_id property found in request')  # nopep8
@@ -262,8 +271,6 @@ class TracimRequest(Request):
262 271
                 config=request.registry.settings['CFG']
263 272
             )
264 273
             content = api.get_one(content_id=content_id, workspace=workspace, content_type=ContentType.Any)  # nopep8
265
-        except JSONDecodeError as exc:
266
-            raise ContentNotFound('Invalid JSON content') from exc
267 274
         except NoResultFound as exc:
268 275
             raise ContentNotFound(
269 276
                 'Content {} does not exist '
@@ -286,7 +293,10 @@ class TracimRequest(Request):
286 293
         try:
287 294
             login = None
288 295
             if 'user_id' in request.matchdict:
289
-                login = request.matchdict['user_id']
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
299
+                login = int(request.matchdict['user_id'])
290 300
             if not login:
291 301
                 raise UserNotFoundInTracimRequest('You request a candidate user but the context not permit to found one')  # nopep8
292 302
             user = uapi.get_one(login)
@@ -329,7 +339,10 @@ class TracimRequest(Request):
329 339
         workspace_id = ''
330 340
         try:
331 341
             if 'workspace_id' in request.matchdict:
332
-                workspace_id = request.matchdict['workspace_id']
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
345
+                workspace_id = int(request.matchdict['workspace_id'])
333 346
             if not workspace_id:
334 347
                 raise WorkspaceNotFoundInTracimRequest('No workspace_id property found in request')  # nopep8
335 348
             wapi = WorkspaceApi(
@@ -338,8 +351,6 @@ class TracimRequest(Request):
338 351
                 config=request.registry.settings['CFG']
339 352
             )
340 353
             workspace = wapi.get_one(workspace_id)
341
-        except JSONDecodeError as exc:
342
-            raise WorkspaceNotFound('Invalid JSON content') from exc
343 354
         except NoResultFound as exc:
344 355
             raise WorkspaceNotFound(
345 356
                 'Workspace {} does not exist '
@@ -362,6 +373,11 @@ class TracimRequest(Request):
362 373
         try:
363 374
             if 'new_workspace_id' in request.json_body:
364 375
                 workspace_id = request.json_body['new_workspace_id']
376
+                if not isinstance(workspace_id, int):
377
+                    if workspace_id.isdecimal():
378
+                        workspace_id = int(workspace_id)
379
+                    else:
380
+                        raise InvalidWorkspaceId('workspace_id is not a correct integer')  # nopep8
365 381
             if not workspace_id:
366 382
                 raise WorkspaceNotFoundInTracimRequest('No new_workspace_id property found in body')  # nopep8
367 383
             wapi = WorkspaceApi(

+ 1 - 0
tracim/lib/webdav/utils.py View File

@@ -176,6 +176,7 @@ class FakeFileStream(object):
176 176
         is_temporary = self._file_name.startswith('.~') or self._file_name.startswith('~')
177 177
 
178 178
         file = self._api.create(
179
+            filename=self._file_name,
179 180
             content_type=ContentType.File,
180 181
             workspace=self._workspace,
181 182
             parent=self._parent,

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

@@ -110,22 +110,9 @@ class SetContentStatus(object):
110 110
         self.status = status
111 111
 
112 112
 
113
-class HTMLDocumentUpdate(object):
113
+class TextBasedContentUpdate(object):
114 114
     """
115
-    Html Document update model
116
-    """
117
-    def __init__(
118
-            self,
119
-            label: str,
120
-            raw_content: str,
121
-    ) -> None:
122
-        self.label = label
123
-        self.raw_content = raw_content
124
-
125
-
126
-class ThreadUpdate(object):
127
-    """
128
-    Thread update model
115
+    TextBasedContent update model
129 116
     """
130 117
     def __init__(
131 118
             self,
@@ -351,10 +338,6 @@ class ContentInContext(object):
351 338
         return self.content.content_id
352 339
 
353 340
     @property
354
-    def id(self) -> int:
355
-        return self.content_id
356
-
357
-    @property
358 341
     def parent_id(self) -> int:
359 342
         """
360 343
         Return parent_id of the content
@@ -458,10 +441,6 @@ class RevisionInContext(object):
458 441
         return self.revision.content_id
459 442
 
460 443
     @property
461
-    def id(self) -> int:
462
-        return self.content_id
463
-
464
-    @property
465 444
     def parent_id(self) -> int:
466 445
         """
467 446
         Return parent_id of the content

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

@@ -94,6 +94,26 @@ class TestCommentsEndpoint(FunctionalTest):
94 94
         assert len(res.json_body) == 4
95 95
         assert comment == res.json_body[3]
96 96
 
97
+    def test_api__post_content_comment__err_400__empty_raw_content(self) -> None:
98
+        """
99
+        Get alls comments of a content
100
+        """
101
+        self.testapp.authorization = (
102
+            'Basic',
103
+            (
104
+                'admin@admin.admin',
105
+                'admin@admin.admin'
106
+            )
107
+        )
108
+        params = {
109
+            'raw_content': ''
110
+        }
111
+        res = self.testapp.post_json(
112
+            '/api/v2/workspaces/2/contents/7/comments',
113
+            params=params,
114
+            status=400
115
+        )
116
+
97 117
     def test_api__delete_content_comment__ok_200__user_is_owner_and_workspace_manager(self) -> None:  # nopep8
98 118
         """
99 119
         delete comment (user is workspace_manager and owner)

+ 268 - 25
tracim/tests/functional/test_contents.py View File

@@ -13,22 +13,6 @@ class TestHtmlDocuments(FunctionalTest):
13 13
 
14 14
     fixtures = [BaseFixture, ContentFixtures]
15 15
 
16
-    def test_api__get_html_document__err_400__wrong_content_type(self) -> None:
17
-        """
18
-        Get one html document of a content
19
-        """
20
-        self.testapp.authorization = (
21
-            'Basic',
22
-            (
23
-                'admin@admin.admin',
24
-                'admin@admin.admin'
25
-            )
26
-        )
27
-        res = self.testapp.get(
28
-            '/api/v2/workspaces/2/html-documents/7',
29
-            status=400
30
-        )   # nopep8
31
-
32 16
     def test_api__get_html_document__ok_200__legacy_slug(self) -> None:
33 17
         """
34 18
         Get one html document of a content
@@ -44,7 +28,7 @@ class TestHtmlDocuments(FunctionalTest):
44 28
         res = self.testapp.get(
45 29
             '/api/v2/workspaces/2/html-documents/6',
46 30
             status=200
47
-        )   # nopep8
31
+        )
48 32
         content = res.json_body
49 33
         assert content['content_type'] == 'html-documents'
50 34
         assert content['content_id'] == 6
@@ -85,7 +69,7 @@ class TestHtmlDocuments(FunctionalTest):
85 69
         res = self.testapp.get(
86 70
             '/api/v2/workspaces/2/html-documents/6',
87 71
             status=200
88
-        )   # nopep8
72
+        )
89 73
         content = res.json_body
90 74
         assert content['content_type'] == 'html-documents'
91 75
         assert content['content_id'] == 6
@@ -112,6 +96,123 @@ class TestHtmlDocuments(FunctionalTest):
112 96
         assert content['last_modifier']['avatar_url'] is None
113 97
         assert content['raw_content'] == '<p>To cook a great Tiramisu, you need many ingredients.</p>'  # nopep8
114 98
 
99
+    def test_api__get_html_document__err_400__wrong_content_type(self) -> None:
100
+        """
101
+        Get one html document of a content content 7 is not html_document
102
+        """
103
+        self.testapp.authorization = (
104
+            'Basic',
105
+            (
106
+                'admin@admin.admin',
107
+                'admin@admin.admin'
108
+            )
109
+        )
110
+        res = self.testapp.get(
111
+            '/api/v2/workspaces/2/html-documents/7',
112
+            status=400
113
+        )
114
+
115
+    def test_api__get_html_document__err_400__content_does_not_exist(self) -> None:  # nopep8
116
+        """
117
+        Get one html document of a content (content 170 does not exist in db
118
+        """
119
+        self.testapp.authorization = (
120
+            'Basic',
121
+            (
122
+                'admin@admin.admin',
123
+                'admin@admin.admin'
124
+            )
125
+        )
126
+        res = self.testapp.get(
127
+            '/api/v2/workspaces/2/html-documents/170',
128
+            status=400
129
+        )
130
+
131
+    def test_api__get_html_document__err_400__content_not_in_workspace(self) -> None:  # nopep8
132
+        """
133
+        Get one html document of a content (content 6 is in workspace 2)
134
+        """
135
+        self.testapp.authorization = (
136
+            'Basic',
137
+            (
138
+                'admin@admin.admin',
139
+                'admin@admin.admin'
140
+            )
141
+        )
142
+        res = self.testapp.get(
143
+            '/api/v2/workspaces/1/html-documents/6',
144
+            status=400
145
+        )
146
+
147
+    def test_api__get_html_document__err_400__workspace_does_not_exist(self) -> None:  # nopep8
148
+        """
149
+        Get one html document of a content (Workspace 40 does not exist)
150
+        """
151
+        self.testapp.authorization = (
152
+            'Basic',
153
+            (
154
+                'admin@admin.admin',
155
+                'admin@admin.admin'
156
+            )
157
+        )
158
+        res = self.testapp.get(
159
+            '/api/v2/workspaces/40/html-documents/6',
160
+            status=400
161
+        )
162
+
163
+    def test_api__get_html_document__err_400__workspace_id_is_not_int(self) -> None:  # nopep8
164
+        """
165
+        Get one html document of a content, workspace id is not int
166
+        """
167
+        self.testapp.authorization = (
168
+            'Basic',
169
+            (
170
+                'admin@admin.admin',
171
+                'admin@admin.admin'
172
+            )
173
+        )
174
+        res = self.testapp.get(
175
+            '/api/v2/workspaces/coucou/html-documents/6',
176
+            status=400
177
+        )
178
+
179
+    def test_api__get_html_document__err_400__content_id_is_not_int(self) -> None:  # nopep8
180
+        """
181
+        Get one html document of a content, content_id is not int
182
+        """
183
+        self.testapp.authorization = (
184
+            'Basic',
185
+            (
186
+                'admin@admin.admin',
187
+                'admin@admin.admin'
188
+            )
189
+        )
190
+        res = self.testapp.get(
191
+            '/api/v2/workspaces/2/html-documents/coucou',
192
+            status=400
193
+        )
194
+
195
+    def test_api__update_html_document__err_400__empty_label(self) -> None:  # nopep8
196
+        """
197
+        Update(put) one html document of a content
198
+        """
199
+        self.testapp.authorization = (
200
+            'Basic',
201
+            (
202
+                'admin@admin.admin',
203
+                'admin@admin.admin'
204
+            )
205
+        )
206
+        params = {
207
+            'label': '',
208
+            'raw_content': '<p> Le nouveau contenu </p>',
209
+        }
210
+        res = self.testapp.put_json(
211
+            '/api/v2/workspaces/2/html-documents/6',
212
+            params=params,
213
+            status=400
214
+        )
215
+
115 216
     def test_api__update_html_document__ok_200__nominal_case(self) -> None:
116 217
         """
117 218
         Update(put) one html document of a content
@@ -158,7 +259,7 @@ class TestHtmlDocuments(FunctionalTest):
158 259
         res = self.testapp.get(
159 260
             '/api/v2/workspaces/2/html-documents/6',
160 261
             status=200
161
-        )   # nopep8
262
+        )
162 263
         content = res.json_body
163 264
         assert content['content_type'] == 'html-documents'
164 265
         assert content['content_id'] == 6
@@ -284,7 +385,7 @@ class TestHtmlDocuments(FunctionalTest):
284 385
         res = self.testapp.get(
285 386
             '/api/v2/workspaces/2/html-documents/6',
286 387
             status=200
287
-        )   # nopep8
388
+        )
288 389
         content = res.json_body
289 390
         assert content['content_type'] == 'html-documents'
290 391
         assert content['content_id'] == 6
@@ -301,12 +402,32 @@ class TestHtmlDocuments(FunctionalTest):
301 402
         res = self.testapp.get(
302 403
             '/api/v2/workspaces/2/html-documents/6',
303 404
             status=200
304
-        )   # nopep8
405
+        )
305 406
         content = res.json_body
306 407
         assert content['content_type'] == 'html-documents'
307 408
         assert content['content_id'] == 6
308 409
         assert content['status'] == 'closed-deprecated'
309 410
 
411
+    def test_api__set_html_document_status__err_400__wrong_status(self) -> None:
412
+        """
413
+        Get one html document of a content
414
+        """
415
+        self.testapp.authorization = (
416
+            'Basic',
417
+            (
418
+                'admin@admin.admin',
419
+                'admin@admin.admin'
420
+            )
421
+        )
422
+        params = {
423
+            'status': 'unexistant-status',
424
+        }
425
+        res = self.testapp.put_json(
426
+            '/api/v2/workspaces/2/html-documents/6/status',
427
+            params=params,
428
+            status=400
429
+        )
430
+
310 431
 
311 432
 class TestThreads(FunctionalTest):
312 433
     """
@@ -330,7 +451,7 @@ class TestThreads(FunctionalTest):
330 451
         res = self.testapp.get(
331 452
             '/api/v2/workspaces/2/threads/6',
332 453
             status=400
333
-        )   # nopep8
454
+        )
334 455
 
335 456
     def test_api__get_thread__ok_200__nominal_case(self) -> None:
336 457
         """
@@ -373,9 +494,89 @@ class TestThreads(FunctionalTest):
373 494
         assert content['last_modifier']['avatar_url'] is None
374 495
         assert content['raw_content'] == 'What is the best cake?'  # nopep8
375 496
 
497
+    def test_api__get_thread__err_400__content_does_not_exist(self) -> None:
498
+        """
499
+        Get one thread (content 170 does not exist)
500
+        """
501
+        self.testapp.authorization = (
502
+            'Basic',
503
+            (
504
+                'admin@admin.admin',
505
+                'admin@admin.admin'
506
+            )
507
+        )
508
+        res = self.testapp.get(
509
+            '/api/v2/workspaces/2/threads/170',
510
+            status=400
511
+        )
512
+
513
+    def test_api__get_thread__err_400__content_not_in_workspace(self) -> None:
514
+        """
515
+        Get one thread(content 7 is in workspace 2)
516
+        """
517
+        self.testapp.authorization = (
518
+            'Basic',
519
+            (
520
+                'admin@admin.admin',
521
+                'admin@admin.admin'
522
+            )
523
+        )
524
+        res = self.testapp.get(
525
+            '/api/v2/workspaces/1/threads/7',
526
+            status=400
527
+        )
528
+
529
+    def test_api__get_thread__err_400__workspace_does_not_exist(self) -> None:  # nopep8
530
+        """
531
+        Get one thread (Workspace 40 does not exist)
532
+        """
533
+        self.testapp.authorization = (
534
+            'Basic',
535
+            (
536
+                'admin@admin.admin',
537
+                'admin@admin.admin'
538
+            )
539
+        )
540
+        res = self.testapp.get(
541
+            '/api/v2/workspaces/40/threads/7',
542
+            status=400
543
+        )
544
+
545
+    def test_api__get_thread__err_400__workspace_id_is_not_int(self) -> None:  # nopep8
546
+        """
547
+        Get one thread, workspace id is not int
548
+        """
549
+        self.testapp.authorization = (
550
+            'Basic',
551
+            (
552
+                'admin@admin.admin',
553
+                'admin@admin.admin'
554
+            )
555
+        )
556
+        res = self.testapp.get(
557
+            '/api/v2/workspaces/coucou/threads/7',
558
+            status=400
559
+        )
560
+
561
+    def test_api__get_thread__err_400_content_id_is_not_int(self) -> None:  # nopep8
562
+        """
563
+        Get one thread, content id is not int
564
+        """
565
+        self.testapp.authorization = (
566
+            'Basic',
567
+            (
568
+                'admin@admin.admin',
569
+                'admin@admin.admin'
570
+            )
571
+        )
572
+        res = self.testapp.get(
573
+            '/api/v2/workspaces/2/threads/coucou',
574
+            status=400
575
+        )
576
+
376 577
     def test_api__update_thread__ok_200__nominal_case(self) -> None:
377 578
         """
378
-        Update(put) one html document of a content
579
+        Update(put) thread
379 580
         """
380 581
         self.testapp.authorization = (
381 582
             'Basic',
@@ -443,11 +644,32 @@ class TestThreads(FunctionalTest):
443 644
         assert content['last_modifier'] == content['author']
444 645
         assert content['raw_content'] == '<p> Le nouveau contenu </p>'
445 646
 
647
+    def test_api__update_thread__err_400__empty_label(self) -> None:
648
+        """
649
+        Update(put) thread
650
+        """
651
+        self.testapp.authorization = (
652
+            'Basic',
653
+            (
654
+                'admin@admin.admin',
655
+                'admin@admin.admin'
656
+            )
657
+        )
658
+        params = {
659
+            'label': '',
660
+            'raw_content': '<p> Le nouveau contenu </p>',
661
+        }
662
+        res = self.testapp.put_json(
663
+            '/api/v2/workspaces/2/threads/7',
664
+            params=params,
665
+            status=400
666
+        )
667
+
446 668
     def test_api__get_thread_revisions__ok_200__nominal_case(
447 669
             self
448 670
     ) -> None:
449 671
         """
450
-        Get one html document of a content
672
+        Get threads revisions
451 673
         """
452 674
         self.testapp.authorization = (
453 675
             'Basic',
@@ -505,7 +727,7 @@ class TestThreads(FunctionalTest):
505 727
 
506 728
     def test_api__set_thread_status__ok_200__nominal_case(self) -> None:
507 729
         """
508
-        Get one html document of a content
730
+        Set thread status
509 731
         """
510 732
         self.testapp.authorization = (
511 733
             'Basic',
@@ -544,3 +766,24 @@ class TestThreads(FunctionalTest):
544 766
         assert content['content_type'] == 'thread'
545 767
         assert content['content_id'] == 7
546 768
         assert content['status'] == 'closed-deprecated'
769
+
770
+    def test_api__set_thread_status__ok_400__wrong_status(self) -> None:
771
+        """
772
+        Set thread status
773
+        """
774
+        self.testapp.authorization = (
775
+            'Basic',
776
+            (
777
+                'admin@admin.admin',
778
+                'admin@admin.admin'
779
+            )
780
+        )
781
+        params = {
782
+            'status': 'unexistant-status',
783
+        }
784
+
785
+        res = self.testapp.put_json(
786
+            '/api/v2/workspaces/2/threads/7/status',
787
+            params=params,
788
+            status=400
789
+        )

+ 2 - 2
tracim/tests/functional/test_user.py View File

@@ -118,7 +118,7 @@ class TestUserWorkspaceEndpoint(FunctionalTest):
118 118
         assert 'message' in res.json.keys()
119 119
         assert 'details' in res.json.keys()
120 120
 
121
-    def test_api__get_user_workspaces__err_404__user_does_not_exist(self):
121
+    def test_api__get_user_workspaces__err_400__user_does_not_exist(self):
122 122
         """
123 123
         Check obtain all workspaces reachables for one user who does
124 124
         not exist
@@ -131,7 +131,7 @@ class TestUserWorkspaceEndpoint(FunctionalTest):
131 131
                 'admin@admin.admin'
132 132
             )
133 133
         )
134
-        res = self.testapp.get('/api/v2/users/5/workspaces', status=404)
134
+        res = self.testapp.get('/api/v2/users/5/workspaces', status=400)
135 135
         assert isinstance(res.json, dict)
136 136
         assert 'code' in res.json.keys()
137 137
         assert 'message' in res.json.keys()

+ 54 - 12
tracim/tests/functional/test_workspaces.py View File

@@ -83,7 +83,7 @@ class TestWorkspaceEndpoint(FunctionalTest):
83 83
         assert sidebar_entry['hexcolor'] == "#757575"
84 84
         assert sidebar_entry['fa_icon'] == "calendar"
85 85
 
86
-    def test_api__get_workspace__err_403__unallowed_user(self) -> None:
86
+    def test_api__get_workspace__err_400__unallowed_user(self) -> None:
87 87
         """
88 88
         Check obtain workspace unreachable for user
89 89
         """
@@ -94,7 +94,7 @@ class TestWorkspaceEndpoint(FunctionalTest):
94 94
                 'foobarbaz'
95 95
             )
96 96
         )
97
-        res = self.testapp.get('/api/v2/workspaces/1', status=403)
97
+        res = self.testapp.get('/api/v2/workspaces/1', status=400)
98 98
         assert isinstance(res.json, dict)
99 99
         assert 'code' in res.json.keys()
100 100
         assert 'message' in res.json.keys()
@@ -117,7 +117,7 @@ class TestWorkspaceEndpoint(FunctionalTest):
117 117
         assert 'message' in res.json.keys()
118 118
         assert 'details' in res.json.keys()
119 119
 
120
-    def test_api__get_workspace__err_403__workspace_does_not_exist(self) -> None:  # nopep8
120
+    def test_api__get_workspace__err_400__workspace_does_not_exist(self) -> None:  # nopep8
121 121
         """
122 122
         Check obtain workspace who does not exist with an existing user.
123 123
         """
@@ -128,7 +128,7 @@ class TestWorkspaceEndpoint(FunctionalTest):
128 128
                 'admin@admin.admin'
129 129
             )
130 130
         )
131
-        res = self.testapp.get('/api/v2/workspaces/5', status=403)
131
+        res = self.testapp.get('/api/v2/workspaces/5', status=400)
132 132
         assert isinstance(res.json, dict)
133 133
         assert 'code' in res.json.keys()
134 134
         assert 'message' in res.json.keys()
@@ -164,7 +164,7 @@ class TestWorkspaceMembersEndpoint(FunctionalTest):
164 164
         # by correct value when avatar feature will be enabled
165 165
         assert user_role['user']['avatar_url'] is None
166 166
 
167
-    def test_api__get_workspace_members__err_403__unallowed_user(self):
167
+    def test_api__get_workspace_members__err_400__unallowed_user(self):
168 168
         """
169 169
         Check obtain workspace members list with an unreachable workspace for
170 170
         user
@@ -176,7 +176,7 @@ class TestWorkspaceMembersEndpoint(FunctionalTest):
176 176
                 'foobarbaz'
177 177
             )
178 178
         )
179
-        res = self.testapp.get('/api/v2/workspaces/3/members', status=403)
179
+        res = self.testapp.get('/api/v2/workspaces/3/members', status=400)
180 180
         assert isinstance(res.json, dict)
181 181
         assert 'code' in res.json.keys()
182 182
         assert 'message' in res.json.keys()
@@ -199,7 +199,7 @@ class TestWorkspaceMembersEndpoint(FunctionalTest):
199 199
         assert 'message' in res.json.keys()
200 200
         assert 'details' in res.json.keys()
201 201
 
202
-    def test_api__get_workspace_members__err_403__workspace_does_not_exist(self):  # nopep8
202
+    def test_api__get_workspace_members__err_400__workspace_does_not_exist(self):  # nopep8
203 203
         """
204 204
         Check obtain workspace members list with an existing user but
205 205
         an unexisting workspace
@@ -211,7 +211,7 @@ class TestWorkspaceMembersEndpoint(FunctionalTest):
211 211
                 'admin@admin.admin'
212 212
             )
213 213
         )
214
-        res = self.testapp.get('/api/v2/workspaces/5/members', status=403)
214
+        res = self.testapp.get('/api/v2/workspaces/5/members', status=400)
215 215
         assert isinstance(res.json, dict)
216 216
         assert 'code' in res.json.keys()
217 217
         assert 'message' in res.json.keys()
@@ -739,7 +739,7 @@ class TestWorkspaceContents(FunctionalTest):
739 739
 
740 740
     # Error case
741 741
 
742
-    def test_api__get_workspace_content__err_403__unallowed_user(self):
742
+    def test_api__get_workspace_content__err_400__unallowed_user(self):
743 743
         """
744 744
         Check obtain workspace content list with an unreachable workspace for
745 745
         user
@@ -751,7 +751,7 @@ class TestWorkspaceContents(FunctionalTest):
751 751
                 'foobarbaz'
752 752
             )
753 753
         )
754
-        res = self.testapp.get('/api/v2/workspaces/3/contents', status=403)
754
+        res = self.testapp.get('/api/v2/workspaces/3/contents', status=400)
755 755
         assert isinstance(res.json, dict)
756 756
         assert 'code' in res.json.keys()
757 757
         assert 'message' in res.json.keys()
@@ -774,7 +774,7 @@ class TestWorkspaceContents(FunctionalTest):
774 774
         assert 'message' in res.json.keys()
775 775
         assert 'details' in res.json.keys()
776 776
 
777
-    def test_api__get_workspace_content__err_403__workspace_does_not_exist(self):  # nopep8
777
+    def test_api__get_workspace_content__err_400__workspace_does_not_exist(self):  # nopep8
778 778
         """
779 779
         Check obtain workspace contents list with an existing user but
780 780
         an unexisting workspace
@@ -786,7 +786,7 @@ class TestWorkspaceContents(FunctionalTest):
786 786
                 'admin@admin.admin'
787 787
             )
788 788
         )
789
-        res = self.testapp.get('/api/v2/workspaces/5/contents', status=403)
789
+        res = self.testapp.get('/api/v2/workspaces/5/contents', status=400)
790 790
         assert isinstance(res.json, dict)
791 791
         assert 'code' in res.json.keys()
792 792
         assert 'message' in res.json.keys()
@@ -834,6 +834,48 @@ 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__err_400__empty_label(self) -> None:  # nopep8
838
+        """
839
+        Create generic content
840
+        """
841
+        self.testapp.authorization = (
842
+            'Basic',
843
+            (
844
+                'admin@admin.admin',
845
+                'admin@admin.admin'
846
+            )
847
+        )
848
+        params = {
849
+            'label': '',
850
+            'content_type': 'markdownpage',
851
+        }
852
+        res = self.testapp.post_json(
853
+            '/api/v2/workspaces/1/contents',
854
+            params=params,
855
+            status=400
856
+        )
857
+
858
+    def test_api__post_content_create_generic_content__err_400__wrong_content_type(self) -> None:  # nopep8
859
+        """
860
+        Create generic content
861
+        """
862
+        self.testapp.authorization = (
863
+            'Basic',
864
+            (
865
+                'admin@admin.admin',
866
+                'admin@admin.admin'
867
+            )
868
+        )
869
+        params = {
870
+            'label': 'GenericCreatedContent',
871
+            'content_type': 'unexistent-content-type',
872
+        }
873
+        res = self.testapp.post_json(
874
+            '/api/v2/workspaces/1/contents',
875
+            params=params,
876
+            status=400,
877
+        )
878
+
837 879
     def test_api_put_move_content__ok_200__nominal_case(self):
838 880
         """
839 881
         Move content

+ 102 - 30
tracim/tests/library/test_content_api.py View File

@@ -128,10 +128,20 @@ class TestContentApi(DefaultTest):
128 128
             session=self.session,
129 129
             config=self.app_config,
130 130
         )
131
-        item = api.create(ContentType.Folder, workspace, None,
132
-                          'not_deleted', True)
133
-        item2 = api.create(ContentType.Folder, workspace, None,
134
-                           'to_delete', True)
131
+        item = api.create(
132
+            content_type=ContentType.Folder,
133
+            workspace=workspace,
134
+            parent=None,
135
+            label='not_deleted',
136
+            do_save=True
137
+        )
138
+        item2 = api.create(
139
+            content_type=ContentType.Folder,
140
+            workspace=workspace,
141
+            parent=None,
142
+            label='to_delete',
143
+            do_save=True
144
+        )
135 145
         uid = user.user_id
136 146
         wid = workspace.workspace_id
137 147
         transaction.commit()
@@ -229,10 +239,20 @@ class TestContentApi(DefaultTest):
229 239
             session=self.session,
230 240
             config=self.app_config,
231 241
         )
232
-        item = api.create(ContentType.Folder, workspace, None,
233
-                          'not_archived', True)
234
-        item2 = api.create(ContentType.Folder, workspace, None,
235
-                           'to_archive', True)
242
+        item = api.create(
243
+            content_type=ContentType.Folder,
244
+            workspace=workspace,
245
+            parent=None,
246
+            label='not_archived',
247
+            do_save=True
248
+        )
249
+        item2 = api.create(
250
+            content_type=ContentType.Folder,
251
+            workspace=workspace,
252
+            parent=None,
253
+            label='to_archive',
254
+            do_save=True
255
+        )
236 256
         uid = user.user_id
237 257
         wid = workspace.workspace_id
238 258
         transaction.commit()
@@ -337,9 +357,20 @@ class TestContentApi(DefaultTest):
337 357
             session=self.session,
338 358
             config=self.app_config,
339 359
         )
340
-        item = api.create(ContentType.Folder, workspace, None,
341
-                          'thefolder', True)
342
-        item2 = api.create(ContentType.File, workspace, None, 'thefile', True)
360
+        item = api.create(
361
+            content_type=ContentType.Folder,
362
+            workspace=workspace,
363
+            parent=None,
364
+            label='thefolder',
365
+            do_save=True
366
+        )
367
+        item2 = api.create(
368
+            content_type=ContentType.File,
369
+            workspace=workspace,
370
+            parent=None,
371
+            label='thefile',
372
+            do_save=True
373
+        )
343 374
         uid = user.user_id
344 375
         wid = workspace.workspace_id
345 376
         transaction.commit()
@@ -475,7 +506,7 @@ class TestContentApi(DefaultTest):
475 506
             session=self.session,
476 507
             config=self.app_config,
477 508
         )
478
-        c = api.create(ContentType.Folder, workspace, None, 'parent', True)
509
+        c = api.create(ContentType.Folder, workspace, None, 'parent', '', True)
479 510
         with new_revision(
480 511
             session=self.session,
481 512
             tm=transaction.manager,
@@ -515,7 +546,7 @@ class TestContentApi(DefaultTest):
515 546
             session=self.session,
516 547
             config=self.app_config,
517 548
         )
518
-        c = api.create(ContentType.Folder, workspace, None, 'parent', True)
549
+        c = api.create(ContentType.Folder, workspace, None, 'parent', '', True)
519 550
         with new_revision(
520 551
             session=self.session,
521 552
             tm=transaction.manager,
@@ -625,6 +656,7 @@ class TestContentApi(DefaultTest):
625 656
             workspace,
626 657
             None,
627 658
             'folder a',
659
+            '',
628 660
             True
629 661
         )
630 662
         with self.session.no_autoflush:
@@ -661,6 +693,7 @@ class TestContentApi(DefaultTest):
661 693
             workspace2,
662 694
             None,
663 695
             'folder b',
696
+            '',
664 697
             True
665 698
         )
666 699
 
@@ -744,6 +777,7 @@ class TestContentApi(DefaultTest):
744 777
             workspace,
745 778
             None,
746 779
             'folder a',
780
+            '',
747 781
             True
748 782
         )
749 783
         with self.session.no_autoflush:
@@ -780,6 +814,7 @@ class TestContentApi(DefaultTest):
780 814
             workspace2,
781 815
             None,
782 816
             'folder b',
817
+            '',
783 818
             True
784 819
         )
785 820
         api2.copy(
@@ -860,6 +895,7 @@ class TestContentApi(DefaultTest):
860 895
             workspace,
861 896
             None,
862 897
             'folder a',
898
+            '',
863 899
             True
864 900
         )
865 901
         with self.session.no_autoflush:
@@ -1248,8 +1284,13 @@ class TestContentApi(DefaultTest):
1248 1284
             config=self.app_config,
1249 1285
         )
1250 1286
 
1251
-        p = api.create(ContentType.Page, workspace, None,
1252
-                       'this_is_a_page', True)
1287
+        p = api.create(
1288
+            content_type=ContentType.Page,
1289
+            workspace=workspace,
1290
+            parent=None,
1291
+            label='this_is_a_page',
1292
+            do_save=True
1293
+        )
1253 1294
 
1254 1295
         u1id = user1.user_id
1255 1296
         u2id = user2.user_id
@@ -1453,8 +1494,13 @@ class TestContentApi(DefaultTest):
1453 1494
             session=self.session,
1454 1495
             config=self.app_config,
1455 1496
         )
1456
-        p = api.create(ContentType.File, workspace, None,
1457
-                       'this_is_a_page', True)
1497
+        p = api.create(
1498
+            content_type=ContentType.File,
1499
+            workspace=workspace,
1500
+            parent=None,
1501
+            label='this_is_a_page',
1502
+            do_save=True
1503
+        )
1458 1504
 
1459 1505
         u1id = user1.user_id
1460 1506
         u2id = user2.user_id
@@ -1666,8 +1712,13 @@ class TestContentApi(DefaultTest):
1666 1712
             show_archived=True,
1667 1713
             config=self.app_config,
1668 1714
         )
1669
-        p = api.create(ContentType.File, workspace, None,
1670
-                       'this_is_a_page', True)
1715
+        p = api.create(
1716
+            content_type=ContentType.File,
1717
+            workspace=workspace,
1718
+            parent=None,
1719
+            label='this_is_a_page',
1720
+            do_save=True
1721
+        )
1671 1722
 
1672 1723
         u1id = user1.user_id
1673 1724
         u2id = user2.user_id
@@ -1822,8 +1873,13 @@ class TestContentApi(DefaultTest):
1822 1873
             config=self.app_config,
1823 1874
             show_deleted=True,
1824 1875
         )
1825
-        p = api.create(ContentType.File, workspace, None,
1826
-                       'this_is_a_page', True)
1876
+        p = api.create(
1877
+            content_type=ContentType.File,
1878
+            workspace=workspace,
1879
+            parent=None,
1880
+            label='this_is_a_page',
1881
+            do_save=True
1882
+        )
1827 1883
 
1828 1884
         u1id = user1.user_id
1829 1885
         u2id = user2.user_id
@@ -1957,9 +2013,9 @@ class TestContentApi(DefaultTest):
1957 2013
             config=self.app_config,
1958 2014
         )
1959 2015
         a = api.create(ContentType.Folder, workspace, None,
1960
-                       'this is randomized folder', True)
2016
+                       'this is randomized folder', '', True)
1961 2017
         p = api.create(ContentType.Page, workspace, a,
1962
-                       'this is randomized label content', True)
2018
+                       'this is randomized label content', '', True)
1963 2019
 
1964 2020
         with new_revision(
1965 2021
             session=self.session,
@@ -2013,9 +2069,9 @@ class TestContentApi(DefaultTest):
2013 2069
             config=self.app_config,
2014 2070
         )
2015 2071
         a = api.create(ContentType.Folder, workspace, None,
2016
-                       'this is randomized folder', True)
2072
+                       'this is randomized folder', '', True)
2017 2073
         p = api.create(ContentType.Page, workspace, a,
2018
-                       'this is dummy label content', True)
2074
+                       'this is dummy label content', '', True)
2019 2075
 
2020 2076
         with new_revision(
2021 2077
             tm=transaction.manager,
@@ -2065,11 +2121,27 @@ class TestContentApi(DefaultTest):
2065 2121
             session=self.session,
2066 2122
             config=self.app_config,
2067 2123
         )
2068
-        a = api.create(ContentType.Folder, workspace, None,
2069
-                       'this is randomized folder', True)
2070
-        p1 = api.create(ContentType.Page, workspace, a,
2071
-                        'this is dummy label content', True)
2072
-        p2 = api.create(ContentType.Page, workspace, a, 'Hey ! Jon !', True)
2124
+        a = api.create(
2125
+            content_type=ContentType.Folder,
2126
+            workspace=workspace,
2127
+            parent=None,
2128
+            label='this is randomized folder',
2129
+            do_save=True
2130
+        )
2131
+        p1 = api.create(
2132
+            content_type=ContentType.Page,
2133
+            workspace=workspace,
2134
+            parent=a,
2135
+            label='this is dummy label content',
2136
+            do_save=True
2137
+        )
2138
+        p2 = api.create(
2139
+            content_type=ContentType.Page,
2140
+            workspace=workspace,
2141
+            parent=a,
2142
+            label='Hey ! Jon !',
2143
+            do_save=True
2144
+        )
2073 2145
 
2074 2146
         with new_revision(
2075 2147
             session=self.session,

+ 1 - 0
tracim/tests/library/test_webdav.py View File

@@ -95,6 +95,7 @@ class TestWebDav(StandardTest):
95 95
                        self.session,
96 96
                        self.app_config
97 97
                        ).get_one_by_email(email)
98
+
98 99
     def _put_new_text_file(
99 100
             self,
100 101
             provider,

+ 4 - 18
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
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
@@ -33,13 +30,9 @@ COMMENT_ENDPOINTS_TAG = 'Comments'
33 30
 class CommentController(Controller):
34 31
 
35 32
     @hapic.with_api_doc(tags=[COMMENT_ENDPOINTS_TAG])
36
-    @hapic.handle_exception(NotAuthenticated, HTTPStatus.UNAUTHORIZED)
37
-    @hapic.handle_exception(InsufficientUserRoleInWorkspace, HTTPStatus.FORBIDDEN)
38
-    @hapic.handle_exception(WorkspaceNotFound, HTTPStatus.FORBIDDEN)
39
-    @hapic.handle_exception(AuthenticationFailed, HTTPStatus.FORBIDDEN)
40 33
     @require_workspace_role(UserRoleInWorkspace.READER)
41 34
     @hapic.input_path(WorkspaceAndContentIdPathSchema())
42
-    @hapic.output_body(CommentSchema(many=True),)
35
+    @hapic.output_body(CommentSchema(many=True))
43 36
     def content_comments(self, context, request: TracimRequest, hapic_data=None):
44 37
         """
45 38
         Get all comments related to a content in asc order (first is the oldest)
@@ -63,14 +56,11 @@ class CommentController(Controller):
63 56
         ]
64 57
 
65 58
     @hapic.with_api_doc(tags=[COMMENT_ENDPOINTS_TAG])
66
-    @hapic.handle_exception(NotAuthenticated, HTTPStatus.UNAUTHORIZED)
67
-    @hapic.handle_exception(InsufficientUserRoleInWorkspace, HTTPStatus.FORBIDDEN)
68
-    @hapic.handle_exception(WorkspaceNotFound, HTTPStatus.FORBIDDEN)
69
-    @hapic.handle_exception(AuthenticationFailed, HTTPStatus.FORBIDDEN)
59
+    @hapic.handle_exception(EmptyCommentContentNotAllowed, HTTPStatus.BAD_REQUEST)  # nopep8
70 60
     @require_workspace_role(UserRoleInWorkspace.CONTRIBUTOR)
71 61
     @hapic.input_path(WorkspaceAndContentIdPathSchema())
72 62
     @hapic.input_body(SetCommentSchema())
73
-    @hapic.output_body(CommentSchema(),)
63
+    @hapic.output_body(CommentSchema())
74 64
     def add_comment(self, context, request: TracimRequest, hapic_data=None):
75 65
         """
76 66
         Add new comment
@@ -95,10 +85,6 @@ class CommentController(Controller):
95 85
         return api.get_content_in_context(comment)
96 86
 
97 87
     @hapic.with_api_doc(tags=[COMMENT_ENDPOINTS_TAG])
98
-    @hapic.handle_exception(NotAuthenticated, HTTPStatus.UNAUTHORIZED)
99
-    @hapic.handle_exception(InsufficientUserRoleInWorkspace, HTTPStatus.FORBIDDEN)
100
-    @hapic.handle_exception(WorkspaceNotFound, HTTPStatus.FORBIDDEN)
101
-    @hapic.handle_exception(AuthenticationFailed, HTTPStatus.FORBIDDEN)
102 88
     @require_comment_ownership_or_role(
103 89
         minimal_required_role_for_anyone=UserRoleInWorkspace.WORKSPACE_MANAGER,
104 90
         minimal_required_role_for_owner=UserRoleInWorkspace.CONTRIBUTOR,

+ 9 - 29
tracim/views/contents_api/html_document_controller.py View File

@@ -15,19 +15,15 @@ from tracim import TracimRequest
15 15
 from tracim.extensions import hapic
16 16
 from tracim.lib.core.content import ContentApi
17 17
 from tracim.views.controllers import Controller
18
-from tracim.views.core_api.schemas import HtmlDocumentContentSchema
19
-from tracim.views.core_api.schemas import HtmlDocumentRevisionSchema
18
+from tracim.views.core_api.schemas import TextBasedContentSchema
19
+from tracim.views.core_api.schemas import TextBasedRevisionSchema
20
+from tracim.views.core_api.schemas import TextBasedContentModifySchema
20 21
 from tracim.views.core_api.schemas import SetContentStatusSchema
21
-from tracim.views.core_api.schemas import HtmlDocumentModifySchema
22 22
 from tracim.views.core_api.schemas import WorkspaceAndContentIdPathSchema
23 23
 from tracim.views.core_api.schemas import NoContentSchema
24 24
 from tracim.lib.utils.authorization import require_content_types
25 25
 from tracim.lib.utils.authorization import require_workspace_role
26
-from tracim.exceptions import WorkspaceNotFound
27
-from tracim.exceptions import ContentTypeNotAllowed
28
-from tracim.exceptions import InsufficientUserRoleInWorkspace
29
-from tracim.exceptions import NotAuthenticated
30
-from tracim.exceptions import AuthenticationFailed
26
+from tracim.exceptions import EmptyLabelNotAllowed
31 27
 from tracim.models.context_models import ContentInContext
32 28
 from tracim.models.context_models import RevisionInContext
33 29
 from tracim.models.contents import ContentTypeLegacy as ContentType
@@ -40,15 +36,10 @@ HTML_DOCUMENT_ENDPOINTS_TAG = 'HTML documents'
40 36
 class HTMLDocumentController(Controller):
41 37
 
42 38
     @hapic.with_api_doc(tags=[HTML_DOCUMENT_ENDPOINTS_TAG])
43
-    @hapic.handle_exception(NotAuthenticated, HTTPStatus.UNAUTHORIZED)
44
-    @hapic.handle_exception(InsufficientUserRoleInWorkspace, HTTPStatus.FORBIDDEN)
45
-    @hapic.handle_exception(WorkspaceNotFound, HTTPStatus.FORBIDDEN)
46
-    @hapic.handle_exception(AuthenticationFailed, HTTPStatus.FORBIDDEN)
47
-    @hapic.handle_exception(ContentTypeNotAllowed, HTTPStatus.BAD_REQUEST)
48 39
     @require_workspace_role(UserRoleInWorkspace.READER)
49 40
     @require_content_types([html_documents_type])
50 41
     @hapic.input_path(WorkspaceAndContentIdPathSchema())
51
-    @hapic.output_body(HtmlDocumentContentSchema())
42
+    @hapic.output_body(TextBasedContentSchema())
52 43
     def get_html_document(self, context, request: TracimRequest, hapic_data=None) -> ContentInContext:  # nopep8
53 44
         """
54 45
         Get html document content
@@ -66,15 +57,12 @@ class HTMLDocumentController(Controller):
66 57
         return api.get_content_in_context(content)
67 58
 
68 59
     @hapic.with_api_doc(tags=[HTML_DOCUMENT_ENDPOINTS_TAG])
69
-    @hapic.handle_exception(NotAuthenticated, HTTPStatus.UNAUTHORIZED)
70
-    @hapic.handle_exception(InsufficientUserRoleInWorkspace, HTTPStatus.FORBIDDEN)
71
-    @hapic.handle_exception(WorkspaceNotFound, HTTPStatus.FORBIDDEN)
72
-    @hapic.handle_exception(AuthenticationFailed, HTTPStatus.FORBIDDEN)
60
+    @hapic.handle_exception(EmptyLabelNotAllowed, HTTPStatus.BAD_REQUEST)
73 61
     @require_workspace_role(UserRoleInWorkspace.CONTRIBUTOR)
74 62
     @require_content_types([html_documents_type])
75 63
     @hapic.input_path(WorkspaceAndContentIdPathSchema())
76
-    @hapic.input_body(HtmlDocumentModifySchema())
77
-    @hapic.output_body(HtmlDocumentContentSchema())
64
+    @hapic.input_body(TextBasedContentModifySchema())
65
+    @hapic.output_body(TextBasedContentSchema())
78 66
     def update_html_document(self, context, request: TracimRequest, hapic_data=None) -> ContentInContext:  # nopep8
79 67
         """
80 68
         update_html_document
@@ -104,14 +92,10 @@ class HTMLDocumentController(Controller):
104 92
         return api.get_content_in_context(content)
105 93
 
106 94
     @hapic.with_api_doc(tags=[HTML_DOCUMENT_ENDPOINTS_TAG])
107
-    @hapic.handle_exception(NotAuthenticated, HTTPStatus.UNAUTHORIZED)
108
-    @hapic.handle_exception(InsufficientUserRoleInWorkspace, HTTPStatus.FORBIDDEN)
109
-    @hapic.handle_exception(WorkspaceNotFound, HTTPStatus.FORBIDDEN)
110
-    @hapic.handle_exception(AuthenticationFailed, HTTPStatus.FORBIDDEN)
111 95
     @require_workspace_role(UserRoleInWorkspace.READER)
112 96
     @require_content_types([html_documents_type])
113 97
     @hapic.input_path(WorkspaceAndContentIdPathSchema())
114
-    @hapic.output_body(HtmlDocumentRevisionSchema(many=True))
98
+    @hapic.output_body(TextBasedRevisionSchema(many=True))
115 99
     def get_html_document_revisions(
116 100
             self,
117 101
             context,
@@ -138,10 +122,6 @@ class HTMLDocumentController(Controller):
138 122
         ]
139 123
 
140 124
     @hapic.with_api_doc(tags=[HTML_DOCUMENT_ENDPOINTS_TAG])
141
-    @hapic.handle_exception(NotAuthenticated, HTTPStatus.UNAUTHORIZED)
142
-    @hapic.handle_exception(InsufficientUserRoleInWorkspace, HTTPStatus.FORBIDDEN)
143
-    @hapic.handle_exception(WorkspaceNotFound, HTTPStatus.FORBIDDEN)
144
-    @hapic.handle_exception(AuthenticationFailed, HTTPStatus.FORBIDDEN)
145 125
     @require_workspace_role(UserRoleInWorkspace.CONTRIBUTOR)
146 126
     @require_content_types([html_documents_type])
147 127
     @hapic.input_path(WorkspaceAndContentIdPathSchema())

+ 9 - 28
tracim/views/contents_api/threads_controller.py View File

@@ -14,18 +14,15 @@ from tracim import TracimRequest
14 14
 from tracim.extensions import hapic
15 15
 from tracim.lib.core.content import ContentApi
16 16
 from tracim.views.controllers import Controller
17
-from tracim.views.core_api.schemas import ThreadContentSchema
18
-from tracim.views.core_api.schemas import ThreadRevisionSchema
17
+from tracim.views.core_api.schemas import TextBasedContentSchema
18
+from tracim.views.core_api.schemas import TextBasedRevisionSchema
19 19
 from tracim.views.core_api.schemas import SetContentStatusSchema
20
-from tracim.views.core_api.schemas import ThreadModifySchema
20
+from tracim.views.core_api.schemas import TextBasedContentModifySchema
21 21
 from tracim.views.core_api.schemas import WorkspaceAndContentIdPathSchema
22 22
 from tracim.views.core_api.schemas import NoContentSchema
23 23
 from tracim.lib.utils.authorization import require_content_types
24 24
 from tracim.lib.utils.authorization import require_workspace_role
25
-from tracim.exceptions import WorkspaceNotFound, ContentTypeNotAllowed
26
-from tracim.exceptions import InsufficientUserRoleInWorkspace
27
-from tracim.exceptions import NotAuthenticated
28
-from tracim.exceptions import AuthenticationFailed
25
+from tracim.exceptions import EmptyLabelNotAllowed
29 26
 from tracim.models.context_models import ContentInContext
30 27
 from tracim.models.context_models import RevisionInContext
31 28
 from tracim.models.contents import ContentTypeLegacy as ContentType
@@ -38,15 +35,10 @@ THREAD_ENDPOINTS_TAG = 'Threads'
38 35
 class ThreadController(Controller):
39 36
 
40 37
     @hapic.with_api_doc(tags=[THREAD_ENDPOINTS_TAG])
41
-    @hapic.handle_exception(NotAuthenticated, HTTPStatus.UNAUTHORIZED)
42
-    @hapic.handle_exception(InsufficientUserRoleInWorkspace, HTTPStatus.FORBIDDEN)
43
-    @hapic.handle_exception(WorkspaceNotFound, HTTPStatus.FORBIDDEN)
44
-    @hapic.handle_exception(AuthenticationFailed, HTTPStatus.FORBIDDEN)
45
-    @hapic.handle_exception(ContentTypeNotAllowed, HTTPStatus.BAD_REQUEST)
46 38
     @require_workspace_role(UserRoleInWorkspace.READER)
47 39
     @require_content_types([thread_type])
48 40
     @hapic.input_path(WorkspaceAndContentIdPathSchema())
49
-    @hapic.output_body(ThreadContentSchema())
41
+    @hapic.output_body(TextBasedContentSchema())
50 42
     def get_thread(self, context, request: TracimRequest, hapic_data=None) -> ContentInContext:  # nopep8
51 43
         """
52 44
         Get thread content
@@ -64,15 +56,12 @@ class ThreadController(Controller):
64 56
         return api.get_content_in_context(content)
65 57
 
66 58
     @hapic.with_api_doc(tags=[THREAD_ENDPOINTS_TAG])
67
-    @hapic.handle_exception(NotAuthenticated, HTTPStatus.UNAUTHORIZED)
68
-    @hapic.handle_exception(InsufficientUserRoleInWorkspace, HTTPStatus.FORBIDDEN)
69
-    @hapic.handle_exception(WorkspaceNotFound, HTTPStatus.FORBIDDEN)
70
-    @hapic.handle_exception(AuthenticationFailed, HTTPStatus.FORBIDDEN)
59
+    @hapic.handle_exception(EmptyLabelNotAllowed, HTTPStatus.BAD_REQUEST)
71 60
     @require_workspace_role(UserRoleInWorkspace.CONTRIBUTOR)
72 61
     @require_content_types([thread_type])
73 62
     @hapic.input_path(WorkspaceAndContentIdPathSchema())
74
-    @hapic.input_body(ThreadModifySchema())
75
-    @hapic.output_body(ThreadContentSchema())
63
+    @hapic.input_body(TextBasedContentModifySchema())
64
+    @hapic.output_body(TextBasedContentSchema())
76 65
     def update_thread(self, context, request: TracimRequest, hapic_data=None) -> ContentInContext:  # nopep8
77 66
         """
78 67
         update thread
@@ -102,14 +91,10 @@ class ThreadController(Controller):
102 91
         return api.get_content_in_context(content)
103 92
 
104 93
     @hapic.with_api_doc(tags=[THREAD_ENDPOINTS_TAG])
105
-    @hapic.handle_exception(NotAuthenticated, HTTPStatus.UNAUTHORIZED)
106
-    @hapic.handle_exception(InsufficientUserRoleInWorkspace, HTTPStatus.FORBIDDEN)
107
-    @hapic.handle_exception(WorkspaceNotFound, HTTPStatus.FORBIDDEN)
108
-    @hapic.handle_exception(AuthenticationFailed, HTTPStatus.FORBIDDEN)
109 94
     @require_workspace_role(UserRoleInWorkspace.READER)
110 95
     @require_content_types([thread_type])
111 96
     @hapic.input_path(WorkspaceAndContentIdPathSchema())
112
-    @hapic.output_body(ThreadRevisionSchema(many=True))
97
+    @hapic.output_body(TextBasedRevisionSchema(many=True))
113 98
     def get_thread_revisions(
114 99
             self,
115 100
             context,
@@ -136,10 +121,6 @@ class ThreadController(Controller):
136 121
         ]
137 122
 
138 123
     @hapic.with_api_doc(tags=[THREAD_ENDPOINTS_TAG])
139
-    @hapic.handle_exception(NotAuthenticated, HTTPStatus.UNAUTHORIZED)
140
-    @hapic.handle_exception(InsufficientUserRoleInWorkspace, HTTPStatus.FORBIDDEN)
141
-    @hapic.handle_exception(WorkspaceNotFound, HTTPStatus.FORBIDDEN)
142
-    @hapic.handle_exception(AuthenticationFailed, HTTPStatus.FORBIDDEN)
143 124
     @require_workspace_role(UserRoleInWorkspace.CONTRIBUTOR)
144 125
     @require_content_types([thread_type])
145 126
     @hapic.input_path(WorkspaceAndContentIdPathSchema())

+ 106 - 80
tracim/views/core_api/schemas.py View File

@@ -2,23 +2,23 @@
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
8
-from tracim.models.contents import CONTENT_DEFAULT_TYPE
9
-from tracim.models.contents import CONTENT_DEFAULT_STATUS
10 9
 from tracim.models.contents import GlobalStatus
11 10
 from tracim.models.contents import open_status
11
+from tracim.models.contents import ContentTypeLegacy as ContentType
12
+from tracim.models.contents import ContentStatusLegacy as ContentStatus
12 13
 from tracim.models.context_models import ContentCreation
13
-from tracim.models.context_models import SetContentStatus
14 14
 from tracim.models.context_models import CommentCreation
15
+from tracim.models.context_models import TextBasedContentUpdate
16
+from tracim.models.context_models import SetContentStatus
15 17
 from tracim.models.context_models import CommentPath
16 18
 from tracim.models.context_models import MoveParams
17 19
 from tracim.models.context_models import WorkspaceAndContentPath
18 20
 from tracim.models.context_models import ContentFilter
19 21
 from tracim.models.context_models import LoginCredentials
20
-from tracim.models.context_models import HTMLDocumentUpdate
21
-from tracim.models.context_models import ThreadUpdate
22 22
 from tracim.models.data import UserRoleInWorkspace
23 23
 
24 24
 
@@ -80,15 +80,30 @@ class UserSchema(UserDigestSchema):
80 80
 
81 81
 
82 82
 class UserIdPathSchema(marshmallow.Schema):
83
-    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
+    )
84 89
 
85 90
 
86 91
 class WorkspaceIdPathSchema(marshmallow.Schema):
87
-    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
+    )
88 98
 
89 99
 
90 100
 class ContentIdPathSchema(marshmallow.Schema):
91
-    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
+    )
92 107
 
93 108
 
94 109
 class WorkspaceAndContentIdPathSchema(
@@ -103,8 +118,9 @@ class WorkspaceAndContentIdPathSchema(
103 118
 class CommentsPathSchema(WorkspaceAndContentIdPathSchema):
104 119
     comment_id = marshmallow.fields.Int(
105 120
         example=6,
106
-        description='id of a comment related to content content_id',
107
-        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"),
108 124
     )
109 125
     @post_load
110 126
     def make_path_object(self, data):
@@ -119,19 +135,22 @@ class FilterContentQuerySchema(marshmallow.Schema):
119 135
                     ' If not set, then return all contents.'
120 136
                     ' If set to 0, then return root contents.'
121 137
                     ' If set to another value, return all contents'
122
-                    ' 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"),
123 140
     )
124 141
     show_archived = marshmallow.fields.Int(
125 142
         example=0,
126 143
         default=0,
127 144
         description='if set to 1, then show archived contents.'
128
-                    ' 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"),
129 147
     )
130 148
     show_deleted = marshmallow.fields.Int(
131 149
         example=0,
132 150
         default=0,
133 151
         description='if set to 1, then show deleted contents.'
134
-                    ' 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"),
135 154
     )
136 155
     show_active = marshmallow.fields.Int(
137 156
         example=1,
@@ -141,7 +160,8 @@ class FilterContentQuerySchema(marshmallow.Schema):
141 160
                     ' Note: active content are content '
142 161
                     'that is neither archived nor deleted. '
143 162
                     'The reason for this parameter to exist is for example '
144
-                    '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"),
145 165
     )
146 166
 
147 167
     @post_load
@@ -205,7 +225,10 @@ class WorkspaceMenuEntrySchema(marshmallow.Schema):
205 225
 
206 226
 
207 227
 class WorkspaceDigestSchema(marshmallow.Schema):
208
-    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
+    )
209 232
     slug = marshmallow.fields.String(example='intranet')
210 233
     label = marshmallow.fields.String(example='Intranet')
211 234
     sidebar_entries = marshmallow.fields.Nested(
@@ -229,8 +252,14 @@ class WorkspaceMemberSchema(marshmallow.Schema):
229 252
         example='contributor',
230 253
         validate=OneOf(UserRoleInWorkspace.get_all_role_slug())
231 254
     )
232
-    user_id = marshmallow.fields.Int(example=3)
233
-    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
+    )
234 263
     user = marshmallow.fields.Nested(
235 264
         UserSchema(only=('public_name', 'avatar_url'))
236 265
     )
@@ -286,7 +315,7 @@ class StatusSchema(marshmallow.Schema):
286 315
 class ContentTypeSchema(marshmallow.Schema):
287 316
     slug = marshmallow.fields.String(
288 317
         example='pagehtml',
289
-        validate=OneOf([content.slug for content in CONTENT_DEFAULT_TYPE]),
318
+        validate=OneOf(ContentType.allowed_types()),
290 319
     )
291 320
     fa_icon = marshmallow.fields.String(
292 321
         example='fa-file-text-o',
@@ -319,11 +348,13 @@ class ContentMoveSchema(marshmallow.Schema):
319 348
         description='id of the new parent content id.',
320 349
         allow_none=True,
321 350
         required=True,
351
+        validate=Range(min=0, error="Value must be positive or 0"),
322 352
     )
323 353
     new_workspace_id = marshmallow.fields.Int(
324 354
         example=2,
325 355
         description='id of the new workspace id.',
326
-        required=True
356
+        required=True,
357
+        validate=Range(min=1, error="Value must be greater than 0"),
327 358
     )
328 359
 
329 360
     @post_load
@@ -338,7 +369,7 @@ class ContentCreationSchema(marshmallow.Schema):
338 369
     )
339 370
     content_type = marshmallow.fields.String(
340 371
         example='html-documents',
341
-        validate=OneOf([content.slug for content in CONTENT_DEFAULT_TYPE]),
372
+        validate=OneOf(ContentType.allowed_types_for_folding()),  # nopep8
342 373
     )
343 374
 
344 375
     @post_load
@@ -347,30 +378,38 @@ class ContentCreationSchema(marshmallow.Schema):
347 378
 
348 379
 
349 380
 class ContentDigestSchema(marshmallow.Schema):
350
-    content_id = marshmallow.fields.Int(example=6)
381
+    content_id = marshmallow.fields.Int(
382
+        example=6,
383
+        validate=Range(min=1, error="Value must be greater than 0"),
384
+    )
351 385
     slug = marshmallow.fields.Str(example='intervention-report-12')
352 386
     parent_id = marshmallow.fields.Int(
353 387
         example=34,
354 388
         allow_none=True,
355
-        default=None
389
+        default=None,
390
+        validate=Range(min=0, error="Value must be positive or 0"),
356 391
     )
357 392
     workspace_id = marshmallow.fields.Int(
358 393
         example=19,
394
+        validate=Range(min=1, error="Value must be greater than 0"),
359 395
     )
360 396
     label = marshmallow.fields.Str(example='Intervention Report 12')
361 397
     content_type = marshmallow.fields.Str(
362 398
         example='html-documents',
363
-        validate=OneOf([content.slug for content in CONTENT_DEFAULT_TYPE]),
399
+        validate=OneOf(ContentType.allowed_types()),
364 400
     )
365 401
     sub_content_types = marshmallow.fields.List(
366
-        marshmallow.fields.String(),
402
+        marshmallow.fields.String(
403
+            example='html-content',
404
+            validate=OneOf(ContentType.allowed_types())
405
+        ),
367 406
         description='list of content types allowed as sub contents. '
368 407
                     'This field is required for folder contents, '
369 408
                     'set it to empty list in other cases'
370 409
     )
371 410
     status = marshmallow.fields.Str(
372 411
         example='closed-deprecated',
373
-        validate=OneOf([status.slug for status in CONTENT_DEFAULT_STATUS]),
412
+        validate=OneOf(ContentStatus.allowed_values()),
374 413
         description='this slug is found in content_type available statuses',
375 414
         default=open_status
376 415
     )
@@ -403,20 +442,15 @@ class ContentSchema(ContentDigestSchema):
403 442
     last_modifier = marshmallow.fields.Nested(UserDigestSchema)
404 443
 
405 444
 
406
-class ThreadContentSchema(ContentSchema):
407
-    content_type = marshmallow.fields.Str(
408
-        example='thread',
409
-        validate=OneOf([content.slug for content in CONTENT_DEFAULT_TYPE]),
445
+class TextBasedDataAbstractSchema(marshmallow.Schema):
446
+    raw_content = marshmallow.fields.String(
447
+        description='Content of the object, may be raw text or <b>html</b> for example'  # nopep8
410 448
     )
411
-    raw_content = marshmallow.fields.String('Description of Thread')
412 449
 
413 450
 
414
-class HtmlDocumentContentSchema(ContentSchema):
415
-    content_type = marshmallow.fields.Str(
416
-        example='html-documents',
417
-        validate=OneOf([content.slug for content in CONTENT_DEFAULT_TYPE]),
418
-    )
419
-    raw_content = marshmallow.fields.String('<p>Html page Content!</p>')
451
+class TextBasedContentSchema(ContentSchema, TextBasedDataAbstractSchema):
452
+    pass
453
+
420 454
 
421 455
 #####
422 456
 # Revision
@@ -424,8 +458,16 @@ class HtmlDocumentContentSchema(ContentSchema):
424 458
 
425 459
 
426 460
 class RevisionSchema(ContentDigestSchema):
427
-    comment_ids = marshmallow.fields.List(marshmallow.fields.Int(example=4))
428
-    revision_id = marshmallow.fields.Int(example=12)
461
+    comment_ids = marshmallow.fields.List(
462
+        marshmallow.fields.Int(
463
+            example=4,
464
+            validate=Range(min=1, error="Value must be greater than 0"),
465
+        )
466
+    )
467
+    revision_id = marshmallow.fields.Int(
468
+        example=12,
469
+        validate=Range(min=1, error="Value must be greater than 0"),
470
+    )
429 471
     created = marshmallow.fields.DateTime(
430 472
         format=DATETIME_FORMAT,
431 473
         description='Content creation date',
@@ -433,27 +475,19 @@ class RevisionSchema(ContentDigestSchema):
433 475
     author = marshmallow.fields.Nested(UserDigestSchema)
434 476
 
435 477
 
436
-class ThreadRevisionSchema(RevisionSchema):
437
-    content_type = marshmallow.fields.Str(
438
-        example='thread',
439
-        validate=OneOf([content.slug for content in CONTENT_DEFAULT_TYPE]),
440
-    )
441
-    raw_content = marshmallow.fields.String('Description of Thread')
442
-
443
-
444
-class HtmlDocumentRevisionSchema(RevisionSchema):
445
-    content_type = marshmallow.fields.Str(
446
-        example='html-documents',
447
-        validate=OneOf([content.slug for content in CONTENT_DEFAULT_TYPE]),
448
-    )
449
-    raw_content = marshmallow.fields.String('<p>Html page Content!</p>')
450
-
478
+class TextBasedRevisionSchema(RevisionSchema, TextBasedDataAbstractSchema):
479
+    pass
451 480
 
452
-####
453 481
 
454 482
 class CommentSchema(marshmallow.Schema):
455
-    content_id = marshmallow.fields.Int(example=6)
456
-    parent_id = marshmallow.fields.Int(example=34)
483
+    content_id = marshmallow.fields.Int(
484
+        example=6,
485
+        validate=Range(min=1, error="Value must be greater than 0"),
486
+    )
487
+    parent_id = marshmallow.fields.Int(
488
+        example=34,
489
+        validate=Range(min=0, error="Value must be positive or 0"),
490
+    )
457 491
     raw_content = marshmallow.fields.String(
458 492
         example='<p>This is just an html comment !</p>'
459 493
     )
@@ -464,43 +498,35 @@ class CommentSchema(marshmallow.Schema):
464 498
     )
465 499
 
466 500
 
467
-class ContentModifySchema(marshmallow.Schema):
468
-    label = marshmallow.fields.String(
469
-        example='contract for client XXX',
470
-        description='New title of the content'
501
+class SetCommentSchema(marshmallow.Schema):
502
+    raw_content = marshmallow.fields.String(
503
+        example='<p>This is just an html comment !</p>'
471 504
     )
472 505
 
506
+    @post_load()
507
+    def create_comment(self, data):
508
+        return CommentCreation(**data)
473 509
 
474
-class HtmlDocumentModifySchema(ContentModifySchema):
475
-    raw_content = marshmallow.fields.String('<p>Html page Content!</p>')
476
-
477
-    @post_load
478
-    def html_document_update(self, data):
479
-        return HTMLDocumentUpdate(**data)
480
-
481
-
482
-class ThreadModifySchema(ContentModifySchema):
483
-    raw_content = marshmallow.fields.String('Description of Thread')
484 510
 
485
-    @post_load
486
-    def thread_update(self, data):
487
-        return ThreadUpdate(**data)
511
+class ContentModifyAbstractSchema(marshmallow.Schema):
512
+    label = marshmallow.fields.String(
513
+        required=True,
514
+        example='contract for client XXX',
515
+        description='New title of the content'
516
+    )
488 517
 
489 518
 
490
-class SetCommentSchema(marshmallow.Schema):
491
-    raw_content = marshmallow.fields.String(
492
-        example='<p>This is just an html comment !</p>'
493
-    )
519
+class TextBasedContentModifySchema(ContentModifyAbstractSchema, TextBasedDataAbstractSchema):  # nopep8
494 520
 
495 521
     @post_load
496
-    def create_comment(self, data):
497
-        return CommentCreation(**data)
522
+    def text_based_content_update(self, data):
523
+        return TextBasedContentUpdate(**data)
498 524
 
499 525
 
500 526
 class SetContentStatusSchema(marshmallow.Schema):
501 527
     status = marshmallow.fields.Str(
502 528
         example='closed-deprecated',
503
-        validate=OneOf([status.slug for status in CONTENT_DEFAULT_STATUS]),
529
+        validate=OneOf(ContentStatus.allowed_values()),
504 530
         description='this slug is found in content_type available statuses',
505 531
         default=open_status,
506 532
         required=True,

+ 0 - 3
tracim/views/core_api/session_controller.py View File

@@ -24,11 +24,9 @@ class SessionController(Controller):
24 24
     @hapic.with_api_doc(tags=[SESSION_ENDPOINTS_TAG])
25 25
     @hapic.input_headers(LoginOutputHeaders())
26 26
     @hapic.input_body(BasicAuthSchema())
27
-    @hapic.handle_exception(AuthenticationFailed, HTTPStatus.FORBIDDEN)
28 27
     # TODO - G.M - 17-04-2018 - fix output header ?
29 28
     # @hapic.output_headers()
30 29
     @hapic.output_body(UserSchema(),)
31
-    #@hapic.output_body(NoContentSchema(), default_http_code=HTTPStatus.NO_CONTENT)  # nopep8
32 30
     def login(self, context, request: TracimRequest, hapic_data=None):
33 31
         """
34 32
         Logs user into the system
@@ -54,7 +52,6 @@ class SessionController(Controller):
54 52
         return
55 53
 
56 54
     @hapic.with_api_doc(tags=[SESSION_ENDPOINTS_TAG])
57
-    @hapic.handle_exception(NotAuthenticated, HTTPStatus.UNAUTHORIZED)
58 55
     @hapic.output_body(UserSchema(),)
59 56
     def whoami(self, context, request: TracimRequest, hapic_data=None):
60 57
         """

+ 5 - 7
tracim/views/core_api/system_controller.py View File

@@ -5,7 +5,7 @@ from tracim.exceptions import InsufficientUserProfile
5 5
 from tracim.lib.utils.authorization import require_profile
6 6
 from tracim.models import Group
7 7
 from tracim.models.applications import applications
8
-from tracim.models.contents import CONTENT_DEFAULT_TYPE
8
+from tracim.models.contents import ContentTypeLegacy as ContentType
9 9
 
10 10
 try:  # Python 3.5+
11 11
     from http import HTTPStatus
@@ -20,11 +20,10 @@ from tracim.views.core_api.schemas import ContentTypeSchema
20 20
 
21 21
 SYSTEM_ENDPOINTS_TAG = 'System'
22 22
 
23
+
23 24
 class SystemController(Controller):
24 25
 
25 26
     @hapic.with_api_doc(tags=[SYSTEM_ENDPOINTS_TAG])
26
-    @hapic.handle_exception(NotAuthenticated, HTTPStatus.UNAUTHORIZED)
27
-    @hapic.handle_exception(InsufficientUserProfile, HTTPStatus.FORBIDDEN)
28 27
     @require_profile(Group.TIM_USER)
29 28
     @hapic.output_body(ApplicationSchema(many=True),)
30 29
     def applications(self, context, request: TracimRequest, hapic_data=None):
@@ -34,16 +33,15 @@ class SystemController(Controller):
34 33
         return applications
35 34
 
36 35
     @hapic.with_api_doc(tags=[SYSTEM_ENDPOINTS_TAG])
37
-    @hapic.handle_exception(NotAuthenticated, HTTPStatus.UNAUTHORIZED)
38
-    @hapic.handle_exception(InsufficientUserProfile, HTTPStatus.FORBIDDEN)
39 36
     @require_profile(Group.TIM_USER)
40 37
     @hapic.output_body(ContentTypeSchema(many=True),)
41 38
     def content_types(self, context, request: TracimRequest, hapic_data=None):
42 39
         """
43 40
         Get list of alls content types availables in this tracim instance.
44 41
         """
45
-
46
-        return CONTENT_DEFAULT_TYPE
42
+        content_types_slugs = ContentType.allowed_types_for_folding()
43
+        content_types = [ContentType(slug) for slug in content_types_slugs]
44
+        return content_types
47 45
 
48 46
     def bind(self, configurator: Configurator) -> None:
49 47
         """

+ 0 - 3
tracim/views/core_api/user_controller.py View File

@@ -26,9 +26,6 @@ USER_ENDPOINTS_TAG = 'Users'
26 26
 class UserController(Controller):
27 27
 
28 28
     @hapic.with_api_doc(tags=[USER_ENDPOINTS_TAG])
29
-    @hapic.handle_exception(NotAuthenticated, HTTPStatus.UNAUTHORIZED)
30
-    @hapic.handle_exception(InsufficientUserProfile, HTTPStatus.FORBIDDEN)
31
-    @hapic.handle_exception(UserDoesNotExist, HTTPStatus.NOT_FOUND)
32 29
     @require_same_user_or_profile(Group.TIM_ADMIN)
33 30
     @hapic.input_path(UserIdPathSchema())
34 31
     @hapic.output_body(WorkspaceDigestSchema(many=True),)

+ 5 - 33
tracim/views/core_api/workspace_controller.py View File

@@ -11,16 +11,14 @@ from tracim import TracimRequest
11 11
 from tracim.lib.core.workspace import WorkspaceApi
12 12
 from tracim.lib.core.content import ContentApi
13 13
 from tracim.lib.core.userworkspace import RoleApi
14
-from tracim.lib.utils.authorization import require_workspace_role, \
15
-    require_candidate_workspace_role
14
+from tracim.lib.utils.authorization import require_workspace_role
15
+from tracim.lib.utils.authorization import require_candidate_workspace_role
16 16
 from tracim.models.data import UserRoleInWorkspace
17 17
 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
-from tracim.exceptions import NotAuthenticated, InsufficientUserRoleInWorkspace
21
-from tracim.exceptions import WorkspaceNotFoundInTracimRequest
20
+from tracim.exceptions import EmptyLabelNotAllowed
22 21
 from tracim.exceptions import WorkspacesDoNotMatch
23
-from tracim.exceptions import WorkspaceNotFound
24 22
 from tracim.views.controllers import Controller
25 23
 from tracim.views.core_api.schemas import FilterContentQuerySchema
26 24
 from tracim.views.core_api.schemas import ContentMoveSchema
@@ -40,9 +38,6 @@ WORKSPACE_ENDPOINTS_TAG = 'Workspaces'
40 38
 class WorkspaceController(Controller):
41 39
 
42 40
     @hapic.with_api_doc(tags=[WORKSPACE_ENDPOINTS_TAG])
43
-    @hapic.handle_exception(NotAuthenticated, HTTPStatus.UNAUTHORIZED)
44
-    @hapic.handle_exception(InsufficientUserRoleInWorkspace, HTTPStatus.FORBIDDEN)
45
-    @hapic.handle_exception(WorkspaceNotFound, HTTPStatus.FORBIDDEN)
46 41
     @require_workspace_role(UserRoleInWorkspace.READER)
47 42
     @hapic.input_path(WorkspaceIdPathSchema())
48 43
     @hapic.output_body(WorkspaceSchema())
@@ -60,9 +55,6 @@ class WorkspaceController(Controller):
60 55
         return wapi.get_workspace_with_context(request.current_workspace)
61 56
 
62 57
     @hapic.with_api_doc(tags=[WORKSPACE_ENDPOINTS_TAG])
63
-    @hapic.handle_exception(NotAuthenticated, HTTPStatus.UNAUTHORIZED)
64
-    @hapic.handle_exception(InsufficientUserRoleInWorkspace, HTTPStatus.FORBIDDEN)
65
-    @hapic.handle_exception(WorkspaceNotFound, HTTPStatus.FORBIDDEN)
66 58
     @require_workspace_role(UserRoleInWorkspace.READER)
67 59
     @hapic.input_path(WorkspaceIdPathSchema())
68 60
     @hapic.output_body(WorkspaceMemberSchema(many=True))
@@ -89,9 +81,6 @@ class WorkspaceController(Controller):
89 81
         ]
90 82
 
91 83
     @hapic.with_api_doc(tags=[WORKSPACE_ENDPOINTS_TAG])
92
-    @hapic.handle_exception(NotAuthenticated, HTTPStatus.UNAUTHORIZED)
93
-    @hapic.handle_exception(InsufficientUserRoleInWorkspace, HTTPStatus.FORBIDDEN)
94
-    @hapic.handle_exception(WorkspaceNotFound, HTTPStatus.FORBIDDEN)
95 84
     @require_workspace_role(UserRoleInWorkspace.READER)
96 85
     @hapic.input_path(WorkspaceIdPathSchema())
97 86
     @hapic.input_query(FilterContentQuerySchema())
@@ -125,10 +114,8 @@ class WorkspaceController(Controller):
125 114
         return contents
126 115
 
127 116
     @hapic.with_api_doc(tags=[WORKSPACE_ENDPOINTS_TAG])
128
-    @hapic.handle_exception(NotAuthenticated, HTTPStatus.UNAUTHORIZED)
129
-    @hapic.handle_exception(InsufficientUserRoleInWorkspace, HTTPStatus.FORBIDDEN)
130
-    @hapic.handle_exception(WorkspaceNotFound, HTTPStatus.FORBIDDEN)
131 117
     @require_workspace_role(UserRoleInWorkspace.CONTRIBUTOR)
118
+    @hapic.handle_exception(EmptyLabelNotAllowed, HTTPStatus.BAD_REQUEST)
132 119
     @hapic.input_path(WorkspaceIdPathSchema())
133 120
     @hapic.input_body(ContentCreationSchema())
134 121
     @hapic.output_body(ContentDigestSchema())
@@ -137,7 +124,7 @@ class WorkspaceController(Controller):
137 124
             context,
138 125
             request: TracimRequest,
139 126
             hapic_data=None,
140
-    ) -> typing.List[ContentInContext]:
127
+    ) -> ContentInContext:
141 128
         """
142 129
         create a generic empty content
143 130
         """
@@ -158,9 +145,6 @@ class WorkspaceController(Controller):
158 145
         return content
159 146
 
160 147
     @hapic.with_api_doc(tags=[WORKSPACE_ENDPOINTS_TAG])
161
-    @hapic.handle_exception(NotAuthenticated, HTTPStatus.UNAUTHORIZED)
162
-    @hapic.handle_exception(WorkspaceNotFound, HTTPStatus.FORBIDDEN)
163
-    @hapic.handle_exception(InsufficientUserRoleInWorkspace, HTTPStatus.FORBIDDEN)
164 148
     @hapic.handle_exception(WorkspacesDoNotMatch, HTTPStatus.BAD_REQUEST)
165 149
     @require_workspace_role(UserRoleInWorkspace.CONTENT_MANAGER)
166 150
     @require_candidate_workspace_role(UserRoleInWorkspace.CONTENT_MANAGER)
@@ -213,9 +197,6 @@ class WorkspaceController(Controller):
213 197
         return api.get_content_in_context(updated_content)
214 198
 
215 199
     @hapic.with_api_doc(tags=[WORKSPACE_ENDPOINTS_TAG])
216
-    @hapic.handle_exception(NotAuthenticated, HTTPStatus.UNAUTHORIZED)
217
-    @hapic.handle_exception(InsufficientUserRoleInWorkspace, HTTPStatus.FORBIDDEN)
218
-    @hapic.handle_exception(WorkspaceNotFound, HTTPStatus.FORBIDDEN)
219 200
     @require_workspace_role(UserRoleInWorkspace.CONTENT_MANAGER)
220 201
     @hapic.input_path(WorkspaceAndContentIdPathSchema())
221 202
     @hapic.output_body(NoContentSchema(), default_http_code=HTTPStatus.NO_CONTENT)  # nopep8
@@ -248,9 +229,6 @@ class WorkspaceController(Controller):
248 229
         return
249 230
 
250 231
     @hapic.with_api_doc(tags=[WORKSPACE_ENDPOINTS_TAG])
251
-    @hapic.handle_exception(NotAuthenticated, HTTPStatus.UNAUTHORIZED)
252
-    @hapic.handle_exception(InsufficientUserRoleInWorkspace, HTTPStatus.FORBIDDEN)
253
-    @hapic.handle_exception(WorkspaceNotFound, HTTPStatus.FORBIDDEN)
254 232
     @require_workspace_role(UserRoleInWorkspace.CONTENT_MANAGER)
255 233
     @hapic.input_path(WorkspaceAndContentIdPathSchema())
256 234
     @hapic.output_body(NoContentSchema(), default_http_code=HTTPStatus.NO_CONTENT)  # nopep8
@@ -284,9 +262,6 @@ class WorkspaceController(Controller):
284 262
         return
285 263
 
286 264
     @hapic.with_api_doc(tags=[WORKSPACE_ENDPOINTS_TAG])
287
-    @hapic.handle_exception(NotAuthenticated, HTTPStatus.UNAUTHORIZED)
288
-    @hapic.handle_exception(InsufficientUserRoleInWorkspace, HTTPStatus.FORBIDDEN)
289
-    @hapic.handle_exception(WorkspaceNotFound, HTTPStatus.FORBIDDEN)
290 265
     @require_workspace_role(UserRoleInWorkspace.CONTENT_MANAGER)
291 266
     @hapic.input_path(WorkspaceAndContentIdPathSchema())
292 267
     @hapic.output_body(NoContentSchema(), default_http_code=HTTPStatus.NO_CONTENT)  # nopep8
@@ -316,9 +291,6 @@ class WorkspaceController(Controller):
316 291
         return
317 292
 
318 293
     @hapic.with_api_doc(tags=[WORKSPACE_ENDPOINTS_TAG])
319
-    @hapic.handle_exception(NotAuthenticated, HTTPStatus.UNAUTHORIZED)
320
-    @hapic.handle_exception(InsufficientUserRoleInWorkspace, HTTPStatus.FORBIDDEN)
321
-    @hapic.handle_exception(WorkspaceNotFound, HTTPStatus.FORBIDDEN)
322 294
     @require_workspace_role(UserRoleInWorkspace.CONTENT_MANAGER)
323 295
     @hapic.input_path(WorkspaceAndContentIdPathSchema())
324 296
     @hapic.output_body(NoContentSchema(), default_http_code=HTTPStatus.NO_CONTENT)  # nopep8