Browse Source

Merge branch 'develop' of github.com:tracim/tracim_backend into feature/614_file_content_endpoints

Guénaël Muller 6 years ago
parent
commit
4570537e01

+ 1 - 1
README.md View File

112
 
112
 
113
 run tracim_backend web api:
113
 run tracim_backend web api:
114
 
114
 
115
-    pserve developement.ini
115
+    pserve development.ini
116
 
116
 
117
 run wsgidav server:
117
 run wsgidav server:
118
 
118
 

+ 13 - 0
tracim/exceptions.py View File

163
 class EmptyCommentContentNotAllowed(EmptyValueNotAllowed):
163
 class EmptyCommentContentNotAllowed(EmptyValueNotAllowed):
164
     pass
164
     pass
165
 
165
 
166
+
167
+class RoleDoesNotExist(TracimException):
168
+    pass
169
+
170
+
171
+class EmailValidationFailed(TracimException):
172
+    pass
173
+
174
+
175
+class UserCreationFailed(TracimException):
176
+    pass
177
+
178
+
166
 class ParentNotFound(NotFound):
179
 class ParentNotFound(NotFound):
167
     pass
180
     pass
168
 
181
 

+ 223 - 117
tracim/lib/core/content.py View File

170
             self._show_temporary = previous_show_temporary
170
             self._show_temporary = previous_show_temporary
171
 
171
 
172
     def get_content_in_context(self, content: Content) -> ContentInContext:
172
     def get_content_in_context(self, content: Content) -> ContentInContext:
173
-        return ContentInContext(content, self._session, self._config)
173
+        return ContentInContext(content, self._session, self._config, self._user)  # nopep8
174
 
174
 
175
     def get_revision_in_context(self, revision: ContentRevisionRO) -> RevisionInContext:  # nopep8
175
     def get_revision_in_context(self, revision: ContentRevisionRO) -> RevisionInContext:  # nopep8
176
         # TODO - G.M - 2018-06-173 - create revision in context object
176
         # TODO - G.M - 2018-06-173 - create revision in context object
352
     ) -> Query:
352
     ) -> Query:
353
         return self._base_query(workspace)
353
         return self._base_query(workspace)
354
 
354
 
355
-    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]:
356
-        """
357
-        This method returns child items (folders or items) for left bar treeview.
358
-
359
-        :param parent:
360
-        :param workspace:
361
-        :param filter_by_allowed_content_types:
362
-        :param removed_item_ids:
363
-        :param allowed_node_types: This parameter allow to hide folders for which the given type of content is not allowed.
364
-               For example, if you want to move a Page from a folder to another, you should show only folders that accept pages
365
-        :return:
366
-        """
367
-        filter_by_allowed_content_types = filter_by_allowed_content_types or []  # FDV
368
-        removed_item_ids = removed_item_ids or []  # FDV
369
-
370
-        if not allowed_node_types:
371
-            allowed_node_types = [ContentType.Folder]
372
-        elif allowed_node_types==ContentType.Any:
373
-            allowed_node_types = ContentType.all()
374
-
375
-        parent_id = parent.content_id if parent else None
376
-        folders = self._base_query(workspace).\
377
-            filter(Content.parent_id==parent_id).\
378
-            filter(Content.type.in_(allowed_node_types)).\
379
-            filter(Content.content_id.notin_(removed_item_ids)).\
380
-            all()
381
-
382
-        if not filter_by_allowed_content_types or \
383
-                        len(filter_by_allowed_content_types)<=0:
384
-            # Standard case for the left treeview: we want to show all contents
385
-            # in the left treeview... so we still filter because for example
386
-            # comments must not appear in the treeview
387
-            return [folder for folder in folders \
388
-                    if folder.type in ContentType.allowed_types_for_folding()]
389
-
390
-        # Now this is a case of Folders only (used for moving content)
391
-        # When moving a content, you must get only folders that allow to be filled
392
-        # with the type of content you want to move
393
-        result = []
394
-        for folder in folders:
395
-            for allowed_content_type in filter_by_allowed_content_types:
396
-
397
-                is_folder = folder.type == ContentType.Folder
398
-                content_type__allowed = folder.properties['allowed_content'][allowed_content_type] == True
399
-
400
-                if is_folder and content_type__allowed:
401
-                    result.append(folder)
402
-                    break
403
-
404
-        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
405
 
406
 
406
     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:
407
         # TODO - G.M - 2018-07-16 - raise Exception instead of assert
408
         # TODO - G.M - 2018-07-16 - raise Exception instead of assert
742
             ),
743
             ),
743
         ))
744
         ))
744
 
745
 
746
+
745
     def get_pdf_preview_path(
747
     def get_pdf_preview_path(
746
             self,
748
             self,
747
             content_id: int,
749
             content_id: int,
839
         )
841
         )
840
         return jpg_preview_path
842
         return jpg_preview_path
841
 
843
 
842
-    def get_all(self, parent_id: int=None, content_type: str=ContentType.Any, workspace: Workspace=None) -> typing.List[Content]:
843
-        assert parent_id is None or isinstance(parent_id, int) # DYN_REMOVE
844
-        assert content_type is not None# DYN_REMOVE
845
-        assert isinstance(content_type, str) # DYN_REMOVE
846
-
844
+    def _get_all_query(
845
+        self,
846
+        parent_id: int = None,
847
+        content_type: str = ContentType.Any,
848
+        workspace: Workspace = None
849
+    ) -> Query:
850
+        """
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:
856
+        """
857
+        assert parent_id is None or isinstance(parent_id, int)
858
+        assert content_type is not None
847
         resultset = self._base_query(workspace)
859
         resultset = self._base_query(workspace)
848
 
860
 
849
         if content_type!=ContentType.Any:
861
         if content_type!=ContentType.Any:
850
-            resultset = resultset.filter(Content.type==content_type)
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()))
851
 
866
 
852
         if parent_id:
