Browse Source

Merge branch 'develop' of github.com:tracim/tracim_backend into feature/612_user_account_endpoint

Guénaël Muller 6 years ago
parent
commit
435e31e474

+ 7 - 0
.travis.yml View File

5
   - "3.5"
5
   - "3.5"
6
   - "3.6"
6
   - "3.6"
7
 
7
 
8
+addons:
9
+  apt:
10
+    packages:
11
+    - libreoffice
12
+    - imagemagick
13
+    - libmagickwand-dev
14
+    - ghostscript
8
 services:
15
 services:
9
   - docker
16
   - docker
10
   - redis-server
17
   - redis-server

+ 8 - 1
README.md View File

20
     sudo apt install git
20
     sudo apt install git
21
     sudo apt install python3 python3-venv python3-dev python3-pip
21
     sudo apt install python3 python3-venv python3-dev python3-pip
22
     sudo apt install redis-server
22
     sudo apt install redis-server
23
+    sudo apt install zlib1g-dev libjpeg-dev
24
+    sudo apt install imagemagick libmagickwand-dev ghostscript
25
+
26
+for better preview support:
27
+
28
+    sudo apt install libreoffice # most office documents file and text format
29
+    sudo apt install inkscape # for .svg files.
23
 
30
 
24
 ### Get the source ###
31
 ### Get the source ###
25
 
32
 
105
 
112
 
106
 run tracim_backend web api:
113
 run tracim_backend web api:
107
 
114
 
108
-    pserve developement.ini
115
+    pserve development.ini
109
 
116
 
110
 run wsgidav server:
117
 run wsgidav server:
111
 
118
 

+ 11 - 0
development.ini.sample View File

175
 ## Do not set http:// prefix.
175
 ## Do not set http:// prefix.
176
 # wsgidav.client.base_url = 127.0.0.1:<WSGIDAV_PORT>
176
 # wsgidav.client.base_url = 127.0.0.1:<WSGIDAV_PORT>
177
 
177
 
178
+### Preview
179
+## You can parametrized allowed jpg preview dimension list, if not set, default
180
+## is 256x256. First {width}x{length} items is default preview dimensions.
181
+## all items should be separated by ',' and you should be really careful to do
182
+## set anything else than '{int}x{int}' item and ', ' separator
183
+# preview.jpg.allowed_dims = 256x256,1000x1000
184
+## Preview dimensions can be set as restricted, if set as restricted, access
185
+## endpoint to  to get any other preview dimensions than allowed_dims will
186
+## return error
187
+# preview.jpg.restricted_dims = True
188
+
178
 ###
189
 ###
179
 # wsgi server configuration
190
 # wsgi server configuration
180
 ###
191
 ###

+ 3 - 1
setup.py View File

35
     'filedepot',
35
     'filedepot',
36
     'babel',
36
     'babel',
37
     'python-slugify',
37
     'python-slugify',
38
+    'preview-generator',
38
     # mail-notifier
39
     # mail-notifier
39
     'mako',
40
     'mako',
40
     'lxml',
41
     'lxml',
48
     'pytest-cov',
49
     'pytest-cov',
49
     'pep8',
50
     'pep8',
50
     'mypy',
51
     'mypy',
51
-    'requests'
52
+    'requests',
53
+    'Pillow'
52
 ]
54
 ]
53
 
55
 
