Browse Source

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

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

+ 1 - 1
README.md View File

@@ -112,7 +112,7 @@ to stop them:
112 112
 
113 113
 run tracim_backend web api:
114 114
 
115
-    pserve developement.ini
115
+    pserve development.ini
116 116
 
117 117
 run wsgidav server:
118 118
 

+ 13 - 0
tracim/exceptions.py View File

@@ -163,6 +163,19 @@ class EmptyLabelNotAllowed(EmptyValueNotAllowed):
163 163
 class EmptyCommentContentNotAllowed(EmptyValueNotAllowed):
164 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 179
 class ParentNotFound(NotFound):
167 180
     pass
168 181
 

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

@@ -170,7 +170,7 @@ class ContentApi(object):
170 170
             self._show_temporary = previous_show_temporary
171 171
 
172 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 175
     def get_revision_in_context(self, revision: ContentRevisionRO) -> RevisionInContext:  # nopep8
176 176
         # TODO - G.M - 2018-06-173 - create revision in context object
@@ -352,56 +352,57 @@ class ContentApi(object):
352 352
     ) -> Query:
353 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 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 408
         # TODO - G.M - 2018-07-16 - raise Exception instead of assert
@@ -742,6 +743,7 @@ class ContentApi(object):
742 743
             ),
743 744
         ))
744 745
 
746
+
745 747
     def get_pdf_preview_path(
746 748
             self,
747 749
             content_id: int,
@@ -839,42 +841,59 @@ class ContentApi(object):
839 841
         )
840 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 859
         resultset = self._base_query(workspace)
848 860
 
849 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 867
         if parent_id:
853 868
             resultset = resultset.filter(Content.parent_id==parent_id)
854 869
         if parent_id == 0 or parent_id is False:
855 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 897
     # TODO find an other name to filter on is_deleted / is_archived
879 898
     def get_all_with_filter(self, parent_id: int=None, content_type: str=ContentType.Any, workspace: Workspace=None) -> typing.List[Content]:
880 899
         assert parent_id is None or isinstance(parent_id, int) # DYN_REMOVE
@@ -955,47 +974,136 @@ class ContentApi(object):
955 974
             .filter(Workspace.is_deleted.is_(False)) \
956 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 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 1033
             # INFO - D.A. - 2015-05-20
990 1034
             # We do not want to show only one item if the last 10 items are
991 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 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 1108
     def set_allowed_content(self, folder: Content, allowed_content_dict:dict):