867
         if parent_id:
853
             resultset = resultset.filter(Content.parent_id==parent_id)
868
             resultset = resultset.filter(Content.parent_id==parent_id)
854
         if parent_id == 0 or parent_id is False:
869
         if parent_id == 0 or parent_id is False:
855
             resultset = resultset.filter(Content.parent_id == None)
870
             resultset = resultset.filter(Content.parent_id == None)
856
-        # parent_id == None give all contents
857
-
858
-        return resultset.all()
859
-
860
-    def get_children(self, parent_id: int, content_types: list, workspace: Workspace=None) -> typing.List[Content]:
861
-        """
862
-        Return parent_id childs of given content_types
863
-        :param parent_id: parent id
864
-        :param content_types: list of types
865
-        :param workspace: workspace filter
866
-        :return: list of content
867
-        """
868
-        resultset = self._base_query(workspace)
869
-        resultset = resultset.filter(Content.type.in_(content_types))
870
 
871
 
871
-        if parent_id:
872
-            resultset = resultset.filter(Content.parent_id==parent_id)
873
-        if parent_id is False:
874
-            resultset = resultset.filter(Content.parent_id == None)
872
+        return resultset
875
 
873
 
876
-        return resultset.all()
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()
877
 
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
878
     # 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
879
     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]:
880
         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
955
             .filter(Workspace.is_deleted.is_(False)) \
974
             .filter(Workspace.is_deleted.is_(False)) \
956
             .subquery()
975
             .subquery()
957
 
976
 
958
-        not_read_content_ids_query = self._session.query(
959
-            distinct(not_read_revisions.c.content_id)
960
-        )
961
-        not_read_content_ids = list(map(
962
-            itemgetter(0),
963
-            not_read_content_ids_query,
964
-        ))
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
+        """
965
 
1005
 
966
-        not_read_contents = self._base_query(workspace) \
967
-            .filter(Content.content_id.in_(not_read_content_ids)) \
968
-            .order_by(desc(Content.updated))
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
+            )
969
 
1019
 
970
-        if content_type != ContentType.Any:
971
-            not_read_contents = not_read_contents.filter(
972
-                Content.type==content_type)
973
-        else:
974
-            not_read_contents = not_read_contents.filter(
975
-                Content.type!=ContentType.Folder)
1020
+        resultset = resultset.order_by(desc(Content.updated))
976
 
1021
 
977
-        if parent_id:
978
-            not_read_contents = not_read_contents.filter(
979
-                Content.parent_id==parent_id)
980
-
981
-        result = []
982
-        for item in not_read_contents:
983
-            new_item = None
984
-            if ContentType.Comment == item.type:
985
-                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
986
             else:
1028
             else:
987
-                new_item = item
1029
+                related_active_content = content
988
 
1030
 
1031
+            if not before_datetime:
1032
+                before_datetime = datetime.datetime.now()
989
             # INFO - D.A. - 2015-05-20
1033
             # INFO - D.A. - 2015-05-20
990
             # 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
991
             # comments about one thread for example
1035
             # comments about one thread for example
992
-            if new_item not in result:
993
-                result.append(new_item)
994
-
995
-            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:
996
                 break
1044
                 break
997
 
1045
 
998
-        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
999
 
1107
 
1000
     def set_allowed_content(self, folder: Content, allowed_content_dict:dict):
1108
     def set_allowed_content(self, folder: Content, allowed_content_dict:dict):
1001
         """
