Browse Source

Merge pull request #100 from tracim/feature/671_read_unread_endpoints

inkhey 5 years ago
parent
commit
677832d7ad
No account linked to committer's email

+ 217 - 174
tracim/lib/core/content.py View File

@@ -163,7 +163,7 @@ class ContentApi(object):
163 163
             self._show_temporary = previous_show_temporary
164 164
 
165 165
     def get_content_in_context(self, content: Content) -> ContentInContext:
166
-        return ContentInContext(content, self._session, self._config)
166
+        return ContentInContext(content, self._session, self._config, self._user)  # nopep8
167 167
 
168 168
     def get_revision_in_context(self, revision: ContentRevisionRO) -> RevisionInContext:  # nopep8
169 169
         # TODO - G.M - 2018-06-173 - create revision in context object
@@ -343,56 +343,57 @@ class ContentApi(object):
343 343
     ) -> Query:
344 344
         return self._base_query(workspace)
345 345
 
346
-    def get_child_folders(self, parent: Content=None, workspace: Workspace=None, filter_by_allowed_content_types: list=[], removed_item_ids: list=[], allowed_node_types=None) -> typing.List[Content]:
347
-        """
348
-        This method returns child items (folders or items) for left bar treeview.
349
-
350
-        :param parent:
351
-        :param workspace:
352
-        :param filter_by_allowed_content_types:
353
-        :param removed_item_ids:
354
-        :param allowed_node_types: This parameter allow to hide folders for which the given type of content is not allowed.
355
-               For example, if you want to move a Page from a folder to another, you should show only folders that accept pages
356
-        :return:
357
-        """
358
-        filter_by_allowed_content_types = filter_by_allowed_content_types or []  # FDV
359
-        removed_item_ids = removed_item_ids or []  # FDV
360
-
361
-        if not allowed_node_types:
362
-            allowed_node_types = [ContentType.Folder]
363
-        elif allowed_node_types==ContentType.Any:
364
-            allowed_node_types = ContentType.all()
365
-
366
-        parent_id = parent.content_id if parent else None
367
-        folders = self._base_query(workspace).\
368
-            filter(Content.parent_id==parent_id).\
369
-            filter(Content.type.in_(allowed_node_types)).\
370
-            filter(Content.content_id.notin_(removed_item_ids)).\
371
-            all()
372
-
373
-        if not filter_by_allowed_content_types or \
374
-                        len(filter_by_allowed_content_types)<=0:
375
-            # Standard case for the left treeview: we want to show all contents
376
-            # in the left treeview... so we still filter because for example
377
-            # comments must not appear in the treeview
378
-            return [folder for folder in folders \
379
-                    if folder.type in ContentType.allowed_types_for_folding()]
380
-
381
-        # Now this is a case of Folders only (used for moving content)
382
-        # When moving a content, you must get only folders that allow to be filled
383
-        # with the type of content you want to move
384
-        result = []
385
-        for folder in folders:
386
-            for allowed_content_type in filter_by_allowed_content_types:
387
-
388
-                is_folder = folder.type == ContentType.Folder
389
-                content_type__allowed = folder.properties['allowed_content'][allowed_content_type] == True
390
-
391
-                if is_folder and content_type__allowed:
392
-                    result.append(folder)
393
-                    break
394
-
395
-        return result
346
+    # TODO - G.M - 2018-07-17 - [Cleanup] Drop this method if unneeded
347
+    # 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]:
348
+    #     """
349
+    #     This method returns child items (folders or items) for left bar treeview.
350
+    # 
351
+    #     :param parent:
352
+    #     :param workspace:
353
+    #     :param filter_by_allowed_content_types:
354
+    #     :param removed_item_ids:
355
+    #     :param allowed_node_types: This parameter allow to hide folders for which the given type of content is not allowed.
356
+    #            For example, if you want to move a Page from a folder to another, you should show only folders that accept pages
357
+    #     :return:
358
+    #     """
359
+    #     filter_by_allowed_content_types = filter_by_allowed_content_types or []  # FDV
360
+    #     removed_item_ids = removed_item_ids or []  # FDV
361
+    # 
362
+    #     if not allowed_node_types:
363
+    #         allowed_node_types = [ContentType.Folder]
364
+    #     elif allowed_node_types==ContentType.Any:
365
+    #         allowed_node_types = ContentType.all()
366
+    # 
367
+    #     parent_id = parent.content_id if parent else None
368
+    #     folders = self._base_query(workspace).\
369
+    #         filter(Content.parent_id==parent_id).\
370
+    #         filter(Content.type.in_(allowed_node_types)).\
371
+    #         filter(Content.content_id.notin_(removed_item_ids)).\
372
+    #         all()
373
+    # 
374
+    #     if not filter_by_allowed_content_types or \
375
+    #                     len(filter_by_allowed_content_types)<=0:
376
+    #         # Standard case for the left treeview: we want to show all contents
377
+    #         # in the left treeview... so we still filter because for example
378
+    #         # comments must not appear in the treeview
379
+    #         return [folder for folder in folders \
380
+    #                 if folder.type in ContentType.allowed_types_for_folding()]
381
+    # 
382
+    #     # Now this is a case of Folders only (used for moving content)
383
+    #     # When moving a content, you must get only folders that allow to be filled
384
+    #     # with the type of content you want to move
385
+    #     result = []
386
+    #     for folder in folders:
387
+    #         for allowed_content_type in filter_by_allowed_content_types:
388
+    # 
389
+    #             is_folder = folder.type == ContentType.Folder
390
+    #             content_type__allowed = folder.properties['allowed_content'][allowed_content_type] == True
391
+    # 
392
+    #             if is_folder and content_type__allowed:
393
+    #                 result.append(folder)
394
+    #                 break
395
+    # 
396
+    #     return result
396 397
 