1001 1109
         """
@@ -1178,11 +1286,7 @@ class ContentApi(object):
1178 1286
                        do_flush: bool=True,
1179 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 1291
     def mark_read__workspace(self,
1188 1292
                        workspace : Workspace,
@@ -1190,11 +1294,10 @@ class ContentApi(object):
1190 1294
                        do_flush: bool=True,
1191 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 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 1302
     def mark_read(self, content: Content,
1200 1303
                   read_datetime: datetime=None,
@@ -1249,7 +1352,10 @@ class ContentApi(object):
1249 1352
             .filter(ContentRevisionRO.content_id==content.content_id).all()
1250 1353
 
1251 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 1360
         for child in content.get_valid_children():
1255 1361
             self.mark_unread(child, do_flush=False)

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

@@ -6,6 +6,7 @@ import transaction
6 6
 import typing as typing
7 7
 
8 8
 from tracim.exceptions import NotificationNotSend
9
+from tracim.exceptions import EmailValidationFailed
9 10
 from tracim.lib.mail_notifier.notifier import get_email_manager
10 11
 from sqlalchemy.orm import Session
11 12
 
@@ -13,9 +14,11 @@ from tracim import CFG
13 14
 from tracim.models.auth import User
14 15
 from tracim.models.auth import Group
15 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 19
 from tracim.exceptions import AuthenticationFailed
18 20
 from tracim.models.context_models import UserInContext
21
+from tracim.models.context_models import TypeUser
19 22
 
20 23
 
21 24
 class UserApi(object):
@@ -68,7 +71,17 @@ class UserApi(object):
68 71
             raise UserDoesNotExist('User "{}" not found in database'.format(email)) from exc  # nopep8
69 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 83
     # FIXME - G.M - 24-04-2018 - Duplicate method with get_one.
84
+
72 85
     def get_one_by_id(self, id: int) -> User:
73 86
         return self.get_one(user_id=id)
74 87
 
@@ -83,6 +96,40 @@ class UserApi(object):
83 96
     def get_all(self) -> typing.Iterable[User]:
84 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 133
     # Check methods
87 134
 
88 135
     def user_with_email_exists(self, email: str) -> bool:
@@ -112,6 +159,15 @@ class UserApi(object):
112 159
 
113 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 171
     def update(
116 172
             self,
117 173
             user: User,
@@ -125,6 +181,9 @@ class UserApi(object):
125 181
             user.display_name = name
126 182
 
127 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 187
             user.email = email
129 188
 
130 189
         if password is not None:
@@ -176,7 +235,11 @@ class UserApi(object):
176 235
         """Previous create_user method"""
177 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 241
         user.email = email
242
+        user.display_name = email.split('@')[0]
180 243
 
181 244
         for group in groups:
182 245
             user.groups.append(group)

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

@@ -3,6 +3,7 @@ import typing
3 3
 
4 4
 from tracim import CFG
5 5
 from tracim.models.context_models import UserRoleWorkspaceInContext
6
+from tracim.models.roles import WorkspaceRoles
6 7
 
7 8
 __author__ = 'damien'
8 9
 
@@ -11,40 +12,55 @@ from sqlalchemy.orm import Query
11 12
 from tracim.models.auth import User
12 13
 from tracim.models.data import Workspace
13 14
 from tracim.models.data import UserRoleInWorkspace
14
-from tracim.models.data import RoleType
15 15
 
16 16
 
17 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 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 59
     def get_user_role_workspace_with_context(
46 60
             self,
47
-            user_role: UserRoleInWorkspace
61
+            user_role: UserRoleInWorkspace,
62
+            newly_created:bool = None,
63
+            email_sent: bool = None,
48 64
     ) -> UserRoleWorkspaceInContext:
49 65
         """
50 66
         Return WorkspaceInContext object from Workspace
@@ -54,27 +70,11 @@ class RoleApi(object):
54 70
             user_role=user_role,
55 71
             dbsession=self._session,
56 72
             config=self._config,
73
+            newly_created=newly_created,
74
+            email_sent=email_sent,
57 75
         )