1109
         """
1178
                        do_flush: bool=True,
1286
                        do_flush: bool=True,
1179
                        recursive: bool=True
1287
                        recursive: bool=True
1180
                        ):
1288
                        ):
1181
-
1182
-        itemset = self.get_last_unread(None, ContentType.Any)
1183
-
1184
-        for item in itemset:
1185
-            self.mark_read(item, read_datetime, do_flush, recursive)
1289
+        return self.mark_read__workspace(None, read_datetime, do_flush, recursive) # nopep8
1186
 
1290
 
1187
     def mark_read__workspace(self,
1291
     def mark_read__workspace(self,
1188
                        workspace : Workspace,
1292
                        workspace : Workspace,
1190
                        do_flush: bool=True,
1294
                        do_flush: bool=True,
1191
                        recursive: bool=True
1295
                        recursive: bool=True
1192
                        ):
1296
                        ):
1193
-
1194
-        itemset = self.get_last_unread(None, ContentType.Any, workspace)
1195
-
1297
+        itemset = self.get_last_active(workspace)
1196
         for item in itemset:
1298
         for item in itemset:
1197
-            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)
1198
 
1301
 
1199
     def mark_read(self, content: Content,
1302
     def mark_read(self, content: Content,
1200
                   read_datetime: datetime=None,
1303
                   read_datetime: datetime=None,
1249
             .filter(ContentRevisionRO.content_id==content.content_id).all()
1352
             .filter(ContentRevisionRO.content_id==content.content_id).all()
1250
 
1353
 
1251
         for revision in revisions:
1354
         for revision in revisions:
1252
-            del revision.read_by[self._user]
1355
+            try:
1356
+                del revision.read_by[self._user]
1357
+            except KeyError:
1358
+                pass
1253
 
1359
 
1254
         for child in content.get_valid_children():
1360
         for child in content.get_valid_children():
1255
             self.mark_unread(child, do_flush=False)
1361
             self.mark_unread(child, do_flush=False)

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

6
 import typing as typing
6
 import typing as typing
7
 
7
 
8
 from tracim.exceptions import NotificationNotSend
8
 from tracim.exceptions import NotificationNotSend
9
+from tracim.exceptions import EmailValidationFailed
9
 from tracim.lib.mail_notifier.notifier import get_email_manager
10
 from tracim.lib.mail_notifier.notifier import get_email_manager
10
 from sqlalchemy.orm import Session
11
 from sqlalchemy.orm import Session
11
 
12
 
13
 from tracim.models.auth import User
14
 from tracim.models.auth import User
14
 from tracim.models.auth import Group
15
 from tracim.models.auth import Group
15
 from sqlalchemy.orm.exc import NoResultFound
16
 from sqlalchemy.orm.exc import NoResultFound
16
-from tracim.exceptions import WrongUserPassword, UserDoesNotExist
17
+from tracim.exceptions import UserDoesNotExist
18
+from tracim.exceptions import WrongUserPassword
17
 from tracim.exceptions import AuthenticationFailed
19
 from tracim.exceptions import AuthenticationFailed
18
 from tracim.models.context_models import UserInContext
20
 from tracim.models.context_models import UserInContext
21
+from tracim.models.context_models import TypeUser
19
 
22
 
20
 
23
 
21
 class UserApi(object):
24
 class UserApi(object):
68
             raise UserDoesNotExist('User "{}" not found in database'.format(email)) from exc  # nopep8
71
             raise UserDoesNotExist('User "{}" not found in database'.format(email)) from exc  # nopep8
69
         return user
72
         return user
70
 
73
 
74
+    def get_one_by_public_name(self, public_name: str) -> User:
75
+        """
76
+        Get one user by public_name
77
+        """
78
+        try:
79
+            user = self._base_query().filter(User.display_name == public_name).one()
80
+        except NoResultFound as exc:
81
+            raise UserDoesNotExist('User "{}" not found in database'.format(public_name)) from exc  # nopep8
82
+        return user
71
     # FIXME - G.M - 24-04-2018 - Duplicate method with get_one.
83
     # FIXME - G.M - 24-04-2018 - Duplicate method with get_one.
84
+
72
     def get_one_by_id(self, id: int) -> User:
85
     def get_one_by_id(self, id: int) -> User:
73
         return self.get_one(user_id=id)
86
         return self.get_one(user_id=id)
74
 
87
 
83
     def get_all(self) -> typing.Iterable[User]:
96
     def get_all(self) -> typing.Iterable[User]:
84
         return self._session.query(User).order_by(User.display_name).all()
97
         return self._session.query(User).order_by(User.display_name).all()
85
 
98
 
99
+    def find(
100
+            self,
101
+            user_id: int=None,
102
+            email: str=None,
103
+            public_name: str=None
104
+    ) -> typing.Tuple[TypeUser, User]:
105
+        """
106
+        Find existing user from all theses params.
107
+        Check is made in this order: user_id, email, public_name
108
+        If no user found raise UserDoesNotExist exception
109
+        """
110
+        user = None
111
+
112
+        if user_id:
113
+            try:
114
+                user = self.get_one(user_id)
115
+                return TypeUser.USER_ID, user
116
+            except UserDoesNotExist:
117
+                pass
118
+        if email:
119
+            try:
120
+                user = self.get_one_by_email(email)
121
+                return TypeUser.EMAIL, user
122
+            except UserDoesNotExist:
123
+                pass
124
+        if public_name:
125
+            try:
126
+                user = self.get_one_by_public_name(public_name)
127
+                return TypeUser.PUBLIC_NAME, user
128
+            except UserDoesNotExist:
129
+                pass
130
+
131
+        raise UserDoesNotExist('User not found with any of given params.')
132
+
86
     # Check methods
133
     # Check methods
87
 
134
 
88
     def user_with_email_exists(self, email: str) -> bool:
135
     def user_with_email_exists(self, email: str) -> bool:
112
 
159
 
113
     # Actions
160
     # Actions
114
 
161
 
162
+    def _check_email(self, email: str) -> bool:
163
+        # TODO - G.M - 2018-07-05 - find a better way to check email
164
+        if not email:
165
+            return False
166
+        email = email.split('@')
167
+        if len(email) != 2:
168
+            return False
169
+        return True
170
+
115
     def update(
171
     def update(
116
             self,
172
             self,
117
             user: User,
173
             user: User,
125
             user.display_name = name
181
             user.display_name = name
126
 
182
 
127
         if email is not None:
183
         if email is not None:
184
+            email_exist = self._check_email(email)
185
+            if not email_exist:
186
+                raise EmailValidationFailed('Email given form {} is uncorrect'.format(email))  # nopep8
128
             user.email = email
187
             user.email = email
129
 
188
 
130
         if password is not None:
189
         if password is not None:
176
         """Previous create_user method"""
235
         """Previous create_user method"""
177
         user = User()
236
         user = User()
178
 
237
 
238
+        email_exist = self._check_email(email)
239
+        if not email_exist:
240
+            raise EmailValidationFailed('Email given form {} is uncorrect'.format(email))  # nopep8
179
         user.email = email
241
         user.email = email
242
+        user.display_name = email.split('@')[0]
180
 
243
 
181
         for group in groups:
244
         for group in groups:
182
             user.groups.append(group)
245
             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

+ 113 - 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
10
 from tracim.models.auth import Profile
11
 from tracim.models.auth import Profile
11
 from tracim.models.data import Content
12
 from tracim.models.data import Content
12
 from tracim.models.data import ContentRevisionRO
13
 from tracim.models.data import ContentRevisionRO
13
-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
14
 from tracim.models.workspace_menu_entries import default_workspace_menu_entry
17
 from tracim.models.workspace_menu_entries import default_workspace_menu_entry
15
 from tracim.models.workspace_menu_entries import WorkspaceMenuEntry
18
 from tracim.models.workspace_menu_entries import WorkspaceMenuEntry
16
 from tracim.models.contents import ContentTypeLegacy as ContentType
19
 from tracim.models.contents import ContentTypeLegacy as ContentType
88
         self.height = height
91
         self.height = height
89
 
92
 
90
 
93
 
94
+class WorkspaceAndUserPath(object):
95
+    """
96
+    Paths params with workspace id and user_id
97
+    """
98
+    def __init__(self, workspace_id: int, user_id: int):
99
+        self.workspace_id = workspace_id
100
+        self.user_id = workspace_id
101
+
102
+
103
+class UserWorkspaceAndContentPath(object):
104
+    """
105
+    Paths params with user_id, workspace id and content_id model
106
+    """
107
+    def __init__(self, user_id: int, workspace_id: int, content_id: int) -> None:  # nopep8
108
+        self.content_id = content_id
109
+        self.workspace_id = workspace_id
110
+        self.user_id = user_id
111
+
112
+
91
 class CommentPath(object):
113
 class CommentPath(object):
92
     """