397 398
     def create(self, content_type: str, workspace: Workspace, parent: Content=None, label: str ='', filename: str = '', do_save=False, is_temporary: bool=False, do_notify=True) -> Content:
398 399
         # TODO - G.M - 2018-07-16 - raise Exception instead of assert
@@ -724,10 +725,21 @@ class ContentApi(object):
724 725
             ),
725 726
         ))
726 727
 
727
-    def get_all(self, parent_id: int=None, content_type: str=ContentType.Any, workspace: Workspace=None) -> typing.List[Content]:
728
-        assert parent_id is None or isinstance(parent_id, int) # DYN_REMOVE
729
-        if not content_type:
730
-            content_type = ContentType.Any
728
+    def _get_all_query(
729
+        self,
730
+        parent_id: int = None,
731
+        content_type: str = ContentType.Any,
732
+        workspace: Workspace = None
733
+    ) -> Query:
734
+        """
735
+        Extended filter for better "get all data" query
736
+        :param parent_id: filter by parent_id
737
+        :param content_type: filter by content_type slug
738
+        :param workspace: filter by workspace
739
+        :return:
740
+        """
741
+        assert parent_id is None or isinstance(parent_id, int)
742
+        assert content_type is not None
731 743
         resultset = self._base_query(workspace)
732 744
 
733 745
         if content_type!=ContentType.Any:
@@ -740,28 +752,32 @@ class ContentApi(object):
740 752
             resultset = resultset.filter(Content.parent_id==parent_id)
741 753
         if parent_id == 0 or parent_id is False:
742 754
             resultset = resultset.filter(Content.parent_id == None)
743
-        # parent_id == None give all contents
744
-
745
-        return resultset.all()
746
-
747
-    def get_children(self, parent_id: int, content_types: list, workspace: Workspace=None) -> typing.List[Content]:
748
-        """
749
-        Return parent_id childs of given content_types
750
-        :param parent_id: parent id
751
-        :param content_types: list of types
752
-        :param workspace: workspace filter
753
-        :return: list of content
754
-        """
755
-        resultset = self._base_query(workspace)
756
-        resultset = resultset.filter(Content.type.in_(content_types))
757 755
 
758
-        if parent_id:
759
-            resultset = resultset.filter(Content.parent_id==parent_id)
760
-        if parent_id is False:
761
-            resultset = resultset.filter(Content.parent_id == None)
756
+        return resultset
762 757
 
