Parcourir la source

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

inkhey il y a 6 ans
Parent
révision
677832d7ad
Aucun compte lié à l'adresse email de l'auteur

+ 217 - 174
tracim/lib/core/content.py Voir le fichier

163
             self._show_temporary = previous_show_temporary
163
             self._show_temporary = previous_show_temporary
164
 
164
 
165
     def get_content_in_context(self, content: Content) -> ContentInContext:
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
     def get_revision_in_context(self, revision: ContentRevisionRO) -> RevisionInContext:  # nopep8
168
     def get_revision_in_context(self, revision: ContentRevisionRO) -> RevisionInContext:  # nopep8
169
         # TODO - G.M - 2018-06-173 - create revision in context object
169
         # TODO - G.M - 2018-06-173 - create revision in context object
343
     ) -> Query:
343
     ) -> Query:
344
         return self._base_query(workspace)
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
     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
     def create(self, content_type: str, workspace: Workspace, parent: Content=None, label: str ='', filename: str = '', do_save=False, is_temporary: bool=False, do_notify=True) -> Content:
398
         # TODO - G.M - 2018-07-16 - raise Exception instead of assert
399
         # TODO - G.M - 2018-07-16 - raise Exception instead of assert
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
         resultset = self._base_query(workspace)
743
         resultset = self._base_query(workspace)
732
 
744
 
733
         if content_type!=ContentType.Any:
745
         if content_type!=ContentType.Any:
740
             resultset = resultset.filter(Content.parent_id==parent_id)
752
             resultset = resultset.filter(Content.parent_id==parent_id)
741
         if parent_id == 0 or parent_id is False:
753
         if parent_id == 0 or parent_id is False:
742
             resultset = resultset.filter(Content.parent_id == None)
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
     # TODO find an other name to filter on is_deleted / is_archived
781
     # TODO find an other name to filter on is_deleted / is_archived
766
     def get_all_with_filter(self, parent_id: int=None, content_type: str=ContentType.Any, workspace: Workspace=None) -> typing.List[Content]:
782
     def get_all_with_filter(self, parent_id: int=None, content_type: str=ContentType.Any, workspace: Workspace=None) -> typing.List[Content]:
767
         assert parent_id is None or isinstance(parent_id, int) # DYN_REMOVE
783
         assert parent_id is None or isinstance(parent_id, int) # DYN_REMOVE
781
 
797
 
782
         return resultset.all()
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
             else:
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
             # INFO - D.A. - 2015-05-20
856
             # INFO - D.A. - 2015-05-20
876
             # We do not want to show only one item if the last 10 items are
857
             # We do not want to show only one item if the last 10 items are
877
             # comments about one thread for example
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
                 break
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
     def set_allowed_content(self, folder: Content, allowed_content_dict:dict):
931
     def set_allowed_content(self, folder: Content, allowed_content_dict:dict):
887
         """
932
         """
1064
                        do_flush: bool=True,
1109
                        do_flush: bool=True,