114
     """
93
     Paths params with workspace id and content_id and comment_id model
115
     Paths params with workspace id and content_id and comment_id model
120
     """
142
     """
121
     def __init__(
143
     def __init__(
122
             self,
144
             self,
145
+            workspace_id: int = None,
123
             parent_id: int = None,
146
             parent_id: int = None,
124
             show_archived: int = 0,
147
             show_archived: int = 0,
125
             show_deleted: int = 0,
148
             show_deleted: int = 0,
126
             show_active: int = 1,
149
             show_active: int = 1,
150
+            content_type: str = None,
151
+            offset: int = None,
152
+            limit: int = None,
127
     ) -> None:
153
     ) -> None:
128
         self.parent_id = parent_id
154
         self.parent_id = parent_id
155
+        self.workspace_id = workspace_id
129
         self.show_archived = bool(show_archived)
156
         self.show_archived = bool(show_archived)
130
         self.show_deleted = bool(show_deleted)
157
         self.show_deleted = bool(show_deleted)
131
         self.show_active = bool(show_active)
158
         self.show_active = bool(show_active)
159
+        self.limit = limit
160
+        self.offset = offset
161
+        self.content_type = content_type
162
+
163
+
164
+class ActiveContentFilter(object):
165
+    def __init__(
166
+            self,
167
+            limit: int = None,
168
+            before_datetime: datetime = None,
169
+    ):
170
+        self.limit = limit
171
+        self.before_datetime = before_datetime
172
+
173
+
174
+class ContentIdsQuery(object):
175
+    def __init__(
176
+            self,
177
+            contents_ids: typing.List[int] = None,
178
+    ):
179
+        self.contents_ids = contents_ids
180
+
181
+
182
+class RoleUpdate(object):
183
+    """
184
+    Update role
185
+    """
186
+    def __init__(
187
+        self,
188
+        role: str,
189
+    ):
190
+        self.role = role
191
+
192
+
193
+class WorkspaceMemberInvitation(object):
194
+    """
195
+    Workspace Member Invitation
196
+    """
197
+    def __init__(
198
+        self,
199
+        user_id: int,
200
+        user_email_or_public_name: str,
201
+        role: str,
202
+    ):
203
+        self.role = role
204
+        self.user_email_or_public_name = user_email_or_public_name
205
+        self.user_id = user_id
206
+
207
+
208
+class WorkspaceUpdate(object):
209
+    """
210
+    Update workspace
211
+    """
212
+    def __init__(
213
+        self,
214
+        label: str,
215
+        description: str,
216
+    ):
217
+        self.label = label
218
+        self.description = description
132
 
219
 
133
 
220
 
134
 class ContentCreation(object):
221
 class ContentCreation(object):
181
         self.raw_content = raw_content
268
         self.raw_content = raw_content
182
 
269
 
183
 
270
 
271
+class TypeUser(Enum):
272
+    """Params used to find user"""
273
+    USER_ID = 'found_id'
274
+    EMAIL = 'found_email'
275
+    PUBLIC_NAME = 'found_public_name'
276
+
277
+
184
 class UserInContext(object):
278
 class UserInContext(object):
185
     """
279
     """
186
     Interface to get User data and User data related to context.
280
     Interface to get User data and User data related to context.
310
             user_role: UserRoleInWorkspace,
404
             user_role: UserRoleInWorkspace,
311
             dbsession: Session,
405
             dbsession: Session,
312
             config: CFG,
406
             config: CFG,
407
+            # Extended params
408
+            newly_created: bool = None,
409
+            email_sent: bool = None
313
     )-> None:
410
     )-> None:
314
         self.user_role = user_role
411
         self.user_role = user_role
315
         self.dbsession = dbsession
412
         self.dbsession = dbsession
316
         self.config = config
413
         self.config = config
414
+        # Extended params
415
+        self.newly_created = newly_created
416
+        self.email_sent = email_sent
317
 
417
 
318
     @property
418
     @property
319
     def user_id(self) -> int:
419
     def user_id(self) -> int:
353
         'contributor', 'content-manager', 'workspace-manager'
453
         'contributor', 'content-manager', 'workspace-manager'
354
         :return: user workspace role as slug.
454
         :return: user workspace role as slug.
355
         """
455
         """
356
-        return UserRoleInWorkspace.SLUG[self.user_role.role]
456
+        return WorkspaceRoles.get_role_from_level(self.user_role.role).slug
457
+
458
+    @property
459
+    def is_active(self) -> bool:
460
+        return self.user.is_active
357
 
461
 
358
     @property
462
     @property
359
     def user(self) -> UserInContext:
463
     def user(self) -> UserInContext:
385
     Interface to get Content data and Content data related to context.
489
     Interface to get Content data and Content data related to context.
386
     """
490
     """
387
 
491
 
388
-    def __init__(self, content: Content, dbsession: Session, config: CFG):
492
+    def __init__(self, content: Content, dbsession: Session, config: CFG, user: User=None):  # nopep8
389
         self.content = content
493
         self.content = content
390
         self.dbsession = dbsession
494
         self.dbsession = dbsession
391
         self.config = config
495
         self.config = config
496
+        self._user = user
392
 
497
 
393
     # Default
498
     # Default
394
     @property
499
     @property
481
     def slug(self):
586
     def slug(self):
482
         return slugify(self.content.label)
587
         return slugify(self.content.label)
483
 
588
 
589
+    @property
590
+    def read_by_user(self):
591
+        assert self._user
592
+        return not self.content.has_new_information_for(self._user)
593
+
484
 
594
 
485
 class RevisionInContext(object):
595
 class RevisionInContext(object):
486
     """
