Browse Source

Merge branch 'develop' of github.com:tracim/tracim_v2 into develop

AlexiCauvin 6 years ago
parent
commit
04aa38600f

+ 4 - 0
backend/tracim_backend/__init__.py View File

1
 # -*- coding: utf-8 -*-
1
 # -*- coding: utf-8 -*-
2
+
2
 try:  # Python 3.5+
3
 try:  # Python 3.5+
3
     from http import HTTPStatus
4
     from http import HTTPStatus
4
 except ImportError:
5
 except ImportError:
27
 from tracim_backend.views.core_api.workspace_controller import WorkspaceController
28
 from tracim_backend.views.core_api.workspace_controller import WorkspaceController
28
 from tracim_backend.views.contents_api.comment_controller import CommentController
29
 from tracim_backend.views.contents_api.comment_controller import CommentController
29
 from tracim_backend.views.contents_api.file_controller import FileController
30
 from tracim_backend.views.contents_api.file_controller import FileController
31
+from tracim_backend.views.contents_api.folder_controller import FolderController
30
 from tracim_backend.views.frontend import FrontendController
32
 from tracim_backend.views.frontend import FrontendController
31
 from tracim_backend.views.errors import ErrorSchema
33
 from tracim_backend.views.errors import ErrorSchema
32
 from tracim_backend.exceptions import NotAuthenticated
34
 from tracim_backend.exceptions import NotAuthenticated
115
     html_document_controller = HTMLDocumentController()
117
     html_document_controller = HTMLDocumentController()
116
     thread_controller = ThreadController()
118
     thread_controller = ThreadController()
117
     file_controller = FileController()
119
     file_controller = FileController()
120
+    folder_controller = FolderController()
118
     configurator.include(session_controller.bind, route_prefix=BASE_API_V2)
121
     configurator.include(session_controller.bind, route_prefix=BASE_API_V2)
119
     configurator.include(system_controller.bind, route_prefix=BASE_API_V2)
122
     configurator.include(system_controller.bind, route_prefix=BASE_API_V2)
120
     configurator.include(user_controller.bind, route_prefix=BASE_API_V2)
123
     configurator.include(user_controller.bind, route_prefix=BASE_API_V2)
123
     configurator.include(html_document_controller.bind, route_prefix=BASE_API_V2)  # nopep8
126
     configurator.include(html_document_controller.bind, route_prefix=BASE_API_V2)  # nopep8
124
     configurator.include(thread_controller.bind, route_prefix=BASE_API_V2)
127
     configurator.include(thread_controller.bind, route_prefix=BASE_API_V2)
125
     configurator.include(file_controller.bind, route_prefix=BASE_API_V2)
128
     configurator.include(file_controller.bind, route_prefix=BASE_API_V2)
129
+    configurator.include(folder_controller.bind, route_prefix=BASE_API_V2)
126
 
130
 
127
     if app_config.FRONTEND_SERVE:
131
     if app_config.FRONTEND_SERVE:
128
         configurator.include('pyramid_mako')
132
         configurator.include('pyramid_mako')

+ 2 - 0
backend/tracim_backend/exceptions.py View File

204
 class PreviewDimNotAllowed(TracimException):
204
 class PreviewDimNotAllowed(TracimException):
205
     pass
205
     pass
206
 
206
 
207
+class UnallowedSubContent(TracimException):
208
+    pass
207
 
209
 
208
 class TooShortAutocompleteString(TracimException):
210
 class TooShortAutocompleteString(TracimException):
209
     pass
211
     pass

+ 71 - 15
backend/tracim_backend/lib/core/content.py View File

26
 from tracim_backend.lib.utils.utils import cmp_to_key
26
 from tracim_backend.lib.utils.utils import cmp_to_key
27
 from tracim_backend.lib.core.notifications import NotifierFactory
27
 from tracim_backend.lib.core.notifications import NotifierFactory
28
 from tracim_backend.exceptions import SameValueError
28
 from tracim_backend.exceptions import SameValueError
29
+from tracim_backend.exceptions import UnallowedSubContent
30
+from tracim_backend.exceptions import ContentTypeNotExist
29
 from tracim_backend.exceptions import PageOfPreviewNotFound
31
 from tracim_backend.exceptions import PageOfPreviewNotFound
30
 from tracim_backend.exceptions import PreviewDimNotAllowed
32
 from tracim_backend.exceptions import PreviewDimNotAllowed
31
 from tracim_backend.exceptions import RevisionDoesNotMatchThisContent
33
 from tracim_backend.exceptions import RevisionDoesNotMatchThisContent
408
 
410
 
409
     def create(self, content_type_slug: str, workspace: Workspace, parent: Content=None, label: str = '', filename: str = '', do_save=False, is_temporary: bool=False, do_notify=True) -> Content:
411
     def create(self, content_type_slug: str, workspace: Workspace, parent: Content=None, label: str = '', filename: str = '', do_save=False, is_temporary: bool=False, do_notify=True) -> Content:
410
         # TODO - G.M - 2018-07-16 - raise Exception instead of assert
412
         # TODO - G.M - 2018-07-16 - raise Exception instead of assert
411
-        assert content_type_slug in CONTENT_TYPES.query_allowed_types_slugs()
412
         assert content_type_slug != CONTENT_TYPES.Any_SLUG
413
         assert content_type_slug != CONTENT_TYPES.Any_SLUG
413
         assert not (label and filename)
414
         assert not (label and filename)
414
 
415
 
415
         if content_type_slug == CONTENT_TYPES.Folder.slug and not label:
416
         if content_type_slug == CONTENT_TYPES.Folder.slug and not label:
416
             label = self.generate_folder_label(workspace, parent)
417
             label = self.generate_folder_label(workspace, parent)
417
 
418
 
419
+        # TODO BS 2018-08-13: Despite that workspace is required, create_comment
420
+        # can call here with None. Must update create_comment tu require the
421
+        # workspace.
422
+        if not workspace:
423
+            workspace = parent.workspace
424
+
425
+        content_type = CONTENT_TYPES.get_one_by_slug(content_type_slug)
426
+        if parent and parent.properties and 'allowed_content' in parent.properties:
427
+            if content_type.slug not in parent.properties['allowed_content'] or not parent.properties['allowed_content'][content_type.slug]:
428
+                raise UnallowedSubContent(' SubContent of type {subcontent_type}  not allowed in content {content_id}'.format(  # nopep8
429
+                    subcontent_type=content_type.slug,
430
+                    content_id=parent.content_id,
431
+                ))
432
+        if not workspace and parent:
433
+            workspace = parent.workspace
434
+
435
+        if workspace:
436
+            if content_type.slug not in workspace.get_allowed_content_types():
437
+                raise UnallowedSubContent(
438
+                    ' SubContent of type {subcontent_type}  not allowed in workspace {content_id}'.format(  # nopep8
439
+                        subcontent_type=content_type.slug,
440
+                        content_id=workspace.workspace_id,
441
+                    )
442
+                )
443
+
418
         content = Content()
444
         content = Content()
419
 
445
 
420
         if filename:
446
         if filename:
433
 
459
 
434
         content.owner = self._user
460
         content.owner = self._user
435
         content.parent = parent
461
         content.parent = parent
462
+
436
         content.workspace = workspace
463
         content.workspace = workspace
437
-        content.type = content_type_slug
464
+        content.type = content_type.slug
438
         content.is_temporary = is_temporary
465
         content.is_temporary = is_temporary
439
         content.revision_type = ActionDescription.CREATION
466
         content.revision_type = ActionDescription.CREATION
440
 
467
 
450
         return content
477
         return content
451
 
478
 
452
     def create_comment(self, workspace: Workspace=None, parent: Content=None, content:str ='', do_save=False) -> Content:
479
     def create_comment(self, workspace: Workspace=None, parent: Content=None, content:str ='', do_save=False) -> Content:
480
+        # TODO: check parent allowed_type and workspace allowed_ type
453
         assert parent and parent.type != CONTENT_TYPES.Folder.slug
481
         assert parent and parent.type != CONTENT_TYPES.Folder.slug
454
         if not content:
482
         if not content:
455
             raise EmptyCommentContentNotAllowed()
483
             raise EmptyCommentContentNotAllowed()
456
-        item = Content()
457
-        item.owner = self._user
458
-        item.parent = parent
459
-        if not workspace:
460
-            workspace = item.parent.workspace
461
-        item.workspace = workspace
462
-        item.type = CONTENT_TYPES.Comment.slug
484
+
485
+        item = self.create(
486
+            content_type_slug=CONTENT_TYPES.Comment.slug,
487
+            workspace=workspace,
488
+            parent=parent,
489
+            do_notify=False,
490
+            do_save=False,
491
+            label='',
492
+        )
463
         item.description = content
493
         item.description = content
464
-        item.label = ''
465
         item.revision_type = ActionDescription.COMMENT
494
         item.revision_type = ActionDescription.COMMENT
466
 
495
 
467
         if do_save:
496
         if do_save:
1076
     #
1105
     #
1077
     #     return result
1106
     #     return result
1078
 
1107
 
1079
-    def set_allowed_content(self, folder: Content, allowed_content_dict:dict):
1108
+    def _set_allowed_content(self, content: Content, allowed_content_dict: dict) -> None:  # nopep8
1080
         """
1109
         """
1081
-        :param folder: the given folder instance
1110
+        :param content: the given content instance
1082
         :param allowed_content_dict: must be something like this:
1111
         :param allowed_content_dict: must be something like this:
1083
             dict(
1112
             dict(
1084
                 folder = True
1113
                 folder = True
1086
                 file = False,
1115
                 file = False,
1087
                 page = True
1116
                 page = True
1088
             )
1117
             )
1089
-        :return:
1118
+        :return: nothing
1119
+        """
1120
+        properties = content.properties.copy()
1121
+        properties['allowed_content'] = allowed_content_dict
1122
+        content.properties = properties
1123
+
1124
+    def set_allowed_content(self, content: Content, allowed_content_type_slug_list: typing.List[str]) -> None:  # nopep8
1125
+        """
1126
+        :param content: the given content instance
1127
+        :param allowed_content_type_slug_list: list of content_type_slug to
1128
+        accept as subcontent.
1129
+        :return: nothing
1130
+        """
1131
+        allowed_content_dict = {}
1132
+        for allowed_content_type_slug in allowed_content_type_slug_list:
1133
+            if allowed_content_type_slug not in CONTENT_TYPES.extended_endpoint_allowed_types_slug():
1134
+                raise ContentTypeNotExist('Content_type {} does not exist'.format(allowed_content_type_slug))  # nopep8
1135
+            allowed_content_dict[allowed_content_type_slug] = True
1136
+
1137
+        self._set_allowed_content(content, allowed_content_dict)
1138
+
1139
+    def restore_content_default_allowed_content(self, content: Content) -> None:
1140
+        """
1141
+        Return to default allowed_content_types
1142
+        :param content: the given content instance
1143
+        :return: nothing
1090
         """
1144
         """
1091
-        properties = dict(allowed_content = allowed_content_dict)
1092
-        folder.properties = properties
1145
+        if content._properties and 'allowed_content' in content._properties:
1146
+            properties = content.properties.copy()
1147
+            del properties['allowed_content']
1148
+            content.properties = properties
1093
 
1149
 
1094
     def set_status(self, content: Content, new_status: str):
1150
     def set_status(self, content: Content, new_status: str):
1095
         if new_status in CONTENT_STATUS.get_all_slugs_values():
1151
         if new_status in CONTENT_STATUS.get_all_slugs_values():

+ 0 - 8
backend/tracim_backend/lib/webdav/resources.py View File

295
             parent=self.content
295
             parent=self.content
296
         )
296
         )
297
 
297
 
298
-        subcontent = dict(
299
-            folder=True,
300
-            thread=True,
301
-            file=True,
302
-            page=True
303
-        )
304
-
305
-        self.content_api.set_allowed_content(folder, subcontent)
306
         self.content_api.save(folder)
298
         self.content_api.save(folder)
307
 
299
 
308
         transaction.commit()
300
         transaction.commit()