54
 mysql_require = [
56
 mysql_require = [

+ 5 - 0
tracim/__init__.py View File

1
 # -*- coding: utf-8 -*-
1
 # -*- coding: utf-8 -*-
2
+
3
+
2
 try:  # Python 3.5+
4
 try:  # Python 3.5+
3
     from http import HTTPStatus
5
     from http import HTTPStatus
4
 except ImportError:
6
 except ImportError:
27
 from tracim.views.core_api.user_controller import UserController
29
 from tracim.views.core_api.user_controller import UserController
28
 from tracim.views.core_api.workspace_controller import WorkspaceController
30
 from tracim.views.core_api.workspace_controller import WorkspaceController
29
 from tracim.views.contents_api.comment_controller import CommentController
31
 from tracim.views.contents_api.comment_controller import CommentController
32
+from tracim.views.contents_api.file_controller import FileController
30
 from tracim.views.errors import ErrorSchema
33
 from tracim.views.errors import ErrorSchema
31
 from tracim.exceptions import NotAuthenticated
34
 from tracim.exceptions import NotAuthenticated
32
 from tracim.exceptions import UserNotActive
35
 from tracim.exceptions import UserNotActive
111
     comment_controller = CommentController()
114
     comment_controller = CommentController()
112
     html_document_controller = HTMLDocumentController()
115
     html_document_controller = HTMLDocumentController()
113
     thread_controller = ThreadController()
116
     thread_controller = ThreadController()
117
+    file_controller = FileController()
114
     configurator.include(session_controller.bind, route_prefix=BASE_API_V2)
118
     configurator.include(session_controller.bind, route_prefix=BASE_API_V2)
115
     configurator.include(system_controller.bind, route_prefix=BASE_API_V2)
119
     configurator.include(system_controller.bind, route_prefix=BASE_API_V2)
116
     configurator.include(user_controller.bind, route_prefix=BASE_API_V2)
120
     configurator.include(user_controller.bind, route_prefix=BASE_API_V2)
118
     configurator.include(comment_controller.bind, route_prefix=BASE_API_V2)
122
     configurator.include(comment_controller.bind, route_prefix=BASE_API_V2)
119
     configurator.include(html_document_controller.bind, route_prefix=BASE_API_V2)  # nopep8
123
     configurator.include(html_document_controller.bind, route_prefix=BASE_API_V2)  # nopep8
120
     configurator.include(thread_controller.bind, route_prefix=BASE_API_V2)
124
     configurator.include(thread_controller.bind, route_prefix=BASE_API_V2)
125
+    configurator.include(file_controller.bind, route_prefix=BASE_API_V2)
121
 
126
 
122
     hapic.add_documentation_view(
127
     hapic.add_documentation_view(
123
         '/api/v2/doc',
128
         '/api/v2/doc',

+ 33 - 0
tracim/config.py View File

411
         #     self.RADICALE_CLIENT_BASE_URL_HOST,
411
         #     self.RADICALE_CLIENT_BASE_URL_HOST,
412
         #     self.RADICALE_CLIENT_BASE_URL_PREFIX,
412
         #     self.RADICALE_CLIENT_BASE_URL_PREFIX,
413
         # )
413
         # )
414
+        self.PREVIEW_JPG_RESTRICTED_DIMS = asbool(settings.get(
415
+            'preview.jpg.restricted_dims', False
416
+        ))
417
+        preview_jpg_allowed_dims_str = settings.get('preview.jpg.allowed_dims', '')  # nopep8
418
+        allowed_dims = []
419
+        if preview_jpg_allowed_dims_str:
420
+            for sizes in preview_jpg_allowed_dims_str.split(','):
421
+                parts = sizes.split('x')
422
+                assert len(parts) == 2
423
+                width, height = parts
424
+                assert width.isdecimal()
425
+                assert height.isdecimal()
426
+                size = PreviewDim(int(width), int(height))
427
+                allowed_dims.append(size)
428
+
429
+        if not allowed_dims:
430
+            size = PreviewDim(256, 256)
431
+            allowed_dims.append(size)
432
+
433
+        self.PREVIEW_JPG_ALLOWED_DIMS = allowed_dims
414
 
434
 
415
     def configure_filedepot(self):
435
     def configure_filedepot(self):
416
         depot_storage_name = self.DEPOT_STORAGE_NAME
436
         depot_storage_name = self.DEPOT_STORAGE_NAME
427
 
447
 
428
         TREEVIEW_FOLDERS = 'folders'
448
         TREEVIEW_FOLDERS = 'folders'
429
         TREEVIEW_ALL = 'all'
449
         TREEVIEW_ALL = 'all'
450
+
451
+
452
+class PreviewDim(object):
453
+
454
+    def __init__(self, width: int, height: int) -> None:
455
+        self.width = width
456
+        self.height = height
457
+
458
+    def __repr__(self):
459
+        return "<PreviewDim width:{width} height:{height}>".format(
460
+            width=self.width,
461
+            height=self.height,
462
+        )

+ 24 - 0
tracim/exceptions.py View File

177
     pass
177
     pass
178
 
178
 
179
 
179
 
180
+class RoleDoesNotExist(TracimException):
181
+    pass
182
+
183
+
184
+class EmailValidationFailed(TracimException):
185
+    pass
186
+
187
+
188
+class UserCreationFailed(TracimException):
189
+    pass
190
+
191
+
180
 class ParentNotFound(NotFound):
192
 class ParentNotFound(NotFound):
181
     pass
193
     pass
194
+
195
+
196
+class RevisionDoesNotMatchThisContent(TracimException):
197
+    pass
198
+
199
+
200
+class PageOfPreviewNotFound(NotFound):
201
+    pass
202
+
203
+
204
+class PreviewDimNotAllowed(TracimException):
205
+    pass

+ 349 - 118
tracim/lib/core/content.py View File

7
 from operator import itemgetter
7
 from operator import itemgetter
8
 
8
 
9
 import transaction
9
 import transaction
10
+from preview_generator.manager import PreviewManager
10
 from sqlalchemy import func
11
 from sqlalchemy import func
11
 from sqlalchemy.orm import Query
12
 from sqlalchemy.orm import Query
12
 from depot.manager import DepotManager
13
 from depot.manager import DepotManager
25
 from tracim.lib.utils.utils import cmp_to_key
26
 from tracim.lib.utils.utils import cmp_to_key
26
 from tracim.lib.core.notifications import NotifierFactory
27
 from tracim.lib.core.notifications import NotifierFactory
27
 from tracim.exceptions import SameValueError
28
 from tracim.exceptions import SameValueError
29
+from tracim.exceptions import PageOfPreviewNotFound
30
+from tracim.exceptions import PreviewDimNotAllowed
31
+from tracim.exceptions import RevisionDoesNotMatchThisContent
28
 from tracim.exceptions import EmptyCommentContentNotAllowed
32
 from tracim.exceptions import EmptyCommentContentNotAllowed
29
 from tracim.exceptions import EmptyLabelNotAllowed
33
 from tracim.exceptions import EmptyLabelNotAllowed
30
 from tracim.exceptions import ContentNotFound
34
 from tracim.exceptions import ContentNotFound
43
 from tracim.models.data import Workspace
47
 from tracim.models.data import Workspace
44
 from tracim.lib.utils.translation import fake_translator as _
48
 from tracim.lib.utils.translation import fake_translator as _
45
 from tracim.models.context_models import RevisionInContext
49
 from tracim.models.context_models import RevisionInContext
50
+from tracim.models.context_models import PreviewAllowedDim
46
 from tracim.models.context_models import ContentInContext
51
 from tracim.models.context_models import ContentInContext
47
 
52
 
48
 __author__ = 'damien'
53
 __author__ = 'damien'
49
 
54
 
50
 
55
 
56
+# TODO - G.M - 2018-07-24 - [Cleanup] Is this method already needed ?
51
 def compare_content_for_sorting_by_type_and_name(
57
 def compare_content_for_sorting_by_type_and_name(
52
         content1: Content,
58
         content1: Content,
53
         content2: Content
59
         content2: Content
86
         else:
92
         else:
87
             return 0
93
             return 0
88
 
94
 
89
-
95
+# TODO - G.M - 2018-07-24 - [Cleanup] Is this method already needed ?
90
 def compare_tree_items_for_sorting_by_type_and_name(
96
 def compare_tree_items_for_sorting_by_type_and_name(
91
         item1: NodeTreeItem,
97
         item1: NodeTreeItem,
92
         item2: NodeTreeItem
98
         item2: NodeTreeItem
133
         self._show_all_type_of_contents_in_treeview = all_content_in_treeview
139
         self._show_all_type_of_contents_in_treeview = all_content_in_treeview
134
         self._force_show_all_types = force_show_all_types
140
         self._force_show_all_types = force_show_all_types
135
         self._disable_user_workspaces_filter = disable_user_workspaces_filter
141
         self._disable_user_workspaces_filter = disable_user_workspaces_filter
142
+        self.preview_manager = PreviewManager(self._config.PREVIEW_CACHE_DIR, create_folder=True)  # nopep8
136
 
143
 
137
     @contextmanager
144
     @contextmanager
138
     def show(
145
     def show(
163
             self._show_temporary = previous_show_temporary
170
             self._show_temporary = previous_show_temporary
164
 
171
 
165
     def get_content_in_context(self, content: Content) -> ContentInContext:
172
     def get_content_in_context(self, content: Content) -> ContentInContext:
166
-        return ContentInContext(content, self._session, self._config)
173
+        return ContentInContext(content, self._session, self._config, self._user)  # nopep8
167
 
174
 
168
     def get_revision_in_context(self, revision: ContentRevisionRO) -> RevisionInContext:  # nopep8
175
     def get_revision_in_context(self, revision: ContentRevisionRO) -> RevisionInContext:  # nopep8
169
         # TODO - G.M - 2018-06-173 - create revision in context object
176
         # TODO - G.M - 2018-06-173 - create revision in context object
190
         return self._session.query(Content)\
197
         return self._session.query(Content)\
191
             .join(ContentRevisionRO, self._get_revision_join())
198
             .join(ContentRevisionRO, self._get_revision_join())
192
 
199
 
200
+    # TODO - G.M - 2018-07-24 - [Cleanup] Is this method already needed ?
193
     @classmethod
201
     @classmethod
194
     def sort_tree_items(
202
     def sort_tree_items(
195
         cls,
203
         cls,
205
 
213
 
206
         return content_list
214
         return content_list
207
 
215
 
216
+    # TODO - G.M - 2018-07-24 - [Cleanup] Is this method already needed ?
208
     @classmethod
217
     @classmethod
209
     def sort_content(
218
     def sort_content(
210
         cls,
219
         cls,
343
     ) -> Query:
352
     ) -> Query:
344
         return self._base_query(workspace)
353
         return self._base_query(workspace)
345
 
354
 
346
-    def get_child_folders(self, parent: Content=None, workspace: Workspace=None, filter_by_allowed_content_types: list=[], removed_item_ids: list=[], allowed_node_types=None) -> typing.List[Content]:
347
-        """
348
-        This method returns child items (folders or items) for left bar treeview.
349
-
350
-        :param parent:
351
-        :param workspace:
352
-        :param filter_by_allowed_content_types:
353
-        :param removed_item_ids:
354
-        :param allowed_node_types: This parameter allow to hide folders for which the given type of content is not allowed.
355
-               For example, if you want to move a Page from a folder to another, you should show only folders that accept pages
356
-        :return:
357
-        """
358
-        filter_by_allowed_content_types = filter_by_allowed_content_types or []  # FDV
359
-        removed_item_ids = removed_item_ids or []  # FDV
360
-
361
-        if not allowed_node_types:
362
-            allowed_node_types = [ContentType.Folder]
363
-        elif allowed_node_types==ContentType.Any:
364
-            allowed_node_types = ContentType.all()
365
-
366
-        parent_id = parent.content_id if parent else None
367
-        folders = self._base_query(workspace).\
368
-            filter(Content.parent_id==parent_id).\
369
-            filter(Content.type.in_(allowed_node_types)).\
370
-            filter(Content.content_id.notin_(removed_item_ids)).\
371
-            all()
372
-
373
-        if not filter_by_allowed_content_types or \
374
-                        len(filter_by_allowed_content_types)<=0:
375
-            # Standard case for the left treeview: we want to show all contents
376
-            # in the left treeview... so we still filter because for example
377
-            # comments must not appear in the treeview
378
-            return [folder for folder in folders \
379
-                    if folder.type in ContentType.allowed_types_for_folding()]
380
-
381
-        # Now this is a case of Folders only (used for moving content)
382
-        # When moving a content, you must get only folders that allow to be filled
383
-        # with the type of content you want to move
384
-        result = []
385
-        for folder in folders:
386
-            for allowed_content_type in filter_by_allowed_content_types:
387
-
388
-                is_folder = folder.type == ContentType.Folder
389
-                content_type__allowed = folder.properties['allowed_content'][allowed_content_type] == True
390
-
391
-                if is_folder and content_type__allowed:
392
-                    result.append(folder)
393
-                    break
394
-
395
-        return result
355
+    # TODO - G.M - 2018-07-17 - [Cleanup] Drop this method if unneeded
356
+    # def get_child_folders(self, parent: Content=None, workspace: Workspace=None, filter_by_allowed_content_types: list=[], removed_item_ids: list=[], allowed_node_types=None) -> typing.List[Content]:
357
+    #     """
358
+    #     This method returns child items (folders or items) for left bar treeview.
359
+    # 
360
+    #     :param parent:
361
+    #     :param workspace:
362
+    #     :param filter_by_allowed_content_types:
363
+    #     :param removed_item_ids:
364
+    #     :param allowed_node_types: This parameter allow to hide folders for which the given type of content is not allowed.
365
+    #            For example, if you want to move a Page from a folder to another, you should show only folders that accept pages
366
+    #     :return:
367
+    #     """
368
+    #     filter_by_allowed_content_types = filter_by_allowed_content_types or []  # FDV
369
+    #     removed_item_ids = removed_item_ids or []  # FDV
370
+    # 
371
+    #     if not allowed_node_types:
372
+    #         allowed_node_types = [ContentType.Folder]
373
+    #     elif allowed_node_types==ContentType.Any:
374
+    #         allowed_node_types = ContentType.all()
375
+    # 
376
+    #     parent_id = parent.content_id if parent else None
377
+    #     folders = self._base_query(workspace).\
378
+    #         filter(Content.parent_id==parent_id).\
379
+    #         filter(Content.type.in_(allowed_node_types)).\
380
+    #         filter(Content.content_id.notin_(removed_item_ids)).\
381
+    #         all()
382
+    # 
383
+    #     if not filter_by_allowed_content_types or \
384
+    #                     len(filter_by_allowed_content_types)<=0:
385
+    #         # Standard case for the left treeview: we want to show all contents
386
+    #         # in the left treeview... so we still filter because for example
387
+    #         # comments must not appear in the treeview
388
+    #         return [folder for folder in folders \
389
+    #                 if folder.type in ContentType.allowed_types_for_folding()]
390
+    # 
391
+    #     # Now this is a case of Folders only (used for moving content)
392
+    #     # When moving a content, you must get only folders that allow to be filled
393
+    #     # with the type of content you want to move
394
+    #     result = []
395
+    #     for folder in folders:
396
+    #         for allowed_content_type in filter_by_allowed_content_types:
397
+    # 
398
+    #             is_folder = folder.type == ContentType.Folder
399
+    #             content_type__allowed = folder.properties['allowed_content'][allowed_content_type] == True
400
+    # 
401
+    #             if is_folder and content_type__allowed:
402
+    #                 result.append(folder)
403
+    #                 break
404
+    # 
405
+    #     return result
396
 
406
 
397
     def create(self, content_type: str, workspace: Workspace, parent: Content=None, label: str ='', filename: str = '', do_save=False, is_temporary: bool=False, do_notify=True) -> Content:
407
     def create(self, content_type: str, workspace: Workspace, parent: Content=None, label: str ='', filename: str = '', do_save=False, is_temporary: bool=False, do_notify=True) -> Content:
398
         # TODO - G.M - 2018-07-16 - raise Exception instead of assert
408
         # TODO - G.M - 2018-07-16 - raise Exception instead of assert
499
             raise ContentNotFound('Content "{}" not found in database'.format(content_id)) from exc  # nopep8
509
             raise ContentNotFound('Content "{}" not found in database'.format(content_id)) from exc  # nopep8
500
         return content
510
         return content
501
 
511
 
502
-    def get_one_revision(self, revision_id: int = None) -> ContentRevisionRO:
512
+    def get_one_revision(self, revision_id: int = None, content: Content= None) -> ContentRevisionRO:  # nopep8
503
         """
513
         """
504
         This method allow us to get directly any revision with its id
514
         This method allow us to get directly any revision with its id
505
         :param revision_id: The content's revision's id that we want to return
515
         :param revision_id: The content's revision's id that we want to return
516
+        :param content: The content related to the revision, if None do not
517
+        check if revision is related to this content.
506
         :return: An item Content linked with the correct revision
518
         :return: An item Content linked with the correct revision
507
         """
519
         """
508
         assert revision_id is not None# DYN_REMOVE
520
         assert revision_id is not None# DYN_REMOVE
509
 
521
 
510
         revision = self._session.query(ContentRevisionRO).filter(ContentRevisionRO.revision_id == revision_id).one()
522
         revision = self._session.query(ContentRevisionRO).filter(ContentRevisionRO.revision_id == revision_id).one()
511
-
523
+        if content and revision.content_id != content.content_id:
524
+            raise RevisionDoesNotMatchThisContent(
525
+                'revision {revision_id} is not a revision of content {content_id}'.format(  # nopep8
526
+                    revision_id=revision.revision_id,
527
+                    content_id=content.content_id,
528
+                    )
529
+            )
512
         return revision
530
         return revision
513
 
531
 
514
     # INFO - A.P - 2017-07-03 - python file object getter
532
     # INFO - A.P - 2017-07-03 - python file object getter
631
             )\
649
             )\
632
             .one()
650
             .one()
633
 
651
 
652
+    # TODO - G.M - 2018-07-24 - [Cleanup] Is this method already needed ?
634
     def get_folder_with_workspace_path_labels(
653
     def get_folder_with_workspace_path_labels(
635
             self,
654
             self,
636
             path_labels: [str],
655
             path_labels: [str],
724
             ),
743
             ),
725
         ))
744
         ))
726
 
745
 
727
-    def get_all(self, parent_id: int=None, content_type: str=ContentType.Any, workspace: Workspace=None) -> typing.List[Content]:
728
-        assert parent_id is None or isinstance(parent_id, int) # DYN_REMOVE
729
-        assert content_type is not None# DYN_REMOVE
730
-        assert isinstance(content_type, str) # DYN_REMOVE
731
 
746
 
732
-        resultset = self._base_query(workspace)
747
+    def get_pdf_preview_path(
748
+            self,
749
+            content_id: int,
750
+            revision_id: int,
751
+            page: int
752
+    ) -> str:
753
+        """
754
+        Get pdf preview of revision of content
755
+        :param content_id: id of content
756
+        :param revision_id: id of content revision
757
+        :param page: page number of the preview, useful for multipage content
758
+        :return: preview_path as string
759
+        """
760
+        file_path = self.get_one_revision_filepath(revision_id)
761
+        if page >= self.preview_manager.get_page_nb(file_path):
762
+            raise PageOfPreviewNotFound(
763
+                'page {page} of content {content_id} does not exist'.format(
764
+                    page=page,
765
+                    content_id=content_id
766
+                ),
767
+            )
768
+        jpg_preview_path = self.preview_manager.get_pdf_preview(
769
+            file_path,
770
+            page=page
771
+        )
772
+        return jpg_preview_path
733
 
773
 
734
-        if content_type!=ContentType.Any:
735
-            resultset = resultset.filter(Content.type==content_type)
774
+    def get_full_pdf_preview_path(self, revision_id: int) -> str:
775
+        """
776
+        Get full(multiple page) pdf preview of revision of content
777
+        :param revision_id: id of revision
778
+        :return: path of the full pdf preview of this revision
779
+        """
780
+        file_path = self.get_one_revision_filepath(revision_id)
781
+        pdf_preview_path = self.preview_manager.get_pdf_preview(file_path)
782
+        return pdf_preview_path
736
 
783
 
737
-        if parent_id:
738
-            resultset = resultset.filter(Content.parent_id==parent_id)
739
-        if parent_id == 0 or parent_id is False:
740
-            resultset = resultset.filter(Content.parent_id == None)
741
-        # parent_id == None give all contents
784
+    def get_jpg_preview_allowed_dim(self) -> PreviewAllowedDim:
785
+        """
786
+        Get jpg preview allowed dimensions and strict bool param.
787
+        """
788
+        return PreviewAllowedDim(
789
+            self._config.PREVIEW_JPG_RESTRICTED_DIMS,
790
+            self._config.PREVIEW_JPG_ALLOWED_DIMS,
791
+        )
742
 
792
 
743
-        return resultset.all()
793
+    def get_jpg_preview_path(
794
+        self,
795
+        content_id: int,
796
+        revision_id: int,
797
+        page: int,
798
+        width: int = None,
799
+        height: int = None,
800
+    ) -> str:
801
+        """
802
+        Get jpg preview of revision of content
803
+        :param content_id: id of content
804
+        :param revision_id: id of content revision
805
+        :param page: page number of the preview, useful for multipage content
806
+        :param width: width in pixel
807
+        :param height: height in pixel
808
+        :return: preview_path as string
809
+        """
810
+        file_path = self.get_one_revision_filepath(revision_id)
811
+        if page >= self.preview_manager.get_page_nb(file_path):
812
+            raise Exception(
813
+                'page {page} of revision {revision_id} of content {content_id} does not exist'.format(  # nopep8
814
+                    page=page,
815
+                    revision_id=revision_id,
816
+                    content_id=content_id,
817
+                ),
818
+            )
819
+        if not width and not height:
820
+            width = self._config.PREVIEW_JPG_ALLOWED_DIMS[0].width
821
+            height = self._config.PREVIEW_JPG_ALLOWED_DIMS[0].height
822
+
823
+        allowed_dim = False
824
+        for preview_dim in self._config.PREVIEW_JPG_ALLOWED_DIMS:
825
+            if width == preview_dim.width and height == preview_dim.height:
826
+                allowed_dim = True
827
+                break
828
+
829
+        if not allowed_dim and self._config.PREVIEW_JPG_RESTRICTED_DIMS:
830
+            raise PreviewDimNotAllowed(
831
+                'Size {width}x{height} is not allowed for jpeg preview'.format(
832
+                    width=width,
833
+                    height=height,
834
+                )
835
+            )
836
+        jpg_preview_path = self.preview_manager.get_jpeg_preview(
837
+            file_path,
838
+            page=page,
839
+            width=width,
840
+            height=height,
841
+        )
842
+        return jpg_preview_path
744
 
843
 
745
-    def get_children(self, parent_id: int, content_types: list, workspace: Workspace=None) -> typing.List[Content]:
844
+    def _get_all_query(
845
+        self,
846
+        parent_id: int = None,
847
+        content_type: str = ContentType.Any,
848
+        workspace: Workspace = None
849
+    ) -> Query:
746
         """
850
         """
747
-        Return parent_id childs of given content_types
748
-        :param parent_id: parent id
749
-        :param content_types: list of types
750
-        :param workspace: workspace filter
751
-        :return: list of content
851
+        Extended filter for better "get all data" query
852
+        :param parent_id: filter by parent_id
853
+        :param content_type: filter by content_type slug
854
+        :param workspace: filter by workspace
855
+        :return:
752
         """
856
         """
857
+        assert parent_id is None or isinstance(parent_id, int)
858
+        assert content_type is not None
753
         resultset = self._base_query(workspace)
859
         resultset = self._base_query(workspace)
754
-        resultset = resultset.filter(Content.type.in_(content_types))
860
+
861
+        if content_type!=ContentType.Any:
862
+            # INFO - G.M - 2018-07-05 - convert with
863
+            #  content type object to support legacy slug
864
+            content_type_object = ContentType(content_type)
865
+            resultset = resultset.filter(Content.type.in_(content_type_object.get_slug_aliases()))
755
 
866
 
756
         if parent_id:
867
         if parent_id:
757
             resultset = resultset.filter(Content.parent_id==parent_id)
868
             resultset = resultset.filter(Content.parent_id==parent_id)
758
-        if parent_id is False:
869
+        if parent_id == 0 or parent_id is False:
759
             resultset = resultset.filter(Content.parent_id == None)
870
             resultset = resultset.filter(Content.parent_id == None)
760
 
871
 
761
-        return resultset.all()
872
+        return resultset
873
+
874
+    def get_all(self, parent_id: int=None, content_type: str=ContentType.Any, workspace: Workspace=None) -> typing.List[Content]:
875
+        return self._get_all_query(parent_id, content_type, workspace).all()
762
 
876
 
877
+    # TODO - G.M - 2018-07-17 - [Cleanup] Drop this method if unneeded
878
+    # def get_children(self, parent_id: int, content_types: list, workspace: Workspace=None) -> typing.List[Content]:
879
+    #     """
880
+    #     Return parent_id childs of given content_types
881
+    #     :param parent_id: parent id
882
+    #     :param content_types: list of types
883
+    #     :param workspace: workspace filter
884
+    #     :return: list of content
885
+    #     """
886
+    #     resultset = self._base_query(workspace)
887
+    #     resultset = resultset.filter(Content.type.in_(content_types))
888
+    #
889
+    #     if parent_id:
890
+    #         resultset = resultset.filter(Content.parent_id==parent_id)
891
+    #     if parent_id is False:
892
+    #         resultset = resultset.filter(Content.parent_id == None)
893
+    #
894
+    #     return resultset.all()
895
+
896
+    # TODO - G.M - 2018-07-17 - [Cleanup] Drop this method if unneeded
763
     # TODO find an other name to filter on is_deleted / is_archived
897
     # TODO find an other name to filter on is_deleted / is_archived
764
     def get_all_with_filter(self, parent_id: int=None, content_type: str=ContentType.Any, workspace: Workspace=None) -> typing.List[Content]:
898
     def get_all_with_filter(self, parent_id: int=None, content_type: str=ContentType.Any, workspace: Workspace=None) -> typing.List[Content]:
765
         assert parent_id is None or isinstance(parent_id, int) # DYN_REMOVE
899
         assert parent_id is None or isinstance(parent_id, int) # DYN_REMOVE
779
 
913
 
780
         return resultset.all()
914
         return resultset.all()
781
 
915
 
916
+    # TODO - G.M - 2018-07-24 - [Cleanup] Is this method already needed ?
782
     def get_all_without_exception(self, content_type: str, workspace: Workspace=None) -> typing.List[Content]:
917
     def get_all_without_exception(self, content_type: str, workspace: Workspace=None) -> typing.List[Content]:
783
         assert content_type is not None# DYN_REMOVE
918
         assert content_type is not None# DYN_REMOVE
784
 
919
 
839
             .filter(Workspace.is_deleted.is_(False)) \
974
             .filter(Workspace.is_deleted.is_(False)) \
840
             .subquery()
975
             .subquery()
841
 
976
 
842
-        not_read_content_ids_query = self._session.query(
843
-            distinct(not_read_revisions.c.content_id)
844
-        )
845
-        not_read_content_ids = list(map(
846
-            itemgetter(0),
847
-            not_read_content_ids_query,
848
-        ))
849
-
850
-        not_read_contents = self._base_query(workspace) \
851
-            .filter(Content.content_id.in_(not_read_content_ids)) \
852
-            .order_by(desc(Content.updated))
977
+    # TODO - G.M - 2018-07-17 - [Cleanup] Drop this method if unneeded
978
+    # def get_all_without_exception(self, content_type: str, workspace: Workspace=None) -> typing.List[Content]:
979
+    #     assert content_type is not None# DYN_REMOVE
980
+    #
981
+    #     resultset = self._base_query(workspace)
982
+    #
983
+    #     if content_type != ContentType.Any:
984
+    #         resultset = resultset.filter(Content.type==content_type)
985
+    #
986
+    #     return resultset.all()
987
+
988
+    def get_last_active(
989
+            self,
990
+            workspace: Workspace=None,
991
+            limit: typing.Optional[int]=None,
992
+            before_datetime: typing.Optional[datetime.datetime]= None,
993
+            content_ids: typing.Optional[typing.List[int]] = None,
994
+    ) -> typing.List[Content]:
995
+        """
996
+        get contents list sorted by last update
997
+        (last modification of content itself or one of this comment)
998
+        :param workspace: Workspace to check
999
+        :param limit: maximum number of elements to return
1000
+        :param before_datetime: date from where we check older content.
1001
+        :param content_ids: restrict selection to some content ids and
1002
+        related Comments
1003
+        :return: list of content
1004
+        """
853
 
1005
 
854
-        if content_type != ContentType.Any:
855
-            not_read_contents = not_read_contents.filter(
856
-                Content.type==content_type)
857
-        else:
858
-            not_read_contents = not_read_contents.filter(
859
-                Content.type!=ContentType.Folder)
1006
+        resultset = self._get_all_query(
1007
+            workspace=workspace,
1008
+        )
1009
+        if content_ids:
1010
+            resultset = resultset.filter(
1011
+                or_(
1012
+                    Content.content_id.in_(content_ids),
1013
+                    and_(
1014
+                        Content.parent_id.in_(content_ids),
1015
+                        Content.type == ContentType.Comment
1016
+                    )
1017
+                )
1018
+            )
860
 
1019
 
861
-        if parent_id:
862
-            not_read_contents = not_read_contents.filter(
863
-                Content.parent_id==parent_id)
1020
+        resultset = resultset.order_by(desc(Content.updated))
864
 
1021
 
865
-        result = []
866
-        for item in not_read_contents:
867
-            new_item = None
868
-            if ContentType.Comment == item.type:
869
-                new_item = item.parent
1022
+        active_contents = []
1023
+        too_recent_content = []
1024
+        for content in resultset:
1025
+            related_active_content = None
1026
+            if ContentType.Comment == content.type:
1027
+                related_active_content = content.parent
870
             else:
1028
             else:
871
-                new_item = item
1029
+                related_active_content = content
872
 
1030
 
1031
+            if not before_datetime:
1032
+                before_datetime = datetime.datetime.now()
873
             # INFO - D.A. - 2015-05-20
1033
             # INFO - D.A. - 2015-05-20
874
             # We do not want to show only one item if the last 10 items are
1034
             # We do not want to show only one item if the last 10 items are
875
             # comments about one thread for example
1035
             # comments about one thread for example
876
-            if new_item not in result:
877
-                result.append(new_item)
878
-
879
-            if len(result) >= limit:
1036
+            if related_active_content not in active_contents and related_active_content not in too_recent_content:  # nopep8
1037
+                # we verify that content is old enough
1038
+                if content.updated < before_datetime:
1039
+                    active_contents.append(related_active_content)
1040
+                else:
1041
+                    too_recent_content.append(related_active_content)
1042
+
1043
+            if limit and len(active_contents) >= limit:
880
                 break
1044
                 break
881
 
1045
 
882
-        return result
1046
+        return active_contents
1047
+
1048
+    # TODO - G.M - 2018-07-19 - Find a way to update this method to something
1049
+    # usable and efficient for tracim v2 to get content with read/unread status
1050
+    # instead of relying on has_new_information_for()
1051
+    # def get_last_unread(self, parent_id: typing.Optional[int], content_type: str,
1052
+    #                     workspace: Workspace=None, limit=10) -> typing.List[Content]:
1053
+    #     assert parent_id is None or isinstance(parent_id, int) # DYN_REMOVE
1054
+    #     assert content_type is not None# DYN_REMOVE
1055
+    #     assert isinstance(content_type, str) # DYN_REMOVE
1056
+    #
1057
+    #     read_revision_ids = self._session.query(RevisionReadStatus.revision_id) \
1058
+    #         .filter(RevisionReadStatus.user_id==self._user_id)
1059
+    #
1060
+    #     not_read_revisions = self._revisions_base_query(workspace) \
1061
+    #         .filter(~ContentRevisionRO.revision_id.in_(read_revision_ids)) \
1062
+    #         .filter(ContentRevisionRO.workspace_id == Workspace.workspace_id) \
1063
+    #         .filter(Workspace.is_deleted.is_(False)) \
1064
+    #         .subquery()
1065
+    #
1066
+    #     not_read_content_ids_query = self._session.query(
1067
+    #         distinct(not_read_revisions.c.content_id)
1068
+    #     )
1069
+    #     not_read_content_ids = list(map(
1070
+    #         itemgetter(0),
1071
+    #         not_read_content_ids_query,
1072
+    #     ))
1073
+    #
1074
+    #     not_read_contents = self._base_query(workspace) \
1075
+    #         .filter(Content.content_id.in_(not_read_content_ids)) \
1076
+    #         .order_by(desc(Content.updated))
1077
+    #
1078
+    #     if content_type != ContentType.Any:
1079
+    #         not_read_contents = not_read_contents.filter(
1080
+    #             Content.type==content_type)
1081
+    #     else:
1082
+    #         not_read_contents = not_read_contents.filter(
1083
+    #             Content.type!=ContentType.Folder)
1084
+    #
1085
+    #     if parent_id:
1086
+    #         not_read_contents = not_read_contents.filter(
1087
+    #             Content.parent_id==parent_id)
1088
+    #
1089
+    #     result = []
1090
+    #     for item in not_read_contents:
1091
+    #         new_item = None
1092
+    #         if ContentType.Comment == item.type:
1093
+    #             new_item = item.parent
1094
+    #         else:
1095
+    #             new_item = item
1096
+    #
1097
+    #         # INFO - D.A. - 2015-05-20
1098
+    #         # We do not want to show only one item if the last 10 items are
1099
+    #         # comments about one thread for example
1100
+    #         if new_item not in result:
1101
+    #             result.append(new_item)
1102
+    #
1103
+    #         if len(result) >= limit:
1104
+    #             break
1105
+    #
1106
+    #     return result
883
 
1107
 
884
     def set_allowed_content(self, folder: Content, allowed_content_dict:dict):
1108
     def set_allowed_content(self, folder: Content, allowed_content_dict:dict):
885
         """
1109
         """
985
         self.save(item, do_notify=False)
1209
         self.save(item, do_notify=False)
986
 
1210
 
987
         for child in item.children:
1211
         for child in item.children:
988
-            with new_revision(child):
1212
+            with new_revision(
1213
+                session=self._session,
1214
+                tm=transaction.manager,
1215
+                content=child
1216
+            ):
989
                 self.move_recursively(child, item, new_workspace)
1217
                 self.move_recursively(child, item, new_workspace)
990
         return
1218
         return
991
 
1219
 
1058
                        do_flush: bool=True,
1286
                        do_flush: bool=True,
1059
                        recursive: bool=True
1287
                        recursive: bool=True
1060
                        ):
1288
                        ):
1061
-
1062
-        itemset = self.get_last_unread(None, ContentType.Any)
1063
-
1064
-        for item in itemset:
1065
-            self.mark_read(item, read_datetime, do_flush, recursive)
1289
+        return self.mark_read__workspace(None, read_datetime, do_flush, recursive) # nopep8
1066
 
1290
 
1067
     def mark_read__workspace(self,
1291
     def mark_read__workspace(self,
1068
                        workspace : Workspace,
1292
                        workspace : Workspace,
1070
                        do_flush: bool=True,
1294
                        do_flush: bool=True,
1071
                        recursive: bool=True
1295
                        recursive: bool=True
1072
                        ):
1296
                        ):
1073
-
1074
-        itemset = self.get_last_unread(None, ContentType.Any, workspace)
1075
-
1297
+        itemset = self.get_last_active(workspace)
1076
         for item in itemset:
1298
         for item in itemset:
1077
-            self.mark_read(item, read_datetime, do_flush, recursive)
1299
+            if item.has_new_information_for(self._user):
1300
+                self.mark_read(item, read_datetime, do_flush, recursive)
1078
 
1301
 
1079
     def mark_read(self, content: Content,
1302
     def mark_read(self, content: Content,
1080
                   read_datetime: datetime=None,
1303
                   read_datetime: datetime=None,
1129
             .filter(ContentRevisionRO.content_id==content.content_id).all()
1352
             .filter(ContentRevisionRO.content_id==content.content_id).all()
1130
 
1353
 
1131
         for revision in revisions:
1354
         for revision in revisions:
1132
-            del revision.read_by[self._user]
1355
+            try:
1356
+                del revision.read_by[self._user]
1357
+            except KeyError:
1358
+                pass
1133
 
1359
 
1134
         for child in content.get_valid_children():
1360
         for child in content.get_valid_children():
1135
             self.mark_unread(child, do_flush=False)
1361
             self.mark_unread(child, do_flush=False)
1232
 
1458
 
1233
         return ContentType.sorted(content_types)
1459
         return ContentType.sorted(content_types)
1234
 
1460
 
1461
+    # TODO - G.M - 2018-07-24 - [Cleanup] Is this method already needed ?
1235
     def exclude_unavailable(
1462
     def exclude_unavailable(
1236
         self,
1463
         self,
1237
         contents: typing.List[Content],
1464
         contents: typing.List[Content],
1245
                 contents.remove(content)
1472
                 contents.remove(content)
1246
         return contents
1473
         return contents
1247
 
1474
 
1475
+    # TODO - G.M - 2018-07-24 - [Cleanup] Is this method already needed ?
1248
     def content_under_deleted(self, content: Content) -> bool:
1476
     def content_under_deleted(self, content: Content) -> bool:
1249
         if content.parent:
1477
         if content.parent:
1250
             if content.parent.is_deleted:
1478
             if content.parent.is_deleted:
1253
                 return self.content_under_deleted(content.parent)
1481
                 return self.content_under_deleted(content.parent)
1254
         return False
1482
         return False
1255
 
1483
 
1484
+    # TODO - G.M - 2018-07-24 - [Cleanup] Is this method already needed ?
1256
     def content_under_archived(self, content: Content) -> bool:
1485
     def content_under_archived(self, content: Content) -> bool:
1257
         if content.parent:
1486
         if content.parent:
1258
             if content.parent.is_archived:
1487
             if content.parent.is_archived:
1261
                 return self.content_under_archived(content.parent)
1490
                 return self.content_under_archived(content.parent)
1262
         return False
1491
         return False
1263
 
1492
 
1493
+    # TODO - G.M - 2018-07-24 - [Cleanup] Is this method already needed ?
1264
     def find_one_by_unique_property(
1494
     def find_one_by_unique_property(
1265
             self,
1495
             self,
1266
             property_name: str,
1496
             property_name: str,
1289
         )
1519
         )
1290
         return query.one()
1520
         return query.one()
1291
 
1521
 
1522
+    # TODO - G.M - 2018-07-24 - [Cleanup] Is this method already needed ?
1292
     def generate_folder_label(
1523
     def generate_folder_label(
1293
             self,
1524
             self,
1294
             workspace: Workspace,
1525
             workspace: Workspace,

+ 63 - 1
tracim/lib/core/user.py View File

9
 from tracim import CFG
9
 from tracim import CFG
10
 from tracim.models.auth import User
10
 from tracim.models.auth import User
11
 from tracim.models.auth import Group
11
 from tracim.models.auth import Group
12
-from tracim.exceptions import WrongUserPassword
13
 from tracim.exceptions import NoUserSetted
12
 from tracim.exceptions import NoUserSetted
14
 from tracim.exceptions import PasswordDoNotMatch
13
 from tracim.exceptions import PasswordDoNotMatch
14
+from tracim.exceptions import EmailValidationFailed
15
 from tracim.exceptions import UserDoesNotExist
15
 from tracim.exceptions import UserDoesNotExist
16
+from tracim.exceptions import WrongUserPassword
16
 from tracim.exceptions import AuthenticationFailed
17
 from tracim.exceptions import AuthenticationFailed
17
 from tracim.exceptions import NotificationNotSend
18
 from tracim.exceptions import NotificationNotSend
18
 from tracim.exceptions import UserNotActive
19
 from tracim.exceptions import UserNotActive
19
 from tracim.models.context_models import UserInContext
20
 from tracim.models.context_models import UserInContext
20
 from tracim.lib.mail_notifier.notifier import get_email_manager
21
 from tracim.lib.mail_notifier.notifier import get_email_manager
22
+from tracim.models.context_models import TypeUser
21
 
23
 
22
 
24
 
23
 class UserApi(object):
25
 class UserApi(object):
70
             raise UserDoesNotExist('User "{}" not found in database'.format(email)) from exc  # nopep8
72
             raise UserDoesNotExist('User "{}" not found in database'.format(email)) from exc  # nopep8
71
         return user
73
         return user
72
 
74
 
75
+    def get_one_by_public_name(self, public_name: str) -> User:
76
+        """
77
+        Get one user by public_name
78
+        """
79
+        try:
80
+            user = self._base_query().filter(User.display_name == public_name).one()
81
+        except NoResultFound as exc:
82
+            raise UserDoesNotExist('User "{}" not found in database'.format(public_name)) from exc  # nopep8
83
+        return user
73
     # FIXME - G.M - 24-04-2018 - Duplicate method with get_one.
84
     # FIXME - G.M - 24-04-2018 - Duplicate method with get_one.
85
+
74
     def get_one_by_id(self, id: int) -> User:
86
     def get_one_by_id(self, id: int) -> User:
75
         return self.get_one(user_id=id)
87
         return self.get_one(user_id=id)
76
 
88
 
85
     def get_all(self) -> typing.Iterable[User]:
97
     def get_all(self) -> typing.Iterable[User]:
86
         return self._session.query(User).order_by(User.display_name).all()
98
         return self._session.query(User).order_by(User.display_name).all()
87
 
99
 
100
+    def find(
101
+            self,
102
+            user_id: int=None,
103
+            email: str=None,
104
+            public_name: str=None
105
+    ) -> typing.Tuple[TypeUser, User]:
106
+        """
107
+        Find existing user from all theses params.
108
+        Check is made in this order: user_id, email, public_name
109
+        If no user found raise UserDoesNotExist exception
110
+        """
111
+        user = None
112
+
113
+        if user_id:
114
+            try:
115
+                user = self.get_one(user_id)
116
+                return TypeUser.USER_ID, user
117
+            except UserDoesNotExist:
118
+                pass
119
+        if email:
120
+            try:
121
+                user = self.get_one_by_email(email)
122
+                return TypeUser.EMAIL, user
123
+            except UserDoesNotExist:
124
+                pass
125
+        if public_name:
126
+            try:
127
+                user = self.get_one_by_public_name(public_name)
128
+                return TypeUser.PUBLIC_NAME, user
129
+            except UserDoesNotExist:
130
+                pass
131
+
132
+        raise UserDoesNotExist('User not found with any of given params.')
133
+
88
     # Check methods
134
     # Check methods
89
 
135
 
90
     def user_with_email_exists(self, email: str) -> bool:
136
     def user_with_email_exists(self, email: str) -> bool:
182
         )
228
         )
183
         return user
229
         return user
184
 
230
 
231
+    def _check_email(self, email: str) -> bool:
232
+        # TODO - G.M - 2018-07-05 - find a better way to check email
233
+        if not email:
234
+            return False
235
+        email = email.split('@')
236
+        if len(email) != 2:
237
+            return False
238
+        return True
239
+
185
     def update(
240
     def update(
186
             self,
241
             self,
187
             user: User,
242
             user: User,
196
             user.display_name = name
251
             user.display_name = name
197
 
252
 
198
         if email is not None:
253
         if email is not None:
254
+            email_exist = self._check_email(email)
255
+            if not email_exist:
256
+                raise EmailValidationFailed('Email given form {} is uncorrect'.format(email))  # nopep8
199
             user.email = email
257
             user.email = email
200
 
258
 
201
         if password is not None:
259
         if password is not None:
260
         """Previous create_user method"""
318
         """Previous create_user method"""
261
         user = User()
319
         user = User()
262
 
320
 
321
+        email_exist = self._check_email(email)
322
+        if not email_exist:
323
+            raise EmailValidationFailed('Email given form {} is uncorrect'.format(email))  # nopep8
263
         user.email = email
324
         user.email = email
325
+        user.display_name = email.split('@')[0]
264
 
326
 
265
         for group in groups:
327
         for group in groups:
266
             user.groups.append(group)
328
             user.groups.append(group)

+ 108 - 72
tracim/lib/core/userworkspace.py View File

3
 
3
 
4
 from tracim import CFG
4
 from tracim import CFG
5
 from tracim.models.context_models import UserRoleWorkspaceInContext
5
 from tracim.models.context_models import UserRoleWorkspaceInContext
6
+from tracim.models.roles import WorkspaceRoles
6
 
7
 
7
 __author__ = 'damien'
8
 __author__ = 'damien'
8
 
9
 
11
 from tracim.models.auth import User
12
 from tracim.models.auth import User
12
 from tracim.models.data import Workspace
13
 from tracim.models.data import Workspace
13
 from tracim.models.data import UserRoleInWorkspace
14
 from tracim.models.data import UserRoleInWorkspace
14
-from tracim.models.data import RoleType
15
 
15
 
16
 
16
 
17
 class RoleApi(object):
17
 class RoleApi(object):
18
 
18
 
19
-    ALL_ROLE_VALUES = UserRoleInWorkspace.get_all_role_values()
19
+    # TODO - G.M - 29-06-2018 - [Cleanup] Drop this
20
+    # ALL_ROLE_VALUES = UserRoleInWorkspace.get_all_role_values()
20
     # Dict containing readable members roles for given role
21
     # Dict containing readable members roles for given role
21
-    members_read_rights = {
22
-        UserRoleInWorkspace.NOT_APPLICABLE: [],
23
-        UserRoleInWorkspace.READER: [
24
-            UserRoleInWorkspace.WORKSPACE_MANAGER,
25
-        ],
26
-        UserRoleInWorkspace.CONTRIBUTOR: [
27
-            UserRoleInWorkspace.WORKSPACE_MANAGER,
28
-            UserRoleInWorkspace.CONTENT_MANAGER,
29
-            UserRoleInWorkspace.CONTRIBUTOR,
30
-        ],
31
-        UserRoleInWorkspace.CONTENT_MANAGER: [
32
-            UserRoleInWorkspace.WORKSPACE_MANAGER,
33
-            UserRoleInWorkspace.CONTENT_MANAGER,
34
-            UserRoleInWorkspace.CONTRIBUTOR,
35
-            UserRoleInWorkspace.READER,
36
-        ],
37
-        UserRoleInWorkspace.WORKSPACE_MANAGER: [
38
-            UserRoleInWorkspace.WORKSPACE_MANAGER,
39
-            UserRoleInWorkspace.CONTENT_MANAGER,
40
-            UserRoleInWorkspace.CONTRIBUTOR,
41
-            UserRoleInWorkspace.READER,
42
-        ],
43
-    }
22
+    # members_read_rights = {
23
+    #     UserRoleInWorkspace.NOT_APPLICABLE: [],
24
+    #     UserRoleInWorkspace.READER: [
25
+    #         UserRoleInWorkspace.WORKSPACE_MANAGER,
26
+    #     ],
27
+    #     UserRoleInWorkspace.CONTRIBUTOR: [
28
+    #         UserRoleInWorkspace.WORKSPACE_MANAGER,
29
+    #         UserRoleInWorkspace.CONTENT_MANAGER,
30
+    #         UserRoleInWorkspace.CONTRIBUTOR,
31
+    #     ],
32
+    #     UserRoleInWorkspace.CONTENT_MANAGER: [
33
+    #         UserRoleInWorkspace.WORKSPACE_MANAGER,
34
+    #         UserRoleInWorkspace.CONTENT_MANAGER,
35
+    #         UserRoleInWorkspace.CONTRIBUTOR,
36
+    #         UserRoleInWorkspace.READER,
37
+    #     ],
38
+    #     UserRoleInWorkspace.WORKSPACE_MANAGER: [
39
+    #         UserRoleInWorkspace.WORKSPACE_MANAGER,
40
+    #         UserRoleInWorkspace.CONTENT_MANAGER,
41
+    #         UserRoleInWorkspace.CONTRIBUTOR,
42
+    #         UserRoleInWorkspace.READER,
43
+    #     ],
44
+    # }
45
+
46
+    # TODO - G.M - 29-06-2018 - [Cleanup] Drop this
47
+    # @classmethod
48
+    # def role_can_read_member_role(cls, reader_role: int, tested_role: int) \
49
+    #         -> bool:
50
+    #     """
51
+    #     :param reader_role: role as viewer
52
+    #     :param tested_role: role as viwed
53
+    #     :return: True if given role can view member role in workspace.
54
+    #     """
55
+    #     if reader_role in cls.members_read_rights:
56
+    #         return tested_role in cls.members_read_rights[reader_role]
57
+    #     return False
44
 
58
 
45
     def get_user_role_workspace_with_context(
59
     def get_user_role_workspace_with_context(
46
             self,
60
             self,
47
-            user_role: UserRoleInWorkspace
61
+            user_role: UserRoleInWorkspace,
62
+            newly_created:bool = None,
63
+            email_sent: bool = None,
48
     ) -> UserRoleWorkspaceInContext:
64
     ) -> UserRoleWorkspaceInContext:
49
         """
65
         """
50
         Return WorkspaceInContext object from Workspace
66
         Return WorkspaceInContext object from Workspace
54
             user_role=user_role,
70
             user_role=user_role,
55
             dbsession=self._session,
71
             dbsession=self._session,
56
             config=self._config,
72
             config=self._config,
73
+            newly_created=newly_created,
74
+            email_sent=email_sent,
57
         )
75
         )
58
         return workspace
76
         return workspace
59
 
77
 
60
-    @classmethod
61
-    def role_can_read_member_role(cls, reader_role: int, tested_role: int) \
62
-            -> bool:
63
-        """
64
-        :param reader_role: role as viewer
65
-        :param tested_role: role as viwed
66
-        :return: True if given role can view member role in workspace.
67
-        """
68
-        if reader_role in cls.members_read_rights:
69
-            return tested_role in cls.members_read_rights[reader_role]
70
-        return False
71
-
72
-    @classmethod
73
-    def create_role(cls) -> UserRoleInWorkspace:
74
-        role = UserRoleInWorkspace()
75
-
76
-        return role
77
-
78
     def __init__(
78
     def __init__(
79
         self,
79
         self,
80
         session: Session,
80
         session: Session,
98
     def get_one(self, user_id: int, workspace_id: int) -> UserRoleInWorkspace:
98
     def get_one(self, user_id: int, workspace_id: int) -> UserRoleInWorkspace:
99
         return self._get_one_rsc(user_id, workspace_id).one()
99
         return self._get_one_rsc(user_id, workspace_id).one()
100
 
100
 
101
+    def update_role(
102
+        self,
103
+        role: UserRoleInWorkspace,
104
+        role_level: int,
105
+        with_notif: typing.Optional[bool] = None,
106
+        save_now: bool=False,
107
+    ):
108
+        """
109
+        Update role of user in this workspace
110
+        :param role: UserRoleInWorkspace object
111
+        :param role_level: level of new role wanted
112
+        :param with_notif: is user notification enabled in this workspace ?
113
+        :param save_now: database flush
114
+        :return: updated role
115
+        """
116
+        role.role = role_level
117
+        if with_notif is not None:
118
+            role.do_notify == with_notif
119
+        if save_now:
120
+            self.save(role)
121
+
122
+        return role
123
+
101
     def create_one(
124
     def create_one(
102
         self,
125
         self,
103
         user: User,
126
         user: User,
106
         with_notif: bool,
129
         with_notif: bool,
107
         flush: bool=True
130
         flush: bool=True
108
     ) -> UserRoleInWorkspace:
131
     ) -> UserRoleInWorkspace:
109
-        role = self.create_role()
132
+        role = UserRoleInWorkspace()
110
         role.user_id = user.user_id
133
         role.user_id = user.user_id
111
         role.workspace = workspace
134
         role.workspace = workspace
112
         role.role = role_level
135
         role.role = role_level
120
         if flush:
143
         if flush:
121
             self._session.flush()
144
             self._session.flush()
122
 
145
 
123
-    def _get_all_for_user(self, user_id) -> typing.List[UserRoleInWorkspace]:
124
-        return self._session.query(UserRoleInWorkspace)\
125
-            .filter(UserRoleInWorkspace.user_id == user_id)
126
-
127
-    def get_all_for_user(self, user: User) -> typing.List[UserRoleInWorkspace]:
128
-        return self._get_all_for_user(user.user_id).all()
129
-
130
-    def get_all_for_user_order_by_workspace(
131
-        self,
132
-        user_id: int
133
-    ) -> typing.List[UserRoleInWorkspace]:
134
-        return self._get_all_for_user(user_id)\
135
-            .join(UserRoleInWorkspace.workspace).order_by(Workspace.label).all()
136
-
137
     def get_all_for_workspace(
146
     def get_all_for_workspace(
138
         self,
147
         self,
139
         workspace:Workspace
148
         workspace:Workspace
145
     def save(self, role: UserRoleInWorkspace) -> None:
154
     def save(self, role: UserRoleInWorkspace) -> None:
146
         self._session.flush()
155
         self._session.flush()
147
 
156
 
148
-    # TODO - G.M - 07-06-2018 - [Cleanup] Check if this method is already needed
149
-    @classmethod
150
-    def get_roles_for_select_field(cls) -> typing.List[RoleType]:
151
-        """
152
-
153
-        :return: list of DictLikeClass instances representing available Roles
154
-        (to be used in select fields)
155
-        """
156
-        result = list()
157
 
157
 
158
-        for role_id in UserRoleInWorkspace.get_all_role_values():
159
-            role = RoleType(role_id)
160
-            result.append(role)
158
+    # TODO - G.M - 29-06-2018 - [Cleanup] Drop this
159
+    # @classmethod
160
+    # def role_can_read_member_role(cls, reader_role: int, tested_role: int) \
161
+    #         -> bool:
162
+    #     """
163
+    #     :param reader_role: role as viewer
164
+    #     :param tested_role: role as viwed
165
+    #     :return: True if given role can view member role in workspace.
166
+    #     """
167
+    #     if reader_role in cls.members_read_rights:
168
+    #         return tested_role in cls.members_read_rights[reader_role]
169
+    #     return False
170
+    # def _get_all_for_user(self, user_id) -> typing.List[UserRoleInWorkspace]:
171
+    #     return self._session.query(UserRoleInWorkspace)\
172
+    #         .filter(UserRoleInWorkspace.user_id == user_id)
173
+    #
174
+    # def get_all_for_user(self, user: User) -> typing.List[UserRoleInWorkspace]:
175
+    #     return self._get_all_for_user(user.user_id).all()
176
+    #
177
+    # def get_all_for_user_order_by_workspace(
178
+    #     self,
179
+    #     user_id: int
180
+    # ) -> typing.List[UserRoleInWorkspace]:
181
+    #     return self._get_all_for_user(user_id)\
182
+    #         .join(UserRoleInWorkspace.workspace).order_by(Workspace.label).all()
161
 
183
 
162
-        return result
184
+    # TODO - G.M - 07-06-2018 - [Cleanup] Check if this method is already needed
185
+    # @classmethod
186
+    # def get_roles_for_select_field(cls) -> typing.List[RoleType]:
187
+    #     """
188
+    #
189
+    #     :return: list of DictLikeClass instances representing available Roles
190
+    #     (to be used in select fields)
191
+    #     """
192
+    #     result = list()
193
+    #
194
+    #     for role_id in UserRoleInWorkspace.get_all_role_values():
195
+    #         role = RoleType(role_id)
196
+    #         result.append(role)
197
+    #
198
+    #     return result

+ 27 - 1
tracim/lib/core/workspace.py View File

5
 from sqlalchemy.orm import Session
5
 from sqlalchemy.orm import Session
6
 
6
 
7
 from tracim import CFG
7
 from tracim import CFG
8
+from tracim.exceptions import EmptyLabelNotAllowed
8
 from tracim.lib.utils.translation import fake_translator as _
9
 from tracim.lib.utils.translation import fake_translator as _
9
 
10
 
10
 from tracim.lib.core.userworkspace import RoleApi
11
 from tracim.lib.core.userworkspace import RoleApi
69
             save_now: bool=False,
70
             save_now: bool=False,
70
     ) -> Workspace:
71
     ) -> Workspace:
71
         if not label:
72
         if not label:
72
-            label = self.generate_label()
73
+            raise EmptyLabelNotAllowed('Workspace label cannot be empty')
73
 
74
 
74
         workspace = Workspace()
75
         workspace = Workspace()
75
         workspace.label = label
76
         workspace.label = label
105
 
106
 
106
         return workspace
107
         return workspace
107
 
108
 
109
+    def update_workspace(
110
+            self,
111
+            workspace: Workspace,
112
+            label: str,
113
+            description: str,
114
+            save_now: bool=False,
115
+    ) -> Workspace:
116
+        """
117
+        Update workspace
118
+        :param workspace: workspace to update
119
+        :param label: new label of workspace
120
+        :param description: new description
121
+        :param save_now: database flush
122
+        :return: updated workspace
123
+        """
124
+        if not label:
125
+            raise EmptyLabelNotAllowed('Workspace label cannot be empty')
126
+        workspace.label = label
127
+        workspace.description = description
128
+
129
+        if save_now:
130
+            self.save(workspace)
131
+
132
+        return workspace
133
+
108
     def get_one(self, id):
134
     def get_one(self, id):
109
         return self._base_query().filter(Workspace.workspace_id == id).one()
135
         return self._base_query().filter(Workspace.workspace_id == id).one()
110
 
136
 

+ 22 - 0
tracim/models/contents.py View File

251
                 return
251
                 return
252
         raise ContentTypeNotExist()
252
         raise ContentTypeNotExist()
253
 
253
 
254
+    def get_slug_aliases(self) -> typing.List[str]:
255
+        """
256
+        Get all slug aliases of a content,
257
+        useful for legacy code convertion
258
+        """
259
+        # TODO - G.M - 2018-07-05 - Remove this legacy compat code
260
+        # when possible.
261
+        page_alias = [self.Page, self.PageLegacy]
262
+        if self.slug in page_alias:
263
+            return page_alias
264
+        else:
265
+            return [self.slug]
266
+
254
     @classmethod
267
     @classmethod
255
     def all(cls) -> typing.List[str]:
268
     def all(cls) -> typing.List[str]:
256
         return cls.allowed_types()
269
         return cls.allowed_types()
261
         return contents_types
274
         return contents_types
262
 
275
 
263
     @classmethod
276
     @classmethod
277
+    def allowed_type_values(cls) -> typing.List[str]:
278
+        """
279
+        All content type slug + special values like any
280
+        """
281
+        content_types = cls.allowed_types()
282
+        content_types.append(ContentTypeLegacy.Any)
283
+        return content_types
284
+
285
+    @classmethod
264
     def allowed_types_for_folding(cls):
286
     def allowed_types_for_folding(cls):
265
         # This method is used for showing only "main"
287
         # This method is used for showing only "main"
266
         # types in the left-side treeview
288
         # types in the left-side treeview

+ 173 - 3
tracim/models/context_models.py View File

1
 # coding=utf-8
1
 # coding=utf-8
2
 import typing
2
 import typing
3
 from datetime import datetime
3
 from datetime import datetime
4
+from enum import Enum
4
 
5
 
5
 from slugify import slugify
6
 from slugify import slugify
6
 from sqlalchemy.orm import Session
7
 from sqlalchemy.orm import Session
7
 from tracim import CFG
8
 from tracim import CFG
9
+from tracim.config import PreviewDim
8
 from tracim.models import User
10
 from tracim.models import User
9
 from tracim.models.auth import Profile
11
 from tracim.models.auth import Profile
10
 from tracim.models.data import Content
12
 from tracim.models.data import Content
11
 from tracim.models.data import ContentRevisionRO
13
 from tracim.models.data import ContentRevisionRO
12
-from tracim.models.data import Workspace, UserRoleInWorkspace
14
+from tracim.models.data import Workspace
15
+from tracim.models.data import UserRoleInWorkspace
16
+from tracim.models.roles import WorkspaceRoles
13
 from tracim.models.workspace_menu_entries import default_workspace_menu_entry
17
 from tracim.models.workspace_menu_entries import default_workspace_menu_entry
14
 from tracim.models.workspace_menu_entries import WorkspaceMenuEntry
18
 from tracim.models.workspace_menu_entries import WorkspaceMenuEntry
15
 from tracim.models.contents import ContentTypeLegacy as ContentType
19
 from tracim.models.contents import ContentTypeLegacy as ContentType
16
 
20
 
17
 
21
 
22
+class PreviewAllowedDim(object):
23
+
24
+    def __init__(
25
+            self,
26
+            restricted:bool,
27
+            dimensions: typing.List[PreviewDim]
28
+    ) -> None:
29
+        self.restricted = restricted
30
+        self.dimensions = dimensions
31
+
32
+
18
 class MoveParams(object):
33
 class MoveParams(object):
19
     """
34
     """
20
     Json body params for move action model
35
     Json body params for move action model
104
         self.workspace_id = workspace_id
119
         self.workspace_id = workspace_id
105
 
120
 
106
 
121
 
122
+class WorkspaceAndContentRevisionPath(object):
123
+    """
124
+    Paths params with workspace id and content_id model
125
+    """
126
+    def __init__(self, workspace_id: int, content_id: int, revision_id) -> None:
127
+        self.content_id = content_id
128
+        self.revision_id = revision_id
129
+        self.workspace_id = workspace_id
130
+
131
+
132
+class ContentPreviewSizedPath(object):
133
+    """
134
+    Paths params with workspace id and content_id, width, heigth
135
+    """
136
+    def __init__(self, workspace_id: int, content_id: int, width: int, height: int) -> None:  # nopep8
137
+        self.content_id = content_id
138
+        self.workspace_id = workspace_id
139
+        self.width = width
140
+        self.height = height
141
+
142
+
143
+class RevisionPreviewSizedPath(object):
144
+    """
145
+    Paths params with workspace id and content_id, revision_id width, heigth
146
+    """
147
+    def __init__(self, workspace_id: int, content_id: int, revision_id: int, width: int, height: int) -> None:  # nopep8
148
+        self.content_id = content_id
149
+        self.revision_id = revision_id
150
+        self.workspace_id = workspace_id
151
+        self.width = width
152
+        self.height = height
153
+
154
+
155
+class WorkspaceAndUserPath(object):
156
+    """
157
+    Paths params with workspace id and user_id
158
+    """
159
+    def __init__(self, workspace_id: int, user_id: int):
160
+        self.workspace_id = workspace_id
161
+        self.user_id = workspace_id
162
+
163
+
164
+class UserWorkspaceAndContentPath(object):
165
+    """
166
+    Paths params with user_id, workspace id and content_id model
167
+    """
168
+    def __init__(self, user_id: int, workspace_id: int, content_id: int) -> None:  # nopep8
169
+        self.content_id = content_id
170
+        self.workspace_id = workspace_id
171
+        self.user_id = user_id
172
+
173
+
107
 class CommentPath(object):
174
 class CommentPath(object):
108
     """
175
     """
109
     Paths params with workspace id and content_id and comment_id model
176
     Paths params with workspace id and content_id and comment_id model
119
         self.comment_id = comment_id
186
         self.comment_id = comment_id
120
 
187
 
121
 
188
 
189
+class PageQuery(object):
190
+    """
191
+    Page query model
192
+    """
193
+    def __init__(
194
+            self,
195
+            page: int = 0
196
+    ):
197
+        self.page = page
198
+
199
+
122
 class ContentFilter(object):
200
 class ContentFilter(object):
123
     """
201
     """
124
     Content filter model
202
     Content filter model
125
     """
203
     """
126
     def __init__(
204
     def __init__(
127
             self,
205
             self,
206
+            workspace_id: int = None,
128
             parent_id: int = None,
207
             parent_id: int = None,
129
             show_archived: int = 0,
208
             show_archived: int = 0,
130
             show_deleted: int = 0,
209
             show_deleted: int = 0,
131
             show_active: int = 1,
210
             show_active: int = 1,
211
+            content_type: str = None,
212
+            offset: int = None,
213
+            limit: int = None,
132
     ) -> None:
214
     ) -> None:
133
         self.parent_id = parent_id
215
         self.parent_id = parent_id
216
+        self.workspace_id = workspace_id
134
         self.show_archived = bool(show_archived)
217
         self.show_archived = bool(show_archived)
135
         self.show_deleted = bool(show_deleted)
218
         self.show_deleted = bool(show_deleted)
136
         self.show_active = bool(show_active)
219
         self.show_active = bool(show_active)
220
+        self.limit = limit
221
+        self.offset = offset
222
+        self.content_type = content_type
223
+
224
+
225
+class ActiveContentFilter(object):
226
+    def __init__(
227
+            self,
228
+            limit: int = None,
229
+            before_datetime: datetime = None,
230
+    ):
231
+        self.limit = limit
232
+        self.before_datetime = before_datetime
233
+
234
+
235
+class ContentIdsQuery(object):
236
+    def __init__(
237
+            self,
238
+            contents_ids: typing.List[int] = None,
239
+    ):
240
+        self.contents_ids = contents_ids
241
+
242
+
243
+class RoleUpdate(object):
244
+    """
245
+    Update role
246
+    """
247
+    def __init__(
248
+        self,
249
+        role: str,
250
+    ):
251
+        self.role = role
252
+
253
+
254
+class WorkspaceMemberInvitation(object):
255
+    """
256
+    Workspace Member Invitation
257
+    """
258
+    def __init__(
259
+        self,
260
+        user_id: int,
261
+        user_email_or_public_name: str,
262
+        role: str,
263
+    ):
264
+        self.role = role
265
+        self.user_email_or_public_name = user_email_or_public_name
266
+        self.user_id = user_id
267
+
268
+
269
+class WorkspaceUpdate(object):
270
+    """
271
+    Update workspace
272
+    """
273
+    def __init__(
274
+        self,
275
+        label: str,
276
+        description: str,
277
+    ):
278
+        self.label = label
279
+        self.description = description
137
 
280
 
138
 
281
 
139
 class ContentCreation(object):
282
 class ContentCreation(object):
186
         self.raw_content = raw_content
329
         self.raw_content = raw_content
187
 
330
 
188
 
331
 
332
+class TypeUser(Enum):
333
+    """Params used to find user"""
334
+    USER_ID = 'found_id'
335
+    EMAIL = 'found_email'
336
+    PUBLIC_NAME = 'found_public_name'
337
+
338
+
189
 class UserInContext(object):
339
 class UserInContext(object):
190
     """
340
     """
191
     Interface to get User data and User data related to context.
341
     Interface to get User data and User data related to context.
315
             user_role: UserRoleInWorkspace,
465
             user_role: UserRoleInWorkspace,
316
             dbsession: Session,
466
             dbsession: Session,
317
             config: CFG,
467
             config: CFG,
468
+            # Extended params
469
+            newly_created: bool = None,
470
+            email_sent: bool = None
318
     )-> None:
471
     )-> None:
319
         self.user_role = user_role
472
         self.user_role = user_role
320
         self.dbsession = dbsession
473
         self.dbsession = dbsession
321
         self.config = config
474
         self.config = config
475
+        # Extended params
476
+        self.newly_created = newly_created
477
+        self.email_sent = email_sent
322
 
478
 
323
     @property
479
     @property
324
     def user_id(self) -> int:
480
     def user_id(self) -> int:
358
         'contributor', 'content-manager', 'workspace-manager'
514
         'contributor', 'content-manager', 'workspace-manager'
359
         :return: user workspace role as slug.
515
         :return: user workspace role as slug.
360
         """
516
         """
361
-        return UserRoleInWorkspace.SLUG[self.user_role.role]
517
+        return WorkspaceRoles.get_role_from_level(self.user_role.role).slug
518
+
519
+    @property
520
+    def is_active(self) -> bool:
521
+        return self.user.is_active
362
 
522
 
363
     @property
523
     @property
364
     def user(self) -> UserInContext:
524
     def user(self) -> UserInContext:
390
     Interface to get Content data and Content data related to context.
550
     Interface to get Content data and Content data related to context.
391
     """
551
     """
392
 
552
 
393
-    def __init__(self, content: Content, dbsession: Session, config: CFG):
553
+    def __init__(self, content: Content, dbsession: Session, config: CFG, user: User=None):  # nopep8
394
         self.content = content
554
         self.content = content
395
         self.dbsession = dbsession
555
         self.dbsession = dbsession
396
         self.config = config
556
         self.config = config
557
+        self._user = user
397
 
558
 
398
     # Default
559
     # Default
399
     @property
560
     @property
486
     def slug(self):
647
     def slug(self):
487
         return slugify(self.content.label)
648
         return slugify(self.content.label)
488
 
649
 
650
+    @property
651
+    def read_by_user(self):
652
+        assert self._user
653
+        return not self.content.has_new_information_for(self._user)
654
+
489
 
655
 
490
 class RevisionInContext(object):
656
 class RevisionInContext(object):
491
     """
657
     """
519
         return self.revision.label
685
         return self.revision.label
520
 
686
 
521
     @property
687
     @property
688
+    def revision_type(self) -> str:
689
+        return self.revision.revision_type
690
+
691
+    @property
522
     def content_type(self) -> str:
692
     def content_type(self) -> str:
523
         content_type = ContentType(self.revision.type)
693
         content_type = ContentType(self.revision.type)
524
         if content_type:
694
         if content_type:

+ 47 - 32
tracim/models/data.py View File

30
 from tracim.exceptions import ContentRevisionUpdateError
30
 from tracim.exceptions import ContentRevisionUpdateError
31
 from tracim.models.meta import DeclarativeBase
31
 from tracim.models.meta import DeclarativeBase
32
 from tracim.models.auth import User
32
 from tracim.models.auth import User
33
+from tracim.models.roles import WorkspaceRoles
33
 
34
 
34
 DEFAULT_PROPERTIES = dict(
35
 DEFAULT_PROPERTIES = dict(
35
     allowed_content=dict(
36
     allowed_content=dict(
124
     workspace = relationship('Workspace', remote_side=[Workspace.workspace_id], backref='roles', lazy='joined')
125
     workspace = relationship('Workspace', remote_side=[Workspace.workspace_id], backref='roles', lazy='joined')
125
     user = relationship('User', remote_side=[User.user_id], backref='roles')
126
     user = relationship('User', remote_side=[User.user_id], backref='roles')
126
 
127
 
127
-    NOT_APPLICABLE = 0
128
-    READER = 1
129
-    CONTRIBUTOR = 2
130
-    CONTENT_MANAGER = 4
131
-    WORKSPACE_MANAGER = 8
132
-
133
-    SLUG = {
134
-        NOT_APPLICABLE: 'not-applicable',
135
-        READER: 'reader',
136
-        CONTRIBUTOR: 'contributor',
137
-        CONTENT_MANAGER: 'content-manager',
138
-        WORKSPACE_MANAGER: 'workspace-manager',
139
-    }
128
+    NOT_APPLICABLE = WorkspaceRoles.NOT_APPLICABLE.level
129
+    READER = WorkspaceRoles.READER.level
130
+    CONTRIBUTOR = WorkspaceRoles.CONTRIBUTOR.level
131
+    CONTENT_MANAGER = WorkspaceRoles.CONTENT_MANAGER.level
132
+    WORKSPACE_MANAGER = WorkspaceRoles.WORKSPACE_MANAGER.level
140
 
133
 
141
-    LABEL = dict()
142
-    LABEL[0] = l_('N/A')
143
-    LABEL[1] = l_('Reader')
144
-    LABEL[2] = l_('Contributor')
145
-    LABEL[4] = l_('Content Manager')
146
-    LABEL[8] = l_('Workspace Manager')
134
+    # TODO - G.M - 10-04-2018 - [Cleanup] Drop this
135
+    # SLUG = {
136
+    #     NOT_APPLICABLE: 'not-applicable',
137
+    #     READER: 'reader',
138
+    #     CONTRIBUTOR: 'contributor',
139
+    #     CONTENT_MANAGER: 'content-manager',
140
+    #     WORKSPACE_MANAGER: 'workspace-manager',
141
+    # }
142
+
143
+    # LABEL = dict()
144
+    # LABEL[0] = l_('N/A')
145
+    # LABEL[1] = l_('Reader')
146
+    # LABEL[2] = l_('Contributor')
147
+    # LABEL[4] = l_('Content Manager')
148
+    # LABEL[8] = l_('Workspace Manager')
147
     # TODO - G.M - 10-04-2018 - [Cleanup] Drop this
149
     # TODO - G.M - 10-04-2018 - [Cleanup] Drop this
148
     #
150
     #
149
     # STYLE = dict()
151
     # STYLE = dict()
170
     #     return UserRoleInWorkspace.STYLE[self.role]
172
     #     return UserRoleInWorkspace.STYLE[self.role]
171
     #
173
     #
172
 
174
 
175
+    def role_object(self):
176
+        return WorkspaceRoles.get_role_from_level(level=self.role)
177
+
173
     def role_as_label(self):
178
     def role_as_label(self):
174
-        return UserRoleInWorkspace.LABEL[self.role]
179
+        return self.role_object().label
175
 
180
 
176
     @classmethod
181
     @classmethod
177
     def get_all_role_values(cls) -> typing.List[int]:
182
     def get_all_role_values(cls) -> typing.List[int]:
178
         """
183
         """
179
         Return all valid role value
184
         Return all valid role value
180
         """
185
         """
181
-        return [
182
-            UserRoleInWorkspace.READER,
183
-            UserRoleInWorkspace.CONTRIBUTOR,
184
-            UserRoleInWorkspace.CONTENT_MANAGER,
185
-            UserRoleInWorkspace.WORKSPACE_MANAGER
186
-        ]
186
+        return [role.level for role in WorkspaceRoles.get_all_valid_role()]
187
 
187
 
188
     @classmethod
188
     @classmethod
189
     def get_all_role_slug(cls) -> typing.List[str]:
189
     def get_all_role_slug(cls) -> typing.List[str]:
193
         # INFO - G.M - 25-05-2018 - Be carefull, as long as this method
193
         # INFO - G.M - 25-05-2018 - Be carefull, as long as this method
194
         # and get_all_role_values are both used for API, this method should
194
         # and get_all_role_values are both used for API, this method should
195
         # return item in the same order as get_all_role_values
195
         # return item in the same order as get_all_role_values
196
-        return [cls.SLUG[value] for value in cls.get_all_role_values()]
196
+        return [role.slug for role in WorkspaceRoles.get_all_valid_role()]
197
 
197
 
198
-
199
-class RoleType(object):
200
-    def __init__(self, role_id):
201
-        self.role_type_id = role_id
202
-        # TODO - G.M - 10-04-2018 - [Cleanup] Drop this
198
+# TODO - G.M - 10-04-2018 - [Cleanup] Drop this
199
+# class RoleType(object):
200
+#     def __init__(self, role_id):
201
+#         self.role_type_id = role_id
203
         # self.fa_icon = UserRoleInWorkspace.ICON[role_id]
202
         # self.fa_icon = UserRoleInWorkspace.ICON[role_id]
204
         # self.role_label = UserRoleInWorkspace.LABEL[role_id]
203
         # self.role_label = UserRoleInWorkspace.LABEL[role_id]
205
         # self.css_style = UserRoleInWorkspace.STYLE[role_id]
204
         # self.css_style = UserRoleInWorkspace.STYLE[role_id]
1257
     def get_last_action(self) -> ActionDescription:
1256
     def get_last_action(self) -> ActionDescription:
1258
         return ActionDescription(self.revision_type)
1257
         return ActionDescription(self.revision_type)
1259
 
1258
 
1259
+    def get_simple_last_activity_date(self) -> datetime_root.datetime:
1260
+        """
1261
+        Get last activity_date, comments_included. Do not search recursively
1262
+        in revision or children.
1263
+        :return:
1264
+        """
1265
+        last_revision_date = self.updated
1266
+        for comment in self.get_comments():
1267
+            if comment.updated > last_revision_date:
1268
+                last_revision_date = comment.updated
1269
+        return last_revision_date
1270
+
1260
     def get_last_activity_date(self) -> datetime_root.datetime:
1271
     def get_last_activity_date(self) -> datetime_root.datetime:
1272
+        """
1273
+        Get last activity date with complete recursive search
1274
+        :return:
1275
+        """
1261
         last_revision_date = self.updated
1276
         last_revision_date = self.updated
1262
         for revision in self.revisions:
1277
         for revision in self.revisions:
1263
             if revision.updated > last_revision_date:
1278
             if revision.updated > last_revision_date:

+ 61 - 0
tracim/models/roles.py View File

1
+import typing
2
+from enum import Enum
3
+
4
+from tracim.exceptions import RoleDoesNotExist
5
+
6
+
7
+class WorkspaceRoles(Enum):
8
+    """
9
+    Available role for workspace.
10
+    All roles should have a unique level and unique slug.
11
+    level is role value store in database and is also use for
12
+    permission check.
13
+    slug is for http endpoints and other place where readability is
14
+    needed.
15
+    """
16
+    NOT_APPLICABLE = (0, 'not-applicable')
17
+    READER = (1, 'reader')
18
+    CONTRIBUTOR = (2, 'contributor')
19
+    CONTENT_MANAGER = (4, 'content-manager')
20
+    WORKSPACE_MANAGER = (8, 'workspace-manager')
21
+
22
+    def __init__(self, level, slug):
23
+        self.level = level
24
+        self.slug = slug
25
+    
26
+    @property
27
+    def label(self):
28
+        """ Return valid label associated to role"""
29
+        # TODO - G.M - 2018-06-180 - Make this work correctly
30
+        return self.slug
31
+
32
+    @classmethod
33
+    def get_all_valid_role(cls) -> typing.List['WorkspaceRoles']:
34
+        """
35
+        Return all valid role value
36
+        """
37
+        return [item for item in list(WorkspaceRoles) if item.level > 0]
38
+
39
+    @classmethod
40
+    def get_role_from_level(cls, level: int) -> 'WorkspaceRoles':
41
+        """
42
+        Obtain Workspace role from a level value
43
+        :param level: level value as int
44
+        :return: correct workspace role related
45
+        """
46
+        roles = [item for item in list(WorkspaceRoles) if item.level == level]
47
+        if len(roles) != 1:
48
+            raise RoleDoesNotExist()
49
+        return roles[0]
50
+
51
+    @classmethod
52
+    def get_role_from_slug(cls, slug: str) -> 'WorkspaceRoles':
53
+        """
54
+        Obtain Workspace role from a slug value
55
+        :param slug: slug value as str
56
+        :return: correct workspace role related
57
+        """
58
+        roles = [item for item in list(WorkspaceRoles) if item.slug == slug]
59
+        if len(roles) != 1:
60
+            raise RoleDoesNotExist()
61
+        return roles[0]

+ 13 - 1
tracim/tests/__init__.py View File

25
 from tracim.extensions import hapic
25
 from tracim.extensions import hapic
26
 from tracim import web
26
 from tracim import web
27
 from webtest import TestApp
27
 from webtest import TestApp
28
+from io import BytesIO
29
+from PIL import Image
28
 
30
 
29
 
31
 
30
 def eq_(a, b, msg=None):
32
 def eq_(a, b, msg=None):
54
     assert content_query.count() > 0
56
     assert content_query.count() > 0
55
 
57
 
56
 
58
 
59
+def create_1000px_png_test_image():
60
+    file = BytesIO()
61
+    image = Image.new('RGBA', size=(1000, 1000), color=(0, 0, 0))
62
+    image.save(file, 'png')
63
+    file.name = 'test_image.png'
64
+    file.seek(0)
65
+    return file
66
+
67
+
57
 class FunctionalTest(unittest.TestCase):
68
 class FunctionalTest(unittest.TestCase):
58
 
69
 
59
     fixtures = [BaseFixture]
70
     fixtures = [BaseFixture]
68
             'depot_storage_dir': '/tmp/test/depot',
79
             'depot_storage_dir': '/tmp/test/depot',
69
             'depot_storage_name': 'test',
80
             'depot_storage_name': 'test',
70
             'preview_cache_dir': '/tmp/test/preview_cache',
81
             'preview_cache_dir': '/tmp/test/preview_cache',
71
-
82
+            'preview.jpg.restricted_dims': True,
83
+            'email.notification.activated': 'false',
72
         }
84
         }
73
         hapic.reset_context()
85
         hapic.reset_context()
74
         self.engine = get_engine(self.settings)
86
         self.engine = get_engine(self.settings)

File diff suppressed because it is too large
+ 1487 - 142
tracim/tests/functional/test_contents.py


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

2
 """
2
 """
3
 Tests for /api/v2/users subpath endpoints.
3
 Tests for /api/v2/users subpath endpoints.
4
 """
4
 """
5
+from time import sleep
6
+import pytest
5
 import transaction
7
 import transaction
6
 
8
 
7
 from tracim import models
9
 from tracim import models
8
-from tracim.lib.core.group import GroupApi
10
+from tracim.lib.core.content import ContentApi
9
 from tracim.lib.core.user import UserApi
11
 from tracim.lib.core.user import UserApi
12
+from tracim.lib.core.group import GroupApi
13
+from tracim.lib.core.workspace import WorkspaceApi
10
 from tracim.models import get_tm_session
14
 from tracim.models import get_tm_session
15
+from tracim.models.contents import ContentTypeLegacy as ContentType
16
+from tracim.models.revision_protection import new_revision
11
 from tracim.tests import FunctionalTest
17
 from tracim.tests import FunctionalTest
12
 from tracim.fixtures.content import Content as ContentFixtures
18
 from tracim.fixtures.content import Content as ContentFixtures
13
 from tracim.fixtures.users_and_groups import Base as BaseFixture
19
 from tracim.fixtures.users_and_groups import Base as BaseFixture
14
 
20
 
15
 
21
 
22
+class TestUserRecentlyActiveContentEndpoint(FunctionalTest):
23
+    """
24
+    Tests for /api/v2/users/{user_id}/workspaces/{workspace_id}/contents/recently_active # nopep8
25
+    """
26
+    fixtures = [BaseFixture]
27
+
28
+    def test_api__get_recently_active_content__ok__200__nominal_case(self):
29
+
30
+        # init DB
31
+        dbsession = get_tm_session(self.session_factory, transaction.manager)
32
+        admin = dbsession.query(models.User) \
33
+            .filter(models.User.email == 'admin@admin.admin') \
34
+            .one()
35
+        workspace_api = WorkspaceApi(
36
+            current_user=admin,
37
+            session=dbsession,
38
+            config=self.app_config
39
+
40
+        )
41
+        workspace = WorkspaceApi(
42
+            current_user=admin,
43
+            session=dbsession,
44
+            config=self.app_config,
45
+        ).create_workspace(
46
+            'test workspace',
47
+            save_now=True
48
+        )
49
+        workspace2 = WorkspaceApi(
50
+            current_user=admin,
51
+            session=dbsession,
52
+            config=self.app_config,
53
+        ).create_workspace(
54
+            'test workspace2',
55
+            save_now=True
56
+        )
57
+
58
+        api = ContentApi(
59
+            current_user=admin,
60
+            session=dbsession,
61
+            config=self.app_config,
62
+        )
63
+        main_folder_workspace2 = api.create(ContentType.Folder, workspace2, None, 'Hepla', '', True)  # nopep8
64
+        main_folder = api.create(ContentType.Folder, workspace, None, 'this is randomized folder', '', True)  # nopep8
65
+        # creation order test
66
+        firstly_created = api.create(ContentType.Page, workspace, main_folder, 'creation_order_test', '', True)  # nopep8
67
+        secondly_created = api.create(ContentType.Page, workspace, main_folder, 'another creation_order_test', '', True)  # nopep8
68
+        # update order test
69
+        firstly_created_but_recently_updated = api.create(ContentType.Page, workspace, main_folder, 'update_order_test', '', True)  # nopep8
70
+        secondly_created_but_not_updated = api.create(ContentType.Page, workspace, main_folder, 'another update_order_test', '', True)  # nopep8
71
+        with new_revision(
72
+            session=dbsession,
73
+            tm=transaction.manager,
74
+            content=firstly_created_but_recently_updated,
75
+        ):
76
+            firstly_created_but_recently_updated.description = 'Just an update'
77
+        api.save(firstly_created_but_recently_updated)
78
+        # comment change order
79
+        firstly_created_but_recently_commented = api.create(ContentType.Page, workspace, main_folder, 'this is randomized label content', '', True)  # nopep8
80
+        secondly_created_but_not_commented = api.create(ContentType.Page, workspace, main_folder, 'this is another randomized label content', '', True)  # nopep8
81
+        comments = api.create_comment(workspace, firstly_created_but_recently_commented, 'juste a super comment', True)  # nopep8
82
+        content_workspace_2 = api.create(ContentType.Page, workspace2,main_folder_workspace2, 'content_workspace_2', '',True)  # nopep8
83
+        dbsession.flush()
84
+        transaction.commit()
85
+
86
+        self.testapp.authorization = (
87
+            'Basic',
88
+            (
89
+                'admin@admin.admin',
90
+                'admin@admin.admin'
91
+            )
92
+        )
93
+        res = self.testapp.get('/api/v2/users/1/workspaces/{}/contents/recently_active'.format(workspace.workspace_id), status=200) # nopep8
94
+        res = res.json_body
95
+        assert len(res) == 7
96
+        for elem in res:
97
+            assert isinstance(elem['content_id'], int)
98
+            assert isinstance(elem['content_type'], str)
99
+            assert elem['content_type'] != 'comments'
100
+            assert isinstance(elem['is_archived'], bool)
101
+            assert isinstance(elem['is_deleted'], bool)
102
+            assert isinstance(elem['label'], str)
103
+            assert isinstance(elem['parent_id'], int) or elem['parent_id'] is None
104
+            assert isinstance(elem['show_in_ui'], bool)
105
+            assert isinstance(elem['slug'], str)
106
+            assert isinstance(elem['status'], str)
107
+            assert isinstance(elem['sub_content_types'], list)
108
+            for sub_content_type in elem['sub_content_types']:
109
+                assert isinstance(sub_content_type, str)
110
+            assert isinstance(elem['workspace_id'], int)
111
+        # comment is newest than page2
112
+        assert res[0]['content_id'] == firstly_created_but_recently_commented.content_id
113
+        assert res[1]['content_id'] == secondly_created_but_not_commented.content_id
114
+        # last updated content is newer than other one despite creation
115
+        # of the other is more recent
116
+        assert res[2]['content_id'] == firstly_created_but_recently_updated.content_id
117
+        assert res[3]['content_id'] == secondly_created_but_not_updated.content_id
118
+        # creation order is inverted here as last created is last active
119
+        assert res[4]['content_id'] == secondly_created.content_id
120
+        assert res[5]['content_id'] == firstly_created.content_id
121
+        # folder subcontent modification does not change folder order
122
+        assert res[6]['content_id'] == main_folder.content_id
123
+
124
+    @pytest.mark.skip('Test should be fixed')
125
+    def test_api__get_recently_active_content__ok__200__limit_2_multiple(self):
126
+        # TODO - G.M - 2018-07-20 - Better fix for this test, do not use sleep()
127
+        # anymore to fix datetime lack of precision.
128
+
129
+        # init DB
130
+        dbsession = get_tm_session(self.session_factory, transaction.manager)
131
+        admin = dbsession.query(models.User) \
132
+            .filter(models.User.email == 'admin@admin.admin') \
133
+            .one()
134
+        workspace_api = WorkspaceApi(
135
+            current_user=admin,
136
+            session=dbsession,
137
+            config=self.app_config
138
+
139
+        )
140
+        workspace = WorkspaceApi(
141
+            current_user=admin,
142
+            session=dbsession,
143
+            config=self.app_config,
144
+        ).create_workspace(
145
+            'test workspace',
146
+            save_now=True
147
+        )
148
+        workspace2 = WorkspaceApi(
149
+            current_user=admin,
150
+            session=dbsession,
151
+            config=self.app_config,
152
+        ).create_workspace(
153
+            'test workspace2',
154
+            save_now=True
155
+        )
156
+
157
+        api = ContentApi(
158
+            current_user=admin,
159
+            session=dbsession,
160
+            config=self.app_config,
161
+        )
162
+        main_folder_workspace2 = api.create(ContentType.Folder, workspace2, None, 'Hepla', '', True)  # nopep8
163
+        sleep(1)
164
+        main_folder = api.create(ContentType.Folder, workspace, None, 'this is randomized folder', '', True)  # nopep8
165
+        # creation order test
166
+        firstly_created = api.create(ContentType.Page, workspace, main_folder, 'creation_order_test', '', True)  # nopep8
167
+        sleep(1)
168
+        secondly_created = api.create(ContentType.Page, workspace, main_folder, 'another creation_order_test', '', True)  # nopep8
169
+        # update order test
170
+        firstly_created_but_recently_updated = api.create(ContentType.Page, workspace, main_folder, 'update_order_test', '', True)  # nopep8
171
+        sleep(1)
172
+        secondly_created_but_not_updated = api.create(ContentType.Page, workspace, main_folder, 'another update_order_test', '', True)  # nopep8
173
+        sleep(1)
174
+        with new_revision(
175
+            session=dbsession,
176
+            tm=transaction.manager,
177
+            content=firstly_created_but_recently_updated,
178
+        ):
179
+            firstly_created_but_recently_updated.description = 'Just an update'
180
+        api.save(firstly_created_but_recently_updated)
181
+        # comment change order
182
+        firstly_created_but_recently_commented = api.create(ContentType.Page, workspace, main_folder, 'this is randomized label content', '', True)  # nopep8
183
+        sleep(1)
184
+        secondly_created_but_not_commented = api.create(ContentType.Page, workspace, main_folder, 'this is another randomized label content', '', True)  # nopep8
185
+        sleep(1)
186
+        comments = api.create_comment(workspace, firstly_created_but_recently_commented, 'juste a super comment', True)  # nopep8
187
+        sleep(1)
188
+        content_workspace_2 = api.create(ContentType.Page, workspace2,main_folder_workspace2, 'content_workspace_2', '',True)  # nopep8
189
+        dbsession.flush()
190
+        transaction.commit()
191
+
192
+        self.testapp.authorization = (
193
+            'Basic',
194
+            (
195
+                'admin@admin.admin',
196
+                'admin@admin.admin'
197
+            )
198
+        )
199
+        params = {
200
+            'limit': 2,
201
+        }
202
+        res = self.testapp.get(
203
+            '/api/v2/users/1/workspaces/{}/contents/recently_active'.format(workspace.workspace_id),  # nopep8
204
+            status=200,
205
+            params=params
206
+        ) # nopep8
207
+        res = res.json_body
208
+        assert len(res) == 2
209
+        for elem in res:
210
+            assert isinstance(elem['content_id'], int)
211
+            assert isinstance(elem['content_type'], str)
212
+            assert elem['content_type'] != 'comments'
213
+            assert isinstance(elem['is_archived'], bool)
214
+            assert isinstance(elem['is_deleted'], bool)
215
+            assert isinstance(elem['label'], str)
216
+            assert isinstance(elem['parent_id'], int) or elem['parent_id'] is None
217
+            assert isinstance(elem['show_in_ui'], bool)
218
+            assert isinstance(elem['slug'], str)
219
+            assert isinstance(elem['status'], str)
220
+            assert isinstance(elem['sub_content_types'], list)
221
+            for sub_content_type in elem['sub_content_types']:
222
+                assert isinstance(sub_content_type, str)
223
+            assert isinstance(elem['workspace_id'], int)
224
+        # comment is newest than page2
225
+        assert res[0]['content_id'] == firstly_created_but_recently_commented.content_id
226
+        assert res[1]['content_id'] == secondly_created_but_not_commented.content_id
227
+
228
+        params = {
229
+            'limit': 2,
230
+            'before_datetime': secondly_created_but_not_commented.get_last_activity_date().strftime('%Y-%m-%dT%H:%M:%SZ'),  # nopep8
231
+        }
232
+        res = self.testapp.get(
233
+            '/api/v2/users/1/workspaces/{}/contents/recently_active'.format(workspace.workspace_id),  # nopep8
234
+            status=200,
235
+            params=params
236
+        )
237
+        res = res.json_body
238
+        assert len(res) == 2
239
+        # last updated content is newer than other one despite creation
240
+        # of the other is more recent
241
+        assert res[0]['content_id'] == firstly_created_but_recently_updated.content_id
242
+        assert res[1]['content_id'] == secondly_created_but_not_updated.content_id
243
+
244
+
245
+class TestUserReadStatusEndpoint(FunctionalTest):
246
+    """
247
+    Tests for /api/v2/users/{user_id}/workspaces/{workspace_id}/contents/read_status # nopep8
248
+    """
249
+    def test_api__get_read_status__ok__200__all(self):
250
+
251
+        # init DB
252
+        dbsession = get_tm_session(self.session_factory, transaction.manager)
253
+        admin = dbsession.query(models.User) \
254
+            .filter(models.User.email == 'admin@admin.admin') \
255
+            .one()
256
+        workspace_api = WorkspaceApi(
257
+            current_user=admin,
258
+            session=dbsession,
259
+            config=self.app_config
260
+
261
+        )
262
+        workspace = WorkspaceApi(
263
+            current_user=admin,
264
+            session=dbsession,
265
+            config=self.app_config,
266
+        ).create_workspace(
267
+            'test workspace',
268
+            save_now=True
269
+        )
270
+        workspace2 = WorkspaceApi(
271
+            current_user=admin,
272
+            session=dbsession,
273
+            config=self.app_config,
274
+        ).create_workspace(
275
+            'test workspace2',
276
+            save_now=True
277
+        )
278
+
279
+        api = ContentApi(
280
+            current_user=admin,
281
+            session=dbsession,
282
+            config=self.app_config,
283
+        )
284
+        main_folder_workspace2 = api.create(ContentType.Folder, workspace2, None, 'Hepla', '', True)  # nopep8
285
+        main_folder = api.create(ContentType.Folder, workspace, None, 'this is randomized folder', '', True)  # nopep8
286
+        # creation order test
287
+        firstly_created = api.create(ContentType.Page, workspace, main_folder, 'creation_order_test', '', True)  # nopep8
288
+        secondly_created = api.create(ContentType.Page, workspace, main_folder, 'another creation_order_test', '', True)  # nopep8
289
+        # update order test
290
+        firstly_created_but_recently_updated = api.create(ContentType.Page, workspace, main_folder, 'update_order_test', '', True)  # nopep8
291
+        secondly_created_but_not_updated = api.create(ContentType.Page, workspace, main_folder, 'another update_order_test', '', True)  # nopep8
292
+        with new_revision(
293
+            session=dbsession,
294
+            tm=transaction.manager,
295
+            content=firstly_created_but_recently_updated,
296
+        ):
297
+            firstly_created_but_recently_updated.description = 'Just an update'
298
+        api.save(firstly_created_but_recently_updated)
299
+        # comment change order
300
+        firstly_created_but_recently_commented = api.create(ContentType.Page, workspace, main_folder, 'this is randomized label content', '', True)  # nopep8
301
+        secondly_created_but_not_commented = api.create(ContentType.Page, workspace, main_folder, 'this is another randomized label content', '', True)  # nopep8
302
+        comments = api.create_comment(workspace, firstly_created_but_recently_commented, 'juste a super comment', True)  # nopep8
303
+        content_workspace_2 = api.create(ContentType.Page, workspace2,main_folder_workspace2, 'content_workspace_2', '',True)  # nopep8
304
+        dbsession.flush()
305
+        transaction.commit()
306
+
307
+        self.testapp.authorization = (
308
+            'Basic',
309
+            (
310
+                'admin@admin.admin',
311
+                'admin@admin.admin'
312
+            )
313
+        )
314
+        res = self.testapp.get('/api/v2/users/1/workspaces/{}/contents/read_status'.format(workspace.workspace_id), status=200) # nopep8
315
+        res = res.json_body
316
+        assert len(res) == 7
317
+        for elem in res:
318
+            assert isinstance(elem['content_id'], int)
319
+            assert isinstance(elem['read_by_user'], bool)
320
+        # comment is newest than page2
321
+        assert res[0]['content_id'] == firstly_created_but_recently_commented.content_id
322
+        assert res[1]['content_id'] == secondly_created_but_not_commented.content_id
323
+        # last updated content is newer than other one despite creation
324
+        # of the other is more recent
325
+        assert res[2]['content_id'] == firstly_created_but_recently_updated.content_id
326
+        assert res[3]['content_id'] == secondly_created_but_not_updated.content_id
327
+        # creation order is inverted here as last created is last active
328
+        assert res[4]['content_id'] == secondly_created.content_id
329
+        assert res[5]['content_id'] == firstly_created.content_id
330
+        # folder subcontent modification does not change folder order
331
+        assert res[6]['content_id'] == main_folder.content_id
332
+
333
+    def test_api__get_read_status__ok__200__nominal_case(self):
334
+
335
+        # init DB
336
+        dbsession = get_tm_session(self.session_factory, transaction.manager)
337
+        admin = dbsession.query(models.User) \
338
+            .filter(models.User.email == 'admin@admin.admin') \
339
+            .one()
340
+        workspace_api = WorkspaceApi(
341
+            current_user=admin,
342
+            session=dbsession,
343
+            config=self.app_config
344
+
345
+        )
346
+        workspace = WorkspaceApi(
347
+            current_user=admin,
348
+            session=dbsession,
349
+            config=self.app_config,
350
+        ).create_workspace(
351
+            'test workspace',
352
+            save_now=True
353
+        )
354
+        workspace2 = WorkspaceApi(
355
+            current_user=admin,
356
+            session=dbsession,
357
+            config=self.app_config,
358
+        ).create_workspace(
359
+            'test workspace2',
360
+            save_now=True
361
+        )
362
+
363
+        api = ContentApi(
364
+            current_user=admin,
365
+            session=dbsession,
366
+            config=self.app_config,
367
+        )
368
+        main_folder_workspace2 = api.create(ContentType.Folder, workspace2, None, 'Hepla', '', True)  # nopep8
369
+        main_folder = api.create(ContentType.Folder, workspace, None, 'this is randomized folder', '', True)  # nopep8
370
+        # creation order test
371
+        firstly_created = api.create(ContentType.Page, workspace, main_folder, 'creation_order_test', '', True)  # nopep8
372
+        secondly_created = api.create(ContentType.Page, workspace, main_folder, 'another creation_order_test', '', True)  # nopep8
373
+        # update order test
374
+        firstly_created_but_recently_updated = api.create(ContentType.Page, workspace, main_folder, 'update_order_test', '', True)  # nopep8
375
+        secondly_created_but_not_updated = api.create(ContentType.Page, workspace, main_folder, 'another update_order_test', '', True)  # nopep8
376
+        with new_revision(
377
+            session=dbsession,
378
+            tm=transaction.manager,
379
+            content=firstly_created_but_recently_updated,
380
+        ):
381
+            firstly_created_but_recently_updated.description = 'Just an update'
382
+        api.save(firstly_created_but_recently_updated)
383
+        # comment change order
384
+        firstly_created_but_recently_commented = api.create(ContentType.Page, workspace, main_folder, 'this is randomized label content', '', True)  # nopep8
385
+        secondly_created_but_not_commented = api.create(ContentType.Page, workspace, main_folder, 'this is another randomized label content', '', True)  # nopep8
386
+        comments = api.create_comment(workspace, firstly_created_but_recently_commented, 'juste a super comment', True)  # nopep8
387
+        content_workspace_2 = api.create(ContentType.Page, workspace2,main_folder_workspace2, 'content_workspace_2', '',True)  # nopep8
388
+        dbsession.flush()
389
+        transaction.commit()
390
+
391
+        self.testapp.authorization = (
392
+            'Basic',
393
+            (
394
+                'admin@admin.admin',
395
+                'admin@admin.admin'
396
+            )
397
+        )
398
+        selected_contents_id = [
399
+            firstly_created_but_recently_commented.content_id,
400
+            firstly_created_but_recently_updated.content_id,
401
+            firstly_created.content_id,
402
+            main_folder.content_id,
403
+        ]
404
+        url = '/api/v2/users/1/workspaces/{workspace_id}/contents/read_status?contents_ids={cid1}&contents_ids={cid2}&contents_ids={cid3}&contents_ids={cid4}'.format(  # nopep8
405
+              workspace_id=workspace.workspace_id,
406
+              cid1=selected_contents_id[0],
407
+              cid2=selected_contents_id[1],
408
+              cid3=selected_contents_id[2],
409
+              cid4=selected_contents_id[3],
410
+        )
411
+        res = self.testapp.get(
412
+            url=url,
413
+            status=200,
414
+        )
415
+        res = res.json_body
416
+        assert len(res) == 4
417
+        for elem in res:
418
+            assert isinstance(elem['content_id'], int)
419
+            assert isinstance(elem['read_by_user'], bool)
420
+        # comment is newest than page2
421
+        assert res[0]['content_id'] == firstly_created_but_recently_commented.content_id
422
+        # last updated content is newer than other one despite creation
423
+        # of the other is more recent
424
+        assert res[1]['content_id'] == firstly_created_but_recently_updated.content_id
425
+        # creation order is inverted here as last created is last active
426
+        assert res[2]['content_id'] == firstly_created.content_id
427
+        # folder subcontent modification does not change folder order
428
+        assert res[3]['content_id'] == main_folder.content_id
429
+
430
+
431
+class TestUserSetContentAsRead(FunctionalTest):
432
+    """
433
+    Tests for /api/v2/users/{user_id}/workspaces/{workspace_id}/contents/{content_id}/read  # nopep8
434
+    """
435
+    def test_api_set_content_as_read__ok__200__nominal_case(self):
436
+        # init DB
437
+        dbsession = get_tm_session(self.session_factory, transaction.manager)
438
+        admin = dbsession.query(models.User) \
439
+            .filter(models.User.email == 'admin@admin.admin') \
440
+            .one()
441
+        workspace_api = WorkspaceApi(
442
+            current_user=admin,
443
+            session=dbsession,
444
+            config=self.app_config
445
+
446
+        )
447
+        workspace = WorkspaceApi(
448
+            current_user=admin,
449
+            session=dbsession,
450
+            config=self.app_config,
451
+        ).create_workspace(
452
+            'test workspace',
453
+            save_now=True
454
+        )
455
+        api = ContentApi(
456
+            current_user=admin,
457
+            session=dbsession,
458
+            config=self.app_config,
459
+        )
460
+        main_folder = api.create(ContentType.Folder, workspace, None, 'this is randomized folder', '', True)  # nopep8
461
+        # creation order test
462
+        firstly_created = api.create(ContentType.Page, workspace, main_folder, 'creation_order_test', '', True)  # nopep8
463
+        api.mark_unread(firstly_created)
464
+        dbsession.flush()
465
+        transaction.commit()
466
+
467
+        self.testapp.authorization = (
468
+            'Basic',
469
+            (
470
+                'admin@admin.admin',
471
+                'admin@admin.admin'
472
+            )
473
+        )
474
+        res = self.testapp.get('/api/v2/users/1/workspaces/{}/contents/read_status'.format(workspace.workspace_id), status=200) # nopep8
475
+        assert res.json_body[0]['content_id'] == firstly_created.content_id
476
+        assert res.json_body[0]['read_by_user'] is False
477
+        self.testapp.put(
478
+            '/api/v2/users/{user_id}/workspaces/{workspace_id}/contents/{content_id}/read'.format(  # nopep8
479
+                workspace_id=workspace.workspace_id,
480
+                content_id=firstly_created.content_id,
481
+                user_id=admin.user_id,
482
+            )
483
+        )
484
+        res = self.testapp.get('/api/v2/users/1/workspaces/{}/contents/read_status'.format(workspace.workspace_id), status=200)  # nopep8
485
+        assert res.json_body[0]['content_id'] == firstly_created.content_id
486
+        assert res.json_body[0]['read_by_user'] is True
487
+
488
+    def test_api_set_content_as_read__ok__200__with_comments(self):
489
+        # init DB
490
+        dbsession = get_tm_session(self.session_factory, transaction.manager)
491
+        admin = dbsession.query(models.User) \
492
+            .filter(models.User.email == 'admin@admin.admin') \
493
+            .one()
494
+        workspace_api = WorkspaceApi(
495
+            current_user=admin,
496
+            session=dbsession,
497
+            config=self.app_config
498
+
499
+        )
500
+        workspace = WorkspaceApi(
501
+            current_user=admin,
502
+            session=dbsession,
503
+            config=self.app_config,
504
+        ).create_workspace(
505
+            'test workspace',
506
+            save_now=True
507
+        )
508
+        api = ContentApi(
509
+            current_user=admin,
510
+            session=dbsession,
511
+            config=self.app_config,
512
+        )
513
+        main_folder = api.create(ContentType.Folder, workspace, None, 'this is randomized folder', '', True)  # nopep8
514
+        # creation order test
515
+        firstly_created = api.create(ContentType.Page, workspace, main_folder, 'creation_order_test', '', True)  # nopep8
516
+        comments = api.create_comment(workspace, firstly_created, 'juste a super comment', True)  # nopep8
517
+        api.mark_unread(firstly_created)
518
+        api.mark_unread(comments)
519
+        dbsession.flush()
520
+        transaction.commit()
521
+
522
+        self.testapp.authorization = (
523
+            'Basic',
524
+            (
525
+                'admin@admin.admin',
526
+                'admin@admin.admin'
527
+            )
528
+        )
529
+        res = self.testapp.get('/api/v2/users/1/workspaces/{}/contents/read_status'.format(workspace.workspace_id), status=200) # nopep8
530
+        assert res.json_body[0]['content_id'] == firstly_created.content_id
531
+        assert res.json_body[0]['read_by_user'] is False
532
+        self.testapp.put(
533
+            '/api/v2/users/{user_id}/workspaces/{workspace_id}/contents/{content_id}/read'.format(  # nopep8
534
+                workspace_id=workspace.workspace_id,
535
+                content_id=firstly_created.content_id,
536
+                user_id=admin.user_id,
537
+            )
538
+        )
539
+        res = self.testapp.get('/api/v2/users/1/workspaces/{}/contents/read_status'.format(workspace.workspace_id), status=200)  # nopep8
540
+        assert res.json_body[0]['content_id'] == firstly_created.content_id
541
+        assert res.json_body[0]['read_by_user'] is True
542
+
543
+        # comment is also set as read
544
+        assert comments.has_new_information_for(admin) is False
545
+
546
+
547
+class TestUserSetContentAsUnread(FunctionalTest):
548
+    """
549
+    Tests for /api/v2/users/{user_id}/workspaces/{workspace_id}/contents/{content_id}/unread  # nopep8
550
+    """
551
+    def test_api_set_content_as_unread__ok__200__nominal_case(self):
552
+        # init DB
553
+        dbsession = get_tm_session(self.session_factory, transaction.manager)
554
+        admin = dbsession.query(models.User) \
555
+            .filter(models.User.email == 'admin@admin.admin') \
556
+            .one()
557
+        workspace_api = WorkspaceApi(
558
+            current_user=admin,
559
+            session=dbsession,
560
+            config=self.app_config
561
+
562
+        )
563
+        workspace = WorkspaceApi(
564
+            current_user=admin,
565
+            session=dbsession,
566
+            config=self.app_config,
567
+        ).create_workspace(
568
+            'test workspace',
569
+            save_now=True
570
+        )
571
+        api = ContentApi(
572
+            current_user=admin,
573
+            session=dbsession,
574
+            config=self.app_config,
575
+        )
576
+        main_folder = api.create(ContentType.Folder, workspace, None, 'this is randomized folder', '', True)  # nopep8
577
+        # creation order test
578
+        firstly_created = api.create(ContentType.Page, workspace, main_folder, 'creation_order_test', '', True)  # nopep8
579
+        api.mark_read(firstly_created)
580
+        dbsession.flush()
581
+        transaction.commit()
582
+
583
+        self.testapp.authorization = (
584
+            'Basic',
585
+            (
586
+                'admin@admin.admin',
587
+                'admin@admin.admin'
588
+            )
589
+        )
590
+        res = self.testapp.get('/api/v2/users/1/workspaces/{}/contents/read_status'.format(workspace.workspace_id), status=200) # nopep8
591
+        assert res.json_body[0]['content_id'] == firstly_created.content_id
592
+        assert res.json_body[0]['read_by_user'] is True
593
+        self.testapp.put(
594
+            '/api/v2/users/{user_id}/workspaces/{workspace_id}/contents/{content_id}/unread'.format(  # nopep8
595
+                workspace_id=workspace.workspace_id,
596
+                content_id=firstly_created.content_id,
597
+                user_id=admin.user_id,
598
+            )
599
+        )
600
+        res = self.testapp.get('/api/v2/users/1/workspaces/{}/contents/read_status'.format(workspace.workspace_id), status=200)  # nopep8
601
+        assert res.json_body[0]['content_id'] == firstly_created.content_id
602
+        assert res.json_body[0]['read_by_user'] is False
603
+
604
+    def test_api_set_content_as_unread__ok__200__with_comments(self):
605
+        # init DB
606
+        dbsession = get_tm_session(self.session_factory, transaction.manager)
607
+        admin = dbsession.query(models.User) \
608
+            .filter(models.User.email == 'admin@admin.admin') \
609
+            .one()
610
+        workspace_api = WorkspaceApi(
611
+            current_user=admin,
612
+            session=dbsession,
613
+            config=self.app_config
614
+
615
+        )
616
+        workspace = WorkspaceApi(
617
+            current_user=admin,
618
+            session=dbsession,
619
+            config=self.app_config,
620
+        ).create_workspace(
621
+            'test workspace',
622
+            save_now=True
623
+        )
624
+        api = ContentApi(
625
+            current_user=admin,
626
+            session=dbsession,
627
+            config=self.app_config,
628
+        )
629
+        main_folder = api.create(ContentType.Folder, workspace, None, 'this is randomized folder', '', True)  # nopep8
630
+        # creation order test
631
+        firstly_created = api.create(ContentType.Page, workspace, main_folder, 'creation_order_test', '', True)  # nopep8
632
+        comments = api.create_comment(workspace, firstly_created, 'juste a super comment', True)  # nopep8
633
+        api.mark_read(firstly_created)
634
+        api.mark_read(comments)
635
+        dbsession.flush()
636
+        transaction.commit()
637
+
638
+        self.testapp.authorization = (
639
+            'Basic',
640
+            (
641
+                'admin@admin.admin',
642
+                'admin@admin.admin'
643
+            )
644
+        )
645
+        res = self.testapp.get('/api/v2/users/1/workspaces/{}/contents/read_status'.format(workspace.workspace_id), status=200) # nopep8
646
+        assert res.json_body[0]['content_id'] == firstly_created.content_id
647
+        assert res.json_body[0]['read_by_user'] is True
648
+        self.testapp.put(
649
+            '/api/v2/users/{user_id}/workspaces/{workspace_id}/contents/{content_id}/unread'.format(  # nopep8
650
+                workspace_id=workspace.workspace_id,
651
+                content_id=firstly_created.content_id,
652
+                user_id=admin.user_id,
653
+            )
654
+        )
655
+        res = self.testapp.get('/api/v2/users/1/workspaces/{}/contents/read_status'.format(workspace.workspace_id), status=200)  # nopep8
656
+        assert res.json_body[0]['content_id'] == firstly_created.content_id
657
+        assert res.json_body[0]['read_by_user'] is False
658
+
659
+        assert comments.has_new_information_for(admin) is True
660
+
661
+
662
+class TestUserSetWorkspaceAsRead(FunctionalTest):
663
+    """
664
+    Tests for /api/v2/users/{user_id}/workspaces/{workspace_id}/read
665
+    """
666
+    def test_api_set_content_as_read__ok__200__nominal_case(self):
667
+        # init DB
668
+        dbsession = get_tm_session(self.session_factory, transaction.manager)
669
+        admin = dbsession.query(models.User) \
670
+            .filter(models.User.email == 'admin@admin.admin') \
671
+            .one()
672
+        workspace_api = WorkspaceApi(
673
+            current_user=admin,
674
+            session=dbsession,
675
+            config=self.app_config
676
+
677
+        )
678
+        workspace = WorkspaceApi(
679
+            current_user=admin,
680
+            session=dbsession,
681
+            config=self.app_config,
682
+        ).create_workspace(
683
+            'test workspace',
684
+            save_now=True
685
+        )
686
+        api = ContentApi(
687
+            current_user=admin,
688
+            session=dbsession,
689
+            config=self.app_config,
690
+        )
691
+        main_folder = api.create(ContentType.Folder, workspace, None, 'this is randomized folder', '', True)  # nopep8
692
+        # creation order test
693
+        firstly_created = api.create(ContentType.Page, workspace, main_folder, 'creation_order_test', '', True)  # nopep8
694
+        api.mark_unread(main_folder)
695
+        api.mark_unread(firstly_created)
696
+        dbsession.flush()
697
+        transaction.commit()
698
+
699
+        self.testapp.authorization = (
700
+            'Basic',
701
+            (
702
+                'admin@admin.admin',
703
+                'admin@admin.admin'
704
+            )
705
+        )
706
+        res = self.testapp.get('/api/v2/users/1/workspaces/{}/contents/read_status'.format(workspace.workspace_id), status=200) # nopep8
707
+        assert res.json_body[0]['content_id'] == firstly_created.content_id
708
+        assert res.json_body[0]['read_by_user'] is False
709
+        assert res.json_body[1]['content_id'] == main_folder.content_id
710
+        assert res.json_body[1]['read_by_user'] is False
711
+        self.testapp.put(
712
+            '/api/v2/users/{user_id}/workspaces/{workspace_id}/read'.format(  # nopep8
713
+                workspace_id=workspace.workspace_id,
714
+                content_id=firstly_created.content_id,
715
+                user_id=admin.user_id,
716
+            )
717
+        )
718
+        res = self.testapp.get('/api/v2/users/1/workspaces/{}/contents/read_status'.format(workspace.workspace_id), status=200)  # nopep8
719
+        assert res.json_body[0]['content_id'] == firstly_created.content_id
720
+        assert res.json_body[0]['read_by_user'] is True
721
+        assert res.json_body[1]['content_id'] == main_folder.content_id
722
+        assert res.json_body[1]['read_by_user'] is True
723
+
724
+
16
 class TestUserWorkspaceEndpoint(FunctionalTest):
725
 class TestUserWorkspaceEndpoint(FunctionalTest):
17
-    # -*- coding: utf-8 -*-
18
     """
726
     """
19
     Tests for /api/v2/users/{user_id}/workspaces
727
     Tests for /api/v2/users/{user_id}/workspaces
20
     """
728
     """

+ 633 - 1
tracim/tests/functional/test_workspaces.py View File

2
 """
2
 """
3
 Tests for /api/v2/workspaces subpath endpoints.
3
 Tests for /api/v2/workspaces subpath endpoints.
4
 """
4
 """
5
+
6
+import transaction
7
+from depot.io.utils import FileIntent
8
+
9
+from tracim import models
10
+from tracim.lib.core.content import ContentApi
11
+from tracim.lib.core.workspace import WorkspaceApi
12
+from tracim.models import get_tm_session
13
+from tracim.models.data import ContentType
5
 from tracim.tests import FunctionalTest
14
 from tracim.tests import FunctionalTest
6
 from tracim.tests import set_html_document_slug_to_legacy
15
 from tracim.tests import set_html_document_slug_to_legacy
7
 from tracim.fixtures.content import Content as ContentFixtures
16
 from tracim.fixtures.content import Content as ContentFixtures
83
         assert sidebar_entry['hexcolor'] == "#757575"
92
         assert sidebar_entry['hexcolor'] == "#757575"
84
         assert sidebar_entry['fa_icon'] == "calendar"
93
         assert sidebar_entry['fa_icon'] == "calendar"
85
 
94
 
95
+    def test_api__update_workspace__ok_200__nominal_case(self) -> None:
96
+        """
97
+        Test update workspace
98
+        """
99
+        self.testapp.authorization = (
100
+            'Basic',
101
+            (
102
+                'admin@admin.admin',
103
+                'admin@admin.admin'
104
+            )
105
+        )
106
+        params = {
107
+            'label': 'superworkspace',
108
+            'description': 'mysuperdescription'
109
+        }
110
+        # Before
111
+        res = self.testapp.get(
112
+            '/api/v2/workspaces/1',
113
+            status=200
114
+        )
115
+        assert res.json_body
116
+        workspace = res.json_body
117
+        assert workspace['workspace_id'] == 1
118
+        assert workspace['slug'] == 'business'
119
+        assert workspace['label'] == 'Business'
120
+        assert workspace['description'] == 'All importants documents'
121
+        assert len(workspace['sidebar_entries']) == 7
122
+
123
+        # modify workspace
124
+        res = self.testapp.put_json(
125
+            '/api/v2/workspaces/1',
126
+            status=200,
127
+            params=params,
128
+        )
129
+        assert res.json_body
130
+        workspace = res.json_body
131
+        assert workspace['workspace_id'] == 1
132
+        assert workspace['slug'] == 'superworkspace'
133
+        assert workspace['label'] == 'superworkspace'
134
+        assert workspace['description'] == 'mysuperdescription'
135
+        assert len(workspace['sidebar_entries']) == 7
136
+
137
+        # after
138
+        res = self.testapp.get(
139
+            '/api/v2/workspaces/1',
140
+            status=200
141
+        )
142
+        assert res.json_body
143
+        workspace = res.json_body
144
+        assert workspace['workspace_id'] == 1
145
+        assert workspace['slug'] == 'superworkspace'
146
+        assert workspace['label'] == 'superworkspace'
147
+        assert workspace['description'] == 'mysuperdescription'
148
+        assert len(workspace['sidebar_entries']) == 7
149
+
150
+    def test_api__update_workspace__err_400__empty_label(self) -> None:
151
+        """
152
+        Test update workspace with empty label
153
+        """
154
+        self.testapp.authorization = (
155
+            'Basic',
156
+            (
157
+                'admin@admin.admin',
158
+                'admin@admin.admin'
159
+            )
160
+        )
161
+        params = {
162
+            'label': '',
163
+            'description': 'mysuperdescription'
164
+        }
165
+        res = self.testapp.put_json(
166
+            '/api/v2/workspaces/1',
167
+            status=400,
168
+            params=params,
169
+        )
170
+
171
+    def test_api__create_workspace__ok_200__nominal_case(self) -> None:
172
+        """
173
+        Test create workspace
174
+        """
175
+        self.testapp.authorization = (
176
+            'Basic',
177
+            (
178
+                'admin@admin.admin',
179
+                'admin@admin.admin'
180
+            )
181
+        )
182
+        params = {
183
+            'label': 'superworkspace',
184
+            'description': 'mysuperdescription'
185
+        }
186
+        res = self.testapp.post_json(
187
+            '/api/v2/workspaces',
188
+            status=200,
189
+            params=params,
190
+        )
191
+        assert res.json_body
192
+        workspace = res.json_body
193
+        workspace_id = res.json_body['workspace_id']
194
+        res = self.testapp.get(
195
+            '/api/v2/workspaces/{}'.format(workspace_id),
196
+            status=200
197
+        )
198
+        workspace_2 = res.json_body
199
+        assert workspace == workspace_2
200
+
201
+    def test_api__create_workspace__err_400__empty_label(self) -> None:
202
+        """
203
+        Test create workspace with empty label
204
+        """
205
+        self.testapp.authorization = (
206
+            'Basic',
207
+            (
208
+                'admin@admin.admin',
209
+                'admin@admin.admin'
210
+            )
211
+        )
212
+        params = {
213
+            'label': '',
214
+            'description': 'mysuperdescription'
215
+        }
216
+        res = self.testapp.post_json(
217
+            '/api/v2/workspaces',
218
+            status=400,
219
+            params=params,
220
+        )
221
+
86
     def test_api__get_workspace__err_400__unallowed_user(self) -> None:
222
     def test_api__get_workspace__err_400__unallowed_user(self) -> None:
87
         """
223
         """
88
         Check obtain workspace unreachable for user
224
         Check obtain workspace unreachable for user
159
         assert user_role['role'] == 'workspace-manager'
295
         assert user_role['role'] == 'workspace-manager'
160
         assert user_role['user_id'] == 1
296
         assert user_role['user_id'] == 1
161
         assert user_role['workspace_id'] == 1
297
         assert user_role['workspace_id'] == 1
298
+        assert user_role['workspace']['workspace_id'] == 1
299
+        assert user_role['workspace']['label'] == 'Business'
300
+        assert user_role['workspace']['slug'] == 'business'
162
         assert user_role['user']['public_name'] == 'Global manager'
301
         assert user_role['user']['public_name'] == 'Global manager'
302
+        assert user_role['user']['user_id'] == 1
303
+        assert user_role['is_active'] is True
163
         # TODO - G.M - 24-05-2018 - [Avatar] Replace
304
         # TODO - G.M - 24-05-2018 - [Avatar] Replace
164
         # by correct value when avatar feature will be enabled
305
         # by correct value when avatar feature will be enabled
165
         assert user_role['user']['avatar_url'] is None
306
         assert user_role['user']['avatar_url'] is None
217
         assert 'message' in res.json.keys()
358
         assert 'message' in res.json.keys()
218
         assert 'details' in res.json.keys()
359
         assert 'details' in res.json.keys()
219
 
360
 
361
+    def test_api__create_workspace_member_role__ok_200__user_id(self):
362
+        """
363
+        Create workspace member role
364
+        :return:
365
+        """
366
+        self.testapp.authorization = (
367
+            'Basic',
368
+            (
369
+                'admin@admin.admin',
370
+                'admin@admin.admin'
371
+            )
372
+        )
373
+        # create workspace role
374
+        params = {
375
+            'user_id': 2,
376
+            'user_email_or_public_name': None,
377
+            'role': 'content-manager',
378
+        }
379
+        res = self.testapp.post_json(
380
+            '/api/v2/workspaces/1/members',
381
+            status=200,
382
+            params=params,
383
+        )
384
+        user_role_found = res.json_body
385
+        assert user_role_found['role'] == 'content-manager'
386
+        assert user_role_found['user_id'] == 2
387
+        assert user_role_found['workspace_id'] == 1
388
+        assert user_role_found['newly_created'] is False
389
+        assert user_role_found['email_sent'] is False
390
+
391
+        res = self.testapp.get('/api/v2/workspaces/1/members', status=200).json_body   # nopep8
392
+        assert len(res) == 2
393
+        user_role = res[0]
394
+        assert user_role['role'] == 'workspace-manager'
395
+        assert user_role['user_id'] == 1
396
+        assert user_role['workspace_id'] == 1
397
+        user_role = res[1]
398
+        assert user_role_found['role'] == user_role['role']
399
+        assert user_role_found['user_id'] == user_role['user_id']
400
+        assert user_role_found['workspace_id'] == user_role['workspace_id']
401
+
402
+    def test_api__create_workspace_member_role__ok_200__user_email(self):
403
+        """
404
+        Create workspace member role
405
+        :return:
406
+        """
407
+        self.testapp.authorization = (
408
+            'Basic',
409
+            (
410
+                'admin@admin.admin',
411
+                'admin@admin.admin'
412
+            )
413
+        )
414
+        # create workspace role
415
+        params = {
416
+            'user_id': None,
417
+            'user_email_or_public_name': 'lawrence-not-real-email@fsf.local',
418
+            'role': 'content-manager',
419
+        }
420
+        res = self.testapp.post_json(
421
+            '/api/v2/workspaces/1/members',
422
+            status=200,
423
+            params=params,
424
+        )
425
+        user_role_found = res.json_body
426
+        assert user_role_found['role'] == 'content-manager'
427
+        assert user_role_found['user_id'] == 2
428
+        assert user_role_found['workspace_id'] == 1
429
+        assert user_role_found['newly_created'] is False
430
+        assert user_role_found['email_sent'] is False
431
+
432
+        res = self.testapp.get('/api/v2/workspaces/1/members', status=200).json_body   # nopep8
433
+        assert len(res) == 2
434
+        user_role = res[0]
435
+        assert user_role['role'] == 'workspace-manager'
436
+        assert user_role['user_id'] == 1
437
+        assert user_role['workspace_id'] == 1
438
+        user_role = res[1]
439
+        assert user_role_found['role'] == user_role['role']
440
+        assert user_role_found['user_id'] == user_role['user_id']
441
+        assert user_role_found['workspace_id'] == user_role['workspace_id']
442
+
443
+    def test_api__create_workspace_member_role__ok_200__user_public_name(self):
444
+        """
445
+        Create workspace member role
446
+        :return:
447
+        """
448
+        self.testapp.authorization = (
449
+            'Basic',
450
+            (
451
+                'admin@admin.admin',
452
+                'admin@admin.admin'
453
+            )
454
+        )
455
+        # create workspace role
456
+        params = {
457
+            'user_id': None,
458
+            'user_email_or_public_name': 'Lawrence L.',
459
+            'role': 'content-manager',
460
+        }
461
+        res = self.testapp.post_json(
462
+            '/api/v2/workspaces/1/members',
463
+            status=200,
464
+            params=params,
465
+        )
466
+        user_role_found = res.json_body
467
+        assert user_role_found['role'] == 'content-manager'
468
+        assert user_role_found['user_id'] == 2
469
+        assert user_role_found['workspace_id'] == 1
470
+        assert user_role_found['newly_created'] is False
471
+        assert user_role_found['email_sent'] is False
472
+
473
+        res = self.testapp.get('/api/v2/workspaces/1/members', status=200).json_body   # nopep8
474
+        assert len(res) == 2
475
+        user_role = res[0]
476
+        assert user_role['role'] == 'workspace-manager'
477
+        assert user_role['user_id'] == 1
478
+        assert user_role['workspace_id'] == 1
479
+        user_role = res[1]
480
+        assert user_role_found['role'] == user_role['role']
481
+        assert user_role_found['user_id'] == user_role['user_id']
482
+        assert user_role_found['workspace_id'] == user_role['workspace_id']
483
+
484
+    def test_api__create_workspace_member_role__err_400__nothing(self):
485
+        """
486
+        Create workspace member role
487
+        :return:
488
+        """
489
+        self.testapp.authorization = (
490
+            'Basic',
491
+            (
492
+                'admin@admin.admin',
493
+                'admin@admin.admin'
494
+            )
495
+        )
496
+        # create workspace role
497
+        params = {
498
+            'user_id': None,
499
+            'user_email_or_public_name': None,
500
+            'role': 'content-manager',
501
+        }
502
+        res = self.testapp.post_json(
503
+            '/api/v2/workspaces/1/members',
504
+            status=400,
505
+            params=params,
506
+        )
507
+
508
+    def test_api__create_workspace_member_role__err_400__wrong_user_id(self):
509
+        """
510
+        Create workspace member role
511
+        :return:
512
+        """
513
+        self.testapp.authorization = (
514
+            'Basic',
515
+            (
516
+                'admin@admin.admin',
517
+                'admin@admin.admin'
518
+            )
519
+        )
520
+        # create workspace role
521
+        params = {
522
+            'user_id': 47,
523
+            'user_email_or_public_name': None,
524
+            'role': 'content-manager',
525
+        }
526
+        res = self.testapp.post_json(
527
+            '/api/v2/workspaces/1/members',
528
+            status=400,
529
+            params=params,
530
+        )
531
+
532
+    def test_api__create_workspace_member_role__ok_200__new_user(self):  # nopep8
533
+        """
534
+        Create workspace member role
535
+        :return:
536
+        """
537
+        self.testapp.authorization = (
538
+            'Basic',
539
+            (
540
+                'admin@admin.admin',
541
+                'admin@admin.admin'
542
+            )
543
+        )
544
+        # create workspace role
545
+        params = {
546
+            'user_id': None,
547
+            'user_email_or_public_name': 'nothing@nothing.nothing',
548
+            'role': 'content-manager',
549
+        }
550
+        res = self.testapp.post_json(
551
+            '/api/v2/workspaces/1/members',
552
+            status=200,
553
+            params=params,
554
+        )
555
+        user_role_found = res.json_body
556
+        assert user_role_found['role'] == 'content-manager'
557
+        assert user_role_found['user_id']
558
+        user_id = user_role_found['user_id']
559
+        assert user_role_found['workspace_id'] == 1
560
+        assert user_role_found['newly_created'] is True
561
+        assert user_role_found['email_sent'] is False
562
+
563
+        res = self.testapp.get('/api/v2/workspaces/1/members',
564
+                               status=200).json_body  # nopep8
565
+        assert len(res) == 2
566
+        user_role = res[0]
567
+        assert user_role['role'] == 'workspace-manager'
568
+        assert user_role['user_id'] == 1
569
+        assert user_role['workspace_id'] == 1
570
+        user_role = res[1]
571
+        assert user_role_found['role'] == user_role['role']
572
+        assert user_role_found['user_id'] == user_role['user_id']
573
+        assert user_role_found['workspace_id'] == user_role['workspace_id']
574
+
575
+    def test_api__update_workspace_member_role__ok_200__nominal_case(self):
576
+        """
577
+        Update worskpace member role
578
+        """
579
+        # before
580
+        self.testapp.authorization = (
581
+            'Basic',
582
+            (
583
+                'admin@admin.admin',
584
+                'admin@admin.admin'
585
+            )
586
+        )
587
+        res = self.testapp.get('/api/v2/workspaces/1/members', status=200).json_body   # nopep8
588
+        assert len(res) == 1
589
+        user_role = res[0]
590
+        assert user_role['role'] == 'workspace-manager'
591
+        assert user_role['user_id'] == 1
592
+        assert user_role['workspace_id'] == 1
593
+        # update workspace role
594
+        params = {
595
+            'role': 'content-manager',
596
+        }
597
+        res = self.testapp.put_json(
598
+            '/api/v2/workspaces/1/members/1',
599
+            status=200,
600
+            params=params,
601
+        )
602
+        user_role = res.json_body
603
+        assert user_role['role'] == 'content-manager'
604
+        assert user_role['user_id'] == 1
605
+        assert user_role['workspace_id'] == 1
606
+        # after
607
+        res = self.testapp.get('/api/v2/workspaces/1/members', status=200).json_body   # nopep8
608
+        assert len(res) == 1
609
+        user_role = res[0]
610
+        assert user_role['role'] == 'content-manager'
611
+        assert user_role['user_id'] == 1
612
+        assert user_role['workspace_id'] == 1
613
+
220
 
614
 
221
 class TestWorkspaceContents(FunctionalTest):
615
 class TestWorkspaceContents(FunctionalTest):
222
     """
616
     """
241
         assert len(res) == 3
635
         assert len(res) == 3
242
         content = res[0]
636
         content = res[0]
243
         assert content['content_id'] == 1
637
         assert content['content_id'] == 1
638
+        assert content['content_type'] == 'folder'
244
         assert content['is_archived'] is False
639
         assert content['is_archived'] is False
245
         assert content['is_deleted'] is False
640
         assert content['is_deleted'] is False
246
         assert content['label'] == 'Tools'
641
         assert content['label'] == 'Tools'
252
         assert content['workspace_id'] == 1
647
         assert content['workspace_id'] == 1
253
         content = res[1]
648
         content = res[1]
254
         assert content['content_id'] == 2
649
         assert content['content_id'] == 2
650
+        assert content['content_type'] == 'folder'
255
         assert content['is_archived'] is False
651
         assert content['is_archived'] is False
256
         assert content['is_deleted'] is False
652
         assert content['is_deleted'] is False
257
         assert content['label'] == 'Menus'
653
         assert content['label'] == 'Menus'
263
         assert content['workspace_id'] == 1
659
         assert content['workspace_id'] == 1
264
         content = res[2]
660
         content = res[2]
265
         assert content['content_id'] == 11
661
         assert content['content_id'] == 11
662
+        assert content['content_type'] == 'html-documents'
663
+        assert content['is_archived'] is False
664
+        assert content['is_deleted'] is False
665
+        assert content['label'] == 'Current Menu'
666
+        assert content['parent_id'] == 2
667
+        assert content['show_in_ui'] is True
668
+        assert content['slug'] == 'current-menu'
669
+        assert content['status'] == 'open'
670
+        assert set(content['sub_content_types']) == {'thread', 'html-documents', 'folder', 'file'}  # nopep8
671
+        assert content['workspace_id'] == 1
672
+
673
+    def test_api__get_workspace_content__ok_200__get_default_html_documents(self):
674
+        """
675
+        Check obtain workspace contents with defaults filters + content_filter
676
+        """
677
+        self.testapp.authorization = (
678
+            'Basic',
679
+            (
680
+                'admin@admin.admin',
681
+                'admin@admin.admin'
682
+            )
683
+        )
684
+        params = {
685
+            'content_type': 'html-documents',
686
+        }
687
+        res = self.testapp.get('/api/v2/workspaces/1/contents', status=200, params=params).json_body   # nopep8
688
+        assert len(res) == 1
689
+        content = res[0]
690
+        assert content
691
+        assert content['content_id'] == 11
692
+        assert content['content_type'] == 'html-documents'
266
         assert content['is_archived'] is False
693
         assert content['is_archived'] is False
267
         assert content['is_deleted'] is False
694
         assert content['is_deleted'] is False
268
         assert content['label'] == 'Current Menu'
695
         assert content['label'] == 'Current Menu'
274
         assert content['workspace_id'] == 1
701
         assert content['workspace_id'] == 1
275
 
702
 
276
     # Root related
703
     # Root related
277
-    def test_api__get_workspace_content__ok_200__get_all_root_content__legacy_html_slug(self):
704
+    def test_api__get_workspace_content__ok_200__get_all_root_content__legacy_html_slug(self):  # nopep8
278
         """
705
         """
279
         Check obtain workspace all root contents
706
         Check obtain workspace all root contents
280
         """
707
         """
539
         assert res == []
966
         assert res == []
540
 
967
 
541
     # Folder related
968
     # Folder related
969
+    def test_api__get_workspace_content__ok_200__get_all_filter_content_thread(self):
970
+        # prepare data
971
+        dbsession = get_tm_session(self.session_factory, transaction.manager)
972
+        admin = dbsession.query(models.User) \
973
+            .filter(models.User.email == 'admin@admin.admin') \
974
+            .one()
975
+        workspace_api = WorkspaceApi(
976
+            current_user=admin,
977
+            session=dbsession,
978
+            config=self.app_config
979
+        )
980
+        business_workspace = workspace_api.get_one(1)
981
+        content_api = ContentApi(
982
+            current_user=admin,
983
+            session=dbsession,
984
+            config=self.app_config
985
+        )
986
+        tool_folder = content_api.get_one(1, content_type=ContentType.Any)
987
+        test_thread = content_api.create(
988
+            content_type=ContentType.Thread,
989
+            workspace=business_workspace,
990
+            parent=tool_folder,
991
+            label='Test Thread',
992
+            do_save=False,
993
+            do_notify=False,
994
+        )
995
+        test_thread.description = 'Thread description'
996
+        dbsession.add(test_thread)
997
+        test_file = content_api.create(
998
+            content_type=ContentType.File,
999
+            workspace=business_workspace,
1000
+            parent=tool_folder,
1001
+            label='Test file',
1002
+            do_save=False,
1003
+            do_notify=False,
1004
+        )
1005
+        test_file.file_extension = '.txt'
1006
+        test_file.depot_file = FileIntent(
1007
+            b'Test file',
1008
+            'Test_file.txt',
1009
+            'text/plain',
1010
+        )
1011
+        test_page_legacy = content_api.create(
1012
+            content_type=ContentType.Page,
1013
+            workspace=business_workspace,
1014
+            label='test_page',
1015
+            do_save=False,
1016
+            do_notify=False,
1017
+        )
1018
+        test_page_legacy.type = ContentType.PageLegacy
1019
+        content_api.update_content(test_page_legacy, 'test_page', '<p>PAGE</p>')
1020
+        test_html_document = content_api.create(
1021
+            content_type=ContentType.Page,
1022
+            workspace=business_workspace,
1023
+            label='test_html_page',
1024
+            do_save=False,
1025
+            do_notify=False,
1026
+        )
1027
+        content_api.update_content(test_html_document, 'test_page', '<p>HTML_DOCUMENT</p>')  # nopep8
1028
+        dbsession.flush()
1029
+        transaction.commit()
1030
+        # test-itself
1031
+        params = {
1032
+            'parent_id': 1,
1033
+            'show_archived': 1,
1034
+            'show_deleted': 1,
1035
+            'show_active': 1,
1036
+            'content_type': 'thread',
1037
+        }
1038
+        self.testapp.authorization = (
1039
+            'Basic',
1040
+            (
1041
+                'admin@admin.admin',
1042
+                'admin@admin.admin'
1043
+            )
1044
+        )
1045
+        res = self.testapp.get(
1046
+            '/api/v2/workspaces/1/contents',
1047
+            status=200,
1048
+            params=params,
1049
+        ).json_body
1050
+        assert len(res) == 1
1051
+        content = res[0]
1052
+        assert content['content_type'] == 'thread'
1053
+        assert content['content_id']
1054
+        assert content['is_archived'] is False
1055
+        assert content['is_deleted'] is False
1056
+        assert content['label'] == 'Test Thread'
1057
+        assert content['parent_id'] == 1
1058
+        assert content['show_in_ui'] is True
1059
+        assert content['slug'] == 'test-thread'
1060
+        assert content['status'] == 'open'
1061
+        assert set(content['sub_content_types']) == {'thread', 'html-documents', 'folder', 'file'}  # nopep8
1062
+        assert content['workspace_id'] == 1
1063
+
1064
+    def test_api__get_workspace_content__ok_200__get_all_filter_content_html_and_legacy_page(self):  # nopep8
1065
+        # prepare data
1066
+        dbsession = get_tm_session(self.session_factory, transaction.manager)
1067
+        admin = dbsession.query(models.User) \
1068
+            .filter(models.User.email == 'admin@admin.admin') \
1069
+            .one()
1070
+        workspace_api = WorkspaceApi(
1071
+            current_user=admin,
1072
+            session=dbsession,
1073
+            config=self.app_config
1074
+        )
1075
+        business_workspace = workspace_api.get_one(1)
1076
+        content_api = ContentApi(
1077
+            current_user=admin,
1078
+            session=dbsession,
1079
+            config=self.app_config
1080
+        )
1081
+        tool_folder = content_api.get_one(1, content_type=ContentType.Any)
1082
+        test_thread = content_api.create(
1083
+            content_type=ContentType.Thread,
1084
+            workspace=business_workspace,
1085
+            parent=tool_folder,
1086
+            label='Test Thread',
1087
+            do_save=False,
1088
+            do_notify=False,
1089
+        )
1090
+        test_thread.description = 'Thread description'
1091
+        dbsession.add(test_thread)
1092
+        test_file = content_api.create(
1093
+            content_type=ContentType.File,
1094
+            workspace=business_workspace,
1095
+            parent=tool_folder,
1096
+            label='Test file',
1097
+            do_save=False,
1098
+            do_notify=False,
1099
+        )
1100
+        test_file.file_extension = '.txt'
1101
+        test_file.depot_file = FileIntent(
1102
+            b'Test file',
1103
+            'Test_file.txt',
1104
+            'text/plain',
1105
+        )
1106
+        test_page_legacy = content_api.create(
1107
+            content_type=ContentType.Page,
1108
+            workspace=business_workspace,
1109
+            parent=tool_folder,
1110
+            label='test_page',
1111
+            do_save=False,
1112
+            do_notify=False,
1113
+        )
1114
+        test_page_legacy.type = ContentType.PageLegacy
1115
+        content_api.update_content(test_page_legacy, 'test_page', '<p>PAGE</p>')
1116
+        test_html_document = content_api.create(
1117
+            content_type=ContentType.Page,
1118
+            workspace=business_workspace,
1119
+            parent=tool_folder,
1120
+            label='test_html_page',
1121
+            do_save=False,
1122
+            do_notify=False,
1123
+        )
1124
+        content_api.update_content(test_html_document, 'test_html_page', '<p>HTML_DOCUMENT</p>')  # nopep8
1125
+        dbsession.flush()
1126
+        transaction.commit()
1127
+        # test-itself
1128
+        params = {
1129
+            'parent_id': 1,
1130
+            'show_archived': 1,
1131
+            'show_deleted': 1,
1132
+            'show_active': 1,
1133
+            'content_type': 'html-documents',
1134
+        }
1135
+        self.testapp.authorization = (
1136
+            'Basic',
1137
+            (
1138
+                'admin@admin.admin',
1139
+                'admin@admin.admin'
1140
+            )
1141
+        )
1142
+        res = self.testapp.get(
1143
+            '/api/v2/workspaces/1/contents',
1144
+            status=200,
1145
+            params=params,
1146
+        ).json_body
1147
+        assert len(res) == 2
1148
+        content = res[0]
1149
+        assert content['content_type'] == 'html-documents'
1150
+        assert content['content_id']
1151
+        assert content['is_archived'] is False
1152
+        assert content['is_deleted'] is False
1153
+        assert content['label'] == 'test_page'
1154
+        assert content['parent_id'] == 1
1155
+        assert content['show_in_ui'] is True
1156
+        assert content['slug'] == 'test-page'
1157
+        assert content['status'] == 'open'
1158
+        assert set(content['sub_content_types']) == {'thread', 'html-documents', 'folder', 'file'}  # nopep8
1159
+        assert content['workspace_id'] == 1
1160
+        content = res[1]
1161
+        assert content['content_type'] == 'html-documents'
1162
+        assert content['content_id']
1163
+        assert content['is_archived'] is False
1164
+        assert content['is_deleted'] is False
1165
+        assert content['label'] == 'test_html_page'
1166
+        assert content['parent_id'] == 1
1167
+        assert content['show_in_ui'] is True
1168
+        assert content['slug'] == 'test-html-page'
1169
+        assert content['status'] == 'open'
1170
+        assert set(content['sub_content_types']) == {'thread', 'html-documents', 'folder', 'file'}  # nopep8
1171
+        assert content['workspace_id'] == 1
1172
+        assert res[0]['content_id'] != res[1]['content_id']
542
 
1173
 
543
     def test_api__get_workspace_content__ok_200__get_all_folder_content(self):
1174
     def test_api__get_workspace_content__ok_200__get_all_folder_content(self):
544
         """
1175
         """
549
             'show_archived': 1,
1180
             'show_archived': 1,
550
             'show_deleted': 1,
1181
             'show_deleted': 1,
551
             'show_active': 1,
1182
             'show_active': 1,
1183
+            'content_type': 'any'
552
         }
1184
         }
553
         self.testapp.authorization = (
1185
         self.testapp.authorization = (
554
             'Basic',
1186
             'Basic',

+ 355 - 1
tracim/tests/library/test_content_api.py View File

1
 # -*- coding: utf-8 -*-
1
 # -*- coding: utf-8 -*-
2
-
2
+import datetime
3
 import transaction
3
 import transaction
4
 import pytest
4
 import pytest
5
 
5
 
1977
         eq_(ActionDescription.UNDELETION, updated2.revision_type)
1977
         eq_(ActionDescription.UNDELETION, updated2.revision_type)
1978
         eq_(u1id, updated2.owner_id)
1978
         eq_(u1id, updated2.owner_id)
1979
 
1979
 
1980
+    def test_unit__get_last_active__ok__nominal_case(self):
1981
+        uapi = UserApi(
1982
+            session=self.session,
1983
+            config=self.app_config,
1984
+            current_user=None,
1985
+        )
1986
+        group_api = GroupApi(
1987
+            current_user=None,
1988
+            session=self.session,
1989
+            config=self.app_config,
1990
+        )
1991
+        groups = [group_api.get_one(Group.TIM_USER),
1992
+                  group_api.get_one(Group.TIM_MANAGER),
1993
+                  group_api.get_one(Group.TIM_ADMIN)]
1994
+
1995
+        user = uapi.create_minimal_user(email='this.is@user',
1996
+                                        groups=groups, save_now=True)
1997
+        workspace = WorkspaceApi(
1998
+            current_user=user,
1999
+            session=self.session,
2000
+            config=self.app_config,
2001
+        ).create_workspace(
2002
+            'test workspace',
2003
+            save_now=True
2004
+        )
2005
+        workspace2 = WorkspaceApi(
2006
+            current_user=user,
2007
+            session=self.session,
2008
+            config=self.app_config,
2009
+        ).create_workspace(
2010
+            'test workspace2',
2011
+            save_now=True
2012
+        )
2013
+
2014
+        api = ContentApi(
2015
+            current_user=user,
2016
+            session=self.session,
2017
+            config=self.app_config,
2018
+        )
2019
+        main_folder_workspace2 = api.create(ContentType.Folder, workspace2, None, 'Hepla', '', True)  # nopep8
2020
+        main_folder = api.create(ContentType.Folder, workspace, None, 'this is randomized folder', '', True)  # nopep8
2021
+        # creation order test
2022
+        firstly_created = api.create(ContentType.Page, workspace, main_folder, 'creation_order_test', '', True)  # nopep8
2023
+        secondly_created = api.create(ContentType.Page, workspace, main_folder, 'another creation_order_test', '', True)  # nopep8
2024
+        # update order test
2025
+        firstly_created_but_recently_updated = api.create(ContentType.Page, workspace, main_folder, 'update_order_test', '', True)  # nopep8
2026
+        secondly_created_but_not_updated = api.create(ContentType.Page, workspace, main_folder, 'another update_order_test', '', True)  # nopep8
2027
+        with new_revision(
2028
+            session=self.session,
2029
+            tm=transaction.manager,
2030
+            content=firstly_created_but_recently_updated,
2031
+        ):
2032
+            firstly_created_but_recently_updated.description = 'Just an update'
2033
+        api.save(firstly_created_but_recently_updated)
2034
+        # comment change order
2035
+        firstly_created_but_recently_commented = api.create(ContentType.Page, workspace, main_folder, 'this is randomized label content', '', True)  # nopep8
2036
+        secondly_created_but_not_commented = api.create(ContentType.Page, workspace, main_folder, 'this is another randomized label content', '', True)  # nopep8
2037
+        comments = api.create_comment(workspace, firstly_created_but_recently_commented, 'juste a super comment', True)  # nopep8
2038
+
2039
+        content_workspace_2 = api.create(ContentType.Page, workspace2 ,main_folder_workspace2, 'content_workspace_2', '',True)  # nopep8
2040
+        last_actives = api.get_last_active()
2041
+        assert len(last_actives) == 9
2042
+        # workspace_2 content
2043
+        assert last_actives[0] == content_workspace_2
2044
+        # comment is newest than page2
2045
+        assert last_actives[1] == firstly_created_but_recently_commented
2046
+        assert last_actives[2] == secondly_created_but_not_commented
2047
+        # last updated content is newer than other one despite creation
2048
+        # of the other is more recent
2049
+        assert last_actives[3] == firstly_created_but_recently_updated
2050
+        assert last_actives[4] == secondly_created_but_not_updated
2051
+        # creation order is inverted here as last created is last active
2052
+        assert last_actives[5] == secondly_created
2053
+        assert last_actives[6] == firstly_created
2054
+        # folder subcontent modification does not change folder order
2055
+        assert last_actives[7] == main_folder
2056
+        # folder subcontent modification does not change folder order
2057
+        # (workspace2)
2058
+        assert last_actives[8] == main_folder_workspace2
2059
+
2060
+    def test_unit__get_last_active__ok__workspace_filter_workspace_full(self):
2061
+        uapi = UserApi(
2062
+            session=self.session,
2063
+            config=self.app_config,
2064
+            current_user=None,
2065
+        )
2066
+        group_api = GroupApi(
2067
+            current_user=None,
2068
+            session=self.session,
2069
+            config=self.app_config,
2070
+        )
2071
+        groups = [group_api.get_one(Group.TIM_USER),
2072
+                  group_api.get_one(Group.TIM_MANAGER),
2073
+                  group_api.get_one(Group.TIM_ADMIN)]
2074
+
2075
+        user = uapi.create_minimal_user(email='this.is@user',
2076
+                                        groups=groups, save_now=True)
2077
+        workspace = WorkspaceApi(
2078
+            current_user=user,
2079
+            session=self.session,
2080
+            config=self.app_config,
2081
+        ).create_workspace(
2082
+            'test workspace',
2083
+            save_now=True
2084
+        )
2085
+
2086
+        api = ContentApi(
2087
+            current_user=user,
2088
+            session=self.session,
2089
+            config=self.app_config,
2090
+        )
2091
+        main_folder = api.create(ContentType.Folder, workspace, None, 'this is randomized folder', '', True)  # nopep8
2092
+        # creation order test
2093
+        firstly_created = api.create(ContentType.Page, workspace, main_folder, 'creation_order_test', '', True)  # nopep8
2094
+        secondly_created = api.create(ContentType.Page, workspace, main_folder, 'another creation_order_test', '', True)  # nopep8
2095
+        # update order test
2096
+        firstly_created_but_recently_updated = api.create(ContentType.Page, workspace, main_folder, 'update_order_test', '', True)  # nopep8
2097
+        secondly_created_but_not_updated = api.create(ContentType.Page, workspace, main_folder, 'another update_order_test', '', True)  # nopep8
2098
+        with new_revision(
2099
+            session=self.session,
2100
+            tm=transaction.manager,
2101
+            content=firstly_created_but_recently_updated,
2102
+        ):
2103
+            firstly_created_but_recently_updated.description = 'Just an update'
2104
+        api.save(firstly_created_but_recently_updated)
2105
+        # comment change order
2106
+        firstly_created_but_recently_commented = api.create(ContentType.Page, workspace, main_folder, 'this is randomized label content', '', True)  # nopep8
2107
+        secondly_created_but_not_commented = api.create(ContentType.Page, workspace, main_folder, 'this is another randomized label content', '', True)  # nopep8
2108
+        comments = api.create_comment(workspace, firstly_created_but_recently_commented, 'juste a super comment', True)  # nopep8
2109
+
2110
+        last_actives = api.get_last_active(workspace=workspace)
2111
+        assert len(last_actives) == 7
2112
+        # comment is newest than page2
2113
+        assert last_actives[0] == firstly_created_but_recently_commented
2114
+        assert last_actives[1] == secondly_created_but_not_commented
2115
+        # last updated content is newer than other one despite creation
2116
+        # of the other is more recent
2117
+        assert last_actives[2] == firstly_created_but_recently_updated
2118
+        assert last_actives[3] == secondly_created_but_not_updated
2119
+        # creation order is inverted here as last created is last active
2120
+        assert last_actives[4] == secondly_created
2121
+        assert last_actives[5] == firstly_created
2122
+        # folder subcontent modification does not change folder order
2123
+        assert last_actives[6] == main_folder
2124
+
2125
+    def test_unit__get_last_active__ok__workspace_filter_workspace_content_ids(self):
2126
+        uapi = UserApi(
2127
+            session=self.session,
2128
+            config=self.app_config,
2129
+            current_user=None,
2130
+        )
2131
+        group_api = GroupApi(
2132
+            current_user=None,
2133
+            session=self.session,
2134
+            config=self.app_config,
2135
+        )
2136
+        groups = [group_api.get_one(Group.TIM_USER),
2137
+                  group_api.get_one(Group.TIM_MANAGER),
2138
+                  group_api.get_one(Group.TIM_ADMIN)]
2139
+
2140
+        user = uapi.create_minimal_user(email='this.is@user',
2141
+                                        groups=groups, save_now=True)
2142
+        workspace = WorkspaceApi(
2143
+            current_user=user,
2144
+            session=self.session,
2145
+            config=self.app_config,
2146
+        ).create_workspace(
2147
+            'test workspace',
2148
+            save_now=True
2149
+        )
2150
+
2151
+        api = ContentApi(
2152
+            current_user=user,
2153
+            session=self.session,
2154
+            config=self.app_config,
2155
+        )
2156
+        main_folder = api.create(ContentType.Folder, workspace, None, 'this is randomized folder', '', True)  # nopep8
2157
+        # creation order test
2158
+        firstly_created = api.create(ContentType.Page, workspace, main_folder, 'creation_order_test', '', True)  # nopep8
2159
+        secondly_created = api.create(ContentType.Page, workspace, main_folder, 'another creation_order_test', '', True)  # nopep8
2160
+        # update order test
2161
+        firstly_created_but_recently_updated = api.create(ContentType.Page, workspace, main_folder, 'update_order_test', '', True)  # nopep8
2162
+        secondly_created_but_not_updated = api.create(ContentType.Page, workspace, main_folder, 'another update_order_test', '', True)  # nopep8
2163
+        with new_revision(
2164
+            session=self.session,
2165
+            tm=transaction.manager,
2166
+            content=firstly_created_but_recently_updated,
2167
+        ):
2168
+            firstly_created_but_recently_updated.description = 'Just an update'
2169
+        api.save(firstly_created_but_recently_updated)
2170
+        # comment change order
2171
+        firstly_created_but_recently_commented = api.create(ContentType.Page, workspace, main_folder, 'this is randomized label content', '', True)  # nopep8
2172
+        secondly_created_but_not_commented = api.create(ContentType.Page, workspace, main_folder, 'this is another randomized label content', '', True)  # nopep8
2173
+        comments = api.create_comment(workspace, firstly_created_but_recently_commented, 'juste a super comment', True)  # nopep8
2174
+
2175
+        selected_contents = [
2176
+            firstly_created_but_recently_commented,
2177
+            firstly_created_but_recently_updated,
2178
+            firstly_created,
2179
+            main_folder,
2180
+        ]
2181
+        content_ids = [content.content_id for content in selected_contents]
2182
+        last_actives = api.get_last_active(
2183
+            workspace=workspace,
2184
+            content_ids=content_ids,
2185
+        )
2186
+        assert len(last_actives) == 4
2187
+        # comment is newest than page2
2188
+        assert last_actives[0] == firstly_created_but_recently_commented
2189
+        assert secondly_created_but_not_commented not in last_actives
2190
+        # last updated content is newer than other one despite creation
2191
+        # of the other is more recent
2192
+        assert last_actives[1] == firstly_created_but_recently_updated
2193
+        assert secondly_created_but_not_updated not in last_actives
2194
+        # creation order is inverted here as last created is last active
2195
+        assert secondly_created not in last_actives
2196
+        assert last_actives[2] == firstly_created
2197
+        # folder subcontent modification does not change folder order
2198
+        assert last_actives[3] == main_folder
2199
+
2200
+    def test_unit__get_last_active__ok__workspace_filter_workspace_limit_2_multiples_times(self):  # nopep8
2201
+        uapi = UserApi(
2202
+            session=self.session,
2203
+            config=self.app_config,
2204
+            current_user=None,
2205
+        )
2206
+        group_api = GroupApi(
2207
+            current_user=None,
2208
+            session=self.session,
2209
+            config=self.app_config,
2210
+        )
2211
+        groups = [group_api.get_one(Group.TIM_USER),
2212
+                  group_api.get_one(Group.TIM_MANAGER),
2213
+                  group_api.get_one(Group.TIM_ADMIN)]
2214
+
2215
+        user = uapi.create_minimal_user(email='this.is@user',
2216
+                                        groups=groups, save_now=True)
2217
+        workspace = WorkspaceApi(
2218
+            current_user=user,
2219
+            session=self.session,
2220
+            config=self.app_config,
2221
+        ).create_workspace(
2222
+            'test workspace',
2223
+            save_now=True
2224
+        )
2225
+
2226
+        api = ContentApi(
2227
+            current_user=user,
2228
+            session=self.session,
2229
+            config=self.app_config,
2230
+        )
2231
+        main_folder = api.create(ContentType.Folder, workspace, None, 'this is randomized folder', '', True)  # nopep8
2232
+        # creation order test
2233
+        firstly_created = api.create(ContentType.Page, workspace, main_folder, 'creation_order_test', '', True)  # nopep8
2234
+        secondly_created = api.create(ContentType.Page, workspace, main_folder, 'another creation_order_test', '', True)  # nopep8
2235
+        # update order test
2236
+        firstly_created_but_recently_updated = api.create(ContentType.Page, workspace, main_folder, 'update_order_test', '', True)  # nopep8
2237
+        secondly_created_but_not_updated = api.create(ContentType.Page, workspace, main_folder, 'another update_order_test', '', True)  # nopep8
2238
+        with new_revision(
2239
+            session=self.session,
2240
+            tm=transaction.manager,
2241
+            content=firstly_created_but_recently_updated,
2242
+        ):
2243
+            firstly_created_but_recently_updated.description = 'Just an update'
2244
+        api.save(firstly_created_but_recently_updated)
2245
+        # comment change order
2246
+        firstly_created_but_recently_commented = api.create(ContentType.Page, workspace, main_folder, 'this is randomized label content', '', True)  # nopep8
2247
+        secondly_created_but_not_commented = api.create(ContentType.Page, workspace, main_folder, 'this is another randomized label content', '', True)  # nopep8
2248
+        comments = api.create_comment(workspace, firstly_created_but_recently_commented, 'juste a super comment', True)  # nopep8
2249
+
2250
+        last_actives = api.get_last_active(workspace=workspace, limit=2, before_datetime=datetime.datetime.now())  # nopep8
2251
+        assert len(last_actives) == 2
2252
+        # comment is newest than page2
2253
+        assert last_actives[0] == firstly_created_but_recently_commented
2254
+        assert last_actives[1] == secondly_created_but_not_commented
2255
+
2256
+        last_actives = api.get_last_active(workspace=workspace, limit=2, before_datetime=last_actives[1].get_simple_last_activity_date())  # nopep8
2257
+        assert len(last_actives) == 2
2258
+        # last updated content is newer than other one despite creation
2259
+        # of the other is more recent
2260
+        assert last_actives[0] == firstly_created_but_recently_updated
2261
+        assert last_actives[1] == secondly_created_but_not_updated
2262
+
2263
+        last_actives = api.get_last_active(workspace=workspace, limit=2, before_datetime=last_actives[1].get_simple_last_activity_date())  # nopep8
2264
+        assert len(last_actives) == 2
2265
+        # creation order is inverted here as last created is last active
2266
+        assert last_actives[0] == secondly_created
2267
+        assert last_actives[1] == firstly_created
2268
+
2269
+        last_actives = api.get_last_active(workspace=workspace, limit=2, before_datetime=last_actives[1].get_simple_last_activity_date())  # nopep8
2270
+        assert len(last_actives) == 1
2271
+        # folder subcontent modification does not change folder order
2272
+        assert last_actives[0] == main_folder
2273
+
2274
+    def test_unit__get_last_active__ok__workspace_filter_workspace_empty(self):
2275
+        uapi = UserApi(
2276
+            session=self.session,
2277
+            config=self.app_config,
2278
+            current_user=None,
2279
+        )
2280
+        group_api = GroupApi(
2281
+            current_user=None,
2282
+            session=self.session,
2283
+            config=self.app_config,
2284
+        )
2285
+        groups = [group_api.get_one(Group.TIM_USER),
2286
+                  group_api.get_one(Group.TIM_MANAGER),
2287
+                  group_api.get_one(Group.TIM_ADMIN)]
2288
+
2289
+        user = uapi.create_minimal_user(email='this.is@user',
2290
+                                        groups=groups, save_now=True)
2291
+        workspace = WorkspaceApi(
2292
+            current_user=user,
2293
+            session=self.session,
2294
+            config=self.app_config,
2295
+        ).create_workspace(
2296
+            'test workspace',
2297
+            save_now=True
2298
+        )
2299
+        workspace2 = WorkspaceApi(
2300
+            current_user=user,
2301
+            session=self.session,
2302
+            config=self.app_config,
2303
+        ).create_workspace(
2304
+            'test workspace2',
2305
+            save_now=True
2306
+        )
2307
+        api = ContentApi(
2308
+            current_user=user,
2309
+            session=self.session,
2310
+            config=self.app_config,
2311
+        )
2312
+        main_folder = api.create(ContentType.Folder, workspace, None, 'this is randomized folder', '', True)  # nopep8
2313
+        # creation order test
2314
+        firstly_created = api.create(ContentType.Page, workspace, main_folder, 'creation_order_test', '', True)  # nopep8
2315
+        secondly_created = api.create(ContentType.Page, workspace, main_folder, 'another creation_order_test', '', True)  # nopep8
2316
+        # update order test
2317
+        firstly_created_but_recently_updated = api.create(ContentType.Page, workspace, main_folder, 'update_order_test', '', True)  # nopep8
2318
+        secondly_created_but_not_updated = api.create(ContentType.Page, workspace, main_folder, 'another update_order_test', '', True)  # nopep8
2319
+        with new_revision(
2320
+            session=self.session,
2321
+            tm=transaction.manager,
2322
+            content=firstly_created_but_recently_updated,
2323
+        ):
2324
+            firstly_created_but_recently_updated.description = 'Just an update'
2325
+        api.save(firstly_created_but_recently_updated)
2326
+        # comment change order
2327
+        firstly_created_but_recently_commented = api.create(ContentType.Page, workspace, main_folder, 'this is randomized label content', '', True)  # nopep8
2328
+        secondly_created_but_not_commented = api.create(ContentType.Page, workspace, main_folder, 'this is another randomized label content', '', True)  # nopep8
2329
+        comments = api.create_comment(workspace, firstly_created_but_recently_commented, 'juste a super comment', True)  # nopep8
2330
+
2331
+        last_actives = api.get_last_active(workspace=workspace2)
2332
+        assert len(last_actives) == 0
2333
+
1980
     def test_search_in_label(self):
2334
     def test_search_in_label(self):
1981
         # HACK - D.A. - 2015-03-09
2335
         # HACK - D.A. - 2015-03-09
1982
         # This test is based on a bug which does NOT return results found
2336
         # This test is based on a bug which does NOT return results found

+ 77 - 0
tracim/tests/library/test_role_api.py View File

1
+# coding=utf-8
2
+import pytest
3
+from sqlalchemy.orm.exc import NoResultFound
4
+
5
+from tracim.lib.core.userworkspace import RoleApi
6
+from tracim.models import User
7
+from tracim.models.roles import WorkspaceRoles
8
+from tracim.tests import DefaultTest
9
+from tracim.fixtures.users_and_groups import Base as BaseFixture
10
+from tracim.fixtures.content import Content as ContentFixture
11
+
12
+
13
+class TestRoleApi(DefaultTest):
14
+
15
+    fixtures = [BaseFixture, ContentFixture]
16
+
17
+    def test_unit__get_one__ok__nominal_case(self):
18
+        admin = self.session.query(User)\
19
+            .filter(User.email == 'admin@admin.admin').one()
20
+        rapi = RoleApi(
21
+            current_user=admin,
22
+            session=self.session,
23
+            config=self.config,
24
+        )
25
+        rapi.get_one(admin.user_id, 1)
26
+
27
+    def test_unit__get_one__err__role_does_not_exist(self):
28
+        admin = self.session.query(User)\
29
+            .filter(User.email == 'admin@admin.admin').one()
30
+        rapi = RoleApi(
31
+            current_user=admin,
32
+            session=self.session,
33
+            config=self.config,
34
+        )
35
+        with pytest.raises(NoResultFound):
36
+            rapi.get_one(admin.user_id, 100)  # workspace 100 does not exist
37
+
38
+    def test_unit__create_one__nominal_case(self):
39
+        admin = self.session.query(User)\
40
+            .filter(User.email == 'admin@admin.admin').one()
41
+        workspace = self._create_workspace_and_test(
42
+            'workspace_1',
43
+            admin
44
+        )
45
+        bob = self.session.query(User)\
46
+            .filter(User.email == 'bob@fsf.local').one()
47
+        rapi = RoleApi(
48
+            current_user=admin,
49
+            session=self.session,
50
+            config=self.config,
51
+        )
52
+        created_role = rapi.create_one(
53
+            user=bob,
54
+            workspace=workspace,
55
+            role_level=WorkspaceRoles.CONTENT_MANAGER.level,
56
+            with_notif=False,
57
+        )
58
+        obtain_role = rapi.get_one(bob.user_id, workspace.workspace_id)
59
+        assert created_role == obtain_role
60
+
61
+    def test_unit__get_all_for_usages(self):
62
+        admin = self.session.query(User)\
63
+            .filter(User.email == 'admin@admin.admin').one()
64
+        rapi = RoleApi(
65
+            current_user=admin,
66
+            session=self.session,
67
+            config=self.config,
68
+        )
69
+        workspace = self._create_workspace_and_test(
70
+            'workspace_1',
71
+            admin
72
+        )
73
+        roles = rapi.get_all_for_workspace(workspace)
74
+        len(roles) == 1
75
+        roles[0].user_id == admin.user_id
76
+        roles[0].role == WorkspaceRoles.WORKSPACE_MANAGER.level
77
+

+ 1 - 1
tracim/tests/library/test_user_api.py View File

23
         )
23
         )
24
         u = api.create_minimal_user('bob@bob')
24
         u = api.create_minimal_user('bob@bob')
25
         assert u.email == 'bob@bob'
25
         assert u.email == 'bob@bob'
26
-        assert u.display_name is None
26
+        assert u.display_name == 'bob'
27
 
27
 
28
     def test_unit__create_minimal_user_and_update__ok__nominal_case(self):
28
     def test_unit__create_minimal_user_and_update__ok__nominal_case(self):
29
         api = UserApi(
29
         api = UserApi(

+ 80 - 0
tracim/tests/models/tests_roles.py View File

1
+# coding=utf-8
2
+import unittest
3
+import pytest
4
+from tracim.exceptions import RoleDoesNotExist
5
+from tracim.models.roles import WorkspaceRoles
6
+
7
+
8
+class TestWorkspacesRoles(unittest.TestCase):
9
+    """
10
+    Test for WorkspaceRoles Enum Object
11
+    """
12
+    def test_workspace_roles__ok__all_list(self):
13
+        roles = list(WorkspaceRoles)
14
+        assert len(roles) == 5
15
+        for role in roles:
16
+            assert role
17
+            assert role.slug
18
+            assert isinstance(role.slug, str)
19
+            assert role.level or role.level == 0
20
+            assert isinstance(role.level, int)
21
+            assert role.label
22
+            assert isinstance(role.slug, str)
23
+        assert WorkspaceRoles['READER']
24
+        assert WorkspaceRoles['NOT_APPLICABLE']
25
+        assert WorkspaceRoles['CONTRIBUTOR']
26
+        assert WorkspaceRoles['WORKSPACE_MANAGER']
27
+        assert WorkspaceRoles['CONTENT_MANAGER']
28
+
29
+    def test__workspace_roles__ok__check_model(self):
30
+        role = WorkspaceRoles.WORKSPACE_MANAGER
31
+        assert role
32
+        assert role.slug
33
+        assert isinstance(role.slug, str)
34
+        assert role.level
35
+        assert isinstance(role.level, int)
36
+        assert role.label
37
+        assert isinstance(role.slug, str)
38
+
39
+    def test_workspace_roles__ok__get_all_valid_roles(self):
40
+        roles = WorkspaceRoles.get_all_valid_role()
41
+        assert len(roles) == 4
42
+        for role in roles:
43
+            assert role
44
+            assert role.slug
45
+            assert isinstance(role.slug, str)
46
+            assert role.level or role.level == 0
47
+            assert isinstance(role.level, int)
48
+            assert role.level > 0
49
+            assert role.label
50
+            assert isinstance(role.slug, str)
51
+
52
+    def test_workspace_roles__ok__get_role__from_level__ok__nominal_case(self):
53
+        role = WorkspaceRoles.get_role_from_level(0)
54
+
55
+        assert role
56
+        assert role.slug
57
+        assert isinstance(role.slug, str)
58
+        assert role.level == 0
59
+        assert isinstance(role.level, int)
60
+        assert role.label
61
+        assert isinstance(role.slug, str)
62
+
63
+    def test_workspace_roles__ok__get_role__from_slug__ok__nominal_case(self):
64
+        role = WorkspaceRoles.get_role_from_slug('reader')
65
+
66
+        assert role
67
+        assert role.slug
68
+        assert isinstance(role.slug, str)
69
+        assert role.level > 0
70
+        assert isinstance(role.level, int)
71
+        assert role.label
72
+        assert isinstance(role.slug, str)
73
+
74
+    def test_workspace_roles__ok__get_role__from_level__err__role_does_not_exist(self):  # nopep8
75
+        with pytest.raises(RoleDoesNotExist):
76
+            WorkspaceRoles.get_role_from_level(-1000)
77
+
78
+    def test_workspace_roles__ok__get_role__from_slug__err__role_does_not_exist(self):  # nopep8
79
+        with pytest.raises(RoleDoesNotExist):
80
+            WorkspaceRoles.get_role_from_slug('this slug does not exist')

+ 572 - 0
tracim/views/contents_api/file_controller.py View File

1
+# coding=utf-8
2
+import typing
3
+
4
+import transaction
5
+from depot.manager import DepotManager
6
+from preview_generator.exception import UnavailablePreviewType
7
+from pyramid.config import Configurator
8
+from pyramid.response import FileResponse, FileIter
9
+
10
+try:  # Python 3.5+
11
+    from http import HTTPStatus
12
+except ImportError:
13
+    from http import client as HTTPStatus
14
+
15
+from tracim import TracimRequest
16
+from tracim.extensions import hapic
17
+from tracim.lib.core.content import ContentApi
18
+from tracim.views.controllers import Controller
19
+from tracim.views.core_api.schemas import FileContentSchema
20
+from tracim.views.core_api.schemas import AllowedJpgPreviewDimSchema
21
+from tracim.views.core_api.schemas import ContentPreviewSizedPathSchema
22
+from tracim.views.core_api.schemas import RevisionPreviewSizedPathSchema
23
+from tracim.views.core_api.schemas import PageQuerySchema
24
+from tracim.views.core_api.schemas import WorkspaceAndContentRevisionIdPathSchema  # nopep8
25
+from tracim.views.core_api.schemas import FileRevisionSchema
26
+from tracim.views.core_api.schemas import SetContentStatusSchema
27
+from tracim.views.core_api.schemas import FileContentModifySchema
28
+from tracim.views.core_api.schemas import WorkspaceAndContentIdPathSchema
29
+from tracim.views.core_api.schemas import NoContentSchema
30
+from tracim.lib.utils.authorization import require_content_types
31
+from tracim.lib.utils.authorization import require_workspace_role
32
+from tracim.models.data import UserRoleInWorkspace
33
+from tracim.models.context_models import ContentInContext
34
+from tracim.models.context_models import RevisionInContext
35
+from tracim.models.contents import ContentTypeLegacy as ContentType
36
+from tracim.models.contents import file_type
37
+from tracim.models.revision_protection import new_revision
38
+from tracim.exceptions import EmptyLabelNotAllowed
39
+from tracim.exceptions import PageOfPreviewNotFound
40
+from tracim.exceptions import PreviewDimNotAllowed
41
+
42
+FILE_ENDPOINTS_TAG = 'Files'
43
+
44
+
45
+class FileController(Controller):
46
+    """
47
+    Endpoints for File Content
48
+    """
49
+
50
+    # File data
51
+    @hapic.with_api_doc(tags=[FILE_ENDPOINTS_TAG])
52
+    @require_workspace_role(UserRoleInWorkspace.CONTRIBUTOR)
53
+    @require_content_types([file_type])
54
+    @hapic.input_path(WorkspaceAndContentIdPathSchema())
55
+    # TODO - G.M - 2018-07-24 - Use hapic for input file
56
+    @hapic.output_body(NoContentSchema(), default_http_code=HTTPStatus.NO_CONTENT)  # nopep8
57
+    def upload_file(self, context, request: TracimRequest, hapic_data=None):
58
+        """
59
+        Upload a new version of raw file of content. This will create a new
60
+        revision.
61
+        """
62
+        app_config = request.registry.settings['CFG']
63
+        api = ContentApi(
64
+            current_user=request.current_user,
65
+            session=request.dbsession,
66
+            config=app_config,
67
+        )
68
+        content = api.get_one(
69
+            hapic_data.path.content_id,
70
+            content_type=ContentType.Any
71
+        )
72
+        file = request.POST['files']
73
+        with new_revision(
74
+                session=request.dbsession,
75
+                tm=transaction.manager,
76
+                content=content
77
+        ):
78
+            api.update_file_data(
79
+                content,
80
+                new_filename=file.filename,
81
+                new_mimetype=file.type,
82
+                new_content=file.file,
83
+            )
84
+
85
+        return
86
+
87
+    @hapic.with_api_doc(tags=[FILE_ENDPOINTS_TAG])
88
+    @require_workspace_role(UserRoleInWorkspace.READER)
89
+    @require_content_types([file_type])
90
+    @hapic.input_path(WorkspaceAndContentIdPathSchema())
91
+    @hapic.output_file([])
92
+    def download_file(self, context, request: TracimRequest, hapic_data=None):
93
+        """
94
+        Download raw file of last revision of content.
95
+        """
96
+        app_config = request.registry.settings['CFG']
97
+        api = ContentApi(
98
+            current_user=request.current_user,
99
+            session=request.dbsession,
100
+            config=app_config,
101
+        )
102
+        content = api.get_one(
103
+            hapic_data.path.content_id,
104
+            content_type=ContentType.Any
105
+        )
106
+        file = DepotManager.get().get(content.depot_file)
107
+        response = request.response
108
+        response.content_type = file.content_type
109
+        response.app_iter = FileIter(file)
110
+        return response
111
+
112
+    @hapic.with_api_doc(tags=[FILE_ENDPOINTS_TAG])
113
+    @require_workspace_role(UserRoleInWorkspace.READER)
114
+    @require_content_types([file_type])
115
+    @hapic.input_path(WorkspaceAndContentRevisionIdPathSchema())
116
+    @hapic.output_file([])
117
+    def download_revisions_file(self, context, request: TracimRequest, hapic_data=None):  # nopep8
118
+        """
119
+        Download raw file for specific revision of content.
120
+        """
121
+        app_config = request.registry.settings['CFG']
122
+        api = ContentApi(
123
+            current_user=request.current_user,
124
+            session=request.dbsession,
125
+            config=app_config,
126
+        )
127
+        content = api.get_one(
128
+            hapic_data.path.content_id,
129
+            content_type=ContentType.Any
130
+        )
131
+        revision = api.get_one_revision(
132
+            revision_id=hapic_data.path.revision_id,
133
+            content=content
134
+        )
135
+        file = DepotManager.get().get(revision.depot_file)
136
+        response = request.response
137
+        response.content_type = file.content_type
138
+        response.app_iter = FileIter(file)
139
+        return response
140
+
141
+    # preview
142
+    # pdf
143
+    @hapic.with_api_doc(tags=[FILE_ENDPOINTS_TAG])
144
+    @require_workspace_role(UserRoleInWorkspace.READER)
145
+    @require_content_types([file_type])
146
+    @hapic.handle_exception(UnavailablePreviewType, HTTPStatus.BAD_REQUEST)
147
+    @hapic.handle_exception(PageOfPreviewNotFound, HTTPStatus.BAD_REQUEST)
148
+    @hapic.input_query(PageQuerySchema())
149
+    @hapic.input_path(WorkspaceAndContentIdPathSchema())
150
+    @hapic.output_file([])
151
+    def preview_pdf(self, context, request: TracimRequest, hapic_data=None):
152
+        """
153
+        Obtain a specific page pdf preview of last revision of content.
154
+        """
155
+        app_config = request.registry.settings['CFG']
156
+        api = ContentApi(
157
+            current_user=request.current_user,
158
+            session=request.dbsession,
159
+            config=app_config,
160
+        )
161
+        content = api.get_one(
162
+            hapic_data.path.content_id,
163
+            content_type=ContentType.Any
164
+        )
165
+        pdf_preview_path = api.get_pdf_preview_path(
166
+            content.content_id,
167
+            content.revision_id,
168
+            page=hapic_data.query.page
169
+        )
170
+        return FileResponse(pdf_preview_path)
171
+
172
+    @hapic.with_api_doc(tags=[FILE_ENDPOINTS_TAG])
173
+    @require_workspace_role(UserRoleInWorkspace.READER)
174
+    @require_content_types([file_type])
175
+    @hapic.handle_exception(UnavailablePreviewType, HTTPStatus.BAD_REQUEST)
176
+    @hapic.input_path(WorkspaceAndContentIdPathSchema())
177
+    @hapic.output_file([])
178
+    def preview_pdf_full(self, context, request: TracimRequest, hapic_data=None):  # nopep8
179
+        """
180
+        Obtain a full pdf preview (all page) of last revision of content.
181
+        """
182
+        app_config = request.registry.settings['CFG']
183
+        api = ContentApi(
184
+            current_user=request.current_user,
185
+            session=request.dbsession,
186
+            config=app_config,
187
+        )
188
+        content = api.get_one(
189
+            hapic_data.path.content_id,
190
+            content_type=ContentType.Any
191
+        )
192
+        pdf_preview_path = api.get_full_pdf_preview_path(content.revision_id)
193
+        return FileResponse(pdf_preview_path)
194
+
195
+    @hapic.with_api_doc(tags=[FILE_ENDPOINTS_TAG])
196
+    @require_workspace_role(UserRoleInWorkspace.READER)
197
+    @require_content_types([file_type])
198
+    @hapic.handle_exception(UnavailablePreviewType, HTTPStatus.BAD_REQUEST)
199
+    @hapic.input_path(WorkspaceAndContentRevisionIdPathSchema())
200
+    @hapic.input_query(PageQuerySchema())
201
+    @hapic.output_file([])
202
+    def preview_pdf_revision(self, context, request: TracimRequest, hapic_data=None):  # nopep8
203
+        """
204
+        Obtain a specific page pdf preview of a specific revision of content.
205
+        """
206
+        app_config = request.registry.settings['CFG']
207
+        api = ContentApi(
208
+            current_user=request.current_user,
209
+            session=request.dbsession,
210
+            config=app_config,
211
+        )
212
+        content = api.get_one(
213
+            hapic_data.path.content_id,
214
+            content_type=ContentType.Any
215
+        )
216
+        revision = api.get_one_revision(
217
+            revision_id=hapic_data.path.revision_id,
218
+            content=content
219
+        )
220
+        pdf_preview_path = api.get_pdf_preview_path(
221
+            revision.content_id,
222
+            revision.revision_id,
223
+            page=hapic_data.query.page
224
+        )
225
+        return FileResponse(pdf_preview_path)
226
+
227
+    # jpg
228
+    @hapic.with_api_doc(tags=[FILE_ENDPOINTS_TAG])
229
+    @require_workspace_role(UserRoleInWorkspace.READER)
230
+    @require_content_types([file_type])
231
+    @hapic.handle_exception(PageOfPreviewNotFound, HTTPStatus.BAD_REQUEST)
232
+    @hapic.input_path(WorkspaceAndContentIdPathSchema())
233
+    @hapic.input_query(PageQuerySchema())
234
+    @hapic.output_file([])
235
+    def preview_jpg(self, context, request: TracimRequest, hapic_data=None):
236
+        """
237
+        Obtain normally sied jpg preview of last revision of content.
238
+        """
239
+        app_config = request.registry.settings['CFG']
240
+        api = ContentApi(
241
+            current_user=request.current_user,
242
+            session=request.dbsession,
243
+            config=app_config,
244
+        )
245
+        content = api.get_one(
246
+            hapic_data.path.content_id,
247
+            content_type=ContentType.Any
248
+        )
249
+        allowed_dim = api.get_jpg_preview_allowed_dim()
250
+        jpg_preview_path = api.get_jpg_preview_path(
251
+            content_id=content.content_id,
252
+            revision_id=content.revision_id,
253
+            page=hapic_data.query.page,
254
+            width=allowed_dim.dimensions[0].width,
255
+            height=allowed_dim.dimensions[0].height,
256
+        )
257
+        return FileResponse(jpg_preview_path)
258
+
259
+    @hapic.with_api_doc(tags=[FILE_ENDPOINTS_TAG])
260
+    @require_workspace_role(UserRoleInWorkspace.READER)
261
+    @require_content_types([file_type])
262
+    @hapic.handle_exception(PageOfPreviewNotFound, HTTPStatus.BAD_REQUEST)
263
+    @hapic.handle_exception(PreviewDimNotAllowed, HTTPStatus.BAD_REQUEST)
264
+    @hapic.input_query(PageQuerySchema())
265
+    @hapic.input_path(ContentPreviewSizedPathSchema())
266
+    @hapic.output_file([])
267
+    def sized_preview_jpg(self, context, request: TracimRequest, hapic_data=None):  # nopep8
268
+        """
269
+        Obtain resized jpg preview of last revision of content.
270
+        """
271
+        app_config = request.registry.settings['CFG']
272
+        api = ContentApi(
273
+            current_user=request.current_user,
274
+            session=request.dbsession,
275
+            config=app_config,
276
+        )
277
+        content = api.get_one(
278
+            hapic_data.path.content_id,
279
+            content_type=ContentType.Any
280
+        )
281
+        jpg_preview_path = api.get_jpg_preview_path(
282
+            content_id=content.content_id,
283
+            revision_id=content.revision_id,
284
+            page=hapic_data.query.page,
285
+            height=hapic_data.path.height,
286
+            width=hapic_data.path.width,
287
+        )
288
+        return FileResponse(jpg_preview_path)
289
+
290
+    @hapic.with_api_doc(tags=[FILE_ENDPOINTS_TAG])
291
+    @require_workspace_role(UserRoleInWorkspace.READER)
292
+    @require_content_types([file_type])
293
+    @hapic.handle_exception(PageOfPreviewNotFound, HTTPStatus.BAD_REQUEST)
294
+    @hapic.handle_exception(PreviewDimNotAllowed, HTTPStatus.BAD_REQUEST)
295
+    @hapic.input_path(RevisionPreviewSizedPathSchema())
296
+    @hapic.input_query(PageQuerySchema())
297
+    @hapic.output_file([])
298
+    def sized_preview_jpg_revision(self, context, request: TracimRequest, hapic_data=None):  # nopep8
299
+        """
300
+        Obtain resized jpg preview of a specific revision of content.
301
+        """
302
+        app_config = request.registry.settings['CFG']
303
+        api = ContentApi(
304
+            current_user=request.current_user,
305
+            session=request.dbsession,
306
+            config=app_config,
307
+        )
308
+        content = api.get_one(
309
+            hapic_data.path.content_id,
310
+            content_type=ContentType.Any
311
+        )
312
+        revision = api.get_one_revision(
313
+            revision_id=hapic_data.path.revision_id,
314
+            content=content
315
+        )
316
+        jpg_preview_path = api.get_jpg_preview_path(
317
+            content_id=content.content_id,
318
+            revision_id=revision.revision_id,
319
+            page=hapic_data.query.page,
320
+            height=hapic_data.path.height,
321
+            width=hapic_data.path.width,
322
+        )
323
+        return FileResponse(jpg_preview_path)
324
+
325
+    @hapic.with_api_doc(tags=[FILE_ENDPOINTS_TAG])
326
+    @require_workspace_role(UserRoleInWorkspace.READER)
327
+    @require_content_types([file_type])
328
+    @hapic.input_path(WorkspaceAndContentIdPathSchema())
329
+    @hapic.output_body(AllowedJpgPreviewDimSchema())
330
+    def allowed_dim_preview_jpg(self, context, request: TracimRequest, hapic_data=None):  # nopep8
331
+        """
332
+        Get allowed dimensions of jpg preview. If restricted is true,
333
+        only those dimensions are strictly accepted.
334
+        """
335
+        app_config = request.registry.settings['CFG']
336
+        api = ContentApi(
337
+            current_user=request.current_user,
338
+            session=request.dbsession,
339
+            config=app_config,
340
+        )
341
+        return api.get_jpg_preview_allowed_dim()
342
+
343
+    # File infos
344
+    @hapic.with_api_doc(tags=[FILE_ENDPOINTS_TAG])
345
+    @require_workspace_role(UserRoleInWorkspace.READER)
346
+    @require_content_types([file_type])
347
+    @hapic.input_path(WorkspaceAndContentIdPathSchema())
348
+    @hapic.output_body(FileContentSchema())
349
+    def get_file_infos(self, context, request: TracimRequest, hapic_data=None) -> ContentInContext:  # nopep8
350
+        """
351
+        Get thread content
352
+        """
353
+        app_config = request.registry.settings['CFG']
354
+        api = ContentApi(
355
+            current_user=request.current_user,
356
+            session=request.dbsession,
357
+            config=app_config,
358
+        )
359
+        content = api.get_one(
360
+            hapic_data.path.content_id,
361
+            content_type=ContentType.Any
362
+        )
363
+        return api.get_content_in_context(content)
364
+
365
+    @hapic.with_api_doc(tags=[FILE_ENDPOINTS_TAG])
366
+    @hapic.handle_exception(EmptyLabelNotAllowed, HTTPStatus.BAD_REQUEST)
367
+    @require_workspace_role(UserRoleInWorkspace.CONTRIBUTOR)
368
+    @require_content_types([file_type])
369
+    @hapic.input_path(WorkspaceAndContentIdPathSchema())
370
+    @hapic.input_body(FileContentModifySchema())
371
+    @hapic.output_body(FileContentSchema())
372
+    def update_file_info(self, context, request: TracimRequest, hapic_data=None) -> ContentInContext:  # nopep8
373
+        """
374
+        update thread
375
+        """
376
+        app_config = request.registry.settings['CFG']
377
+        api = ContentApi(
378
+            current_user=request.current_user,
379
+            session=request.dbsession,
380
+            config=app_config,
381
+        )
382
+        content = api.get_one(
383
+            hapic_data.path.content_id,
384
+            content_type=ContentType.Any
385
+        )
386
+        with new_revision(
387
+                session=request.dbsession,
388
+                tm=transaction.manager,
389
+                content=content
390
+        ):
391
+            api.update_content(
392
+                item=content,
393
+                new_label=hapic_data.body.label,
394
+                new_content=hapic_data.body.raw_content,
395
+
396
+            )
397
+            api.save(content)
398
+        return api.get_content_in_context(content)
399
+
400
+    @hapic.with_api_doc(tags=[FILE_ENDPOINTS_TAG])
401
+    @require_workspace_role(UserRoleInWorkspace.READER)
402
+    @require_content_types([file_type])
403
+    @hapic.input_path(WorkspaceAndContentIdPathSchema())
404
+    @hapic.output_body(FileRevisionSchema(many=True))
405
+    def get_file_revisions(
406
+            self,
407
+            context,
408
+            request: TracimRequest,
409
+            hapic_data=None
410
+    ) -> typing.List[RevisionInContext]:
411
+        """
412
+        get file revisions
413
+        """
414
+        app_config = request.registry.settings['CFG']
415
+        api = ContentApi(
416
+            current_user=request.current_user,
417
+            session=request.dbsession,
418
+            config=app_config,
419
+        )
420
+        content = api.get_one(
421
+            hapic_data.path.content_id,
422
+            content_type=ContentType.Any
423
+        )
424
+        revisions = content.revisions
425
+        return [
426
+            api.get_revision_in_context(revision)
427
+            for revision in revisions
428
+        ]
429
+
430
+    @hapic.with_api_doc(tags=[FILE_ENDPOINTS_TAG])
431
+    @hapic.handle_exception(EmptyLabelNotAllowed, HTTPStatus.BAD_REQUEST)
432
+    @require_workspace_role(UserRoleInWorkspace.CONTRIBUTOR)
433
+    @require_content_types([file_type])
434
+    @hapic.input_path(WorkspaceAndContentIdPathSchema())
435
+    @hapic.input_body(SetContentStatusSchema())
436
+    @hapic.output_body(NoContentSchema(), default_http_code=HTTPStatus.NO_CONTENT)  # nopep8
437
+    def set_file_status(self, context, request: TracimRequest, hapic_data=None) -> None:  # nopep8
438
+        """
439
+        set file status
440
+        """
441
+        app_config = request.registry.settings['CFG']
442
+        api = ContentApi(
443
+            current_user=request.current_user,
444
+            session=request.dbsession,
445
+            config=app_config,
446
+        )
447
+        content = api.get_one(
448
+            hapic_data.path.content_id,
449
+            content_type=ContentType.Any
450
+        )
451
+        with new_revision(
452
+                session=request.dbsession,
453
+                tm=transaction.manager,
454
+                content=content
455
+        ):
456
+            api.set_status(
457
+                content,
458
+                hapic_data.body.status,
459
+            )
460
+            api.save(content)
461
+        return
462
+
463
+    def bind(self, configurator: Configurator) -> None:
464
+        """
465
+        Add route to configurator.
466
+        """
467
+
468
+        # file info #
469
+        # Get file info
470
+        configurator.add_route(
471
+            'file_info',
472
+            '/workspaces/{workspace_id}/files/{content_id}',
473
+            request_method='GET'
474
+        )
475
+        configurator.add_view(self.get_file_infos, route_name='file_info')  # nopep8
476
+        # update file
477
+        configurator.add_route(
478
+            'update_file_info',
479
+            '/workspaces/{workspace_id}/files/{content_id}',
480
+            request_method='PUT'
481
+        )  # nopep8
482
+        configurator.add_view(self.update_file_info, route_name='update_file_info')  # nopep8
483
+
484
+        # raw file #
485
+        # upload raw file
486
+        configurator.add_route(
487
+            'upload_file',
488
+            '/workspaces/{workspace_id}/files/{content_id}/raw',  # nopep8
489
+            request_method='PUT'
490
+        )
491
+        configurator.add_view(self.upload_file, route_name='upload_file')  # nopep8
492
+        # download raw file
493
+        configurator.add_route(
494
+            'download_file',
495
+            '/workspaces/{workspace_id}/files/{content_id}/raw',  # nopep8
496
+            request_method='GET'
497
+        )
498
+        configurator.add_view(self.download_file, route_name='download_file')  # nopep8
499
+        # download raw file of revision
500
+        configurator.add_route(
501
+            'download_revision',
502
+            '/workspaces/{workspace_id}/files/{content_id}/revisions/{revision_id}/raw',  # nopep8
503
+            request_method='GET'
504
+        )
505
+        configurator.add_view(self.download_revisions_file, route_name='download_revision')  # nopep8
506
+
507
+        # previews #
508
+        # get preview pdf full
509
+        configurator.add_route(
510
+            'preview_pdf_full',
511
+            '/workspaces/{workspace_id}/files/{content_id}/preview/pdf/full',  # nopep8
512
+            request_method='GET'
513
+        )
514
+        configurator.add_view(self.preview_pdf_full, route_name='preview_pdf_full')  # nopep8
515
+        # get preview pdf
516
+        configurator.add_route(
517
+            'preview_pdf',
518
+            '/workspaces/{workspace_id}/files/{content_id}/preview/pdf',  # nopep8
519
+            request_method='GET'
520
+        )
521
+        configurator.add_view(self.preview_pdf, route_name='preview_pdf')  # nopep8
522
+        # get preview jpg allowed dims
523
+        configurator.add_route(
524
+            'allowed_dim_preview_jpg',
525
+            '/workspaces/{workspace_id}/files/{content_id}/preview/jpg/allowed_dims',  # nopep8
526
+            request_method='GET'
527
+        )
528
+        configurator.add_view(self.allowed_dim_preview_jpg, route_name='allowed_dim_preview_jpg')  # nopep8
529
+        # get preview jpg
530
+        configurator.add_route(
531
+            'preview_jpg',
532
+            '/workspaces/{workspace_id}/files/{content_id}/preview/jpg',  # nopep8
533
+            request_method='GET'
534
+        )
535
+        configurator.add_view(self.preview_jpg, route_name='preview_jpg')  # nopep8
536
+        # get preview jpg with size
537
+        configurator.add_route(
538
+            'sized_preview_jpg',
539
+            '/workspaces/{workspace_id}/files/{content_id}/preview/jpg/{width}x{height}',  # nopep8
540
+            request_method='GET'
541
+        )
542
+        configurator.add_view(self.sized_preview_jpg, route_name='sized_preview_jpg')  # nopep8
543
+        # get jpg preview for revision
544
+        configurator.add_route(
545
+            'sized_preview_jpg_revision',
546
+            '/workspaces/{workspace_id}/files/{content_id}/revisions/{revision_id}/preview/jpg/{width}x{height}',  # nopep8
547
+            request_method='GET'
548
+        )
549
+        configurator.add_view(self.sized_preview_jpg_revision, route_name='sized_preview_jpg_revision')  # nopep8
550
+        # get jpg preview for revision
551
+        configurator.add_route(
552
+            'preview_pdf_revision',
553
+            '/workspaces/{workspace_id}/files/{content_id}/revisions/{revision_id}/preview/pdf',  # nopep8
554
+            request_method='GET'
555
+        )
556
+        configurator.add_view(self.preview_pdf_revision, route_name='preview_pdf_revision')  # nopep8
557
+        # others #
558
+        # get file revisions
559
+        configurator.add_route(
560
+            'file_revisions',
561
+            '/workspaces/{workspace_id}/files/{content_id}/revisions',  # nopep8
562
+            request_method='GET'
563
+        )
564
+        configurator.add_view(self.get_file_revisions, route_name='file_revisions')  # nopep8
565
+
566
+        # get file status
567
+        configurator.add_route(
568
+            'set_file_status',
569
+            '/workspaces/{workspace_id}/files/{content_id}/status',  # nopep8
570
+            request_method='PUT'
571
+        )
572
+        configurator.add_view(self.set_file_status, route_name='set_file_status')  # nopep8

+ 229 - 1
tracim/views/core_api/schemas.py View File

10
 from tracim.models.contents import open_status
10
 from tracim.models.contents import open_status
11
 from tracim.models.contents import ContentTypeLegacy as ContentType
11
 from tracim.models.contents import ContentTypeLegacy as ContentType
12
 from tracim.models.contents import ContentStatusLegacy as ContentStatus
12
 from tracim.models.contents import ContentStatusLegacy as ContentStatus
13
+from tracim.models.context_models import ActiveContentFilter
14
+from tracim.models.context_models import ContentIdsQuery
15
+from tracim.models.context_models import UserWorkspaceAndContentPath
13
 from tracim.models.context_models import ContentCreation
16
 from tracim.models.context_models import ContentCreation
14
 from tracim.models.context_models import UserCreation
17
 from tracim.models.context_models import UserCreation
15
 from tracim.models.context_models import SetEmail
18
 from tracim.models.context_models import SetEmail
16
 from tracim.models.context_models import SetPassword
19
 from tracim.models.context_models import SetPassword
17
 from tracim.models.context_models import UserInfos
20
 from tracim.models.context_models import UserInfos
18
 from tracim.models.context_models import UserProfile
21
 from tracim.models.context_models import UserProfile
22
+from tracim.models.context_models import ContentPreviewSizedPath
23
+from tracim.models.context_models import RevisionPreviewSizedPath
24
+from tracim.models.context_models import PageQuery
25
+from tracim.models.context_models import WorkspaceAndContentRevisionPath
26
+from tracim.models.context_models import WorkspaceMemberInvitation
27
+from tracim.models.context_models import WorkspaceUpdate
28
+from tracim.models.context_models import RoleUpdate
19
 from tracim.models.context_models import CommentCreation
29
 from tracim.models.context_models import CommentCreation
20
 from tracim.models.context_models import TextBasedContentUpdate
30
 from tracim.models.context_models import TextBasedContentUpdate
21
 from tracim.models.context_models import SetContentStatus
31
 from tracim.models.context_models import SetContentStatus
22
 from tracim.models.context_models import CommentPath
32
 from tracim.models.context_models import CommentPath
23
 from tracim.models.context_models import MoveParams
33
 from tracim.models.context_models import MoveParams
24
 from tracim.models.context_models import WorkspaceAndContentPath
34
 from tracim.models.context_models import WorkspaceAndContentPath
35
+from tracim.models.context_models import WorkspaceAndUserPath
25
 from tracim.models.context_models import ContentFilter
36
 from tracim.models.context_models import ContentFilter
26
 from tracim.models.context_models import LoginCredentials
37
 from tracim.models.context_models import LoginCredentials
27
 from tracim.models.data import UserRoleInWorkspace
38
 from tracim.models.data import UserRoleInWorkspace
39
+from tracim.models.data import ActionDescription
28
 
40
 
29
 
41
 
30
 class UserDigestSchema(marshmallow.Schema):
42
 class UserDigestSchema(marshmallow.Schema):
180
     )
192
     )
181
 
193
 
182
 
194
 
195
+class RevisionIdPathSchema(marshmallow.Schema):
196
+    revision_id = marshmallow.fields.Int(example=6, required=True)
197
+
198
+
199
+class WorkspaceAndUserIdPathSchema(
200
+    UserIdPathSchema,
201
+    WorkspaceIdPathSchema
202
+):
203
+    @post_load
204
+    def make_path_object(self, data):
205
+        return WorkspaceAndUserPath(**data)
206
+
207
+
183
 class WorkspaceAndContentIdPathSchema(
208
 class WorkspaceAndContentIdPathSchema(
184
     WorkspaceIdPathSchema,
209
     WorkspaceIdPathSchema,
185
     ContentIdPathSchema
210
     ContentIdPathSchema
189
         return WorkspaceAndContentPath(**data)
214
         return WorkspaceAndContentPath(**data)
190
 
215
 
191
 
216
 
217
+class WidthAndHeightPathSchema(marshmallow.Schema):
218
+    width = marshmallow.fields.Int(example=256)
219
+    height = marshmallow.fields.Int(example=256)
220
+
221
+
222
+class AllowedJpgPreviewSizesSchema(marshmallow.Schema):
223
+    width = marshmallow.fields.Int(example=256)
224
+    height = marshmallow.fields.Int(example=256)
225
+
226
+
227
+class AllowedJpgPreviewDimSchema(marshmallow.Schema):
228
+    restricted = marshmallow.fields.Bool()
229
+    dimensions = marshmallow.fields.Nested(
230
+        AllowedJpgPreviewSizesSchema,
231
+        many=True
232
+    )
233
+
234
+
235
+class WorkspaceAndContentRevisionIdPathSchema(
236
+    WorkspaceIdPathSchema,
237
+    ContentIdPathSchema,
238
+    RevisionIdPathSchema,
239
+):
240
+    @post_load
241
+    def make_path_object(self, data):
242
+        return WorkspaceAndContentRevisionPath(**data)
243
+
244
+
245
+class ContentPreviewSizedPathSchema(
246
+    WorkspaceAndContentIdPathSchema,
247
+    WidthAndHeightPathSchema
248
+):
249
+    @post_load
250
+    def make_path_object(self, data):
251
+        return ContentPreviewSizedPath(**data)
252
+
253
+
254
+class RevisionPreviewSizedPathSchema(
255
+    WorkspaceAndContentRevisionIdPathSchema,
256
+    WidthAndHeightPathSchema
257
+):
258
+    @post_load
259
+    def make_path_object(self, data):
260
+        return RevisionPreviewSizedPath(**data)
261
+
262
+
263
+class UserWorkspaceAndContentIdPathSchema(
264
+    UserIdPathSchema,
265
+    WorkspaceIdPathSchema,
266
+    ContentIdPathSchema,
267
+):
268
+    @post_load
269
+    def make_path_object(self, data):
270
+        return UserWorkspaceAndContentPath(**data)
271
+
272
+
273
+class UserWorkspaceIdPathSchema(
274
+    UserIdPathSchema,
275
+    WorkspaceIdPathSchema,
276
+):
277
+    @post_load
278
+    def make_path_object(self, data):
279
+        return WorkspaceAndUserPath(**data)
280
+
281
+
192
 class CommentsPathSchema(WorkspaceAndContentIdPathSchema):
282
 class CommentsPathSchema(WorkspaceAndContentIdPathSchema):
193
     comment_id = marshmallow.fields.Int(
283
     comment_id = marshmallow.fields.Int(
194
         example=6,
284
         example=6,
202
         return CommentPath(**data)
292
         return CommentPath(**data)
203
 
293
 
204
 
294
 
295
+class PageQuerySchema(marshmallow.Schema):
296
+    page = marshmallow.fields.Int(
297
+        example=2,
298
+        default=0,
299
+        description='allow to show a specific page of a pdf file',
300
+        validate=Range(min=0, error="Value must be positive or 0"),
301
+    )
302
+
303
+    @post_load
304
+    def make_page_query(self, data):
305
+        return PageQuery(**data)
306
+
307
+
205
 class FilterContentQuerySchema(marshmallow.Schema):
308
 class FilterContentQuerySchema(marshmallow.Schema):
206
     parent_id = marshmallow.fields.Int(
309
     parent_id = marshmallow.fields.Int(
207
         example=2,
310
         example=2,
238
                     'to allow to show only archived documents',
341
                     'to allow to show only archived documents',
239
         validate=Range(min=0, max=1, error="Value must be 0 or 1"),
342
         validate=Range(min=0, max=1, error="Value must be 0 or 1"),
240
     )
343
     )
344
+    content_type = marshmallow.fields.String(
345
+        example=ContentType.Any,
346
+        default=ContentType.Any,
347
+        validate=OneOf(ContentType.allowed_type_values())
348
+    )
241
 
349
 
242
     @post_load
350
     @post_load
243
     def make_content_filter(self, data):
351
     def make_content_filter(self, data):
244
         return ContentFilter(**data)
352
         return ContentFilter(**data)
245
 
353
 
354
+
355
+class ActiveContentFilterQuerySchema(marshmallow.Schema):
356
+    limit = marshmallow.fields.Int(
357
+        example=2,
358
+        default=0,
359
+        description='if 0 or not set, return all elements, else return only '
360
+                    'the first limit elem (according to offset)',
361
+        validate=Range(min=0, error="Value must be positive or 0"),
362
+    )
363
+    before_datetime = marshmallow.fields.DateTime(
364
+        format=DATETIME_FORMAT,
365
+        description='return only content lastly updated before this date',
366
+    )
367
+    @post_load
368
+    def make_content_filter(self, data):
369
+        return ActiveContentFilter(**data)
370
+
371
+
372
+class ContentIdsQuerySchema(marshmallow.Schema):
373
+    contents_ids = marshmallow.fields.List(
374
+        marshmallow.fields.Int(
375
+            example=6,
376
+            validate=Range(min=1, error="Value must be greater than 0"),
377
+        )
378
+    )
379
+    @post_load
380
+    def make_contents_ids(self, data):
381
+        return ContentIdsQuery(**data)
382
+
383
+
246
 ###
384
 ###
247
 
385
 
248
 
386
 
387
+class RoleUpdateSchema(marshmallow.Schema):
388
+    role = marshmallow.fields.String(
389
+        example='contributor',
390
+        validate=OneOf(UserRoleInWorkspace.get_all_role_slug())
391
+    )
392
+
393
+    @post_load
394
+    def make_role(self, data):
395
+        return RoleUpdate(**data)
396
+
397
+
398
+class WorkspaceMemberInviteSchema(RoleUpdateSchema):
399
+    user_id = marshmallow.fields.Int(
400
+        example=5,
401
+        default=None,
402
+        allow_none=True,
403
+    )
404
+    user_email_or_public_name = marshmallow.fields.String(
405
+        example='suri@cate.fr',
406
+        default=None,
407
+        allow_none=True,
408
+    )
409
+
410
+    @post_load
411
+    def make_role(self, data):
412
+        return WorkspaceMemberInvitation(**data)
413
+
414
+
249
 class BasicAuthSchema(marshmallow.Schema):
415
 class BasicAuthSchema(marshmallow.Schema):
250
 
416
 
251
     email = marshmallow.fields.Email(
417
     email = marshmallow.fields.Email(
270
     expire_after = marshmallow.fields.String()
436
     expire_after = marshmallow.fields.String()
271
 
437
 
272
 
438
 
439
+class WorkspaceModifySchema(marshmallow.Schema):
440
+    label = marshmallow.fields.String(
441
+        example='My Workspace',
442
+    )
443
+    description = marshmallow.fields.String(
444
+        example='A super description of my workspace.',
445
+    )
446
+
447
+    @post_load
448
+    def make_workspace_modifications(self, data):
449
+        return WorkspaceUpdate(**data)
450
+
451
+
452
+class WorkspaceCreationSchema(WorkspaceModifySchema):
453
+    pass
454
+
455
+
273
 class NoContentSchema(marshmallow.Schema):
456
 class NoContentSchema(marshmallow.Schema):
274
 
457
 
275
     class Meta:
458
     class Meta:
337
         validate=Range(min=1, error="Value must be greater than 0"),
520
         validate=Range(min=1, error="Value must be greater than 0"),
338
     )
521
     )
339
     user = marshmallow.fields.Nested(
522
     user = marshmallow.fields.Nested(
340
-        UserSchema(only=('public_name', 'avatar_url'))
523
+        UserDigestSchema()
341
     )
524
     )
525
+    workspace = marshmallow.fields.Nested(
526
+        WorkspaceDigestSchema(exclude=('sidebar_entries',))
527
+    )
528
+    is_active = marshmallow.fields.Bool()
342
 
529
 
343
     class Meta:
530
     class Meta:
344
         description = 'Workspace Member information'
531
         description = 'Workspace Member information'
345
 
532
 
346
 
533
 
534
+class WorkspaceMemberCreationSchema(WorkspaceMemberSchema):
535
+    newly_created = marshmallow.fields.Bool(
536
+        exemple=False,
537
+        description='Is the user completely new '
538
+                    '(and account was just created) or not ?',
539
+    )
540
+    email_sent = marshmallow.fields.Bool(
541
+        exemple=False,
542
+        description='Has an email been sent to user to inform him about '
543
+                    'this new workspace registration and eventually his account'
544
+                    'creation'
545
+    )
546
+
547
+
347
 class ApplicationConfigSchema(marshmallow.Schema):
548
 class ApplicationConfigSchema(marshmallow.Schema):
348
     pass
549
     pass
349
     #  TODO - G.M - 24-05-2018 - Set this
550
     #  TODO - G.M - 24-05-2018 - Set this
505
     )
706
     )
506
 
707
 
507
 
708
 
709
+class ReadStatusSchema(marshmallow.Schema):
710
+    content_id = marshmallow.fields.Int(
711
+        example=6,
712
+        validate=Range(min=1, error="Value must be greater than 0"),
713
+    )
714
+    read_by_user = marshmallow.fields.Bool(example=False, default=False)
508
 #####
715
 #####
509
 # Content
716
 # Content
510
 #####
717
 #####
529
     )
736
     )
530
 
737
 
531
 
738
 
739
+class FileInfoAbstractSchema(marshmallow.Schema):
740
+    raw_content = marshmallow.fields.String(
741
+        description='raw text or html description of the file'
742
+    )
743
+
744
+
532
 class TextBasedContentSchema(ContentSchema, TextBasedDataAbstractSchema):
745
 class TextBasedContentSchema(ContentSchema, TextBasedDataAbstractSchema):
533
     pass
746
     pass
534
 
747
 
535
 
748
 
749
+class FileContentSchema(ContentSchema, FileInfoAbstractSchema):
750
+    pass
751
+
536
 #####
752
 #####
537
 # Revision
753
 # Revision
538
 #####
754
 #####
549
         example=12,
765
         example=12,
550
         validate=Range(min=1, error="Value must be greater than 0"),
766
         validate=Range(min=1, error="Value must be greater than 0"),
551
     )
767
     )
768
+    revision_type = marshmallow.fields.String(
769
+        example=ActionDescription.CREATION,
770
+        validate=OneOf(ActionDescription.allowed_values()),
771
+    )
552
     created = marshmallow.fields.DateTime(
772
     created = marshmallow.fields.DateTime(
553
         format=DATETIME_FORMAT,
773
         format=DATETIME_FORMAT,
554
         description='Content creation date',
774
         description='Content creation date',
560
     pass
780
     pass
561
 
781
 
562
 
782
 
783
+class FileRevisionSchema(RevisionSchema, FileInfoAbstractSchema):
784
+    pass
785
+
786
+
563
 class CommentSchema(marshmallow.Schema):
787
 class CommentSchema(marshmallow.Schema):
564
     content_id = marshmallow.fields.Int(
788
     content_id = marshmallow.fields.Int(
565
         example=6,
789
         example=6,
604
         return TextBasedContentUpdate(**data)
828
         return TextBasedContentUpdate(**data)
605
 
829
 
606
 
830
 
831
+class FileContentModifySchema(TextBasedContentModifySchema):
832
+    pass
833
+
834
+
607
 class SetContentStatusSchema(marshmallow.Schema):
835
 class SetContentStatusSchema(marshmallow.Schema):
608
     status = marshmallow.fields.Str(
836
     status = marshmallow.fields.Str(
609
         example='closed-deprecated',
837
         example='closed-deprecated',

+ 155 - 13
tracim/views/core_api/user_controller.py View File

1
-import transaction
2
 from pyramid.config import Configurator
1
 from pyramid.config import Configurator
3
-
4
 try:  # Python 3.5+
2
 try:  # Python 3.5+
5
     from http import HTTPStatus
3
     from http import HTTPStatus
6
 except ImportError:
4
 except ImportError:
7
     from http import client as HTTPStatus
5
     from http import client as HTTPStatus
8
 
6
 
9
-from tracim import hapic, TracimRequest
10
-
11
-from tracim.exceptions import WrongUserPassword, PasswordDoNotMatch
12
-from tracim.lib.core.group import GroupApi
13
-from tracim.lib.utils.authorization import require_same_user_or_profile
14
-from tracim.lib.utils.authorization import require_profile
7
+from tracim import hapic
8
+from tracim import TracimRequest
15
 from tracim.models import Group
9
 from tracim.models import Group
16
-from tracim.models.context_models import WorkspaceInContext
10
+from tracim.lib.core.group import GroupApi
17
 from tracim.lib.core.user import UserApi
11
 from tracim.lib.core.user import UserApi
18
 from tracim.lib.core.workspace import WorkspaceApi
12
 from tracim.lib.core.workspace import WorkspaceApi
13
+from tracim.lib.core.content import ContentApi
19
 from tracim.views.controllers import Controller
14
 from tracim.views.controllers import Controller
20
-from tracim.views.core_api.schemas import UserIdPathSchema
15
+from tracim.lib.utils.authorization import require_same_user_or_profile
16
+from tracim.lib.utils.authorization import require_profile
17
+from tracim.exceptions import WrongUserPassword
18
+from tracim.exceptions import PasswordDoNotMatch
21
 from tracim.views.core_api.schemas import UserSchema
19
 from tracim.views.core_api.schemas import UserSchema
22
 from tracim.views.core_api.schemas import SetEmailSchema
20
 from tracim.views.core_api.schemas import SetEmailSchema
23
 from tracim.views.core_api.schemas import SetPasswordSchema
21
 from tracim.views.core_api.schemas import SetPasswordSchema
24
 from tracim.views.core_api.schemas import UserInfosSchema
22
 from tracim.views.core_api.schemas import UserInfosSchema
25
-from tracim.views.core_api.schemas import NoContentSchema
26
 from tracim.views.core_api.schemas import UserCreationSchema
23
 from tracim.views.core_api.schemas import UserCreationSchema
27
 from tracim.views.core_api.schemas import UserProfileSchema
24
 from tracim.views.core_api.schemas import UserProfileSchema
25
+from tracim.views.core_api.schemas import UserIdPathSchema
26
+from tracim.views.core_api.schemas import ReadStatusSchema
27
+from tracim.views.core_api.schemas import ContentIdsQuerySchema
28
+from tracim.views.core_api.schemas import NoContentSchema
29
+from tracim.views.core_api.schemas import UserWorkspaceIdPathSchema
30
+from tracim.views.core_api.schemas import UserWorkspaceAndContentIdPathSchema
31
+from tracim.views.core_api.schemas import ContentDigestSchema
32
+from tracim.views.core_api.schemas import ActiveContentFilterQuerySchema
28
 from tracim.views.core_api.schemas import WorkspaceDigestSchema
33
 from tracim.views.core_api.schemas import WorkspaceDigestSchema
29
 
34
 
30
 USER_ENDPOINTS_TAG = 'Users'
35
 USER_ENDPOINTS_TAG = 'Users'
42
         """
47
         """
43
         app_config = request.registry.settings['CFG']
48
         app_config = request.registry.settings['CFG']
44
         wapi = WorkspaceApi(
49
         wapi = WorkspaceApi(
45
-            current_user=request.current_user,  # User
50
+            current_user=request.candidate_user,  # User
46
             session=request.dbsession,
51
             session=request.dbsession,
47
             config=app_config,
52
             config=app_config,
48
         )
53
         )
49
         
54
         
50
         workspaces = wapi.get_all_for_user(request.candidate_user)
55
         workspaces = wapi.get_all_for_user(request.candidate_user)
51
         return [
56
         return [
52
-            WorkspaceInContext(workspace, request.dbsession, app_config)
57
+            wapi.get_workspace_with_context(workspace)
53
             for workspace in workspaces
58
             for workspace in workspaces
54
         ]
59
         ]
55
 
60
 
236
         )
241
         )
237
         return
242
         return
238
 
243
 
244
+    @hapic.with_api_doc(tags=[USER_ENDPOINTS_TAG])
245
+    @require_same_user_or_profile(Group.TIM_ADMIN)
246
+    @hapic.input_path(UserWorkspaceIdPathSchema())
247
+    @hapic.input_query(ActiveContentFilterQuerySchema())
248
+    @hapic.output_body(ContentDigestSchema(many=True))
249
+    def last_active_content(self, context, request: TracimRequest, hapic_data=None):  # nopep8
250
+        """
251
+        Get last_active_content for user
252
+        """
253
+        app_config = request.registry.settings['CFG']
254
+        content_filter = hapic_data.query
255
+        api = ContentApi(
256
+            current_user=request.candidate_user,  # User
257
+            session=request.dbsession,
258
+            config=app_config,
259
+        )
260
+        wapi = WorkspaceApi(
261
+            current_user=request.candidate_user,  # User
262
+            session=request.dbsession,
263
+            config=app_config,
264
+        )
265
+        workspace = None
266
+        if hapic_data.path.workspace_id:
267
+            workspace = wapi.get_one(hapic_data.path.workspace_id)
268
+        last_actives = api.get_last_active(
269
+            workspace=workspace,
270
+            limit=content_filter.limit or None,
271
+            before_datetime=content_filter.before_datetime or None,
272
+        )
273
+        return [
274
+            api.get_content_in_context(content)
275
+            for content in last_actives
276
+        ]
277
+
278
+    @hapic.with_api_doc(tags=[USER_ENDPOINTS_TAG])
279
+    @require_same_user_or_profile(Group.TIM_ADMIN)
280
+    @hapic.input_path(UserWorkspaceIdPathSchema())
281
+    @hapic.input_query(ContentIdsQuerySchema(), as_list=['contents_ids'])
282
+    @hapic.output_body(ReadStatusSchema(many=True))  # nopep8
283
+    def contents_read_status(self, context, request: TracimRequest, hapic_data=None):  # nopep8
284
+        """
285
+        get user_read status of contents
286
+        """
287
+        app_config = request.registry.settings['CFG']
288
+        content_filter = hapic_data.query
289
+        api = ContentApi(
290
+            current_user=request.candidate_user,  # User
291
+            session=request.dbsession,
292
+            config=app_config,
293
+        )
294
+        wapi = WorkspaceApi(
295
+            current_user=request.candidate_user,  # User
296
+            session=request.dbsession,
297
+            config=app_config,
298
+        )
299
+        workspace = None
300
+        if hapic_data.path.workspace_id:
301
+            workspace = wapi.get_one(hapic_data.path.workspace_id)
302
+        last_actives = api.get_last_active(
303
+            workspace=workspace,
304
+            limit=None,
305
+            before_datetime=None,
306
+            content_ids=hapic_data.query.contents_ids or None
307
+        )
308
+        return [
309
+            api.get_content_in_context(content)
310
+            for content in last_actives
311
+        ]
312
+
313
+    @hapic.with_api_doc(tags=[USER_ENDPOINTS_TAG])
314
+    @require_same_user_or_profile(Group.TIM_ADMIN)
315
+    @hapic.input_path(UserWorkspaceAndContentIdPathSchema())
316
+    @hapic.output_body(NoContentSchema(), default_http_code=HTTPStatus.NO_CONTENT)  # nopep8
317
+    def set_content_as_read(self, context, request: TracimRequest, hapic_data=None):  # nopep8
318
+        """
319
+        set user_read status of content to read
320
+        """
321
+        app_config = request.registry.settings['CFG']
322
+        api = ContentApi(
323
+            current_user=request.candidate_user,
324
+            session=request.dbsession,
325
+            config=app_config,
326
+        )
327
+        api.mark_read(request.current_content, do_flush=True)
328
+        return
329
+
330
+    @hapic.with_api_doc(tags=[USER_ENDPOINTS_TAG])
331
+    @require_same_user_or_profile(Group.TIM_ADMIN)
332
+    @hapic.input_path(UserWorkspaceAndContentIdPathSchema())
333
+    @hapic.output_body(NoContentSchema(), default_http_code=HTTPStatus.NO_CONTENT)  # nopep8
334
+    def set_content_as_unread(self, context, request: TracimRequest, hapic_data=None):  # nopep8
335
+        """
336
+        set user_read status of content to unread
337
+        """
338
+        app_config = request.registry.settings['CFG']
339
+        api = ContentApi(
340
+            current_user=request.candidate_user,
341
+            session=request.dbsession,
342
+            config=app_config,
343
+        )
344
+        api.mark_unread(request.current_content, do_flush=True)
345
+        return
346
+
347
+    @hapic.with_api_doc(tags=[USER_ENDPOINTS_TAG])
348
+    @require_same_user_or_profile(Group.TIM_ADMIN)
349
+    @hapic.input_path(UserWorkspaceIdPathSchema())
350
+    @hapic.output_body(NoContentSchema(), default_http_code=HTTPStatus.NO_CONTENT)  # nopep8
351
+    def set_workspace_as_read(self, context, request: TracimRequest, hapic_data=None):  # nopep8
352
+        """
353
+        set user_read status of all content of workspace to read
354
+        """
355
+        app_config = request.registry.settings['CFG']
356
+        api = ContentApi(
357
+            current_user=request.candidate_user,
358
+            session=request.dbsession,
359
+            config=app_config,
360
+        )
361
+        api.mark_read__workspace(request.current_workspace)
362
+        return
363
+
239
     def bind(self, configurator: Configurator) -> None:
364
     def bind(self, configurator: Configurator) -> None:
240
         """
365
         """
241
         Create all routes and views using pyramid configurator
366
         Create all routes and views using pyramid configurator
277
         # set user profile
402
         # set user profile
278
         configurator.add_route('set_user_profile', '/users/{user_id}/profile', request_method='PUT')  # nopep8
403
         configurator.add_route('set_user_profile', '/users/{user_id}/profile', request_method='PUT')  # nopep8
279
         configurator.add_view(self.set_profile, route_name='set_user_profile')
404
         configurator.add_view(self.set_profile, route_name='set_user_profile')
405
+
406
+        # user content
407
+        configurator.add_route('contents_read_status', '/users/{user_id}/workspaces/{workspace_id}/contents/read_status', request_method='GET')  # nopep8
408
+        configurator.add_view(self.contents_read_status, route_name='contents_read_status')  # nopep8
409
+        # last active content for user
410
+        configurator.add_route('last_active_content', '/users/{user_id}/workspaces/{workspace_id}/contents/recently_active', request_method='GET')  # nopep8
411
+        configurator.add_view(self.last_active_content, route_name='last_active_content')  # nopep8
412
+
413
+        # set content as read/unread
414
+        configurator.add_route('read_content', '/users/{user_id}/workspaces/{workspace_id}/contents/{content_id}/read', request_method='PUT')  # nopep8
415
+        configurator.add_view(self.set_content_as_read, route_name='read_content')  # nopep8
416
+        configurator.add_route('unread_content', '/users/{user_id}/workspaces/{workspace_id}/contents/{content_id}/unread', request_method='PUT')  # nopep8
417
+        configurator.add_view(self.set_content_as_unread, route_name='unread_content')  # nopep8
418
+
419
+        # set workspace as read
420
+        configurator.add_route('read_workspace', '/users/{user_id}/workspaces/{workspace_id}/read', request_method='PUT')  # nopep8
421
+        configurator.add_view(self.set_workspace_as_read, route_name='read_workspace')  # nopep8

+ 170 - 7
tracim/views/core_api/workspace_controller.py View File

1
 import typing
1
 import typing
2
 import transaction
2
 import transaction
3
 from pyramid.config import Configurator
3
 from pyramid.config import Configurator
4
+
5
+from tracim.lib.core.user import UserApi
6
+from tracim.models.roles import WorkspaceRoles
7
+
4
 try:  # Python 3.5+
8
 try:  # Python 3.5+
5
     from http import HTTPStatus
9
     from http import HTTPStatus
6
 except ImportError:
10
 except ImportError:
12
 from tracim.lib.core.content import ContentApi
16
 from tracim.lib.core.content import ContentApi
13
 from tracim.lib.core.userworkspace import RoleApi
17
 from tracim.lib.core.userworkspace import RoleApi
14
 from tracim.lib.utils.authorization import require_workspace_role
18
 from tracim.lib.utils.authorization import require_workspace_role
19
+from tracim.lib.utils.authorization import require_profile
20
+from tracim.models import Group
15
 from tracim.lib.utils.authorization import require_candidate_workspace_role
21
 from tracim.lib.utils.authorization import require_candidate_workspace_role
16
 from tracim.models.data import UserRoleInWorkspace
22
 from tracim.models.data import UserRoleInWorkspace
17
 from tracim.models.data import ActionDescription
23
 from tracim.models.data import ActionDescription
18
 from tracim.models.context_models import UserRoleWorkspaceInContext
24
 from tracim.models.context_models import UserRoleWorkspaceInContext
19
 from tracim.models.context_models import ContentInContext
25
 from tracim.models.context_models import ContentInContext
20
 from tracim.exceptions import EmptyLabelNotAllowed
26
 from tracim.exceptions import EmptyLabelNotAllowed
27
+from tracim.exceptions import EmailValidationFailed
28
+from tracim.exceptions import UserCreationFailed
29
+from tracim.exceptions import UserDoesNotExist
21
 from tracim.exceptions import ContentNotFound
30
 from tracim.exceptions import ContentNotFound
22
 from tracim.exceptions import WorkspacesDoNotMatch
31
 from tracim.exceptions import WorkspacesDoNotMatch
23
 from tracim.exceptions import ParentNotFound
32
 from tracim.exceptions import ParentNotFound
24
 from tracim.views.controllers import Controller
33
 from tracim.views.controllers import Controller
25
 from tracim.views.core_api.schemas import FilterContentQuerySchema
34
 from tracim.views.core_api.schemas import FilterContentQuerySchema
35
+from tracim.views.core_api.schemas import WorkspaceMemberCreationSchema
36
+from tracim.views.core_api.schemas import WorkspaceMemberInviteSchema
37
+from tracim.views.core_api.schemas import RoleUpdateSchema
38
+from tracim.views.core_api.schemas import WorkspaceCreationSchema
39
+from tracim.views.core_api.schemas import WorkspaceModifySchema
40
+from tracim.views.core_api.schemas import WorkspaceAndUserIdPathSchema
26
 from tracim.views.core_api.schemas import ContentMoveSchema
41
 from tracim.views.core_api.schemas import ContentMoveSchema
27
 from tracim.views.core_api.schemas import NoContentSchema
42
 from tracim.views.core_api.schemas import NoContentSchema
28
 from tracim.views.core_api.schemas import ContentCreationSchema
43
 from tracim.views.core_api.schemas import ContentCreationSchema
47
         """
62
         """
48
         Get workspace informations
63
         Get workspace informations
49
         """
64
         """
50
-        wid = hapic_data.path['workspace_id']
51
         app_config = request.registry.settings['CFG']
65
         app_config = request.registry.settings['CFG']
52
         wapi = WorkspaceApi(
66
         wapi = WorkspaceApi(
53
             current_user=request.current_user,  # User
67
             current_user=request.current_user,  # User
57
         return wapi.get_workspace_with_context(request.current_workspace)
71
         return wapi.get_workspace_with_context(request.current_workspace)
58
 
72
 
59
     @hapic.with_api_doc(tags=[WORKSPACE_ENDPOINTS_TAG])
73
     @hapic.with_api_doc(tags=[WORKSPACE_ENDPOINTS_TAG])
74
+    @hapic.handle_exception(EmptyLabelNotAllowed, HTTPStatus.BAD_REQUEST)
75
+    @require_workspace_role(UserRoleInWorkspace.WORKSPACE_MANAGER)
76
+    @hapic.input_path(WorkspaceIdPathSchema())
77
+    @hapic.input_body(WorkspaceModifySchema())
78
+    @hapic.output_body(WorkspaceSchema())
79
+    def update_workspace(self, context, request: TracimRequest, hapic_data=None):  # nopep8
80
+        """
81
+        Update workspace informations
82
+        """
83
+        app_config = request.registry.settings['CFG']
84
+        wapi = WorkspaceApi(
85
+            current_user=request.current_user,  # User
86
+            session=request.dbsession,
87
+            config=app_config,
88
+        )
89
+        wapi.update_workspace(
90
+            request.current_workspace,
91
+            label=hapic_data.body.label,
92
+            description=hapic_data.body.description,
93
+            save_now=True,
94
+        )
95
+        return wapi.get_workspace_with_context(request.current_workspace)
96
+
97
+    @hapic.with_api_doc(tags=[WORKSPACE_ENDPOINTS_TAG])
98
+    @hapic.handle_exception(EmptyLabelNotAllowed, HTTPStatus.BAD_REQUEST)
99
+    @require_profile(Group.TIM_MANAGER)
100
+    @hapic.input_body(WorkspaceCreationSchema())
101
+    @hapic.output_body(WorkspaceSchema())
102
+    def create_workspace(self, context, request: TracimRequest, hapic_data=None):  # nopep8
103
+        """
104
+        create workspace
105
+        """
106
+        app_config = request.registry.settings['CFG']
107
+        wapi = WorkspaceApi(
108
+            current_user=request.current_user,  # User
109
+            session=request.dbsession,
110
+            config=app_config,
111
+        )
112
+        workspace = wapi.create_workspace(
113
+            label=hapic_data.body.label,
114
+            description=hapic_data.body.description,
115
+            save_now=True,
116
+        )
117
+        return wapi.get_workspace_with_context(workspace)
118
+
119
+    @hapic.with_api_doc(tags=[WORKSPACE_ENDPOINTS_TAG])
60
     @require_workspace_role(UserRoleInWorkspace.READER)
120
     @require_workspace_role(UserRoleInWorkspace.READER)
61
     @hapic.input_path(WorkspaceIdPathSchema())
121
     @hapic.input_path(WorkspaceIdPathSchema())
62
     @hapic.output_body(WorkspaceMemberSchema(many=True))
122
     @hapic.output_body(WorkspaceMemberSchema(many=True))
83
         ]
143
         ]
84
 
144
 
85
     @hapic.with_api_doc(tags=[WORKSPACE_ENDPOINTS_TAG])
145
     @hapic.with_api_doc(tags=[WORKSPACE_ENDPOINTS_TAG])
146
+    @require_workspace_role(UserRoleInWorkspace.WORKSPACE_MANAGER)
147
+    @hapic.input_path(WorkspaceAndUserIdPathSchema())
148
+    @hapic.input_body(RoleUpdateSchema())
149
+    @hapic.output_body(WorkspaceMemberSchema())
150
+    def update_workspaces_members_role(
151
+            self,
152
+            context,
153
+            request: TracimRequest,
154
+            hapic_data=None
155
+    ) -> UserRoleWorkspaceInContext:
156
+        """
157
+        Update Members to this workspace
158
+        """
159
+        app_config = request.registry.settings['CFG']
160
+        rapi = RoleApi(
161
+            current_user=request.current_user,
162
+            session=request.dbsession,
163
+            config=app_config,
164
+        )
165
+
166
+        role = rapi.get_one(
167
+            user_id=hapic_data.path.user_id,
168
+            workspace_id=hapic_data.path.workspace_id,
169
+        )
170
+        workspace_role = WorkspaceRoles.get_role_from_slug(hapic_data.body.role)
171
+        role = rapi.update_role(
172
+            role,
173
+            role_level=workspace_role.level
174
+        )
175
+        return rapi.get_user_role_workspace_with_context(role)
176
+
177
+    @hapic.with_api_doc(tags=[WORKSPACE_ENDPOINTS_TAG])
178
+    @hapic.handle_exception(UserCreationFailed, HTTPStatus.BAD_REQUEST)
179
+    @require_workspace_role(UserRoleInWorkspace.WORKSPACE_MANAGER)
180
+    @hapic.input_path(WorkspaceIdPathSchema())
181
+    @hapic.input_body(WorkspaceMemberInviteSchema())
182
+    @hapic.output_body(WorkspaceMemberCreationSchema())
183
+    def create_workspaces_members_role(
184
+            self,
185
+            context,
186
+            request: TracimRequest,
187
+            hapic_data=None
188
+    ) -> UserRoleWorkspaceInContext:
189
+        """
190
+        Add Members to this workspace
191
+        """
192
+        newly_created = False
193
+        email_sent = False
194
+        app_config = request.registry.settings['CFG']
195
+        rapi = RoleApi(
196
+            current_user=request.current_user,
197
+            session=request.dbsession,
198
+            config=app_config,
199
+        )
200
+        uapi = UserApi(
201
+            current_user=request.current_user,
202
+            session=request.dbsession,
203
+            config=app_config,
204
+        )
205
+        try:
206
+            _, user = uapi.find(
207
+                user_id=hapic_data.body.user_id,
208
+                email=hapic_data.body.user_email_or_public_name,
209
+                public_name=hapic_data.body.user_email_or_public_name
210
+            )
211
+        except UserDoesNotExist:
212
+            try:
213
+                # TODO - G.M - 2018-07-05 - [UserCreation] Reenable email
214
+                # notification for creation
215
+                user = uapi.create_user(
216
+                    hapic_data.body.user_email_or_public_name,
217
+                    do_notify=False
218
+                )  # nopep8
219
+                newly_created = True
220
+            except EmailValidationFailed:
221
+                raise UserCreationFailed('no valid mail given')
222
+        role = rapi.create_one(
223
+            user=user,
224
+            workspace=request.current_workspace,
225
+            role_level=WorkspaceRoles.get_role_from_slug(hapic_data.body.role).level,  # nopep8
226
+            with_notif=False,
227
+            flush=True,
228
+        )
229
+        return rapi.get_user_role_workspace_with_context(
230
+            role,
231
+            newly_created=newly_created,
232
+            email_sent=email_sent,
233
+        )
234
+
235
+    @hapic.with_api_doc(tags=[WORKSPACE_ENDPOINTS_TAG])
86
     @require_workspace_role(UserRoleInWorkspace.READER)
236
     @require_workspace_role(UserRoleInWorkspace.READER)
87
     @hapic.input_path(WorkspaceIdPathSchema())
237
     @hapic.input_path(WorkspaceIdPathSchema())
88
     @hapic.input_query(FilterContentQuerySchema())
238
     @hapic.input_query(FilterContentQuerySchema())
109
         contents = api.get_all(
259
         contents = api.get_all(
110
             parent_id=content_filter.parent_id,
260
             parent_id=content_filter.parent_id,
111
             workspace=request.current_workspace,
261
             workspace=request.current_workspace,
262
+            content_type=content_filter.content_type or ContentType.Any,
112
         )
263
         )
113
         contents = [
264
         contents = [
114
             api.get_content_in_context(content) for content in contents
265
             api.get_content_in_context(content) for content in contents
167
             context,
318
             context,
168
             request: TracimRequest,
319
             request: TracimRequest,
169
             hapic_data=None,
320
             hapic_data=None,
170
-    ) -> typing.List[ContentInContext]:
321
+    ) -> ContentInContext:
171
         """
322
         """
172
         move a content
323
         move a content
173
         """
324
         """
216
             context,
367
             context,
217
             request: TracimRequest,
368
             request: TracimRequest,
218
             hapic_data=None,
369
             hapic_data=None,
219
-    ) -> typing.List[ContentInContext]:
370
+    ) -> None:
220
         """
371
         """
221
         delete a content
372
         delete a content
222
         """
373
         """
248
             context,
399
             context,
249
             request: TracimRequest,
400
             request: TracimRequest,
250
             hapic_data=None,
401
             hapic_data=None,
251
-    ) -> typing.List[ContentInContext]:
402
+    ) -> None:
252
         """
403
         """
253
         undelete a content
404
         undelete a content
254
         """
405
         """
281
             context,
432
             context,
282
             request: TracimRequest,
433
             request: TracimRequest,
283
             hapic_data=None,
434
             hapic_data=None,
284
-    ) -> typing.List[ContentInContext]:
435
+    ) -> None:
285
         """
436
         """
286
         archive a content
437
         archive a content
287
         """
438
         """
292
             session=request.dbsession,
443
             session=request.dbsession,
293
             config=app_config,
444
             config=app_config,
294
         )
445
         )
295
-        content = api.get_one(path_data.content_id, content_type=ContentType.Any)
446
+        content = api.get_one(path_data.content_id, content_type=ContentType.Any)  # nopep8
296
         with new_revision(
447
         with new_revision(
297
                 session=request.dbsession,
448
                 session=request.dbsession,
298
                 tm=transaction.manager,
449
                 tm=transaction.manager,
310
             context,
461
             context,
311
             request: TracimRequest,
462
             request: TracimRequest,
312
             hapic_data=None,
463
             hapic_data=None,
313
-    ) -> typing.List[ContentInContext]:
464
+    ) -> None:
314
         """
465
         """
315
         unarchive a content
466
         unarchive a content
316
         """
467
         """
343
         # Workspace
494
         # Workspace
344
         configurator.add_route('workspace', '/workspaces/{workspace_id}', request_method='GET')  # nopep8
495
         configurator.add_route('workspace', '/workspaces/{workspace_id}', request_method='GET')  # nopep8
345
         configurator.add_view(self.workspace, route_name='workspace')
496
         configurator.add_view(self.workspace, route_name='workspace')
497
+        # Create workspace
498
+        configurator.add_route('create_workspace', '/workspaces', request_method='POST')  # nopep8
499
+        configurator.add_view(self.create_workspace, route_name='create_workspace')  # nopep8
500
+        # Update Workspace
501
+        configurator.add_route('update_workspace', '/workspaces/{workspace_id}', request_method='PUT')  # nopep8
502
+        configurator.add_view(self.update_workspace, route_name='update_workspace')  # nopep8
346
         # Workspace Members (Roles)
503
         # Workspace Members (Roles)
347
         configurator.add_route('workspace_members', '/workspaces/{workspace_id}/members', request_method='GET')  # nopep8
504
         configurator.add_route('workspace_members', '/workspaces/{workspace_id}/members', request_method='GET')  # nopep8
348
         configurator.add_view(self.workspaces_members, route_name='workspace_members')  # nopep8
505
         configurator.add_view(self.workspaces_members, route_name='workspace_members')  # nopep8
506
+        # Update Workspace Members roles
507
+        configurator.add_route('update_workspace_member', '/workspaces/{workspace_id}/members/{user_id}', request_method='PUT')  # nopep8
508
+        configurator.add_view(self.update_workspaces_members_role, route_name='update_workspace_member')  # nopep8
509
+        # Create Workspace Members roles
510
+        configurator.add_route('create_workspace_member', '/workspaces/{workspace_id}/members', request_method='POST')  # nopep8
511
+        configurator.add_view(self.create_workspaces_members_role, route_name='create_workspace_member')  # nopep8
349
         # Workspace Content
512
         # Workspace Content
350
         configurator.add_route('workspace_content', '/workspaces/{workspace_id}/contents', request_method='GET')  # nopep8
513
         configurator.add_route('workspace_content', '/workspaces/{workspace_id}/contents', request_method='GET')  # nopep8
351
         configurator.add_view(self.workspace_content, route_name='workspace_content')  # nopep8
514
         configurator.add_view(self.workspace_content, route_name='workspace_content')  # nopep8