596
     """

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

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

28
 from io import BytesIO
28
 from io import BytesIO
29
 from PIL import Image
29
 from PIL import Image
30
 
30
 
31
+
31
 def eq_(a, b, msg=None):
32
 def eq_(a, b, msg=None):
32
     # TODO - G.M - 05-04-2018 - Remove this when all old nose code is removed
33
     # TODO - G.M - 05-04-2018 - Remove this when all old nose code is removed
33
     assert a == b, msg or "%r != %r" % (a, b)
34
     assert a == b, msg or "%r != %r" % (a, b)
79
             'depot_storage_name': 'test',
80
             'depot_storage_name': 'test',
80
             'preview_cache_dir': '/tmp/test/preview_cache',
81
             'preview_cache_dir': '/tmp/test/preview_cache',
81
             'preview.jpg.restricted_dims': True,
82
             'preview.jpg.restricted_dims': True,
82
-
83
+            'email.notification.activated': 'false',
83
         }
84
         }
84
         hapic.reset_context()
85
         hapic.reset_context()
85
         self.engine = get_engine(self.settings)
86
         self.engine = get_engine(self.settings)

+ 715 - 1
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
+
7
+import pytest
8
+import transaction
9
+
10
+from tracim import models
11
+from tracim.lib.core.content import ContentApi
12
+from tracim.lib.core.user import UserApi
13
+from tracim.lib.core.workspace import WorkspaceApi
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
5
 from tracim.tests import FunctionalTest
17
 from tracim.tests import FunctionalTest
6
 from tracim.fixtures.content import Content as ContentFixtures
18
 from tracim.fixtures.content import Content as ContentFixtures
7
 from tracim.fixtures.users_and_groups import Base as BaseFixture
19
 from tracim.fixtures.users_and_groups import Base as BaseFixture
8
 
20
 
9
 
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
+
10
 class TestUserWorkspaceEndpoint(FunctionalTest):
725
 class TestUserWorkspaceEndpoint(FunctionalTest):
11
-    # -*- coding: utf-8 -*-
12
     """
726
     """
13
     Tests for /api/v2/users/{user_id}/workspaces
727
     Tests for /api/v2/users/{user_id}/workspaces
14
     """
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

22
         )
22
         )
23
         u = api.create_minimal_user('bob@bob')
23
         u = api.create_minimal_user('bob@bob')
24
         assert u.email == 'bob@bob'
24
         assert u.email == 'bob@bob'
25
-        assert u.display_name is None
25
+        assert u.display_name == 'bob'
26
 
26
 
27
     def test_unit__create_minimal_user_and_update__ok__nominal_case(self):
27
     def test_unit__create_minimal_user_and_update__ok__nominal_case(self):