+ 22 - 0
backend/tracim_backend/models/contents.py View File

122
             creation_label: str,
122
             creation_label: str,
123
             available_statuses: typing.List[ContentStatus],
123
             available_statuses: typing.List[ContentStatus],
124
             slug_alias: typing.List[str] = None,
124
             slug_alias: typing.List[str] = None,
125
+            allow_sub_content: bool = False,
125
     ):
126
     ):
126
         self.slug = slug
127
         self.slug = slug
127
         self.fa_icon = fa_icon
128
         self.fa_icon = fa_icon
130
         self.creation_label = creation_label
131
         self.creation_label = creation_label
131
         self.available_statuses = available_statuses
132
         self.available_statuses = available_statuses
132
         self.slug_alias = slug_alias
133
         self.slug_alias = slug_alias
134
+        self.allow_sub_content = allow_sub_content
133
 
135
 
134
 
136
 
135
 thread_type = ContentType(
137
 thread_type = ContentType(
177
     label='Folder',
179
     label='Folder',
178
     creation_label='Create a folder',
180
     creation_label='Create a folder',
179
     available_statuses=CONTENT_STATUS.get_all(),
181
     available_statuses=CONTENT_STATUS.get_all(),
182
+    allow_sub_content=True,
180
 )
183
 )
181
 
184
 
182
 
185
 
226
         """
229
         """
227
         content_types = self._content_types.copy()
230
         content_types = self._content_types.copy()
228
         content_types.extend(self._special_contents_types)
231
         content_types.extend(self._special_contents_types)
232
+        content_types.append(self.Event)
229
         for item in content_types:
233
         for item in content_types:
230
             if item.slug == slug or (item.slug_alias and slug in item.slug_alias):  # nopep8
234
             if item.slug == slug or (item.slug_alias and slug in item.slug_alias):  # nopep8
231
                 return item
235
                 return item
241
         allowed_type_slug = [contents_type.slug for contents_type in self._content_types]  # nopep8
245
         allowed_type_slug = [contents_type.slug for contents_type in self._content_types]  # nopep8
242
         return allowed_type_slug
246
         return allowed_type_slug
243
 
247
 
248
+    def extended_endpoint_allowed_types_slug(self) -> typing.List[str]:
249
+        allowed_types_slug = self.endpoint_allowed_types_slug().copy()
250
+        for content_type in self._special_contents_types:
251
+            allowed_types_slug.append(content_type.slug)
252
+        return allowed_types_slug
253
+
244
     def query_allowed_types_slugs(self) -> typing.List[str]:
254
     def query_allowed_types_slugs(self) -> typing.List[str]:
245
         """
255
         """
246
         Return alls allowed types slug : content_type slug + all alias, any
256
         Return alls allowed types slug : content_type slug + all alias, any
257
         allowed_types_slug.extend(self._extra_slugs)
267
         allowed_types_slug.extend(self._extra_slugs)
258
         return allowed_types_slug
268
         return allowed_types_slug
259
 
269
 
270
+    def default_allowed_content_properties(self, slug) -> dict:
271
+        content_type = self.get_one_by_slug(slug)
272
+        if content_type.allow_sub_content:
273
+            sub_content_allowed = self.extended_endpoint_allowed_types_slug()
274
+        else:
275
+            sub_content_allowed = [self.Comment.slug]
276
+
277
+        properties_dict = {}
278
+        for elem in sub_content_allowed:
279
+            properties_dict[elem] = True
280
+        return properties_dict
281
+
260
 
282
 