58 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 78
     def __init__(
79 79
         self,
80 80
         session: Session,
@@ -98,6 +98,29 @@ class RoleApi(object):
98 98
     def get_one(self, user_id: int, workspace_id: int) -> UserRoleInWorkspace:
99 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 124
     def create_one(
102 125
         self,
103 126
         user: User,
@@ -106,7 +129,7 @@ class RoleApi(object):
106 129
         with_notif: bool,
107 130
         flush: bool=True
108 131
     ) -> UserRoleInWorkspace:
109
-        role = self.create_role()
132
+        role = UserRoleInWorkspace()
110 133
         role.user_id = user.user_id
111 134
         role.workspace = workspace
112 135
         role.role = role_level
@@ -120,20 +143,6 @@ class RoleApi(object):
120 143
         if flush:
121 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 146
     def get_all_for_workspace(
138 147
         self,
139 148
         workspace:Workspace
@@ -145,18 +154,45 @@ class RoleApi(object):
145 154
     def save(self, role: UserRoleInWorkspace) -> None:
146 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,6 +5,7 @@ from sqlalchemy.orm import Query
5 5
 from sqlalchemy.orm import Session
6 6
 
7 7
 from tracim import CFG
8
+from tracim.exceptions import EmptyLabelNotAllowed
8 9
 from tracim.lib.utils.translation import fake_translator as _
9 10
 
10 11
 from tracim.lib.core.userworkspace import RoleApi
@@ -69,7 +70,7 @@ class WorkspaceApi(object):
69 70
             save_now: bool=False,
70 71
     ) -> Workspace:
71 72
         if not label:
72
-            label = self.generate_label()
73
+            raise EmptyLabelNotAllowed('Workspace label cannot be empty')
73 74
 
74 75
         workspace = Workspace()
75 76
         workspace.label = label
@@ -105,6 +106,31 @@ class WorkspaceApi(object):
105 106
 
106 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 134
     def get_one(self, id):
109 135
         return self._base_query().filter(Workspace.workspace_id == id).one()
110 136
 

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

@@ -251,6 +251,19 @@ class ContentTypeLegacy(NewContentType):
251 251
                 return
252 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 267
     @classmethod
255 268
     def all(cls) -> typing.List[str]:
256 269
         return cls.allowed_types()
@@ -261,6 +274,15 @@ class ContentTypeLegacy(NewContentType):
261 274
         return contents_types
262 275
 
263 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 286
     def allowed_types_for_folding(cls):
265 287
         # This method is used for showing only "main"
266 288
         # types in the left-side treeview

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

@@ -1,6 +1,7 @@
1 1
 # coding=utf-8
2 2
 import typing
3 3
 from datetime import datetime
4
+from enum import Enum
4 5
 
5 6
 from slugify import slugify
6 7
 from sqlalchemy.orm import Session
@@ -10,7 +11,9 @@ from tracim.models import User
10 11
 from tracim.models.auth import Profile
11 12
 from tracim.models.data import Content
12 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 17
 from tracim.models.workspace_menu_entries import default_workspace_menu_entry
15 18
 from tracim.models.workspace_menu_entries import WorkspaceMenuEntry
16 19
 from tracim.models.contents import ContentTypeLegacy as ContentType
@@ -88,6 +91,25 @@ class RevisionPreviewSizedPath(object):
88 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 113
 class CommentPath(object):
92 114
     """
93 115
     Paths params with workspace id and content_id and comment_id model
@@ -120,15 +142,80 @@ class ContentFilter(object):
120 142
     """
121 143
     def __init__(
122 144
             self,
145
+            workspace_id: int = None,
123 146
             parent_id: int = None,
124 147
             show_archived: int = 0,
125 148
             show_deleted: int = 0,
126 149
             show_active: int = 1,
150
+            content_type: str = None,
151
+            offset: int = None,
152
+            limit: int = None,
127 153
     ) -> None:
128 154
         self.parent_id = parent_id
155
+        self.workspace_id = workspace_id
129 156
         self.show_archived = bool(show_archived)
130 157
         self.show_deleted = bool(show_deleted)
131 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 221
 class ContentCreation(object):
@@ -181,6 +268,13 @@ class TextBasedContentUpdate(object):
181 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 278
 class UserInContext(object):
185 279
     """
186 280
     Interface to get User data and User data related to context.
@@ -310,10 +404,16 @@ class UserRoleWorkspaceInContext(object):
310 404
             user_role: UserRoleInWorkspace,
311 405
             dbsession: Session,
312 406
             config: CFG,
407
+            # Extended params
408
+            newly_created: bool = None,
409
+            email_sent: bool = None
313 410
     )-> None:
314 411
         self.user_role = user_role
315 412
         self.dbsession = dbsession
316 413
         self.config = config
414
+        # Extended params
415
+        self.newly_created = newly_created
416
+        self.email_sent = email_sent
317 417
 
318 418
     @property
319 419
     def user_id(self) -> int:
@@ -353,7 +453,11 @@ class UserRoleWorkspaceInContext(object):
353 453
         'contributor', 'content-manager', 'workspace-manager'
354 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 462
     @property
359 463
     def user(self) -> UserInContext:
@@ -385,10 +489,11 @@ class ContentInContext(object):
385 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 493
         self.content = content
390 494
         self.dbsession = dbsession
391 495
         self.config = config
496
+        self._user = user
392 497
 
393 498
     # Default
394 499
     @property
@@ -481,6 +586,11 @@ class ContentInContext(object):
481 586
     def slug(self):
482 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 595
 class RevisionInContext(object):
486 596
     """

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

@@ -30,6 +30,7 @@ from tracim.lib.utils.translation import get_locale
30 30
 from tracim.exceptions import ContentRevisionUpdateError
31 31
 from tracim.models.meta import DeclarativeBase
32 32
 from tracim.models.auth import User
33
+from tracim.models.roles import WorkspaceRoles
33 34
 
34 35
 DEFAULT_PROPERTIES = dict(
35 36
     allowed_content=dict(
@@ -124,26 +125,27 @@ class UserRoleInWorkspace(DeclarativeBase):
124 125
     workspace = relationship('Workspace', remote_side=[Workspace.workspace_id], backref='roles', lazy='joined')
125 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 149
     # TODO - G.M - 10-04-2018 - [Cleanup] Drop this
148 150
     #
149 151
     # STYLE = dict()
@@ -170,20 +172,18 @@ class UserRoleInWorkspace(DeclarativeBase):
170 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 178
     def role_as_label(self):
174
-        return UserRoleInWorkspace.LABEL[self.role]
179
+        return self.role_object().label
175 180
 
176 181
     @classmethod
177 182
     def get_all_role_values(cls) -> typing.List[int]:
178 183
         """
179 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 188
     @classmethod
189 189
     def get_all_role_slug(cls) -> typing.List[str]:
@@ -193,13 +193,12 @@ class UserRoleInWorkspace(DeclarativeBase):
193 193
         # INFO - G.M - 25-05-2018 - Be carefull, as long as this method
194 194
         # and get_all_role_values are both used for API, this method should
195 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 202
         # self.fa_icon = UserRoleInWorkspace.ICON[role_id]
204 203
         # self.role_label = UserRoleInWorkspace.LABEL[role_id]
205 204
         # self.css_style = UserRoleInWorkspace.STYLE[role_id]
@@ -1257,7 +1256,23 @@ class Content(DeclarativeBase):
1257 1256
     def get_last_action(self) -> ActionDescription:
1258 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 1271
     def get_last_activity_date(self) -> datetime_root.datetime:
1272
+        """
1273
+        Get last activity date with complete recursive search
1274
+        :return:
1275
+        """
1261 1276
         last_revision_date = self.updated
1262 1277
         for revision in self.revisions:
1263 1278
             if revision.updated > last_revision_date:

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

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

+ 715 - 1
tracim/tests/functional/test_user.py View File

@@ -2,13 +2,727 @@
2 2
 """
3 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 17
 from tracim.tests import FunctionalTest
6 18
 from tracim.fixtures.content import Content as ContentFixtures
7 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 725
 class TestUserWorkspaceEndpoint(FunctionalTest):
11
-    # -*- coding: utf-8 -*-
12 726
     """
13 727
     Tests for /api/v2/users/{user_id}/workspaces
14 728
     """

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

@@ -2,6 +2,15 @@
2 2
 """
3 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 14
 from tracim.tests import FunctionalTest
6 15
 from tracim.tests import set_html_document_slug_to_legacy
7 16
 from tracim.fixtures.content import Content as ContentFixtures
@@ -83,6 +92,133 @@ class TestWorkspaceEndpoint(FunctionalTest):
83 92
         assert sidebar_entry['hexcolor'] == "#757575"
84 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 222
     def test_api__get_workspace__err_400__unallowed_user(self) -> None:
87 223
         """
88 224
         Check obtain workspace unreachable for user
@@ -159,7 +295,12 @@ class TestWorkspaceMembersEndpoint(FunctionalTest):
159 295
         assert user_role['role'] == 'workspace-manager'
160 296
         assert user_role['user_id'] == 1
161 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 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 304
         # TODO - G.M - 24-05-2018 - [Avatar] Replace
164 305
         # by correct value when avatar feature will be enabled
165 306
         assert user_role['user']['avatar_url'] is None
@@ -217,6 +358,259 @@ class TestWorkspaceMembersEndpoint(FunctionalTest):
217 358
         assert 'message' in res.json.keys()
218 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 615
 class TestWorkspaceContents(FunctionalTest):
222 616
     """
@@ -241,6 +635,7 @@ class TestWorkspaceContents(FunctionalTest):
241 635
         assert len(res) == 3
242 636
         content = res[0]
243 637
         assert content['content_id'] == 1
638
+        assert content['content_type'] == 'folder'
244 639
         assert content['is_archived'] is False
245 640
         assert content['is_deleted'] is False
246 641
         assert content['label'] == 'Tools'
@@ -252,6 +647,7 @@ class TestWorkspaceContents(FunctionalTest):
252 647
         assert content['workspace_id'] == 1
253 648
         content = res[1]
254 649
         assert content['content_id'] == 2
650
+        assert content['content_type'] == 'folder'
255 651
         assert content['is_archived'] is False
256 652
         assert content['is_deleted'] is False
257 653
         assert content['label'] == 'Menus'
@@ -263,6 +659,37 @@ class TestWorkspaceContents(FunctionalTest):
263 659
         assert content['workspace_id'] == 1
264 660
         content = res[2]
265 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 693
         assert content['is_archived'] is False
267 694
         assert content['is_deleted'] is False
268 695
         assert content['label'] == 'Current Menu'
@@ -274,7 +701,7 @@ class TestWorkspaceContents(FunctionalTest):
274 701
         assert content['workspace_id'] == 1
275 702
 
276 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 706
         Check obtain workspace all root contents
280 707
         """
@@ -539,6 +966,210 @@ class TestWorkspaceContents(FunctionalTest):
539 966
         assert res == []
540 967
 
541 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 1174
     def test_api__get_workspace_content__ok_200__get_all_folder_content(self):
544 1175
         """
@@ -549,6 +1180,7 @@ class TestWorkspaceContents(FunctionalTest):
549 1180
             'show_archived': 1,
550 1181
             'show_deleted': 1,
551 1182
             'show_active': 1,
1183
+            'content_type': 'any'
552 1184
         }
553 1185
         self.testapp.authorization = (
554 1186
             'Basic',

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

@@ -1,5 +1,5 @@
1 1
 # -*- coding: utf-8 -*-
2
-
2
+import datetime
3 3
 import transaction
4 4
 import pytest
5 5
 
@@ -1977,6 +1977,360 @@ class TestContentApi(DefaultTest):
1977 1977
         eq_(ActionDescription.UNDELETION, updated2.revision_type)
1978 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 2334
     def test_search_in_label(self):
1981 2335
         # HACK - D.A. - 2015-03-09
1982 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

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

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

@@ -0,0 +1,80 @@
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,17 +10,24 @@ from tracim.models.contents import GlobalStatus
10 10
 from tracim.models.contents import open_status
11 11
 from tracim.models.contents import ContentTypeLegacy as ContentType
12 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 16
 from tracim.models.context_models import ContentCreation
14 17
 from tracim.models.context_models import ContentPreviewSizedPath
15 18
 from tracim.models.context_models import RevisionPreviewSizedPath
16 19
 from tracim.models.context_models import PageQuery
17 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 24
 from tracim.models.context_models import CommentCreation
19 25
 from tracim.models.context_models import TextBasedContentUpdate
20 26
 from tracim.models.context_models import SetContentStatus
21 27
 from tracim.models.context_models import CommentPath
22 28
 from tracim.models.context_models import MoveParams
23 29
 from tracim.models.context_models import WorkspaceAndContentPath
30
+from tracim.models.context_models import WorkspaceAndUserPath
24 31
 from tracim.models.context_models import ContentFilter
25 32
 from tracim.models.context_models import LoginCredentials
26 33
 from tracim.models.data import UserRoleInWorkspace
@@ -115,6 +122,15 @@ class RevisionIdPathSchema(marshmallow.Schema):
115 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 134
 class WorkspaceAndContentIdPathSchema(
119 135
     WorkspaceIdPathSchema,
120 136
     ContentIdPathSchema
@@ -170,6 +186,25 @@ class RevisionPreviewSizedPathSchema(
170 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 208
 class CommentsPathSchema(WorkspaceAndContentIdPathSchema):
174 209
     comment_id = marshmallow.fields.Int(
175 210
         example=6,
@@ -177,6 +212,7 @@ class CommentsPathSchema(WorkspaceAndContentIdPathSchema):
177 212
         required=True,
178 213
         validate=Range(min=1, error="Value must be greater than 0"),
179 214
     )
215
+
180 216
     @post_load
181 217
     def make_path_object(self, data):
182 218
         return CommentPath(**data)
@@ -231,13 +267,76 @@ class FilterContentQuerySchema(marshmallow.Schema):
231 267
                     'to allow to show only archived documents',
232 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 276
     @post_load
236 277
     def make_content_filter(self, data):
237 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 340
 class BasicAuthSchema(marshmallow.Schema):
242 341
 
243 342
     email = marshmallow.fields.Email(
@@ -262,6 +361,23 @@ class LoginOutputHeaders(marshmallow.Schema):
262 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 381
 class NoContentSchema(marshmallow.Schema):
266 382
 
267 383
     class Meta:
@@ -329,13 +445,31 @@ class WorkspaceMemberSchema(marshmallow.Schema):
329 445
         validate=Range(min=1, error="Value must be greater than 0"),
330 446
     )
331 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 455
     class Meta:
336 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 473
 class ApplicationConfigSchema(marshmallow.Schema):
340 474
     pass
341 475
     #  TODO - G.M - 24-05-2018 - Set this
@@ -497,6 +631,12 @@ class ContentDigestSchema(marshmallow.Schema):
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 641
 # Content
502 642
 #####

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

@@ -1,9 +1,8 @@
1 1
 from pyramid.config import Configurator
2
-from sqlalchemy.orm.exc import NoResultFound
3 2
 
3
+from tracim.lib.core.content import ContentApi
4 4
 from tracim.lib.utils.authorization import require_same_user_or_profile
5 5
 from tracim.models import Group
6
-from tracim.models.context_models import WorkspaceInContext
7 6
 
8 7
 try:  # Python 3.5+
9 8
     from http import HTTPStatus
@@ -12,13 +11,17 @@ except ImportError:
12 11
 
13 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 14
 from tracim.lib.core.workspace import WorkspaceApi
19 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 23
 from tracim.views.core_api.schemas import WorkspaceDigestSchema
24
+from tracim.models.contents import ContentTypeLegacy as ContentType
22 25
 
23 26
 USER_ENDPOINTS_TAG = 'Users'
24 27
 
@@ -35,23 +38,160 @@ class UserController(Controller):
35 38
         """
36 39
         app_config = request.registry.settings['CFG']
37 40
         wapi = WorkspaceApi(
38
-            current_user=request.current_user,  # User
41
+            current_user=request.candidate_user,  # User
39 42
             session=request.dbsession,
40 43
             config=app_config,
41 44
         )
42 45
         
43 46
         workspaces = wapi.get_all_for_user(request.candidate_user)
44 47
         return [
45
-            WorkspaceInContext(workspace, request.dbsession, app_config)
48
+            wapi.get_workspace_with_context(workspace)
46 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 172
     def bind(self, configurator: Configurator) -> None:
50 173
         """
51 174
         Create all routes and views using pyramid configurator
52 175
         for this controller
53 176
         """
54 177
 
55
-        # Applications
178
+        # user worskpace
56 179
         configurator.add_route('user_workspace', '/users/{user_id}/workspaces', request_method='GET')  # nopep8
57 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,6 +1,10 @@
1 1
 import typing
2 2
 import transaction
3 3
 from pyramid.config import Configurator
4
+
5
+from tracim.lib.core.user import UserApi
6
+from tracim.models.roles import WorkspaceRoles
7
+
4 8
 try:  # Python 3.5+
5 9
     from http import HTTPStatus
6 10
 except ImportError:
@@ -12,17 +16,28 @@ from tracim.lib.core.workspace import WorkspaceApi
12 16
 from tracim.lib.core.content import ContentApi
13 17
 from tracim.lib.core.userworkspace import RoleApi
14 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 21
 from tracim.lib.utils.authorization import require_candidate_workspace_role
16 22
 from tracim.models.data import UserRoleInWorkspace
17 23
 from tracim.models.data import ActionDescription
18 24
 from tracim.models.context_models import UserRoleWorkspaceInContext
19 25
 from tracim.models.context_models import ContentInContext
20 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 30
 from tracim.exceptions import ContentNotFound
22 31
 from tracim.exceptions import WorkspacesDoNotMatch
23 32
 from tracim.exceptions import ParentNotFound
24 33
 from tracim.views.controllers import Controller
25 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 41
 from tracim.views.core_api.schemas import ContentMoveSchema
27 42
 from tracim.views.core_api.schemas import NoContentSchema
28 43
 from tracim.views.core_api.schemas import ContentCreationSchema
@@ -47,7 +62,6 @@ class WorkspaceController(Controller):
47 62
         """
48 63
         Get workspace informations
49 64
         """
50
-        wid = hapic_data.path['workspace_id']
51 65
         app_config = request.registry.settings['CFG']
52 66
         wapi = WorkspaceApi(
53 67
             current_user=request.current_user,  # User
@@ -57,6 +71,52 @@ class WorkspaceController(Controller):
57 71
         return wapi.get_workspace_with_context(request.current_workspace)
58 72
 
59 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 120
     @require_workspace_role(UserRoleInWorkspace.READER)
61 121
     @hapic.input_path(WorkspaceIdPathSchema())
62 122
     @hapic.output_body(WorkspaceMemberSchema(many=True))
@@ -83,6 +143,96 @@ class WorkspaceController(Controller):
83 143
         ]
84 144
 
85 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 236
     @require_workspace_role(UserRoleInWorkspace.READER)
87 237
     @hapic.input_path(WorkspaceIdPathSchema())
88 238
     @hapic.input_query(FilterContentQuerySchema())
@@ -109,6 +259,7 @@ class WorkspaceController(Controller):
109 259
         contents = api.get_all(
110 260
             parent_id=content_filter.parent_id,
111 261
             workspace=request.current_workspace,
262
+            content_type=content_filter.content_type or ContentType.Any,
112 263
         )
113 264
         contents = [
114 265
             api.get_content_in_context(content) for content in contents
@@ -167,7 +318,7 @@ class WorkspaceController(Controller):
167 318
             context,
168 319
             request: TracimRequest,
169 320
             hapic_data=None,
170
-    ) -> typing.List[ContentInContext]:
321
+    ) -> ContentInContext:
171 322
         """
172 323
         move a content
173 324
         """
@@ -216,7 +367,7 @@ class WorkspaceController(Controller):
216 367
             context,
217 368
             request: TracimRequest,
218 369
             hapic_data=None,
219
-    ) -> typing.List[ContentInContext]:
370
+    ) -> None:
220 371
         """
221 372
         delete a content
222 373
         """
@@ -248,7 +399,7 @@ class WorkspaceController(Controller):
248 399
             context,
249 400
             request: TracimRequest,
250 401
             hapic_data=None,
251
-    ) -> typing.List[ContentInContext]:
402
+    ) -> None:
252 403
         """
253 404
         undelete a content
254 405
         """