28
         api = UserApi(
28
         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')

+ 141 - 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 ContentPreviewSizedPath
17
 from tracim.models.context_models import ContentPreviewSizedPath
15
 from tracim.models.context_models import RevisionPreviewSizedPath
18
 from tracim.models.context_models import RevisionPreviewSizedPath
16
 from tracim.models.context_models import PageQuery
19
 from tracim.models.context_models import PageQuery
17
 from tracim.models.context_models import WorkspaceAndContentRevisionPath
20
 from tracim.models.context_models import WorkspaceAndContentRevisionPath
21
+from tracim.models.context_models import WorkspaceMemberInvitation
22
+from tracim.models.context_models import WorkspaceUpdate
23
+from tracim.models.context_models import RoleUpdate
18
 from tracim.models.context_models import CommentCreation
24
 from tracim.models.context_models import CommentCreation
19
 from tracim.models.context_models import TextBasedContentUpdate
25
 from tracim.models.context_models import TextBasedContentUpdate
20
 from tracim.models.context_models import SetContentStatus
26
 from tracim.models.context_models import SetContentStatus
21
 from tracim.models.context_models import CommentPath
27
 from tracim.models.context_models import CommentPath
22
 from tracim.models.context_models import MoveParams
28
 from tracim.models.context_models import MoveParams
23
 from tracim.models.context_models import WorkspaceAndContentPath
29
 from tracim.models.context_models import WorkspaceAndContentPath
30
+from tracim.models.context_models import WorkspaceAndUserPath
24
 from tracim.models.context_models import ContentFilter
31
 from tracim.models.context_models import ContentFilter
25
 from tracim.models.context_models import LoginCredentials
32
 from tracim.models.context_models import LoginCredentials
26
 from tracim.models.data import UserRoleInWorkspace
33
 from tracim.models.data import UserRoleInWorkspace
115
     revision_id = marshmallow.fields.Int(example=6, required=True)
122
     revision_id = marshmallow.fields.Int(example=6, required=True)
116
 
123
 
117
 
124
 
125
+class WorkspaceAndUserIdPathSchema(
126
+    UserIdPathSchema,
127
+    WorkspaceIdPathSchema
128
+):
129
+    @post_load
130
+    def make_path_object(self, data):
131
+        return WorkspaceAndUserPath(**data)
132
+
133
+
118
 class WorkspaceAndContentIdPathSchema(
134
 class WorkspaceAndContentIdPathSchema(
119
     WorkspaceIdPathSchema,
135
     WorkspaceIdPathSchema,
120
     ContentIdPathSchema
136
     ContentIdPathSchema
170
         return RevisionPreviewSizedPath(**data)
186
         return RevisionPreviewSizedPath(**data)
171
 
187
 
172
 
188
 
189
+class UserWorkspaceAndContentIdPathSchema(
190
+    UserIdPathSchema,
191
+    WorkspaceIdPathSchema,
192
+    ContentIdPathSchema,
193
+):
194
+    @post_load
195
+    def make_path_object(self, data):
196
+        return UserWorkspaceAndContentPath(**data)
197
+
198
+
199
+class UserWorkspaceIdPathSchema(
200
+    UserIdPathSchema,
201
+    WorkspaceIdPathSchema,
202
+):
203
+    @post_load
204
+    def make_path_object(self, data):
205
+        return WorkspaceAndUserPath(**data)
206
+
207
+
173
 class CommentsPathSchema(WorkspaceAndContentIdPathSchema):
208
 class CommentsPathSchema(WorkspaceAndContentIdPathSchema):
174
     comment_id = marshmallow.fields.Int(
209
     comment_id = marshmallow.fields.Int(
175
         example=6,
210
         example=6,
177
         required=True,
212
         required=True,
178
         validate=Range(min=1, error="Value must be greater than 0"),
213
         validate=Range(min=1, error="Value must be greater than 0"),
179
     )
214
     )
215
+
180
     @post_load
216
     @post_load
181
     def make_path_object(self, data):
217
     def make_path_object(self, data):
182
         return CommentPath(**data)
218
         return CommentPath(**data)
231
                     'to allow to show only archived documents',
267
                     'to allow to show only archived documents',
232
         validate=Range(min=0, max=1, error="Value must be 0 or 1"),
268
         validate=Range(min=0, max=1, error="Value must be 0 or 1"),
233
     )
269
     )
270
+    content_type = marshmallow.fields.String(
271
+        example=ContentType.Any,
272
+        default=ContentType.Any,
273
+        validate=OneOf(ContentType.allowed_type_values())
274
+    )
234
 
275
 
235
     @post_load
276
     @post_load
236
     def make_content_filter(self, data):
277
     def make_content_filter(self, data):
237
         return ContentFilter(**data)
278
         return ContentFilter(**data)
279
+
280
+
281
+class ActiveContentFilterQuerySchema(marshmallow.Schema):
282
+    limit = marshmallow.fields.Int(
283
+        example=2,
284
+        default=0,
285
+        description='if 0 or not set, return all elements, else return only '
286
+                    'the first limit elem (according to offset)',
287
+        validate=Range(min=0, error="Value must be positive or 0"),
288
+    )
289
+    before_datetime = marshmallow.fields.DateTime(
290
+        format=DATETIME_FORMAT,
291
+        description='return only content lastly updated before this date',
292
+    )
293
+    @post_load
294
+    def make_content_filter(self, data):
295
+        return ActiveContentFilter(**data)
296
+
297
+
298
+class ContentIdsQuerySchema(marshmallow.Schema):
299
+    contents_ids = marshmallow.fields.List(
300
+        marshmallow.fields.Int(
301
+            example=6,
302
+            validate=Range(min=1, error="Value must be greater than 0"),
303
+        )
304
+    )
305
+    @post_load
306
+    def make_contents_ids(self, data):
307
+        return ContentIdsQuery(**data)
308
+
238
 ###
309
 ###
239
 
310
 
240
 
311
 
312
+class RoleUpdateSchema(marshmallow.Schema):
313
+    role = marshmallow.fields.String(
314
+        example='contributor',
315
+        validate=OneOf(UserRoleInWorkspace.get_all_role_slug())
316
+    )
317
+
318
+    @post_load
319
+    def make_role(self, data):
320
+        return RoleUpdate(**data)
321
+
322
+
323
+class WorkspaceMemberInviteSchema(RoleUpdateSchema):
324
+    user_id = marshmallow.fields.Int(
325
+        example=5,
326
+        default=None,
327
+        allow_none=True,
328
+    )
329
+    user_email_or_public_name = marshmallow.fields.String(
330
+        example='suri@cate.fr',
331
+        default=None,
332
+        allow_none=True,
333
+    )
334
+
335
+    @post_load
336
+    def make_role(self, data):
337
+        return WorkspaceMemberInvitation(**data)
338
+
339
+
241
 class BasicAuthSchema(marshmallow.Schema):
340
 class BasicAuthSchema(marshmallow.Schema):
242
 
341
 
243
     email = marshmallow.fields.Email(
342
     email = marshmallow.fields.Email(
262
     expire_after = marshmallow.fields.String()
361
     expire_after = marshmallow.fields.String()
263
 
362
 
264
 
363
 
364
+class WorkspaceModifySchema(marshmallow.Schema):
365
+    label = marshmallow.fields.String(
366
+        example='My Workspace',
367
+    )
368
+    description = marshmallow.fields.String(
369
+        example='A super description of my workspace.',
370
+    )
371
+
372
+    @post_load
373
+    def make_workspace_modifications(self, data):
374
+        return WorkspaceUpdate(**data)
375
+
376
+
377
+class WorkspaceCreationSchema(WorkspaceModifySchema):
378
+    pass
379
+
380
+
265
 class NoContentSchema(marshmallow.Schema):
381
 class NoContentSchema(marshmallow.Schema):
266
 
382
 
267
     class Meta:
383
     class Meta:
329
         validate=Range(min=1, error="Value must be greater than 0"),
445
         validate=Range(min=1, error="Value must be greater than 0"),
330
     )
446
     )
331
     user = marshmallow.fields.Nested(
447
     user = marshmallow.fields.Nested(
332
-        UserSchema(only=('public_name', 'avatar_url'))
448
+        UserDigestSchema()
333
     )
449
     )
450
+    workspace = marshmallow.fields.Nested(
451
+        WorkspaceDigestSchema(exclude=('sidebar_entries',))
452
+    )
453
+    is_active = marshmallow.fields.Bool()
334
 
454
 
335
     class Meta:
455
     class Meta:
336
         description = 'Workspace Member information'
456
         description = 'Workspace Member information'
337
 
457
 
338
 
458
 
459
+class WorkspaceMemberCreationSchema(WorkspaceMemberSchema):
460
+    newly_created = marshmallow.fields.Bool(
461
+        exemple=False,
462
+        description='Is the user completely new '
463
+                    '(and account was just created) or not ?',
464
+    )
465
+    email_sent = marshmallow.fields.Bool(
466
+        exemple=False,
467
+        description='Has an email been sent to user to inform him about '
468
+                    'this new workspace registration and eventually his account'
469
+                    'creation'
470
+    )
471
+
472
+
339
 class ApplicationConfigSchema(marshmallow.Schema):
473
 class ApplicationConfigSchema(marshmallow.Schema):
340
     pass
474
     pass
341
     #  TODO - G.M - 24-05-2018 - Set this
475
     #  TODO - G.M - 24-05-2018 - Set this
497
     )