763
-        return resultset.all()
758
+    def get_all(self, parent_id: int=None, content_type: str=ContentType.Any, workspace: Workspace=None) -> typing.List[Content]:
759
+        return self._get_all_query(parent_id, content_type, workspace).all()
764 760
 
761
+    # TODO - G.M - 2018-07-17 - [Cleanup] Drop this method if unneeded
762
+    # def get_children(self, parent_id: int, content_types: list, workspace: Workspace=None) -> typing.List[Content]:
763
+    #     """
764
+    #     Return parent_id childs of given content_types
765
+    #     :param parent_id: parent id
766
+    #     :param content_types: list of types
767
+    #     :param workspace: workspace filter
768
+    #     :return: list of content
769
+    #     """
770
+    #     resultset = self._base_query(workspace)
771
+    #     resultset = resultset.filter(Content.type.in_(content_types))
772
+    #
773
+    #     if parent_id:
774
+    #         resultset = resultset.filter(Content.parent_id==parent_id)
775
+    #     if parent_id is False:
776
+    #         resultset = resultset.filter(Content.parent_id == None)
777
+    #
778
+    #     return resultset.all()
779
+
780
+    # TODO - G.M - 2018-07-17 - [Cleanup] Drop this method if unneeded
765 781
     # TODO find an other name to filter on is_deleted / is_archived
766 782
     def get_all_with_filter(self, parent_id: int=None, content_type: str=ContentType.Any, workspace: Workspace=None) -> typing.List[Content]:
767 783
         assert parent_id is None or isinstance(parent_id, int) # DYN_REMOVE
@@ -781,107 +797,136 @@ class ContentApi(object):
781 797
 
782 798
         return resultset.all()
783 799
 
784
-    def get_all_without_exception(self, content_type: str, workspace: Workspace=None) -> typing.List[Content]:
785
-        assert content_type is not None# DYN_REMOVE
786
-
787
-        resultset = self._base_query(workspace)
788
-
789
-        if content_type != ContentType.Any:
790
-            resultset = resultset.filter(Content.type==content_type)
791
-
792
-        return resultset.all()
793
-
794
-    def get_last_active(self, parent_id: int, content_type: str, workspace: Workspace=None, limit=10) -> typing.List[Content]:
795
-        assert parent_id is None or isinstance(parent_id, int) # DYN_REMOVE
796
-        assert content_type is not None# DYN_REMOVE
797
-        assert isinstance(content_type, str) # DYN_REMOVE
798
-
799
-        resultset = self._base_query(workspace) \
800
-            .filter(Content.workspace_id == Workspace.workspace_id) \
801
-            .filter(Workspace.is_deleted.is_(False)) \
802
-            .order_by(desc(Content.updated))
803
-
804
-        if content_type!=ContentType.Any:
805
-            resultset = resultset.filter(Content.type==content_type)
806
-
807
-        if parent_id:
808
-            resultset = resultset.filter(Content.parent_id==parent_id)
809
-
810
-        result = []
811
-        for item in resultset:
812
-            new_item = None
813
-            if ContentType.Comment == item.type:
814
-                new_item = item.parent
815
-            else:
816
-                new_item = item
817
-
818
-            # INFO - D.A. - 2015-05-20
819
-            # We do not want to show only one item if the last 10 items are
820
-            # comments about one thread for example
821
-            if new_item not in result:
822
-                result.append(new_item)
823
-
824
-            if len(result) >= limit:
825
-                break
826
-
827
-        return result
828
-
829
-    def get_last_unread(self, parent_id: int, content_type: str,
830
-                        workspace: Workspace=None, limit=10) -> typing.List[Content]:
831
-        assert parent_id is None or isinstance(parent_id, int) # DYN_REMOVE
832
-        assert content_type is not None# DYN_REMOVE
833
-        assert isinstance(content_type, str) # DYN_REMOVE
834
-
835
-        read_revision_ids = self._session.query(RevisionReadStatus.revision_id) \
836
-            .filter(RevisionReadStatus.user_id==self._user_id)
837
-
838
-        not_read_revisions = self._revisions_base_query(workspace) \
839
-            .filter(~ContentRevisionRO.revision_id.in_(read_revision_ids)) \
840
-            .filter(ContentRevisionRO.workspace_id == Workspace.workspace_id) \
841
-            .filter(Workspace.is_deleted.is_(False)) \
842
-            .subquery()
800
+    # TODO - G.M - 2018-07-17 - [Cleanup] Drop this method if unneeded
801
+    # def get_all_without_exception(self, content_type: str, workspace: Workspace=None) -> typing.List[Content]:
802
+    #     assert content_type is not None# DYN_REMOVE
803
+    #
804
+    #     resultset = self._base_query(workspace)
805
+    #
806
+    #     if content_type != ContentType.Any:
807
+    #         resultset = resultset.filter(Content.type==content_type)
808
+    #
809
+    #     return resultset.all()
810
+
811
+    def get_last_active(
812
+            self,
813
+            workspace: Workspace=None,
814
+            limit: typing.Optional[int]=None,
815
+            before_datetime: typing.Optional[datetime.datetime]= None,
816
+            content_ids: typing.Optional[typing.List[int]] = None,
817
+    ) -> typing.List[Content]:
818
+        """
819
+        get contents list sorted by last update
820
+        (last modification of content itself or one of this comment)
821
+        :param workspace: Workspace to check
822
+        :param limit: maximum number of elements to return
823
+        :param before_datetime: date from where we check older content.
824
+        :param content_ids: restrict selection to some content ids and
825
+        related Comments
826
+        :return: list of content
827
+        """
843 828
 