261
 CONTENT_TYPES = ContentTypeList(
283
 CONTENT_TYPES = ContentTypeList(
262
     [
284
     [

+ 26 - 11
backend/tracim_backend/models/context_models.py View File

295
     Content creation model
295
     Content creation model
296
     """
296
     """
297
     def __init__(
297
     def __init__(
298
-            self,
299
-            label: str,
300
-            content_type: str,
301
-            parent_id: typing.Optional[int] = None,
298
+        self,
299
+        label: str,
300
+        content_type: str,
301
+        parent_id: typing.Optional[int] = None,
302
     ) -> None:
302
     ) -> None:
303
         self.label = label
303
         self.label = label
304
         self.content_type = content_type
304
         self.content_type = content_type
310
     Comment creation model
310
     Comment creation model
311
     """
311
     """
312
     def __init__(
312
     def __init__(
313
-            self,
314
-            raw_content: str,
313
+        self,
314
+        raw_content: str,
315
     ) -> None:
315
     ) -> None:
316
         self.raw_content = raw_content
316
         self.raw_content = raw_content
317
 
317
 
321
     Set content status
321
     Set content status
322
     """
322
     """
323
     def __init__(
323
     def __init__(
324
-            self,
325
-            status: str,
324
+        self,
325
+        status: str,
326
     ) -> None:
326
     ) -> None:
327
         self.status = status
327
         self.status = status
328
 
328
 
332
     TextBasedContent update model
332
     TextBasedContent update model
333
     """
333
     """
334
     def __init__(
334
     def __init__(
335
-            self,
336
-            label: str,
337
-            raw_content: str,
335
+        self,
336
+        label: str,
337
+        raw_content: str,
338
+    ) -> None:
339
+        self.label = label
340
+        self.raw_content = raw_content
341
+
342
+
343
+class FolderContentUpdate(object):
344
+    """
345
+    Folder Content update model
346
+    """
347
+    def __init__(
348
+        self,
349
+        label: str,
350
+        raw_content: str,
351
+        sub_content_types: typing.List[str],
338
     ) -> None:
352
     ) -> None:
339
         self.label = label
353
         self.label = label
340
         self.raw_content = raw_content
354
         self.raw_content = raw_content
355
+        self.sub_content_types = sub_content_types
341
 
356
 
342
 
357
 
343
 class TypeUser(Enum):
358
 class TypeUser(Enum):

+ 24 - 43
backend/tracim_backend/models/data.py View File

32
 from tracim_backend.models.auth import User
32
 from tracim_backend.models.auth import User
33
 from tracim_backend.models.roles import WorkspaceRoles
33
 from tracim_backend.models.roles import WorkspaceRoles
34
 
34
 
35
-DEFAULT_PROPERTIES = dict(
36
-    allowed_content=dict(
37
-        folder=True,
38
-        file=True,
39
-        page=True,
40
-        thread=True,
41
-    ),
42
-)
43
-
44
 
35
 
45
 class Workspace(DeclarativeBase):
36
 class Workspace(DeclarativeBase):
46
 
37
 
96
 
87
 
97
     def get_allowed_content_types(self):
88
     def get_allowed_content_types(self):
98
         # @see Content.get_allowed_content_types()
89
         # @see Content.get_allowed_content_types()
99
-        return CONTENT_TYPES.endpoint_allowed_types_slug()
90
+        return CONTENT_TYPES.extended_endpoint_allowed_types_slug()
100
 
91
 
101
     def get_valid_children(
92
     def get_valid_children(
102
             self,
93
             self,
545
 
536
 
546
     @classmethod
537
     @classmethod
547
     def check_properties(cls, item):
538
     def check_properties(cls, item):
548
-        if item.type == CONTENT_TYPES.Folder.slug:
549
-            properties = item.properties
550
-            if 'allowed_content' not in properties.keys():
551
-                return False
552
-            if 'folders' not in properties['allowed_content']:
553
-                return False
554
-            if 'files' not in properties['allowed_content']:
555
-                return False
556
-            if 'pages' not in properties['allowed_content']:
557
-                return False
558
-            if 'threads' not in properties['allowed_content']:
559
-                return False
560
-            return True
561
-
539
+        properties = item.properties
562
         if item.type == CONTENT_TYPES.Event.slug:
540
         if item.type == CONTENT_TYPES.Event.slug:
563
-            properties = item.properties
564
             if 'name' not in properties.keys():
541
             if 'name' not in properties.keys():
565
                 return False
542
                 return False
566
             if 'raw' not in properties.keys():
543
             if 'raw' not in properties.keys():
570
             if 'end' not in properties.keys():
547
             if 'end' not in properties.keys():
571
                 return False
548
                 return False
572
             return True
549
             return True
573
-
574
-        # TODO - G.M - 15-03-2018 - Choose only correct Content-type for origin
575
-        # Only content who can be copied need this
576
-        if item.type == CONTENT_TYPES.Any_SLUG:
577
-            properties = item.properties
550
+        else:
551
+            if 'allowed_content' in properties.keys():
552
+                for content_slug, value in properties['allowed_content'].items():  # nopep8
553
+                    if not isinstance(value, bool):
554
+                        return False
555
+                    if not content_slug in CONTENT_TYPES.extended_endpoint_allowed_types_slug():  # nopep8
556
+                        return False
578
             if 'origin' in properties.keys():
557
             if 'origin' in properties.keys():
579
-                return True
580
-        raise NotImplementedError
581
-
582
-    @classmethod
583
-    def reset_properties(cls, item):
584
-        if item.type == CONTENT_TYPES.Folder.slug:
585
-            item.properties = DEFAULT_PROPERTIES
586
-            return
587
-
588
-        raise NotImplementedError
558
+                pass
559
+            return True
589
 
560
 
590
 
561
 
591
 class ContentRevisionRO(DeclarativeBase):
562
 class ContentRevisionRO(DeclarativeBase):
1204
     @hybrid_property
1175
     @hybrid_property
1205
     def properties(self) -> dict:
1176
     def properties(self) -> dict:
1206
         """ return a structure decoded from json content of _properties """
1177
         """ return a structure decoded from json content of _properties """
1178
+
1207
         if not self._properties:
1179
         if not self._properties:
1208
-            return DEFAULT_PROPERTIES
1209
-        return json.loads(self._properties)
1180
+            properties = {}
1181
+        else:
1182
+            properties = json.loads(self._properties)
1183
+        if CONTENT_TYPES.get_one_by_slug(self.type) != CONTENT_TYPES.Event:
1184
+            if not 'allowed_content' in properties:
1185
+                properties['allowed_content'] = CONTENT_TYPES.default_allowed_content_properties(self.type)  # nopep8
1186
+        return properties
1210
 
1187
 
1211
     @properties.setter
1188
     @properties.setter
1212
     def properties(self, properties_struct: dict) -> None:
1189
     def properties(self, properties_struct: dict) -> None:
1347
             allowed_types = self.properties['allowed_content']
1324
             allowed_types = self.properties['allowed_content']
1348
             for type_label, is_allowed in allowed_types.items():
1325
             for type_label, is_allowed in allowed_types.items():
1349
                 if is_allowed:
1326
                 if is_allowed:
1350
-                    types.append(CONTENT_TYPES.get_one_by_slug(type_label))
1327
+                   types.append(
1328
+                        CONTENT_TYPES.get_one_by_slug(type_label)
1329
+                   )
1330
+        # TODO BS 2018-08-13: This try/except is not correct: except exception
1331
+        # if we know what to except.
1351
         except Exception as e:
1332
         except Exception as e:
1352
             print(e.__str__())
1333
             print(e.__str__())
1353
             print('----- /*\ *****')
1334
             print('----- /*\ *****')

+ 761 - 0
backend/tracim_backend/tests/functional/test_contents.py View File

24
 from tracim_backend.fixtures.content import Content as ContentFixtures
24
 from tracim_backend.fixtures.content import Content as ContentFixtures
25
 from tracim_backend.fixtures.users_and_groups import Base as BaseFixture
25
 from tracim_backend.fixtures.users_and_groups import Base as BaseFixture
26
 
26
 
27
+class TestFolder(FunctionalTest):
28
+    """
29
+    Tests for /api/v2/workspaces/{workspace_id}/folders/{content_id}
30
+    endpoint
31
+    """
32
+
33
+    fixtures = [BaseFixture]
34
+
35
+    def test_api__get_folder__ok_200__nominal_case(self) -> None:
36
+        """
37
+        Get one folder content
38
+        """
39
+        dbsession = get_tm_session(self.session_factory, transaction.manager)
40
+        admin = dbsession.query(models.User) \
41
+            .filter(models.User.email == 'admin@admin.admin') \
42
+            .one()
43
+        workspace_api = WorkspaceApi(
44
+            current_user=admin,
45
+            session=dbsession,
46
+            config=self.app_config
47
+        )
48
+        content_api = ContentApi(
49
+            current_user=admin,
50
+            session=dbsession,
51
+            config=self.app_config
52
+        )
53
+        test_workspace = workspace_api.create_workspace(
54
+            label='test',
55
+            save_now=True,
56
+        )
57
+        folder = content_api.create(
58
+            label='test-folder',
59
+            content_type_slug=CONTENT_TYPES.Folder.slug,
60
+            workspace=test_workspace,
61
+            do_save=True,
62
+            do_notify=False
63
+        )
64
+        transaction.commit()
65
+
66
+        self.testapp.authorization = (
67
+            'Basic',
68
+            (
69
+                'admin@admin.admin',
70
+                'admin@admin.admin'
71
+            )
72
+        )
73
+        res = self.testapp.get(
74
+            '/api/v2/workspaces/{workspace_id}/folders/{content_id}'.format(
75
+                workspace_id=test_workspace.workspace_id,
76
+                content_id=folder.content_id,
77
+            ),
78
+            status=200
79
+        )
80
+        content = res.json_body
81
+        assert content['content_type'] == 'folder'
82
+        assert content['content_id'] == folder.content_id
83
+        assert content['is_archived'] is False
84
+        assert content['is_deleted'] is False
85
+        assert content['label'] == 'test-folder'
86
+        assert content['parent_id'] is None
87
+        assert content['show_in_ui'] is True
88
+        assert content['slug'] == 'test-folder'
89
+        assert content['status'] == 'open'
90
+        assert content['workspace_id'] == test_workspace.workspace_id
91
+        assert content['current_revision_id'] == folder.revision_id
92
+        # TODO - G.M - 2018-06-173 - check date format
93
+        assert content['created']
94
+        assert content['author']
95
+        assert content['author']['user_id'] == 1
96
+        assert content['author']['avatar_url'] is None
97
+        assert content['author']['public_name'] == 'Global manager'
98
+        # TODO - G.M - 2018-06-173 - check date format
99
+        assert content['modified']
100
+        assert content['last_modifier']['user_id'] == 1
101
+        assert content['last_modifier']['public_name'] == 'Global manager'
102
+        assert content['last_modifier']['avatar_url'] is None
103
+        assert content['raw_content'] == ''
104
+
105
+    def test_api__get_folder__err_400__wrong_content_type(self) -> None:
106
+        """
107
+        Get one folder of a content content 7 is not folder
108
+        """
109
+        self.testapp.authorization = (
110
+            'Basic',
111
+            (
112
+                'admin@admin.admin',
113
+                'admin@admin.admin'
114
+            )
115
+        )
116
+        dbsession = get_tm_session(self.session_factory, transaction.manager)
117
+        admin = dbsession.query(models.User) \
118
+            .filter(models.User.email == 'admin@admin.admin') \
119
+            .one()
120
+        workspace_api = WorkspaceApi(
121
+            current_user=admin,
122
+            session=dbsession,
123
+            config=self.app_config
124
+        )
125
+        content_api = ContentApi(
126
+            current_user=admin,
127
+            session=dbsession,
128
+            config=self.app_config
129
+        )
130
+        test_workspace = workspace_api.create_workspace(
131
+            label='test',
132
+            save_now=True,
133
+        )
134
+        thread = content_api.create(
135
+            label='thread',
136
+            content_type_slug=CONTENT_TYPES.Thread.slug,
137
+            workspace=test_workspace,
138
+            do_save=True,
139
+            do_notify=False
140
+        )
141
+        transaction.commit()
142
+        self.testapp.get(
143
+            '/api/v2/workspaces/2/folders/7',
144
+            status=400
145
+        )
146
+
147
+    def test_api__get_folder__err_400__content_does_not_exist(self) -> None:  # nopep8
148
+        """
149
+        Get one folder content (content 170 does not exist in db)
150
+        """
151
+        self.testapp.authorization = (
152
+            'Basic',
153
+            (
154
+                'admin@admin.admin',
155
+                'admin@admin.admin'
156
+            )
157
+        )
158
+        dbsession = get_tm_session(self.session_factory, transaction.manager)
159
+        admin = dbsession.query(models.User) \
160
+            .filter(models.User.email == 'admin@admin.admin') \
161
+            .one()
162
+        workspace_api = WorkspaceApi(
163
+            current_user=admin,
164
+            session=dbsession,
165
+            config=self.app_config
166
+        )
167
+        content_api = ContentApi(
168
+            current_user=admin,
169
+            session=dbsession,
170
+            config=self.app_config
171
+        )
172
+        test_workspace = workspace_api.create_workspace(
173
+            label='test',
174
+            save_now=True,
175
+        )
176
+        transaction.commit()
177
+        self.testapp.get(
178
+            '/api/v2/workspaces/{workspace_id}/folders/170'.format(workspace_id=test_workspace.workspace_id),  # nopep8
179
+            status=400
180
+        )
181
+
182
+    def test_api__get_folder__err_400__content_not_in_workspace(self) -> None:  # nopep8
183
+        """
184
+        Get one folders of a content (content is in another workspace)
185
+        """
186
+        self.testapp.authorization = (
187
+            'Basic',
188
+            (
189
+                'admin@admin.admin',
190
+                'admin@admin.admin'
191
+            )
192
+        )
193
+        dbsession = get_tm_session(self.session_factory, transaction.manager)
194
+        admin = dbsession.query(models.User) \
195
+            .filter(models.User.email == 'admin@admin.admin') \
196
+            .one()
197
+        workspace_api = WorkspaceApi(
198
+            current_user=admin,
199
+            session=dbsession,
200
+            config=self.app_config
201
+        )
202
+        content_api = ContentApi(
203
+            current_user=admin,
204
+            session=dbsession,
205
+            config=self.app_config
206
+        )
207
+        test_workspace = workspace_api.create_workspace(
208
+            label='test',
209
+            save_now=True,
210
+        )
211
+        folder = content_api.create(
212
+            label='test_folder',
213
+            content_type_slug=CONTENT_TYPES.Folder.slug,
214
+            workspace=test_workspace,
215
+            do_save=True,
216
+            do_notify=False
217
+        )
218
+        test_workspace2 = workspace_api.create_workspace(
219
+            label='test2',
220
+            save_now=True,
221
+        )
222
+        transaction.commit()
223
+        self.testapp.authorization = (
224
+            'Basic',
225
+            (
226
+                'admin@admin.admin',
227
+                'admin@admin.admin'
228
+            )
229
+        )
230
+        self.testapp.get(
231
+            '/api/v2/workspaces/{workspace_id}/folders/{content_id}'.format(
232
+                workspace_id=test_workspace2.workspace_id,
233
+                content_id=folder.content_id,
234
+            ),
235
+            status=400
236
+        )
237
+
238
+    def test_api__get_folder__err_400__workspace_does_not_exist(self) -> None:  # nopep8
239
+        """
240
+        Get one folder content (Workspace 40 does not exist)
241
+        """
242
+        dbsession = get_tm_session(self.session_factory, transaction.manager)
243
+        admin = dbsession.query(models.User) \
244
+            .filter(models.User.email == 'admin@admin.admin') \
245
+            .one()
246
+        workspace_api = WorkspaceApi(
247
+            current_user=admin,
248
+            session=dbsession,
249
+            config=self.app_config
250
+        )
251
+        content_api = ContentApi(
252
+            current_user=admin,
253
+            session=dbsession,
254
+            config=self.app_config
255
+        )
256
+        test_workspace = workspace_api.create_workspace(
257
+            label='test',
258
+            save_now=True,
259
+        )
260
+        folder = content_api.create(
261
+            label='test_folder',
262
+            content_type_slug=CONTENT_TYPES.Folder.slug,
263
+            workspace=test_workspace,
264
+            do_save=True,
265
+            do_notify=False
266
+        )
267
+        transaction.commit()
268
+        self.testapp.authorization = (
269
+            'Basic',
270
+            (
271
+                'admin@admin.admin',
272
+                'admin@admin.admin'
273
+            )
274
+        )
275
+        self.testapp.get(
276
+            '/api/v2/workspaces/40/folders/{content_id}'.format(content_id=folder.content_id),  # nopep8
277
+            status=400
278
+        )
279
+
280
+    def test_api__get_folder__err_400__workspace_id_is_not_int(self) -> None:  # nopep8
281
+        """
282
+        Get one folder content, workspace id is not int
283
+        """
284
+        dbsession = get_tm_session(self.session_factory, transaction.manager)
285
+        admin = dbsession.query(models.User) \
286
+            .filter(models.User.email == 'admin@admin.admin') \
287
+            .one()
288
+        workspace_api = WorkspaceApi(
289
+            current_user=admin,
290
+            session=dbsession,
291
+            config=self.app_config
292
+        )
293
+        content_api = ContentApi(
294
+            current_user=admin,
295
+            session=dbsession,
296
+            config=self.app_config
297
+        )
298
+        test_workspace = workspace_api.create_workspace(
299
+            label='test',
300
+            save_now=True,
301
+        )
302
+        folder = content_api.create(
303
+            label='test_folder',
304
+            content_type_slug=CONTENT_TYPES.Folder.slug,
305
+            workspace=test_workspace,
306
+            do_save=True,
307
+            do_notify=False
308
+        )
309
+        transaction.commit()
310
+        self.testapp.authorization = (
311
+            'Basic',
312
+            (
313
+                'admin@admin.admin',
314
+                'admin@admin.admin'
315
+            )
316
+        )
317
+        self.testapp.get(
318
+            '/api/v2/workspaces/coucou/folders/{content_id}'.format(content_id=folder.content_id),  # nopep8
319
+            status=400
320
+        )
321
+
322
+    def test_api__get_folder__err_400__content_id_is_not_int(self) -> None:  # nopep8
323
+        """
324
+        Get one folder content, content_id is not int
325
+        """
326
+        dbsession = get_tm_session(self.session_factory, transaction.manager)
327
+        admin = dbsession.query(models.User) \
328
+            .filter(models.User.email == 'admin@admin.admin') \
329
+            .one()
330
+        workspace_api = WorkspaceApi(
331
+            current_user=admin,
332
+            session=dbsession,
333
+            config=self.app_config
334
+        )
335
+        content_api = ContentApi(
336
+            current_user=admin,
337
+            session=dbsession,
338
+            config=self.app_config
339
+        )
340
+        test_workspace = workspace_api.create_workspace(
341
+            label='test',
342
+            save_now=True,
343
+        )
344
+        folder = content_api.create(
345
+            label='test_folder',
346
+            content_type_slug=CONTENT_TYPES.Folder.slug,
347
+            workspace=test_workspace,
348
+            do_save=True,
349
+            do_notify=False
350
+        )
351
+        transaction.commit()
352
+
353
+        self.testapp.authorization = (
354
+            'Basic',
355
+            (
356
+                'admin@admin.admin',
357
+                'admin@admin.admin'
358
+            )
359
+        )
360
+        self.testapp.get(
361
+            '/api/v2/workspaces/{workspace_id}/folders/coucou'.format(workspace_id=test_workspace.workspace_id),  # nopep8
362
+            status=400
363
+        )
364
+
365
+    def test_api__update_folder__err_400__empty_label(self) -> None:  # nopep8
366
+        """
367
+        Update(put) one folder content
368
+        """
369
+        dbsession = get_tm_session(self.session_factory, transaction.manager)
370
+        admin = dbsession.query(models.User) \
371
+            .filter(models.User.email == 'admin@admin.admin') \
372
+            .one()
373
+        workspace_api = WorkspaceApi(
374
+            current_user=admin,
375
+            session=dbsession,
376
+            config=self.app_config
377
+        )
378
+        content_api = ContentApi(
379
+            current_user=admin,
380
+            session=dbsession,
381
+            config=self.app_config
382
+        )
383
+        test_workspace = workspace_api.create_workspace(
384
+            label='test',
385
+            save_now=True,
386
+        )
387
+        folder = content_api.create(
388
+            label='test_folder',
389
+            content_type_slug=CONTENT_TYPES.Folder.slug,
390
+            workspace=test_workspace,
391
+            do_save=True,
392
+            do_notify=False
393
+        )
394
+        transaction.commit()
395
+        self.testapp.authorization = (
396
+            'Basic',
397
+            (
398
+                'admin@admin.admin',
399
+                'admin@admin.admin'
400
+            )
401
+        )
402
+        params = {
403
+            'label': '',
404
+            'raw_content': '<p> Le nouveau contenu </p>',
405
+            'sub_content_types': [CONTENT_TYPES.Folder.slug]
406
+        }
407
+        self.testapp.put_json(
408
+            '/api/v2/workspaces/{workspace_id}/folders/{content_id}'.format(
409
+                workspace_id=test_workspace.workspace_id,
410
+                content_id=folder.content_id,
411
+            ),
412
+            params=params,
413
+            status=400
414
+        )
415
+
416
+    def test_api__update_folder__ok_200__nominal_case(self) -> None:
417
+        """
418
+        Update(put) one html document of a content
419
+        """
420
+        dbsession = get_tm_session(self.session_factory, transaction.manager)
421
+        admin = dbsession.query(models.User) \
422
+            .filter(models.User.email == 'admin@admin.admin') \
423
+            .one()
424
+        workspace_api = WorkspaceApi(
425
+            current_user=admin,
426
+            session=dbsession,
427
+            config=self.app_config
428
+        )
429
+        content_api = ContentApi(
430
+            current_user=admin,
431
+            session=dbsession,
432
+            config=self.app_config
433
+        )
434
+        test_workspace = workspace_api.create_workspace(
435
+            label='test',
436
+            save_now=True,
437
+        )
438
+        folder = content_api.create(
439
+            label='test_folder',
440
+            content_type_slug=CONTENT_TYPES.Folder.slug,
441
+            workspace=test_workspace,
442
+            do_save=True,
443
+            do_notify=False
444
+        )
445
+        transaction.commit()
446
+        self.testapp.authorization = (
447
+            'Basic',
448
+            (
449
+                'admin@admin.admin',
450
+                'admin@admin.admin'
451
+            )
452
+        )
453
+        params = {
454
+            'label': 'My New label',
455
+            'raw_content': '<p> Le nouveau contenu </p>',
456
+            'sub_content_types': [CONTENT_TYPES.Folder.slug]
457
+        }
458
+        res = self.testapp.put_json(
459
+            '/api/v2/workspaces/{workspace_id}/folders/{content_id}'.format(
460
+                workspace_id=test_workspace.workspace_id,
461
+                content_id=folder.content_id,
462
+            ),
463
+            params=params,
464
+            status=200
465
+        )
466
+        content = res.json_body
467
+        assert content['content_type'] == 'folder'
468
+        assert content['content_id'] == folder.content_id
469
+        assert content['is_archived'] is False
470
+        assert content['is_deleted'] is False
471
+        assert content['label'] == 'My New label'
472
+        assert content['parent_id'] is None
473
+        assert content['show_in_ui'] is True
474
+        assert content['slug'] == 'my-new-label'
475
+        assert content['status'] == 'open'
476
+        assert content['workspace_id'] == test_workspace.workspace_id
477
+        assert content['current_revision_id']
478
+        # TODO - G.M - 2018-06-173 - check date format
479
+        assert content['created']
480
+        assert content['author']
481
+        assert content['author']['user_id'] == 1
482
+        assert content['author']['avatar_url'] is None
483
+        assert content['author']['public_name'] == 'Global manager'
484
+        # TODO - G.M - 2018-06-173 - check date format
485
+        assert content['modified']
486
+        assert content['last_modifier'] == content['author']
487
+        assert content['raw_content'] == '<p> Le nouveau contenu </p>'
488
+        assert content['sub_content_types'] == [CONTENT_TYPES.Folder.slug]
489
+
490
+    def test_api__get_folder_revisions__ok_200__nominal_case(
491
+            self
492
+    ) -> None:
493
+        """
494
+        Get one html document of a content
495
+        """
496
+        dbsession = get_tm_session(self.session_factory, transaction.manager)
497
+        admin = dbsession.query(models.User) \
498
+            .filter(models.User.email == 'admin@admin.admin') \
499
+            .one()
500
+        workspace_api = WorkspaceApi(
501
+            current_user=admin,
502
+            session=dbsession,
503
+            config=self.app_config
504
+        )
505
+        content_api = ContentApi(
506
+            current_user=admin,
507
+            session=dbsession,
508
+            config=self.app_config
509
+        )
510
+        test_workspace = workspace_api.create_workspace(
511
+            label='test',
512
+            save_now=True,
513
+        )
514
+        folder = content_api.create(
515
+            label='test-folder',
516
+            content_type_slug=CONTENT_TYPES.Folder.slug,
517
+            workspace=test_workspace,
518
+            do_save=True,
519
+            do_notify=False
520
+        )
521
+        with new_revision(
522
+           session=dbsession,
523
+           tm=transaction.manager,
524
+           content=folder,
525
+        ):
526
+            content_api.update_content(
527
+                folder,
528
+                new_label='test-folder-updated',
529
+                new_content='Just a test'
530
+            )
531
+        content_api.save(folder)
532
+        with new_revision(
533
+           session=dbsession,
534
+           tm=transaction.manager,
535
+           content=folder,
536
+        ):
537
+            content_api.archive(
538
+                folder,
539
+            )
540
+        content_api.save(folder)
541
+        with new_revision(
542
+           session=dbsession,
543
+           tm=transaction.manager,
544
+           content=folder,
545
+        ):
546
+            content_api.unarchive(
547
+                folder,
548
+            )
549
+        content_api.save(folder)
550
+        transaction.commit()
551
+        self.testapp.authorization = (
552
+            'Basic',
553
+            (
554
+                'admin@admin.admin',
555
+                'admin@admin.admin'
556
+            )
557
+        )
558
+        res = self.testapp.get(
559
+            '/api/v2/workspaces/{workspace_id}/folders/{content_id}/revisions'.format(  # nopep8
560
+                workspace_id=test_workspace.workspace_id,
561
+                content_id=folder.content_id,
562
+            ),
563
+            status=200
564
+        )
565
+        revisions = res.json_body
566
+        assert len(revisions) == 4
567
+        revision = revisions[0]
568
+        assert revision['content_type'] == 'folder'
569
+        assert revision['content_id'] == folder.content_id
570
+        assert revision['is_archived'] is False
571
+        assert revision['is_deleted'] is False
572
+        assert revision['label'] == 'test-folder'
573
+        assert revision['parent_id'] is None
574
+        assert revision['show_in_ui'] is True
575
+        assert revision['slug'] == 'test-folder'
576
+        assert revision['status'] == 'open'
577
+        assert revision['workspace_id'] == test_workspace.workspace_id
578
+        assert revision['revision_id']
579
+        assert revision['revision_type'] == 'creation'
580
+        assert revision['sub_content_types']
581
+        # TODO - G.M - 2018-06-173 - Test with real comments
582
+        assert revision['comment_ids'] == []
583
+        # TODO - G.M - 2018-06-173 - check date format
584
+        assert revision['created']
585
+        assert revision['author']
586
+        assert revision['author']['user_id'] == 1
587
+        assert revision['author']['avatar_url'] is None
588
+        assert revision['author']['public_name'] == 'Global manager'
589
+
590
+        revision = revisions[1]
591
+        assert revision['content_type'] == 'folder'
592
+        assert revision['content_id'] == folder.content_id
593
+        assert revision['is_archived'] is False
594
+        assert revision['is_deleted'] is False
595
+        assert revision['label'] == 'test-folder-updated'
596
+        assert revision['parent_id'] is None
597
+        assert revision['show_in_ui'] is True
598
+        assert revision['slug'] == 'test-folder-updated'
599
+        assert revision['status'] == 'open'
600
+        assert revision['workspace_id'] == test_workspace.workspace_id
601
+        assert revision['revision_id']
602
+        assert revision['revision_type'] == 'edition'
603
+        assert revision['sub_content_types']
604
+        # TODO - G.M - 2018-06-173 - Test with real comments
605
+        assert revision['comment_ids'] == []
606
+        # TODO - G.M - 2018-06-173 - check date format
607
+        assert revision['created']
608
+        assert revision['author']
609
+        assert revision['author']['user_id'] == 1
610
+        assert revision['author']['avatar_url'] is None
611
+        assert revision['author']['public_name'] == 'Global manager'
612
+
613
+        revision = revisions[2]
614
+        assert revision['content_type'] == 'folder'
615
+        assert revision['content_id'] == folder.content_id
616
+        assert revision['is_archived'] is True
617
+        assert revision['is_deleted'] is False
618
+        assert revision['label'] != 'test-folder-updated'
619
+        assert revision['label'].startswith('test-folder-updated')
620
+        assert revision['parent_id'] is None
621
+        assert revision['show_in_ui'] is True
622
+        assert revision['slug'] != 'test-folder-updated'
623
+        assert revision['slug'].startswith('test-folder-updated')
624
+        assert revision['status'] == 'open'
625
+        assert revision['workspace_id'] == test_workspace.workspace_id
626
+        assert revision['revision_id']
627
+        assert revision['revision_type'] == 'archiving'
628
+        assert revision['sub_content_types']
629
+        # TODO - G.M - 2018-06-173 - Test with real comments
630
+        assert revision['comment_ids'] == []
631
+        # TODO - G.M - 2018-06-173 - check date format
632
+        assert revision['created']
633
+        assert revision['author']
634
+        assert revision['author']['user_id'] == 1
635
+        assert revision['author']['avatar_url'] is None
636
+        assert revision['author']['public_name'] == 'Global manager'
637
+
638
+        revision = revisions[3]
639
+        assert revision['content_type'] == 'folder'
640
+        assert revision['content_id'] == folder.content_id
641
+        assert revision['is_archived'] is False
642
+        assert revision['is_deleted'] is False
643
+        assert revision['label'].startswith('test-folder-updated')
644
+        assert revision['parent_id'] is None
645
+        assert revision['show_in_ui'] is True
646
+        assert revision['slug'].startswith('test-folder-updated')
647
+        assert revision['status'] == 'open'
648
+        assert revision['workspace_id'] == test_workspace.workspace_id
649
+        assert revision['revision_id']
650
+        assert revision['revision_type'] == 'unarchiving'
651
+        assert revision['sub_content_types']
652
+        # TODO - G.M - 2018-06-173 - Test with real comments
653
+        assert revision['comment_ids'] == []
654
+        # TODO - G.M - 2018-06-173 - check date format
655
+        assert revision['created']
656
+        assert revision['author']
657
+        assert revision['author']['user_id'] == 1
658
+        assert revision['author']['avatar_url'] is None
659
+        assert revision['author']['public_name'] == 'Global manager'
660
+
661
+    def test_api__set_folder_status__ok_200__nominal_case(self) -> None:
662
+        """
663
+        Get one folder content
664
+        """
665
+        dbsession = get_tm_session(self.session_factory, transaction.manager)
666
+        admin = dbsession.query(models.User) \
667
+            .filter(models.User.email == 'admin@admin.admin') \
668
+            .one()
669
+        workspace_api = WorkspaceApi(
670
+            current_user=admin,
671
+            session=dbsession,
672
+            config=self.app_config
673
+        )
674
+        content_api = ContentApi(
675
+            current_user=admin,
676
+            session=dbsession,
677
+            config=self.app_config
678
+        )
679
+        test_workspace = workspace_api.create_workspace(
680
+            label='test',
681
+            save_now=True,
682
+        )
683
+        folder = content_api.create(
684
+            label='test_folder',
685
+            content_type_slug=CONTENT_TYPES.Folder.slug,
686
+            workspace=test_workspace,
687
+            do_save=True,
688
+            do_notify=False
689
+        )
690
+        transaction.commit()
691
+        self.testapp.authorization = (
692
+            'Basic',
693
+            (
694
+                'admin@admin.admin',
695
+                'admin@admin.admin'
696
+            )
697
+        )
698
+        params = {
699
+            'status': 'closed-deprecated',
700
+        }
701
+
702
+        # before
703
+        res = self.testapp.get(
704
+            '/api/v2/workspaces/{workspace_id}/folders/{content_id}'.format(  # nopep8
705
+                # nopep8
706
+                workspace_id=test_workspace.workspace_id,
707
+                content_id=folder.content_id,
708
+            ),
709
+            status=200
710
+        )
711
+        content = res.json_body
712
+        assert content['content_type'] == 'folder'
713
+        assert content['content_id'] == folder.content_id
714
+        assert content['status'] == 'open'
715
+
716
+        # set status
717
+        self.testapp.put_json(
718
+            '/api/v2/workspaces/{workspace_id}/folders/{content_id}/status'.format(  # nopep8
719
+                workspace_id=test_workspace.workspace_id,
720
+                content_id=folder.content_id,
721
+            ),
722
+            params=params,
723
+            status=204
724
+        )
725
+
726
+        # after
727
+        res = self.testapp.get(
728
+            '/api/v2/workspaces/{workspace_id}/folders/{content_id}'.format(
729
+                workspace_id=test_workspace.workspace_id,
730
+                content_id=folder.content_id,
731
+            ),
732
+            status=200
733
+        )
734
+        content = res.json_body
735
+        assert content['content_type'] == 'folder'
736
+        assert content['content_id'] == folder.content_id
737
+        assert content['status'] == 'closed-deprecated'
738
+
739
+    def test_api__set_folder_status__err_400__wrong_status(self) -> None:
740
+        """
741
+        Get one folder content
742
+        """
743
+        self.testapp.authorization = (
744
+            'Basic',
745
+            (
746
+                'admin@admin.admin',
747
+                'admin@admin.admin'
748
+            )
749
+        )
750
+        params = {
751
+            'status': 'unexistant-status',
752
+        }
753
+        dbsession = get_tm_session(self.session_factory, transaction.manager)
754
+        admin = dbsession.query(models.User) \
755
+            .filter(models.User.email == 'admin@admin.admin') \
756
+            .one()
757
+        workspace_api = WorkspaceApi(
758
+            current_user=admin,
759
+            session=dbsession,
760
+            config=self.app_config
761
+        )
762
+        content_api = ContentApi(
763
+            current_user=admin,
764
+            session=dbsession,
765
+            config=self.app_config
766
+        )
767
+        test_workspace = workspace_api.create_workspace(
768
+            label='test',
769
+            save_now=True,
770
+        )
771
+        folder = content_api.create(
772
+            label='test_folder',
773
+            content_type_slug=CONTENT_TYPES.Folder.slug,
774
+            workspace=test_workspace,
775
+            do_save=True,
776
+            do_notify=False
777
+        )
778
+        transaction.commit()
779
+        self.testapp.put_json(
780
+            '/api/v2/workspaces/{workspace_id}/folders/{content_id}/status'.format(  # nopep8
781
+                workspace_id=test_workspace.workspace_id,
782
+                content_id=folder.content_id,
783
+            ),
784
+            params=params,
785
+            status=400
786
+        )
787
+
27
 
788
 
28
 class TestHtmlDocuments(FunctionalTest):
789
 class TestHtmlDocuments(FunctionalTest):
29
     """
790
     """

+ 89 - 22
backend/tracim_backend/tests/functional/test_workspaces.py View File

714
         assert content['show_in_ui'] is True
714
         assert content['show_in_ui'] is True
715
         assert content['slug'] == 'tools'
715
         assert content['slug'] == 'tools'
716
         assert content['status'] == 'open'
716
         assert content['status'] == 'open'
717
-        assert set(content['sub_content_types']) == {'thread', 'html-document', 'folder', 'file'}  # nopep8
717
+        assert len(content['sub_content_types']) > 1
718
+        assert 'comment' in content['sub_content_types']
719
+        assert 'folder' in content['sub_content_types']
718
         assert content['workspace_id'] == 1
720
         assert content['workspace_id'] == 1
719
         content = res[1]
721
         content = res[1]
720
         assert content['content_id'] == 2
722
         assert content['content_id'] == 2
726
         assert content['show_in_ui'] is True
728
         assert content['show_in_ui'] is True
727
         assert content['slug'] == 'menus'
729
         assert content['slug'] == 'menus'
728
         assert content['status'] == 'open'
730
         assert content['status'] == 'open'
729
-        assert set(content['sub_content_types']) == {'thread', 'html-document', 'folder', 'file'}  # nopep8
731
+        assert len(content['sub_content_types']) > 1
732
+        assert 'comment' in content['sub_content_types']
733
+        assert 'folder' in content['sub_content_types']
730
         assert content['workspace_id'] == 1
734
         assert content['workspace_id'] == 1
731
         content = res[2]
735
         content = res[2]
732
         assert content['content_id'] == 11
736
         assert content['content_id'] == 11
738
         assert content['show_in_ui'] is True
742
         assert content['show_in_ui'] is True
739
         assert content['slug'] == 'current-menu'
743
         assert content['slug'] == 'current-menu'
740
         assert content['status'] == 'open'
744
         assert content['status'] == 'open'
741
-        assert set(content['sub_content_types']) == {'thread', 'html-document', 'folder', 'file'}  # nopep8
745
+        assert set(content['sub_content_types']) == {'comment'}
742
         assert content['workspace_id'] == 1
746
         assert content['workspace_id'] == 1
743
 
747
 
744
     def test_api__get_workspace_content__ok_200__get_default_html_documents(self):
748
     def test_api__get_workspace_content__ok_200__get_default_html_documents(self):
768
         assert content['show_in_ui'] is True
772
         assert content['show_in_ui'] is True
769
         assert content['slug'] == 'current-menu'
773
         assert content['slug'] == 'current-menu'
770
         assert content['status'] == 'open'
774
         assert content['status'] == 'open'
771
-        assert set(content['sub_content_types']) == {'thread', 'html-document', 'folder', 'file'}  # nopep8
775
+        assert set(content['sub_content_types']) == {'comment'}
772
         assert content['workspace_id'] == 1
776
         assert content['workspace_id'] == 1
773
 
777
 
774
     # Root related
778
     # Root related
807
         assert content['show_in_ui'] is True
811
         assert content['show_in_ui'] is True
808
         assert content['slug'] == 'new-fruit-salad'
812
         assert content['slug'] == 'new-fruit-salad'
809
         assert content['status'] == 'open'
813
         assert content['status'] == 'open'
810
-        assert set(content['sub_content_types']) == {'thread', 'html-document', 'folder', 'file'}  # nopep8
814
+        assert set(content['sub_content_types']) == {'comment'}
811
         assert content['workspace_id'] == 3
815
         assert content['workspace_id'] == 3
812
 
816
 
813
         content = res[2]
817
         content = res[2]
820
         assert content['show_in_ui'] is True
824
         assert content['show_in_ui'] is True
821
         assert content['slug'].startswith('fruit-salad')
825
         assert content['slug'].startswith('fruit-salad')
822
         assert content['status'] == 'open'
826
         assert content['status'] == 'open'
823
-        assert set(content['sub_content_types']) == {'thread', 'html-document', 'folder', 'file'}  # nopep8
827
+        assert set(content['sub_content_types']) == {'comment'}
824
         assert content['workspace_id'] == 3
828
         assert content['workspace_id'] == 3
825
 
829
 
826
         content = res[3]
830
         content = res[3]
833
         assert content['show_in_ui'] is True
837
         assert content['show_in_ui'] is True
834
         assert content['slug'].startswith('bad-fruit-salad')
838
         assert content['slug'].startswith('bad-fruit-salad')
835
         assert content['status'] == 'open'
839
         assert content['status'] == 'open'
836
-        assert set(content['sub_content_types']) == {'thread', 'html-document', 'folder', 'file'}  # nopep8
840
+        assert set(content['sub_content_types']) == {'comment'}
837
         assert content['workspace_id'] == 3
841
         assert content['workspace_id'] == 3
838
 
842
 
839
     def test_api__get_workspace_content__ok_200__get_all_root_content(self):
843
     def test_api__get_workspace_content__ok_200__get_all_root_content(self):
870
         assert content['show_in_ui'] is True
874
         assert content['show_in_ui'] is True
871
         assert content['slug'] == 'new-fruit-salad'
875
         assert content['slug'] == 'new-fruit-salad'
872
         assert content['status'] == 'open'
876
         assert content['status'] == 'open'
873
-        assert set(content['sub_content_types']) == {'thread', 'html-document', 'folder', 'file'}  # nopep8
877
+        assert set(content['sub_content_types']) == {'comment'}
874
         assert content['workspace_id'] == 3
878
         assert content['workspace_id'] == 3
875
 
879
 
876
         content = res[2]
880
         content = res[2]
883
         assert content['show_in_ui'] is True
887
         assert content['show_in_ui'] is True
884
         assert content['slug'].startswith('fruit-salad')
888
         assert content['slug'].startswith('fruit-salad')
885
         assert content['status'] == 'open'
889
         assert content['status'] == 'open'
886
-        assert set(content['sub_content_types']) == {'thread', 'html-document', 'folder', 'file'}  # nopep8
890
+        assert set(content['sub_content_types']) == {'comment'}
887
         assert content['workspace_id'] == 3
891
         assert content['workspace_id'] == 3
888
 
892
 
889
         content = res[3]
893
         content = res[3]
896
         assert content['show_in_ui'] is True
900
         assert content['show_in_ui'] is True
897
         assert content['slug'].startswith('bad-fruit-salad')
901
         assert content['slug'].startswith('bad-fruit-salad')
898
         assert content['status'] == 'open'
902
         assert content['status'] == 'open'
899
-        assert set(content['sub_content_types']) == {'thread', 'html-document', 'folder', 'file'}  # nopep8
903
+        assert set(content['sub_content_types']) == {'comment'}
900
         assert content['workspace_id'] == 3
904
         assert content['workspace_id'] == 3
901
 
905
 
902
     def test_api__get_workspace_content__ok_200__get_only_active_root_content(self):  # nopep8
906
     def test_api__get_workspace_content__ok_200__get_only_active_root_content(self):  # nopep8
933
         assert content['show_in_ui'] is True
937
         assert content['show_in_ui'] is True
934
         assert content['slug'] == 'new-fruit-salad'
938
         assert content['slug'] == 'new-fruit-salad'
935
         assert content['status'] == 'open'
939
         assert content['status'] == 'open'
936
-        assert set(content['sub_content_types']) == {'thread', 'html-document', 'folder', 'file'}  # nopep8
940
+        assert set(content['sub_content_types']) == {'comment'}
937
         assert content['workspace_id'] == 3
941
         assert content['workspace_id'] == 3
938
 
942
 
939
     def test_api__get_workspace_content__ok_200__get_only_archived_root_content(self):  # nopep8
943
     def test_api__get_workspace_content__ok_200__get_only_archived_root_content(self):  # nopep8
969
         assert content['show_in_ui'] is True
973
         assert content['show_in_ui'] is True
970
         assert content['slug'].startswith('fruit-salad')
974
         assert content['slug'].startswith('fruit-salad')
971
         assert content['status'] == 'open'
975
         assert content['status'] == 'open'
972
-        assert set(content['sub_content_types']) == {'thread', 'html-document', 'folder', 'file'}  # nopep8
976
+        assert set(content['sub_content_types']) == {'comment'}
973
         assert content['workspace_id'] == 3
977
         assert content['workspace_id'] == 3
974
 
978
 
975
     def test_api__get_workspace_content__ok_200__get_only_deleted_root_content(self):  # nopep8
979
     def test_api__get_workspace_content__ok_200__get_only_deleted_root_content(self):  # nopep8
1007
         assert content['show_in_ui'] is True
1011
         assert content['show_in_ui'] is True
1008
         assert content['slug'].startswith('bad-fruit-salad')
1012
         assert content['slug'].startswith('bad-fruit-salad')
1009
         assert content['status'] == 'open'
1013
         assert content['status'] == 'open'
1010
-        assert set(content['sub_content_types']) == {'thread', 'html-document', 'folder', 'file'}  # nopep8
1014
+        assert set(content['sub_content_types']) == {'comment'}
1011
         assert content['workspace_id'] == 3
1015
         assert content['workspace_id'] == 3
1012
 
1016
 
1013
     def test_api__get_workspace_content__ok_200__get_nothing_root_content(self):
1017
     def test_api__get_workspace_content__ok_200__get_nothing_root_content(self):
1129
         assert content['show_in_ui'] is True
1133
         assert content['show_in_ui'] is True
1130
         assert content['slug'] == 'test-thread'
1134
         assert content['slug'] == 'test-thread'
1131
         assert content['status'] == 'open'
1135
         assert content['status'] == 'open'
1132
-        assert set(content['sub_content_types']) == {'thread', 'html-document', 'folder', 'file'}  # nopep8
1136
+        assert set(content['sub_content_types']) == {'comment'}
1133
         assert content['workspace_id'] == 1
1137
         assert content['workspace_id'] == 1
1134
 
1138
 
1135
     def test_api__get_workspace_content__ok_200__get_all_filter_content_html_and_legacy_page(self):  # nopep8
1139
     def test_api__get_workspace_content__ok_200__get_all_filter_content_html_and_legacy_page(self):  # nopep8
1226
         assert content['show_in_ui'] is True
1230
         assert content['show_in_ui'] is True
1227
         assert content['slug'] == 'test-page'
1231
         assert content['slug'] == 'test-page'
1228
         assert content['status'] == 'open'
1232
         assert content['status'] == 'open'
1229
-        assert set(content['sub_content_types']) == {'thread', 'html-document', 'folder', 'file'}  # nopep8
1233
+        assert set(content['sub_content_types']) == {'comment'}  # nopep8
1230
         assert content['workspace_id'] == 1
1234
         assert content['workspace_id'] == 1
1231
         content = res[1]
1235
         content = res[1]
1232
         assert content['content_type'] == 'html-document'
1236
         assert content['content_type'] == 'html-document'
1238
         assert content['show_in_ui'] is True
1242
         assert content['show_in_ui'] is True
1239
         assert content['slug'] == 'test-html-page'
1243
         assert content['slug'] == 'test-html-page'
1240
         assert content['status'] == 'open'
1244
         assert content['status'] == 'open'
1241
-        assert set(content['sub_content_types']) == {'thread', 'html-document', 'folder', 'file'}  # nopep8
1245
+        assert set(content['sub_content_types']) == {'comment'}  # nopep8
1242
         assert content['workspace_id'] == 1
1246
         assert content['workspace_id'] == 1
1243
         assert res[0]['content_id'] != res[1]['content_id']
1247
         assert res[0]['content_id'] != res[1]['content_id']
1244
 
1248
 
1276
         assert content['show_in_ui'] is True
1280
         assert content['show_in_ui'] is True
1277
         assert content['slug'] == 'new-fruit-salad'
1281
         assert content['slug'] == 'new-fruit-salad'
1278
         assert content['status'] == 'open'
1282
         assert content['status'] == 'open'
1279
-        assert set(content['sub_content_types']) == {'thread', 'html-document', 'folder', 'file'}  # nopep8
1283
+        assert set(content['sub_content_types']) == {'comment'}  # nopep8
1280
         assert content['workspace_id'] == 2
1284
         assert content['workspace_id'] == 2
1281
 
1285
 
1282
         content = res[1]
1286
         content = res[1]
1289
         assert content['show_in_ui'] is True
1293
         assert content['show_in_ui'] is True
1290
         assert content['slug'].startswith('fruit-salad')
1294
         assert content['slug'].startswith('fruit-salad')
1291
         assert content['status'] == 'open'
1295
         assert content['status'] == 'open'
1292
-        assert set(content['sub_content_types']) == {'thread', 'html-document', 'folder', 'file'}  # nopep8
1296
+        assert set(content['sub_content_types']) == {'comment'}  # nopep8
1293
         assert content['workspace_id'] == 2
1297
         assert content['workspace_id'] == 2
1294
 
1298
 
1295
         content = res[2]
1299
         content = res[2]
1302
         assert content['show_in_ui'] is True
1306
         assert content['show_in_ui'] is True
1303
         assert content['slug'].startswith('bad-fruit-salad')
1307
         assert content['slug'].startswith('bad-fruit-salad')
1304
         assert content['status'] == 'open'
1308
         assert content['status'] == 'open'
1305
-        assert set(content['sub_content_types']) == {'thread', 'html-document', 'folder', 'file'}  # nopep8
1309
+        assert set(content['sub_content_types']) == {'comment'}  # nopep8
1306
         assert content['workspace_id'] == 2
1310
         assert content['workspace_id'] == 2
1307
 
1311
 
1308
     def test_api__get_workspace_content__ok_200__get_only_active_folder_content(self):  # nopep8
1312
     def test_api__get_workspace_content__ok_200__get_only_active_folder_content(self):  # nopep8
1338
         assert content['show_in_ui'] is True
1342
         assert content['show_in_ui'] is True
1339
         assert content['slug'] == 'new-fruit-salad'
1343
         assert content['slug'] == 'new-fruit-salad'
1340
         assert content['status'] == 'open'
1344
         assert content['status'] == 'open'
1341
-        assert set(content['sub_content_types']) == {'thread', 'html-document', 'folder', 'file'}  # nopep8
1345
+        assert set(content['sub_content_types']) == {'comment'}  # nopep8
1342
         assert content['workspace_id'] == 2
1346
         assert content['workspace_id'] == 2
1343
 
1347
 
1344
     def test_api__get_workspace_content__ok_200__get_only_archived_folder_content(self):  # nopep8
1348
     def test_api__get_workspace_content__ok_200__get_only_archived_folder_content(self):  # nopep8
1374
         assert content['show_in_ui'] is True
1378
         assert content['show_in_ui'] is True
1375
         assert content['slug'].startswith('fruit-salad')
1379
         assert content['slug'].startswith('fruit-salad')
1376
         assert content['status'] == 'open'
1380
         assert content['status'] == 'open'
1377
-        assert set(content['sub_content_types']) == {'thread', 'html-document', 'folder', 'file'}  # nopep8
1381
+        assert set(content['sub_content_types']) == {'comment'}  # nopep8
1378
         assert content['workspace_id'] == 2
1382
         assert content['workspace_id'] == 2
1379
 
1383
 
1380
     def test_api__get_workspace_content__ok_200__get_only_deleted_folder_content(self):  # nopep8
1384
     def test_api__get_workspace_content__ok_200__get_only_deleted_folder_content(self):  # nopep8
1411
         assert content['show_in_ui'] is True
1415
         assert content['show_in_ui'] is True
1412
         assert content['slug'].startswith('bad-fruit-salad')
1416
         assert content['slug'].startswith('bad-fruit-salad')
1413
         assert content['status'] == 'open'
1417
         assert content['status'] == 'open'
1414
-        assert set(content['sub_content_types']) == {'thread', 'html-document', 'folder', 'file'}  # nopep8
1418
+        assert set(content['sub_content_types']) == {'comment'}  # nopep8
1415
         assert content['workspace_id'] == 2
1419
         assert content['workspace_id'] == 2
1416
 
1420
 
1417
     def test_api__get_workspace_content__ok_200__get_nothing_folder_content(self):  # nopep8
1421
     def test_api__get_workspace_content__ok_200__get_nothing_folder_content(self):  # nopep8
1687
             status=400,
1691
             status=400,
1688
         )
1692
         )
1689
 
1693
 
1694
+    def test_api__post_content_create_generic_content__err_400__unallowed_content_type(self) -> None:  # nopep8
1695
+        """
1696
+        Create generic content
1697
+        """
1698
+        dbsession = get_tm_session(self.session_factory, transaction.manager)
1699
+        admin = dbsession.query(models.User) \
1700
+            .filter(models.User.email == 'admin@admin.admin') \
1701
+            .one()
1702
+        workspace_api = WorkspaceApi(
1703
+            current_user=admin,
1704
+            session=dbsession,
1705
+            config=self.app_config
1706
+        )
1707
+        content_api = ContentApi(
1708
+            current_user=admin,
1709
+            session=dbsession,
1710
+            config=self.app_config
1711
+        )
1712
+        test_workspace = workspace_api.create_workspace(
1713
+            label='test',
1714
+            save_now=True,
1715
+        )
1716
+        folder = content_api.create(
1717
+            label='test-folder',
1718
+            content_type_slug=CONTENT_TYPES.Folder.slug,
1719
+            workspace=test_workspace,
1720
+            do_save=False,
1721
+            do_notify=False
1722
+        )
1723
+        content_api.set_allowed_content(folder, [CONTENT_TYPES.Folder.slug])
1724
+        content_api.save(folder)
1725
+        transaction.commit()
1726
+        self.testapp.authorization = (
1727
+            'Basic',
1728
+            (
1729
+                'admin@admin.admin',
1730
+                'admin@admin.admin'
1731
+            )
1732
+        )
1733
+        # unallowed_content_type
1734
+        params = {
1735
+            'label': 'GenericCreatedContent',
1736
+            'content_type': 'markdownpage',
1737
+            'parent_id': folder.content_id
1738
+        }
1739
+        res = self.testapp.post_json(
1740
+            '/api/v2/workspaces/{workspace_id}/contents'.format(workspace_id=test_workspace.workspace_id),
1741
+            params=params,
1742
+            status=400,
1743
+        )
1744
+
1745
+        # allowed_content_type
1746
+        params = {
1747
+            'label': 'GenericCreatedContent',
1748
+            'content_type': 'folder',
1749
+            'parent_id': folder.content_id
1750
+        }
1751
+        res = self.testapp.post_json(
1752
+            '/api/v2/workspaces/{workspace_id}/contents'.format(workspace_id=test_workspace.workspace_id),
1753
+            params=params,
1754
+            status=200,
1755
+        )
1756
+
1690
     def test_api_put_move_content__ok_200__nominal_case(self):
1757
     def test_api_put_move_content__ok_200__nominal_case(self):
1691
         """
1758
         """
1692
         Move content
1759
         Move content

+ 306 - 0
backend/tracim_backend/tests/library/test_content_api.py View File

10
 from tracim_backend.lib.core.group import GroupApi
10
 from tracim_backend.lib.core.group import GroupApi
11
 from tracim_backend.lib.core.user import UserApi
11
 from tracim_backend.lib.core.user import UserApi
12
 from tracim_backend.exceptions import SameValueError
12
 from tracim_backend.exceptions import SameValueError
13
+from tracim_backend.exceptions import EmptyLabelNotAllowed
14
+from tracim_backend.exceptions import UnallowedSubContent
13
 # TODO - G.M - 28-03-2018 - [RoleApi] Re-enable RoleApi
15
 # TODO - G.M - 28-03-2018 - [RoleApi] Re-enable RoleApi
14
 from tracim_backend.lib.core.workspace import RoleApi
16
 from tracim_backend.lib.core.workspace import RoleApi
15
 # TODO - G.M - 28-03-2018 - [WorkspaceApi] Re-enable WorkspaceApi
17
 # TODO - G.M - 28-03-2018 - [WorkspaceApi] Re-enable WorkspaceApi
101
             'value is {} instead of {}'.format(sorteds[1].content_id,
103
             'value is {} instead of {}'.format(sorteds[1].content_id,
102
                                                c1.content_id))
104
                                                c1.content_id))
103
 
105
 
106
+    def test_unit__create_content__OK_nominal_case(self):
107
+        uapi = UserApi(
108
+            session=self.session,
109
+            config=self.app_config,
110
+            current_user=None,
111
+        )
112
+        group_api = GroupApi(
113
+            current_user=None,
114
+            session=self.session,
115
+            config=self.app_config,
116
+        )
117
+        groups = [group_api.get_one(Group.TIM_USER),
118
+                  group_api.get_one(Group.TIM_MANAGER),
119
+                  group_api.get_one(Group.TIM_ADMIN)]
120
+
121
+        user = uapi.create_minimal_user(email='this.is@user',
122
+                                        groups=groups, save_now=True)
123
+        workspace = WorkspaceApi(
124
+            current_user=user,
125
+            session=self.session,
126
+            config=self.app_config,
127
+        ).create_workspace('test workspace', save_now=True)
128
+        api = ContentApi(
129
+            current_user=user,
130
+            session=self.session,
131
+            config=self.app_config,
132
+        )
133
+        item = api.create(
134
+            content_type_slug=CONTENT_TYPES.Folder.slug,
135
+            workspace=workspace,
136
+            parent=None,
137
+            label='not_deleted',
138
+            do_save=True
139
+        )
140
+        assert isinstance(item, Content)
141
+
142
+    def test_unit__create_content__err_empty_label(self):
143
+        uapi = UserApi(
144
+            session=self.session,
145
+            config=self.app_config,
146
+            current_user=None,
147
+        )
148
+        group_api = GroupApi(
149
+            current_user=None,
150
+            session=self.session,
151
+            config=self.app_config,
152
+        )
153
+        groups = [group_api.get_one(Group.TIM_USER),
154
+                  group_api.get_one(Group.TIM_MANAGER),
155
+                  group_api.get_one(Group.TIM_ADMIN)]
156
+
157
+        user = uapi.create_minimal_user(email='this.is@user',
158
+                                        groups=groups, save_now=True)
159
+        workspace = WorkspaceApi(
160
+            current_user=user,
161
+            session=self.session,
162
+            config=self.app_config,
163
+        ).create_workspace('test workspace', save_now=True)
164
+        api = ContentApi(
165
+            current_user=user,
166
+            session=self.session,
167
+            config=self.app_config,
168
+        )
169
+        with pytest.raises(EmptyLabelNotAllowed):
170
+            api.create(
171
+                content_type_slug=CONTENT_TYPES.Thread.slug,
172
+                workspace=workspace,
173
+                parent=None,
174
+                label='',
175
+                do_save=True
176
+            )
177
+
178
+    def test_unit__create_content__err_content_type_not_allowed_in_this_folder(self):
179
+        uapi = UserApi(
180
+            session=self.session,
181
+            config=self.app_config,
182
+            current_user=None,
183
+        )
184
+        group_api = GroupApi(
185
+            current_user=None,
186
+            session=self.session,
187
+            config=self.app_config,
188
+        )
189
+        groups = [group_api.get_one(Group.TIM_USER),
190
+                  group_api.get_one(Group.TIM_MANAGER),
191
+                  group_api.get_one(Group.TIM_ADMIN)]
192
+
193
+        user = uapi.create_minimal_user(email='this.is@user',
194
+                                        groups=groups, save_now=True)
195
+        workspace = WorkspaceApi(
196
+            current_user=user,
197
+            session=self.session,
198
+            config=self.app_config,
199
+        ).create_workspace('test workspace', save_now=True)
200
+        api = ContentApi(
201
+            current_user=user,
202
+            session=self.session,
203
+            config=self.app_config,
204
+        )
205
+        folder = api.create(
206
+            content_type_slug=CONTENT_TYPES.Folder.slug,
207
+            workspace=workspace,
208
+            parent=None,
209
+            label='plop',
210
+            do_save=False
211
+        )
212
+        allowed_content_dict = {CONTENT_TYPES.Folder.slug: True, CONTENT_TYPES.File.slug: False} # nopep8
213
+        api._set_allowed_content(
214
+            folder,
215
+            allowed_content_dict=allowed_content_dict
216
+        )
217
+        api.save(content=folder)
218
+        # not in list -> do not allow
219
+        with pytest.raises(UnallowedSubContent):
220
+            api.create(
221
+                content_type_slug=CONTENT_TYPES.Event.slug,
222
+                workspace=workspace,
223
+                parent=folder,
224
+                label='lapin',
225
+                do_save=True
226
+            )
227
+        # in list but false -> do not allow
228
+        with pytest.raises(UnallowedSubContent):
229
+            api.create(
230
+                content_type_slug=CONTENT_TYPES.File.slug,
231
+                workspace=workspace,
232
+                parent=folder,
233
+                label='lapin',
234
+                do_save=True
235
+            )
236
+        # in list and true -> allow
237
+        api.create(
238
+            content_type_slug=CONTENT_TYPES.Folder.slug,
239
+            workspace=workspace,
240
+            parent=folder,
241
+            label='lapin',
242
+            do_save=True
243
+        )
244
+
245
+    def test_unit__create_content__err_content_type_not_allowed_in_this_workspace(self):
246
+        uapi = UserApi(
247
+            session=self.session,
248
+            config=self.app_config,
249
+            current_user=None,
250
+        )
251
+        group_api = GroupApi(
252
+            current_user=None,
253
+            session=self.session,
254
+            config=self.app_config,
255
+        )
256
+        groups = [group_api.get_one(Group.TIM_USER),
257
+                  group_api.get_one(Group.TIM_MANAGER),
258
+                  group_api.get_one(Group.TIM_ADMIN)]
259
+
260
+        user = uapi.create_minimal_user(email='this.is@user',
261
+                                        groups=groups, save_now=True)
262
+        workspace = WorkspaceApi(
263
+            current_user=user,
264
+            session=self.session,
265
+            config=self.app_config,
266
+        ).create_workspace('test workspace', save_now=True)
267
+        api = ContentApi(
268
+            current_user=user,
269
+            session=self.session,
270
+            config=self.app_config,
271
+        )
272
+        with pytest.raises(UnallowedSubContent):
273
+            api.create(
274
+                content_type_slug=CONTENT_TYPES.Event.slug,
275
+                workspace=workspace,
276
+                parent=None,
277
+                label='lapin',
278
+                do_save=True
279
+           )
280
+
281
+    def test_unit__set_allowed_content__ok__private_method(self):
282
+        uapi = UserApi(
283
+            session=self.session,
284
+            config=self.app_config,
285
+            current_user=None,
286
+        )
287
+        group_api = GroupApi(
288
+            current_user=None,
289
+            session=self.session,
290
+            config=self.app_config,
291
+        )
292
+        groups = [group_api.get_one(Group.TIM_USER),
293
+                  group_api.get_one(Group.TIM_MANAGER),
294
+                  group_api.get_one(Group.TIM_ADMIN)]
295
+
296
+        user = uapi.create_minimal_user(email='this.is@user',
297
+                                        groups=groups, save_now=True)
298
+        workspace = WorkspaceApi(
299
+            current_user=user,
300
+            session=self.session,
301
+            config=self.app_config,
302
+        ).create_workspace('test workspace', save_now=True)
303
+        api = ContentApi(
304
+            current_user=user,
305
+            session=self.session,
306
+            config=self.app_config,
307
+        )
308
+        folder = api.create(
309
+            content_type_slug=CONTENT_TYPES.Folder.slug,
310
+            workspace=workspace,
311
+            parent=None,
312
+            label='plop',
313
+            do_save=False
314
+        )
315
+        allowed_content_dict = {CONTENT_TYPES.Folder.slug: True, CONTENT_TYPES.File.slug: False}  # nopep8
316
+        api._set_allowed_content(
317
+            folder,
318
+            allowed_content_dict=allowed_content_dict
319
+        )
320
+        assert 'allowed_content' in folder.properties
321
+        assert folder.properties['allowed_content'] == {CONTENT_TYPES.Folder.slug: True, CONTENT_TYPES.File.slug: False}
322
+
323
+    def test_unit__set_allowed_content__ok__nominal_case(self):
324
+        uapi = UserApi(
325
+            session=self.session,
326
+            config=self.app_config,
327
+            current_user=None,
328
+        )
329
+        group_api = GroupApi(
330
+            current_user=None,
331
+            session=self.session,
332
+            config=self.app_config,
333
+        )
334
+        groups = [group_api.get_one(Group.TIM_USER),
335
+                  group_api.get_one(Group.TIM_MANAGER),
336
+                  group_api.get_one(Group.TIM_ADMIN)]
337
+
338
+        user = uapi.create_minimal_user(email='this.is@user',
339
+                                        groups=groups, save_now=True)
340
+        workspace = WorkspaceApi(
341
+            current_user=user,
342
+            session=self.session,
343
+            config=self.app_config,
344
+        ).create_workspace('test workspace', save_now=True)
345
+        api = ContentApi(
346
+            current_user=user,
347
+            session=self.session,
348
+            config=self.app_config,
349
+        )
350
+        folder = api.create(
351
+            content_type_slug=CONTENT_TYPES.Folder.slug,
352
+            workspace=workspace,
353
+            parent=None,
354
+            label='plop',
355
+            do_save=False
356
+        )
357
+        allowed_content_type_slug_list = [CONTENT_TYPES.Folder.slug, CONTENT_TYPES.File.slug]  # nopep8
358
+        api.set_allowed_content(
359
+            folder,
360
+            allowed_content_type_slug_list=allowed_content_type_slug_list
361
+        )
362
+        assert 'allowed_content' in folder.properties
363
+        assert folder.properties['allowed_content'] == {CONTENT_TYPES.Folder.slug: True, CONTENT_TYPES.File.slug: True}
364
+
365
+    def test_unit__restore_content_default_allowed_content__ok__nominal_case(self):
366
+        uapi = UserApi(
367
+            session=self.session,
368
+            config=self.app_config,
369
+            current_user=None,
370
+        )
371
+        group_api = GroupApi(
372
+            current_user=None,
373
+            session=self.session,
374
+            config=self.app_config,
375
+        )
376
+        groups = [group_api.get_one(Group.TIM_USER),
377
+                  group_api.get_one(Group.TIM_MANAGER),
378
+                  group_api.get_one(Group.TIM_ADMIN)]
379
+
380
+        user = uapi.create_minimal_user(email='this.is@user',
381
+                                        groups=groups, save_now=True)
382
+        workspace = WorkspaceApi(
383
+            current_user=user,
384
+            session=self.session,
385
+            config=self.app_config,
386
+        ).create_workspace('test workspace', save_now=True)
387
+        api = ContentApi(
388
+            current_user=user,
389
+            session=self.session,
390
+            config=self.app_config,
391
+        )
392
+        folder = api.create(
393
+            content_type_slug=CONTENT_TYPES.Folder.slug,
394
+            workspace=workspace,
395
+            parent=None,
396
+            label='plop',
397
+            do_save=False
398
+        )
399
+        allowed_content_type_slug_list = [CONTENT_TYPES.Folder.slug, CONTENT_TYPES.File.slug]  # nopep8
400
+        api.set_allowed_content(
401
+            folder,
402
+            allowed_content_type_slug_list=allowed_content_type_slug_list
403
+        )
404
+        assert 'allowed_content' in folder.properties
405
+        assert folder.properties['allowed_content'] == {CONTENT_TYPES.Folder.slug: True, CONTENT_TYPES.File.slug: True} # nopep8
406
+        api.restore_content_default_allowed_content(folder)
407
+        assert 'allowed_content' in folder.properties
408
+        assert folder.properties['allowed_content'] == CONTENT_TYPES.default_allowed_content_properties(folder.type)  # nopep8
409
+
104
     def test_delete(self):
410
     def test_delete(self):
105
         uapi = UserApi(
411
         uapi = UserApi(
106
             session=self.session,
412
             session=self.session,

+ 198 - 0
backend/tracim_backend/views/contents_api/folder_controller.py View File

1
+# coding=utf-8
2
+import typing
3
+
4
+import transaction
5
+from pyramid.config import Configurator
6
+from tracim_backend.models.data import UserRoleInWorkspace
7
+
8
+try:  # Python 3.5+
9
+    from http import HTTPStatus
10
+except ImportError:
11
+    from http import client as HTTPStatus
12
+
13
+from tracim_backend import TracimRequest
14
+from tracim_backend.extensions import hapic
15
+from tracim_backend.lib.core.content import ContentApi
16
+from tracim_backend.views.controllers import Controller
17
+from tracim_backend.views.core_api.schemas import TextBasedContentSchema
18
+from tracim_backend.views.core_api.schemas import FolderContentModifySchema
19
+from tracim_backend.views.core_api.schemas import TextBasedRevisionSchema
20
+from tracim_backend.views.core_api.schemas import SetContentStatusSchema
21
+from tracim_backend.views.core_api.schemas import WorkspaceAndContentIdPathSchema  # nopep8
22
+from tracim_backend.views.core_api.schemas import NoContentSchema
23
+from tracim_backend.lib.utils.authorization import require_content_types
24
+from tracim_backend.lib.utils.authorization import require_workspace_role
25
+from tracim_backend.exceptions import EmptyLabelNotAllowed
26
+from tracim_backend.models.context_models import ContentInContext
27
+from tracim_backend.models.context_models import RevisionInContext
28
+from tracim_backend.models.contents import CONTENT_TYPES
29
+from tracim_backend.models.contents import folder_type
30
+from tracim_backend.models.revision_protection import new_revision
31
+
32
+SWAGGER_TAG__Folders_ENDPOINTS = 'Folders'
33
+
34
+
35
+class FolderController(Controller):
36
+
37
+    @hapic.with_api_doc(tags=[SWAGGER_TAG__Folders_ENDPOINTS])
38
+    @require_workspace_role(UserRoleInWorkspace.READER)
39
+    @require_content_types([folder_type])
40
+    @hapic.input_path(WorkspaceAndContentIdPathSchema())
41
+    @hapic.output_body(TextBasedContentSchema())
42
+    def get_folder(self, context, request: TracimRequest, hapic_data=None) -> ContentInContext:  # nopep8
43
+        """
44
+        Get folder info
45
+        """
46
+        app_config = request.registry.settings['CFG']
47
+        api = ContentApi(
48
+            show_archived=True,
49
+            show_deleted=True,
50
+            current_user=request.current_user,
51
+            session=request.dbsession,
52
+            config=app_config,
53
+        )
54
+        content = api.get_one(
55
+            hapic_data.path.content_id,
56
+            content_type=CONTENT_TYPES.Any_SLUG
57
+        )
58
+        return api.get_content_in_context(content)
59
+
60
+    @hapic.with_api_doc(tags=[SWAGGER_TAG__Folders_ENDPOINTS])
61
+    @hapic.handle_exception(EmptyLabelNotAllowed, HTTPStatus.BAD_REQUEST)
62
+    @require_workspace_role(UserRoleInWorkspace.CONTRIBUTOR)
63
+    @require_content_types([folder_type])
64
+    @hapic.input_path(WorkspaceAndContentIdPathSchema())
65
+    @hapic.input_body(FolderContentModifySchema())
66
+    @hapic.output_body(TextBasedContentSchema())
67
+    def update_folder(self, context, request: TracimRequest, hapic_data=None) -> ContentInContext:  # nopep8
68
+        """
69
+        update folder
70
+        """
71
+        app_config = request.registry.settings['CFG']
72
+        api = ContentApi(
73
+            show_archived=True,
74
+            show_deleted=True,
75
+            current_user=request.current_user,
76
+            session=request.dbsession,
77
+            config=app_config,
78
+        )
79
+        content = api.get_one(
80
+            hapic_data.path.content_id,
81
+            content_type=CONTENT_TYPES.Any_SLUG
82
+        )
83
+        with new_revision(
84
+                session=request.dbsession,
85
+                tm=transaction.manager,
86
+                content=content
87
+        ):
88
+            api.update_content(
89
+                item=content,
90
+                new_label=hapic_data.body.label,
91
+                new_content=hapic_data.body.raw_content,
92
+
93
+            )
94
+            api.set_allowed_content(
95
+                content=content,
96
+                allowed_content_type_slug_list=hapic_data.body.sub_content_types  # nopep8
97
+            )
98
+            api.save(content)
99
+        return api.get_content_in_context(content)
100
+
101
+    @hapic.with_api_doc(tags=[SWAGGER_TAG__Folders_ENDPOINTS])
102
+    @require_workspace_role(UserRoleInWorkspace.READER)
103
+    @require_content_types([folder_type])
104
+    @hapic.input_path(WorkspaceAndContentIdPathSchema())
105
+    @hapic.output_body(TextBasedRevisionSchema(many=True))
106
+    def get_folder_revisions(
107
+            self,
108
+            context,
109
+            request: TracimRequest,
110
+            hapic_data=None
111
+    ) -> typing.List[RevisionInContext]:
112
+        """
113
+        get folder revisions
114
+        """
115
+        app_config = request.registry.settings['CFG']
116
+        api = ContentApi(
117
+            show_archived=True,
118
+            show_deleted=True,
119
+            current_user=request.current_user,
120
+            session=request.dbsession,
121
+            config=app_config,
122
+        )
123
+        content = api.get_one(
124
+            hapic_data.path.content_id,
125
+            content_type=CONTENT_TYPES.Any_SLUG
126
+        )
127
+        revisions = content.revisions
128
+        return [
129
+            api.get_revision_in_context(revision)
130
+            for revision in revisions
131
+        ]
132
+
133
+    @hapic.with_api_doc(tags=[SWAGGER_TAG__Folders_ENDPOINTS])
134
+    @require_workspace_role(UserRoleInWorkspace.CONTRIBUTOR)
135
+    @require_content_types([folder_type])
136
+    @hapic.input_path(WorkspaceAndContentIdPathSchema())
137
+    @hapic.input_body(SetContentStatusSchema())
138
+    @hapic.output_body(NoContentSchema(), default_http_code=HTTPStatus.NO_CONTENT)  # nopep8
139
+    def set_folder_status(self, context, request: TracimRequest, hapic_data=None) -> None:  # nopep8
140
+        """
141
+        set folder status
142
+        """
143
+        app_config = request.registry.settings['CFG']
144
+        api = ContentApi(
145
+            show_archived=True,
146
+            show_deleted=True,
147
+            current_user=request.current_user,
148
+            session=request.dbsession,
149
+            config=app_config,
150
+        )
151
+        content = api.get_one(
152
+            hapic_data.path.content_id,
153
+            content_type=CONTENT_TYPES.Any_SLUG
154
+        )
155
+        with new_revision(
156
+                session=request.dbsession,
157
+                tm=transaction.manager,
158
+                content=content
159
+        ):
160
+            api.set_status(
161
+                content,
162
+                hapic_data.body.status,
163
+            )
164
+            api.save(content)
165
+        return
166
+
167
+    def bind(self, configurator: Configurator) -> None:
168
+        # Get folder
169
+        configurator.add_route(
170
+            'folder',
171
+            '/workspaces/{workspace_id}/folders/{content_id}',
172
+            request_method='GET'
173
+        )
174
+        configurator.add_view(self.get_folder, route_name='folder')  # nopep8
175
+
176
+        # update folder
177
+        configurator.add_route(
178
+            'update_folder',
179
+            '/workspaces/{workspace_id}/folders/{content_id}',
180
+            request_method='PUT'
181
+        )  # nopep8
182
+        configurator.add_view(self.update_folder, route_name='update_folder')  # nopep8
183
+
184
+        # get folder revisions
185
+        configurator.add_route(
186
+            'folder_revisions',
187
+            '/workspaces/{workspace_id}/folders/{content_id}/revisions',  # nopep8
188
+            request_method='GET'
189
+        )
190
+        configurator.add_view(self.get_folder_revisions, route_name='folder_revisions')  # nopep8
191
+
192
+        # get folder revisions
193
+        configurator.add_route(
194
+            'set_folder_status',
195
+            '/workspaces/{workspace_id}/folders/{content_id}/status',  # nopep8
196
+            request_method='PUT'
197
+        )
198
+        configurator.add_view(self.set_folder_status, route_name='set_folder_status')  # nopep8

+ 18 - 1
backend/tracim_backend/views/core_api/schemas.py View File

12
 from tracim_backend.models.contents import CONTENT_TYPES
12
 from tracim_backend.models.contents import CONTENT_TYPES
13
 from tracim_backend.models.contents import open_status
13
 from tracim_backend.models.contents import open_status
14
 from tracim_backend.models.context_models import ActiveContentFilter
14
 from tracim_backend.models.context_models import ActiveContentFilter
15
+from tracim_backend.models.context_models import FolderContentUpdate
15
 from tracim_backend.models.context_models import AutocompleteQuery
16
 from tracim_backend.models.context_models import AutocompleteQuery
16
 from tracim_backend.models.context_models import ContentIdsQuery
17
 from tracim_backend.models.context_models import ContentIdsQuery
17
 from tracim_backend.models.context_models import UserWorkspaceAndContentPath
18
 from tracim_backend.models.context_models import UserWorkspaceAndContentPath
701
     sub_content_types = marshmallow.fields.List(
702
     sub_content_types = marshmallow.fields.List(
702
         marshmallow.fields.String(
703
         marshmallow.fields.String(
703
             example='html-content',
704
             example='html-content',
704
-            validate=OneOf(CONTENT_TYPES.endpoint_allowed_types_slug())
705
+            validate=OneOf(CONTENT_TYPES.extended_endpoint_allowed_types_slug())
705
         ),
706
         ),
706
         description='list of content types allowed as sub contents. '
707
         description='list of content types allowed as sub contents. '
707
                     'This field is required for folder contents, '
708
                     'This field is required for folder contents, '
847
         return TextBasedContentUpdate(**data)
848
         return TextBasedContentUpdate(**data)
848
 
849
 
849
 
850
 
851
+class FolderContentModifySchema(ContentModifyAbstractSchema, TextBasedDataAbstractSchema):  # nopep
852
+    sub_content_types = marshmallow.fields.List(
853
+        marshmallow.fields.String(
854
+            example='html-document',
855
+            validate=OneOf(CONTENT_TYPES.extended_endpoint_allowed_types_slug())
856
+        ),
857
+        description='list of content types allowed as sub contents. '
858
+                    'This field is required for folder contents, '
859
+                    'set it to empty list in other cases'
860
+    )
861
+
862
+    @post_load
863
+    def folder_content_update(self, data):
864
+        return FolderContentUpdate(**data)
865
+
866
+
850
 class FileContentModifySchema(TextBasedContentModifySchema):
867
 class FileContentModifySchema(TextBasedContentModifySchema):
851
     pass
868
     pass
852
 
869
 

+ 2 - 0
backend/tracim_backend/views/core_api/workspace_controller.py View File

24
 from tracim_backend.models.context_models import UserRoleWorkspaceInContext
24
 from tracim_backend.models.context_models import UserRoleWorkspaceInContext
25
 from tracim_backend.models.context_models import ContentInContext
25
 from tracim_backend.models.context_models import ContentInContext
26
 from tracim_backend.exceptions import EmptyLabelNotAllowed
26
 from tracim_backend.exceptions import EmptyLabelNotAllowed
27
+from tracim_backend.exceptions import UnallowedSubContent
27
 from tracim_backend.exceptions import EmailValidationFailed
28
 from tracim_backend.exceptions import EmailValidationFailed
28
 from tracim_backend.exceptions import UserCreationFailed
29
 from tracim_backend.exceptions import UserCreationFailed
29
 from tracim_backend.exceptions import UserDoesNotExist
30
 from tracim_backend.exceptions import UserDoesNotExist
276
     @hapic.with_api_doc(tags=[SWAGGER_TAG_WORKSPACE_ENDPOINTS])
277
     @hapic.with_api_doc(tags=[SWAGGER_TAG_WORKSPACE_ENDPOINTS])
277
     @require_workspace_role(UserRoleInWorkspace.CONTRIBUTOR)
278
     @require_workspace_role(UserRoleInWorkspace.CONTRIBUTOR)
278
     @hapic.handle_exception(EmptyLabelNotAllowed, HTTPStatus.BAD_REQUEST)
279
     @hapic.handle_exception(EmptyLabelNotAllowed, HTTPStatus.BAD_REQUEST)
280
+    @hapic.handle_exception(UnallowedSubContent, HTTPStatus.BAD_REQUEST)
279
     @hapic.input_path(WorkspaceIdPathSchema())
281
     @hapic.input_path(WorkspaceIdPathSchema())
280
     @hapic.input_body(ContentCreationSchema())
282
     @hapic.input_body(ContentCreationSchema())
281
     @hapic.output_body(ContentDigestSchema())
283
     @hapic.output_body(ContentDigestSchema())