631
     )
498
 
632
 
499
 
633
 
634
+class ReadStatusSchema(marshmallow.Schema):
635
+    content_id = marshmallow.fields.Int(
636
+        example=6,
637
+        validate=Range(min=1, error="Value must be greater than 0"),
638
+    )
639
+    read_by_user = marshmallow.fields.Bool(example=False, default=False)
500
 #####
640
 #####
501
 # Content
641
 # Content
502
 #####
642
 #####

+ 149 - 9
tracim/views/core_api/user_controller.py View File

1
 from pyramid.config import Configurator
1
 from pyramid.config import Configurator
2
-from sqlalchemy.orm.exc import NoResultFound
3
 
2
 
3
+from tracim.lib.core.content import ContentApi
4
 from tracim.lib.utils.authorization import require_same_user_or_profile
4
 from tracim.lib.utils.authorization import require_same_user_or_profile
5
 from tracim.models import Group
5
 from tracim.models import Group
6
-from tracim.models.context_models import WorkspaceInContext
7
 
6
 
8
 try:  # Python 3.5+
7
 try:  # Python 3.5+
9
     from http import HTTPStatus
8
     from http import HTTPStatus
12
 
11
 
13
 from tracim import hapic, TracimRequest
12
 from tracim import hapic, TracimRequest
14
 
13
 
15
-from tracim.exceptions import NotAuthenticated
16
-from tracim.exceptions import InsufficientUserProfile
17
-from tracim.exceptions import UserDoesNotExist
18
 from tracim.lib.core.workspace import WorkspaceApi
14
 from tracim.lib.core.workspace import WorkspaceApi
19
 from tracim.views.controllers import Controller
15
 from tracim.views.controllers import Controller
20
-from tracim.views.core_api.schemas import UserIdPathSchema
16
+from tracim.views.core_api.schemas import UserIdPathSchema, ReadStatusSchema, \
17
+    ContentIdsQuerySchema
18
+from tracim.views.core_api.schemas import NoContentSchema
19
+from tracim.views.core_api.schemas import UserWorkspaceIdPathSchema
20
+from tracim.views.core_api.schemas import UserWorkspaceAndContentIdPathSchema
21
+from tracim.views.core_api.schemas import ContentDigestSchema
22
+from tracim.views.core_api.schemas import ActiveContentFilterQuerySchema
21
 from tracim.views.core_api.schemas import WorkspaceDigestSchema
23
 from tracim.views.core_api.schemas import WorkspaceDigestSchema
24
+from tracim.models.contents import ContentTypeLegacy as ContentType
22
 
25
 
23
 USER_ENDPOINTS_TAG = 'Users'
26
 USER_ENDPOINTS_TAG = 'Users'
24
 
27
 
35
         """
38
         """
36
         app_config = request.registry.settings['CFG']
39
         app_config = request.registry.settings['CFG']
37
         wapi = WorkspaceApi(
40
         wapi = WorkspaceApi(
38
-            current_user=request.current_user,  # User
41
+            current_user=request.candidate_user,  # User
39
             session=request.dbsession,
42
             session=request.dbsession,
40
             config=app_config,
43
             config=app_config,
41
         )
44
         )
42
         
45
         
43
         workspaces = wapi.get_all_for_user(request.candidate_user)
46
         workspaces = wapi.get_all_for_user(request.candidate_user)
44
         return [
47
         return [
45
-            WorkspaceInContext(workspace, request.dbsession, app_config)
48
+            wapi.get_workspace_with_context(workspace)
46
             for workspace in workspaces
49
             for workspace in workspaces
47
         ]
50
         ]
48
 
51
 