844
-        not_read_content_ids_query = self._session.query(
845
-            distinct(not_read_revisions.c.content_id)
829
+        resultset = self._get_all_query(
830
+            workspace=workspace,
846 831
         )
847
-        not_read_content_ids = list(map(
848
-            itemgetter(0),
849
-            not_read_content_ids_query,
850
-        ))
832
+        if content_ids:
833
+            resultset = resultset.filter(
834
+                or_(
835
+                    Content.content_id.in_(content_ids),
836
+                    and_(
837
+                        Content.parent_id.in_(content_ids),
838
+                        Content.type == ContentType.Comment
839
+                    )
840
+                )
841
+            )
851 842
 
852
-        not_read_contents = self._base_query(workspace) \
853
-            .filter(Content.content_id.in_(not_read_content_ids)) \
854
-            .order_by(desc(Content.updated))
843
+        resultset = resultset.order_by(desc(Content.updated))
855 844
 
856
-        if content_type != ContentType.Any:
857
-            not_read_contents = not_read_contents.filter(
858
-                Content.type==content_type)
859
-        else:
860
-            not_read_contents = not_read_contents.filter(
861
-                Content.type!=ContentType.Folder)
862
-
863
-        if parent_id:
864
-            not_read_contents = not_read_contents.filter(
865
-                Content.parent_id==parent_id)
866
-
867
-        result = []
868
-        for item in not_read_contents:
869
-            new_item = None
870
-            if ContentType.Comment == item.type:
871
-                new_item = item.parent
845
+        active_contents = []
846
+        too_recent_content = []
847
+        for content in resultset:
848
+            related_active_content = None
849
+            if ContentType.Comment == content.type:
850
+                related_active_content = content.parent
872 851
             else:
873
-                new_item = item
852
+                related_active_content = content
874 853
 
854
+            if not before_datetime:
855
+                before_datetime = datetime.datetime.now()
875 856
             # INFO - D.A. - 2015-05-20
876 857
             # We do not want to show only one item if the last 10 items are
877 858
             # comments about one thread for example
878
-            if new_item not in result:
879
-                result.append(new_item)
880
-
881
-            if len(result) >= limit:
859
+            if related_active_content not in active_contents and related_active_content not in too_recent_content:  # nopep8
860
+                # we verify that content is old enough
861
+                if content.updated < before_datetime:
862
+                    active_contents.append(related_active_content)
863
+                else:
864
+                    too_recent_content.append(related_active_content)
865
+
866
+            if limit and len(active_contents) >= limit:
882 867
                 break
883 868
 
884
-        return result
869
+        return active_contents
870
+
871
+    # TODO - G.M - 2018-07-19 - Find a way to update this method to something
872
+    # usable and efficient for tracim v2 to get content with read/unread status
873
+    # instead of relying on has_new_information_for()
874
+    # def get_last_unread(self, parent_id: typing.Optional[int], content_type: str,
875
+    #                     workspace: Workspace=None, limit=10) -> typing.List[Content]:
876
+    #     assert parent_id is None or isinstance(parent_id, int) # DYN_REMOVE
877
+    #     assert content_type is not None# DYN_REMOVE
878
+    #     assert isinstance(content_type, str) # DYN_REMOVE
879
+    #
880
+    #     read_revision_ids = self._session.query(RevisionReadStatus.revision_id) \
881
+    #         .filter(RevisionReadStatus.user_id==self._user_id)
882
+    #
883
+    #     not_read_revisions = self._revisions_base_query(workspace) \
884
+    #         .filter(~ContentRevisionRO.revision_id.in_(read_revision_ids)) \
885
+    #         .filter(ContentRevisionRO.workspace_id == Workspace.workspace_id) \
886
+    #         .filter(Workspace.is_deleted.is_(False)) \
887
+    #         .subquery()
888
+    #
889
+    #     not_read_content_ids_query = self._session.query(
890
+    #         distinct(not_read_revisions.c.content_id)
891
+    #     )
892
+    #     not_read_content_ids = list(map(
893
+    #         itemgetter(0),
894
+    #         not_read_content_ids_query,
895
+    #     ))
896
+    #
897
+    #     not_read_contents = self._base_query(workspace) \
898
+    #         .filter(Content.content_id.in_(not_read_content_ids)) \
899
+    #         .order_by(desc(Content.updated))
900
+    #
901
+    #     if content_type != ContentType.Any:
902
+    #         not_read_contents = not_read_contents.filter(
903
+    #             Content.type==content_type)
904
+    #     else:
905
+    #         not_read_contents = not_read_contents.filter(
906
+    #             Content.type!=ContentType.Folder)
907
+    #
908
+    #     if parent_id:
909
+    #         not_read_contents = not_read_contents.filter(
910
+    #             Content.parent_id==parent_id)
911
+    #
912
+    #     result = []
913
+    #     for item in not_read_contents:
914
+    #         new_item = None
915
+    #         if ContentType.Comment == item.type:
916
+    #             new_item = item.parent
917
+    #         else:
918
+    #             new_item = item
919
+    #
920
+    #         # INFO - D.A. - 2015-05-20
921
+    #         # We do not want to show only one item if the last 10 items are
922
+    #         # comments about one thread for example
923
+    #         if new_item not in result:
924
+    #             result.append(new_item)
925
+    #
926
+    #         if len(result) >= limit:
927
+    #             break
928
+    #
929
+    #     return result
885 930
 
886 931
     def set_allowed_content(self, folder: Content, allowed_content_dict:dict):
887 932
         """
@@ -1064,11 +1109,7 @@ class ContentApi(object):
1064 1109
                        do_flush: bool=True,
1065 1110
                        recursive: bool=True
1066 1111
                        ):
1067
-
1068
-        itemset = self.get_last_unread(None, ContentType.Any)
1069
-
1070
-        for item in itemset:
1071
-            self.mark_read(item, read_datetime, do_flush, recursive)
1112
+        return self.mark_read__workspace(None, read_datetime, do_flush, recursive) # nopep8
1072 1113
 
1073 1114
     def mark_read__workspace(self,
1074 1115
                        workspace : Workspace,
@@ -1076,11 +1117,10 @@ class ContentApi(object):
1076 1117
                        do_flush: bool=True,
1077 1118
                        recursive: bool=True
1078 1119
                        ):
1079
-
1080
-        itemset = self.get_last_unread(None, ContentType.Any, workspace)
1081
-
1120
+        itemset = self.get_last_active(workspace)
1082 1121
         for item in itemset:
1083
-            self.mark_read(item, read_datetime, do_flush, recursive)
1122
+            if item.has_new_information_for(self._user):
1123
+                self.mark_read(item, read_datetime, do_flush, recursive)
1084 1124
 
1085 1125
     def mark_read(self, content: Content,
1086 1126
                   read_datetime: datetime=None,
@@ -1135,7 +1175,10 @@ class ContentApi(object):
1135 1175
             .filter(ContentRevisionRO.content_id==content.content_id).all()
1136 1176
 
1137 1177
         for revision in revisions:
1138
-            del revision.read_by[self._user]
1178
+            try:
1179
+                del revision.read_by[self._user]
1180
+            except KeyError:
1181
+                pass
1139 1182
 
1140 1183
         for child in content.get_valid_children():
1141 1184
             self.mark_unread(child, do_flush=False)

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

@@ -55,6 +55,16 @@ class WorkspaceAndUserPath(object):
55 55
         self.user_id = workspace_id
56 56
 
57 57
 
58
+class UserWorkspaceAndContentPath(object):
59
+    """
60
+    Paths params with user_id, workspace id and content_id model
61
+    """
62
+    def __init__(self, user_id: int, workspace_id: int, content_id: int) -> None:  # nopep8
63
+        self.content_id = content_id
64
+        self.workspace_id = workspace_id
65
+        self.user_id = user_id
66
+
67
+
58 68
 class CommentPath(object):
59 69
     """
60 70
     Paths params with workspace id and content_id and comment_id model
@@ -76,19 +86,43 @@ class ContentFilter(object):
76 86
     """
77 87
     def __init__(
78 88
             self,
89
+            workspace_id: int = None,
79 90
             parent_id: int = None,
80 91
             show_archived: int = 0,
81 92
             show_deleted: int = 0,
82 93
             show_active: int = 1,
83 94
             content_type: str = None,
95
+            offset: int = None,
96
+            limit: int = None,
84 97
     ) -> None:
85 98
         self.parent_id = parent_id
99
+        self.workspace_id = workspace_id
86 100
         self.show_archived = bool(show_archived)
87 101
         self.show_deleted = bool(show_deleted)
88 102
         self.show_active = bool(show_active)
103
+        self.limit = limit
104
+        self.offset = offset
89 105
         self.content_type = content_type
90 106
 
91 107
 
108
+class ActiveContentFilter(object):
109
+    def __init__(
110
+            self,
111
+            limit: int = None,
112
+            before_datetime: datetime = None,
113
+    ):
114
+        self.limit = limit
115
+        self.before_datetime = before_datetime
116
+
117
+
118
+class ContentIdsQuery(object):
119
+    def __init__(
120
+            self,
121
+            contents_ids: typing.List[int] = None,
122
+    ):
123
+        self.contents_ids = contents_ids
124
+
125
+
92 126
 class RoleUpdate(object):
93 127
     """
94 128
     Update role
@@ -315,7 +349,7 @@ class UserRoleWorkspaceInContext(object):
315 349
             dbsession: Session,
316 350
             config: CFG,
317 351
             # Extended params
318
-            newly_created:bool = None,
352
+            newly_created: bool = None,
319 353
             email_sent: bool = None
320 354
     )-> None:
321 355
         self.user_role = user_role
@@ -399,10 +433,11 @@ class ContentInContext(object):
399 433
     Interface to get Content data and Content data related to context.
400 434
     """
401 435
 
402
-    def __init__(self, content: Content, dbsession: Session, config: CFG):
436
+    def __init__(self, content: Content, dbsession: Session, config: CFG, user: User=None):  # nopep8
403 437
         self.content = content
404 438
         self.dbsession = dbsession
405 439
         self.config = config
440
+        self._user = user
406 441
 
407 442
     # Default
408 443
     @property
@@ -495,6 +530,11 @@ class ContentInContext(object):
495 530
     def slug(self):
496 531
         return slugify(self.content.label)
497 532
 
533
+    @property
534
+    def read_by_user(self):
535
+        assert self._user
536
+        return not self.content.has_new_information_for(self._user)
537
+
498 538
 
499 539
 class RevisionInContext(object):
500 540
     """

+ 16 - 0
tracim/models/data.py View File

@@ -1256,7 +1256,23 @@ class Content(DeclarativeBase):
1256 1256
     def get_last_action(self) -> ActionDescription:
1257 1257
         return ActionDescription(self.revision_type)
1258 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
+
1259 1271
     def get_last_activity_date(self) -> datetime_root.datetime:
1272
+        """
1273
+        Get last activity date with complete recursive search
1274
+        :return:
1275
+        """
1260 1276
         last_revision_date = self.updated
1261 1277
         for revision in self.revisions:
1262 1278
             if revision.updated > last_revision_date:

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

+ 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

+ 58 - 0
tracim/views/core_api/schemas.py View File

@@ -10,6 +10,9 @@ 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 WorkspaceMemberInvitation
15 18
 from tracim.models.context_models import WorkspaceUpdate
@@ -129,6 +132,25 @@ class WorkspaceAndContentIdPathSchema(
129 132
         return WorkspaceAndContentPath(**data)
130 133
 
131 134
 
135
+class UserWorkspaceAndContentIdPathSchema(
136
+    UserIdPathSchema,
137
+    WorkspaceIdPathSchema,
138
+    ContentIdPathSchema,
139
+):
140
+    @post_load
141
+    def make_path_object(self, data):
142
+        return UserWorkspaceAndContentPath(**data)
143
+
144
+
145
+class UserWorkspaceIdPathSchema(
146
+    UserIdPathSchema,
147
+    WorkspaceIdPathSchema,
148
+):
149
+    @post_load
150
+    def make_path_object(self, data):
151
+        return WorkspaceAndUserPath(**data)
152
+
153
+
132 154
 class CommentsPathSchema(WorkspaceAndContentIdPathSchema):
133 155
     comment_id = marshmallow.fields.Int(
134 156
         example=6,
@@ -187,6 +209,36 @@ class FilterContentQuerySchema(marshmallow.Schema):
187 209
     @post_load
188 210
     def make_content_filter(self, data):
189 211
         return ContentFilter(**data)
212
+
213
+
214
+class ActiveContentFilterQuerySchema(marshmallow.Schema):
215
+    limit = marshmallow.fields.Int(
216
+        example=2,
217
+        default=0,
218
+        description='if 0 or not set, return all elements, else return only '
219
+                    'the first limit elem (according to offset)',
220
+        validate=Range(min=0, error="Value must be positive or 0"),
221
+    )
222
+    before_datetime = marshmallow.fields.DateTime(
223
+        format=DATETIME_FORMAT,
224
+        description='return only content lastly updated before this date',
225
+    )
226
+    @post_load
227
+    def make_content_filter(self, data):
228
+        return ActiveContentFilter(**data)
229
+
230
+
231
+class ContentIdsQuerySchema(marshmallow.Schema):
232
+    contents_ids = marshmallow.fields.List(
233
+        marshmallow.fields.Int(
234
+            example=6,
235
+            validate=Range(min=1, error="Value must be greater than 0"),
236
+        )
237
+    )
238
+    @post_load
239
+    def make_contents_ids(self, data):
240
+        return ContentIdsQuery(**data)
241
+
190 242
 ###
191 243
 
192 244
 
@@ -512,6 +564,12 @@ class ContentDigestSchema(marshmallow.Schema):
512 564
     )
513 565
 
514 566
 
567
+class ReadStatusSchema(marshmallow.Schema):
568
+    content_id = marshmallow.fields.Int(
569
+        example=6,
570
+        validate=Range(min=1, error="Value must be greater than 0"),
571
+    )
572
+    read_by_user = marshmallow.fields.Bool(example=False, default=False)
515 573
 #####
516 574
 # Content
517 575
 #####

+ 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

+ 1 - 1
tracim/views/core_api/workspace_controller.py View File

@@ -259,7 +259,7 @@ class WorkspaceController(Controller):
259 259
         contents = api.get_all(
260 260
             parent_id=content_filter.parent_id,
261 261
             workspace=request.current_workspace,
262
-            content_type=content_filter.content_type,
262
+            content_type=content_filter.content_type or ContentType.Any,
263 263
         )
264 264
         contents = [
265 265
             api.get_content_in_context(content) for content in contents