@@ -281,7 +432,7 @@ class WorkspaceController(Controller):
281 432
             context,
282 433
             request: TracimRequest,
283 434
             hapic_data=None,
284
-    ) -> typing.List[ContentInContext]:
435
+    ) -> None:
285 436
         """
286 437
         archive a content
287 438
         """
@@ -292,7 +443,7 @@ class WorkspaceController(Controller):
292 443
             session=request.dbsession,
293 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 447
         with new_revision(
297 448
                 session=request.dbsession,
298 449
                 tm=transaction.manager,
@@ -310,7 +461,7 @@ class WorkspaceController(Controller):
310 461
             context,
311 462
             request: TracimRequest,
312 463
             hapic_data=None,
313
-    ) -> typing.List[ContentInContext]:
464
+    ) -> None:
314 465
         """
315 466
         unarchive a content
316 467
         """
@@ -343,9 +494,21 @@ class WorkspaceController(Controller):
343 494
         # Workspace
344 495
         configurator.add_route('workspace', '/workspaces/{workspace_id}', request_method='GET')  # nopep8
345 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 503
         # Workspace Members (Roles)
347 504
         configurator.add_route('workspace_members', '/workspaces/{workspace_id}/members', request_method='GET')  # nopep8
348 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 512
         # Workspace Content
350 513
         configurator.add_route('workspace_content', '/workspaces/{workspace_id}/contents', request_method='GET')  # nopep8
351 514
         configurator.add_view(self.workspace_content, route_name='workspace_content')  # nopep8