52
+    @hapic.with_api_doc(tags=[USER_ENDPOINTS_TAG])
53
+    @require_same_user_or_profile(Group.TIM_ADMIN)
54
+    @hapic.input_path(UserWorkspaceIdPathSchema())
55
+    @hapic.input_query(ActiveContentFilterQuerySchema())
56
+    @hapic.output_body(ContentDigestSchema(many=True))
57
+    def last_active_content(self, context, request: TracimRequest, hapic_data=None):  # nopep8
58
+        """
59
+        Get last_active_content for user
60
+        """
61
+        app_config = request.registry.settings['CFG']
62
+        content_filter = hapic_data.query
63
+        api = ContentApi(
64
+            current_user=request.candidate_user,  # User
65
+            session=request.dbsession,
66
+            config=app_config,
67
+        )
68
+        wapi = WorkspaceApi(
69
+            current_user=request.candidate_user,  # User
70
+            session=request.dbsession,
71
+            config=app_config,
72
+        )
73
+        workspace = None
74
+        if hapic_data.path.workspace_id:
75
+            workspace = wapi.get_one(hapic_data.path.workspace_id)
76
+        last_actives = api.get_last_active(
77
+            workspace=workspace,
78
+            limit=content_filter.limit or None,
79
+            before_datetime=content_filter.before_datetime or None,
80
+        )
81
+        return [
82
+            api.get_content_in_context(content)
83
+            for content in last_actives
84
+        ]
85
+
86
+    @hapic.with_api_doc(tags=[USER_ENDPOINTS_TAG])
87
+    @require_same_user_or_profile(Group.TIM_ADMIN)
88
+    @hapic.input_path(UserWorkspaceIdPathSchema())
89
+    @hapic.input_query(ContentIdsQuerySchema(), as_list=['contents_ids'])
90
+    @hapic.output_body(ReadStatusSchema(many=True))  # nopep8
91
+    def contents_read_status(self, context, request: TracimRequest, hapic_data=None):  # nopep8
92
+        """
93
+        get user_read status of contents
94
+        """
95
+        app_config = request.registry.settings['CFG']
96
+        content_filter = hapic_data.query
97
+        api = ContentApi(
98
+            current_user=request.candidate_user,  # User
99
+            session=request.dbsession,
100
+            config=app_config,
101
+        )
102
+        wapi = WorkspaceApi(
103
+            current_user=request.candidate_user,  # User
104
+            session=request.dbsession,
105
+            config=app_config,
106
+        )
107
+        workspace = None
108
+        if hapic_data.path.workspace_id:
109
+            workspace = wapi.get_one(hapic_data.path.workspace_id)
110
+        last_actives = api.get_last_active(
111
+            workspace=workspace,
112
+            limit=None,
113
+            before_datetime=None,
114
+            content_ids=hapic_data.query.contents_ids or None
115
+        )
116
+        return [
117
+            api.get_content_in_context(content)
118
+            for content in last_actives
119
+        ]
120
+
121
+    @hapic.with_api_doc(tags=[USER_ENDPOINTS_TAG])
122
+    @require_same_user_or_profile(Group.TIM_ADMIN)
123
+    @hapic.input_path(UserWorkspaceAndContentIdPathSchema())
124
+    @hapic.output_body(NoContentSchema(), default_http_code=HTTPStatus.NO_CONTENT)  # nopep8
125
+    def set_content_as_read(self, context, request: TracimRequest, hapic_data=None):  # nopep8
126
+        """
127
+        set user_read status of content to read
128
+        """
129
+        app_config = request.registry.settings['CFG']
130
+        api = ContentApi(
131
+            current_user=request.candidate_user,
132
+            session=request.dbsession,
133
+            config=app_config,
134
+        )
135
+        api.mark_read(request.current_content, do_flush=True)
136
+        return
137
+
138
+    @hapic.with_api_doc(tags=[USER_ENDPOINTS_TAG])
139
+    @require_same_user_or_profile(Group.TIM_ADMIN)
140
+    @hapic.input_path(UserWorkspaceAndContentIdPathSchema())
141
+    @hapic.output_body(NoContentSchema(), default_http_code=HTTPStatus.NO_CONTENT)  # nopep8
142
+    def set_content_as_unread(self, context, request: TracimRequest, hapic_data=None):  # nopep8
143
+        """
144
+        set user_read status of content to unread
145
+        """
146
+        app_config = request.registry.settings['CFG']
147
+        api = ContentApi(
148
+            current_user=request.candidate_user,
149
+            session=request.dbsession,
150
+            config=app_config,
151
+        )
152
+        api.mark_unread(request.current_content, do_flush=True)
153
+        return
154
+
155
+    @hapic.with_api_doc(tags=[USER_ENDPOINTS_TAG])
156
+    @require_same_user_or_profile(Group.TIM_ADMIN)
157
+    @hapic.input_path(UserWorkspaceIdPathSchema())
158
+    @hapic.output_body(NoContentSchema(), default_http_code=HTTPStatus.NO_CONTENT)  # nopep8
159
+    def set_workspace_as_read(self, context, request: TracimRequest, hapic_data=None):  # nopep8
160
+        """
161
+        set user_read status of all content of workspace to read
162
+        """
163
+        app_config = request.registry.settings['CFG']
164
+        api = ContentApi(
165
+            current_user=request.candidate_user,
166
+            session=request.dbsession,
167
+            config=app_config,
168
+        )
169
+        api.mark_read__workspace(request.current_workspace)
170
+        return
171
+
49
     def bind(self, configurator: Configurator) -> None:
172
     def bind(self, configurator: Configurator) -> None:
50
         """
173
         """
51
         Create all routes and views using pyramid configurator
174
         Create all routes and views using pyramid configurator
52
         for this controller
175
         for this controller
53
         """
176
         """
54
 
177
 
55
-        # Applications
178
+        # user worskpace
56
         configurator.add_route('user_workspace', '/users/{user_id}/workspaces', request_method='GET')  # nopep8
179
         configurator.add_route('user_workspace', '/users/{user_id}/workspaces', request_method='GET')  # nopep8
57
         configurator.add_view(self.user_workspace, route_name='user_workspace')
180
         configurator.add_view(self.user_workspace, route_name='user_workspace')
181
+
182
+        # user content
183
+        configurator.add_route('contents_read_status', '/users/{user_id}/workspaces/{workspace_id}/contents/read_status', request_method='GET')  # nopep8
184
+        configurator.add_view(self.contents_read_status, route_name='contents_read_status')  # nopep8
185
+        # last active content for user
186
+        configurator.add_route('last_active_content', '/users/{user_id}/workspaces/{workspace_id}/contents/recently_active', request_method='GET')  # nopep8
187
+        configurator.add_view(self.last_active_content, route_name='last_active_content')  # nopep8
188
+
189
+        # set content as read/unread
190
+        configurator.add_route('read_content', '/users/{user_id}/workspaces/{workspace_id}/contents/{content_id}/read', request_method='PUT')  # nopep8
191
+        configurator.add_view(self.set_content_as_read, route_name='read_content')  # nopep8
192
+        configurator.add_route('unread_content', '/users/{user_id}/workspaces/{workspace_id}/contents/{content_id}/unread', request_method='PUT')  # nopep8
193
+        configurator.add_view(self.set_content_as_unread, route_name='unread_content')  # nopep8
194
+
195
+        # set workspace as read
196
+        configurator.add_route('read_workspace', '/users/{user_id}/workspaces/{workspace_id}/read', request_method='PUT')  # nopep8
197
+        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