1065
                        recursive: bool=True
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
     def mark_read__workspace(self,
1114
     def mark_read__workspace(self,
1074
                        workspace : Workspace,
1115
                        workspace : Workspace,
1076
                        do_flush: bool=True,
1117
                        do_flush: bool=True,
1077
                        recursive: bool=True
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
         for item in itemset:
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
     def mark_read(self, content: Content,
1125
     def mark_read(self, content: Content,
1086
                   read_datetime: datetime=None,
1126
                   read_datetime: datetime=None,
1135
             .filter(ContentRevisionRO.content_id==content.content_id).all()
1175
             .filter(ContentRevisionRO.content_id==content.content_id).all()
1136
 
1176
 
1137
         for revision in revisions:
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
         for child in content.get_valid_children():
1183
         for child in content.get_valid_children():
1141
             self.mark_unread(child, do_flush=False)
1184
             self.mark_unread(child, do_flush=False)

+ 42 - 2
tracim/models/context_models.py Voir le fichier

55
         self.user_id = workspace_id
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
 class CommentPath(object):
68
 class CommentPath(object):
59
     """
69
     """
60
     Paths params with workspace id and content_id and comment_id model
70
     Paths params with workspace id and content_id and comment_id model
76
     """
86
     """
77
     def __init__(
87
     def __init__(
78
             self,
88
             self,
89
+            workspace_id: int = None,
79
             parent_id: int = None,
90
             parent_id: int = None,
80
             show_archived: int = 0,
91
             show_archived: int = 0,
81
             show_deleted: int = 0,
92
             show_deleted: int = 0,
82
             show_active: int = 1,
93
             show_active: int = 1,
83
             content_type: str = None,
94
             content_type: str = None,
95
+            offset: int = None,
96
+            limit: int = None,
84
     ) -> None:
97
     ) -> None:
85
         self.parent_id = parent_id
98
         self.parent_id = parent_id
99
+        self.workspace_id = workspace_id
86
         self.show_archived = bool(show_archived)
100
         self.show_archived = bool(show_archived)
87
         self.show_deleted = bool(show_deleted)
101
         self.show_deleted = bool(show_deleted)
88
         self.show_active = bool(show_active)
102
         self.show_active = bool(show_active)
103
+        self.limit = limit
104
+        self.offset = offset
89
         self.content_type = content_type
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
 class RoleUpdate(object):
126
 class RoleUpdate(object):
93
     """
127
     """
94
     Update role
128
     Update role
315
             dbsession: Session,
349
             dbsession: Session,
316
             config: CFG,
350
             config: CFG,
317
             # Extended params
351
             # Extended params
318
-            newly_created:bool = None,
352
+            newly_created: bool = None,
319
             email_sent: bool = None
353
             email_sent: bool = None
320
     )-> None:
354
     )-> None:
321
         self.user_role = user_role
355
         self.user_role = user_role
399
     Interface to get Content data and Content data related to context.
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
         self.content = content
437
         self.content = content
404
         self.dbsession = dbsession
438
         self.dbsession = dbsession
405
         self.config = config
439
         self.config = config
440
+        self._user = user
406
 
441
 
407
     # Default
442
     # Default
408
     @property
443
     @property
495
     def slug(self):
530
     def slug(self):
496
         return slugify(self.content.label)
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
 class RevisionInContext(object):
539
 class RevisionInContext(object):
500
     """
540
     """

+ 16 - 0
tracim/models/data.py Voir le fichier

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

+ 715 - 1
tracim/tests/functional/test_user.py Voir le fichier

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

+ 355 - 1
tracim/tests/library/test_content_api.py Voir le fichier

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

+ 58 - 0
tracim/views/core_api/schemas.py Voir le fichier

10
 from tracim.models.contents import open_status
10
 from tracim.models.contents import open_status
11
 from tracim.models.contents import ContentTypeLegacy as ContentType
11
 from tracim.models.contents import ContentTypeLegacy as ContentType
12
 from tracim.models.contents import ContentStatusLegacy as ContentStatus
12
 from tracim.models.contents import ContentStatusLegacy as ContentStatus
13
+from tracim.models.context_models import ActiveContentFilter
14
+from tracim.models.context_models import ContentIdsQuery
15
+from tracim.models.context_models import UserWorkspaceAndContentPath
13
 from tracim.models.context_models import ContentCreation
16
 from tracim.models.context_models import ContentCreation
14
 from tracim.models.context_models import WorkspaceMemberInvitation
17
 from tracim.models.context_models import WorkspaceMemberInvitation
15
 from tracim.models.context_models import WorkspaceUpdate
18
 from tracim.models.context_models import WorkspaceUpdate
129
         return WorkspaceAndContentPath(**data)
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
 class CommentsPathSchema(WorkspaceAndContentIdPathSchema):
154
 class CommentsPathSchema(WorkspaceAndContentIdPathSchema):
133
     comment_id = marshmallow.fields.Int(
155
     comment_id = marshmallow.fields.Int(
134
         example=6,
156
         example=6,
187
     @post_load
209
     @post_load
188
     def make_content_filter(self, data):
210
     def make_content_filter(self, data):
189
         return ContentFilter(**data)
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
     )
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
 # Content
574
 # Content
517
 #####
575
 #####

+ 149 - 9
tracim/views/core_api/user_controller.py Voir le fichier

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

+ 1 - 1
tracim/views/core_api/workspace_controller.py Voir le fichier

259
         contents = api.get_all(
259
         contents = api.get_all(
260
             parent_id=content_filter.parent_id,
260
             parent_id=content_filter.parent_id,
261
             workspace=request.current_workspace,
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
         contents = [
264
         contents = [
265
             api.get_content_in_context(content) for content in contents
265
             api.get_content_in_context(content) for content in contents