Bläddra i källkod

Merge branch 'develop' of github.com:tracim/tracim_backend into feature/671_read_unread_endpoints

Guénaël Muller 6 år sedan
förälder
incheckning
ab70a84771

+ 1 - 1
README.md Visa fil

105
 
105
 
106
 run tracim_backend web api:
106
 run tracim_backend web api:
107
 
107
 
108
-    pserve developement.ini
108
+    pserve development.ini
109
 
109
 
110
 run wsgidav server:
110
 run wsgidav server:
111
 
111
 

+ 3 - 0
tracim/exceptions.py Visa fil

162
 
162
 
163
 class EmptyCommentContentNotAllowed(EmptyValueNotAllowed):
163
 class EmptyCommentContentNotAllowed(EmptyValueNotAllowed):
164
     pass
164
     pass
165
+
166
+class ParentNotFound(NotFound):
167
+    pass

+ 6 - 2
tracim/lib/core/content.py Visa fil

746
             # INFO - G.M - 2018-07-05 - convert with
746
             # INFO - G.M - 2018-07-05 - convert with
747
             #  content type object to support legacy slug
747
             #  content type object to support legacy slug
748
             content_type_object = ContentType(content_type)
748
             content_type_object = ContentType(content_type)
749
-            resultset = resultset.filter(Content.type.in_(content_type_object.alias()))
749
+            resultset = resultset.filter(Content.type.in_(content_type_object.get_slug_aliases()))
750
 
750
 
751
         if parent_id:
751
         if parent_id:
752
             resultset = resultset.filter(Content.parent_id==parent_id)
752
             resultset = resultset.filter(Content.parent_id==parent_id)
1032
         self.save(item, do_notify=False)
1032
         self.save(item, do_notify=False)
1033
 
1033
 
1034
         for child in item.children:
1034
         for child in item.children:
1035
-            with new_revision(child):
1035
+            with new_revision(
1036
+                session=self._session,
1037
+                tm=transaction.manager,
1038
+                content=child
1039
+            ):
1036
                 self.move_recursively(child, item, new_workspace)
1040
                 self.move_recursively(child, item, new_workspace)
1037
         return
1041
         return
1038
 
1042
 

+ 50 - 8
tracim/lib/webdav/dav_provider.py Visa fil

70
 
70
 
71
         # If the requested path is the root, then we return a RootResource resource
71
         # If the requested path is the root, then we return a RootResource resource
72
         if path == root_path:
72
         if path == root_path:
73
-            return resources.RootResource(path, environ, user=user, session=session)
73
+            return resources.RootResource(
74
+                path=path,
75
+                environ=environ,
76
+                user=user,
77
+                session=session
78
+            )
74
 
79
 
75
         workspace_api = WorkspaceApi(
80
         workspace_api = WorkspaceApi(
76
             current_user=user,
81
             current_user=user,
111
 
116
 
112
         # Easy cases : path either end with /.deleted, /.archived or /.history, then we return corresponding resources
117
         # Easy cases : path either end with /.deleted, /.archived or /.history, then we return corresponding resources
113
         if path.endswith(SpecialFolderExtension.Archived) and self._show_archive:
118
         if path.endswith(SpecialFolderExtension.Archived) and self._show_archive:
114
-            return resources.ArchivedFolderResource(path, environ, workspace, content)
119
+            return resources.ArchivedFolderResource(
120
+                path=path,
121
+                environ=environ,
122
+                workspace=workspace,
123
+                user=user,
124
+                content=content,
125
+                session=session,
126
+            )
115
 
127
 
116
         if path.endswith(SpecialFolderExtension.Deleted) and self._show_delete:
128
         if path.endswith(SpecialFolderExtension.Deleted) and self._show_delete:
117
-            return resources.DeletedFolderResource(path, environ, workspace, content)
129
+            return resources.DeletedFolderResource(
130
+                path=path,
131
+                environ=environ,
132
+                workspace=workspace,
133
+                user=user,
134
+                content=content,
135
+                session=session,
136
+            )
118
 
137
 
119
         if path.endswith(SpecialFolderExtension.History) and self._show_history:
138
         if path.endswith(SpecialFolderExtension.History) and self._show_history:
120
             is_deleted_folder = re.search(r'/\.deleted/\.history$', path) is not None
139
             is_deleted_folder = re.search(r'/\.deleted/\.history$', path) is not None
124
                 else HistoryType.Archived if is_archived_folder \
143
                 else HistoryType.Archived if is_archived_folder \
125
                 else HistoryType.Standard
144
                 else HistoryType.Standard
126
 
145
 
127
-            return resources.HistoryFolderResource(path, environ, workspace, content, type)
146
+            return resources.HistoryFolderResource(
147
+                path=path,
148
+                environ=environ,
149
+                workspace=workspace,
150
+                user=user,
151
+                content=content,
152
+                session=session,
153
+                type=type
154
+            )
128
 
155
 
129
         # Now that's more complicated, we're trying to find out if the path end with /.history/file_name
156
         # Now that's more complicated, we're trying to find out if the path end with /.history/file_name
130
         is_history_file_folder = re.search(r'/\.history/([^/]+)$', path) is not None
157
         is_history_file_folder = re.search(r'/\.history/([^/]+)$', path) is not None
133
             return resources.HistoryFileFolderResource(
160
             return resources.HistoryFileFolderResource(
134
                 path=path,
161
                 path=path,
135
                 environ=environ,
162
                 environ=environ,
136
-                content=content
163
+                user=user,
164
+                content=content,
165
+                session=session,
137
             )
166
             )
138
-
139
         # And here next step :
167
         # And here next step :
140
         is_history_file = re.search(r'/\.history/[^/]+/\((\d+) - [a-zA-Z]+\) .+', path) is not None
168
         is_history_file = re.search(r'/\.history/[^/]+/\((\d+) - [a-zA-Z]+\) .+', path) is not None
141
 
169
 
147
             content = self.get_content_from_revision(content_revision, content_api)
175
             content = self.get_content_from_revision(content_revision, content_api)
148
 
176
 
149
             if content.type == ContentType.File:
177
             if content.type == ContentType.File:
150
-                return resources.HistoryFileResource(path, environ, content, content_revision)
178
+                return resources.HistoryFileResource(
179
+                    path=path,
180
+                    environ=environ,
181
+                    user=user,
182
+                    content=content,
183
+                    content_revision=content_revision,
184
+                    session=session,
185
+                )
151
             else:
186
             else:
152
-                return resources.HistoryOtherFile(path, environ, content, content_revision)
187
+                return resources.HistoryOtherFile(
188
+                    path=path,
189
+                    environ=environ,
190
+                    user=user,
191
+                    content=content,
192
+                    content_revision=content_revision,
193
+                    session=session,
194
+                )
153
 
195
 
154
         # And if we're still going, the client is asking for a standard Folder/File/Page/Thread so we check the type7
196
         # And if we're still going, the client is asking for a standard Folder/File/Page/Thread so we check the type7
155
         # and return the corresponding resource
197
         # and return the corresponding resource

+ 2 - 0
tracim/lib/webdav/resources.py Visa fil

516
         workspace_api = WorkspaceApi(
516
         workspace_api = WorkspaceApi(
517
             current_user=self.user,
517
             current_user=self.user,
518
             session=self.session,
518
             session=self.session,
519
+            config=self.provider.app_config,
519
         )
520
         )
520
         workspace = self.provider.get_workspace_from_path(
521
         workspace = self.provider.get_workspace_from_path(
521
             normpath(destpath), workspace_api
522
             normpath(destpath), workspace_api
1308
         workspace_api = WorkspaceApi(
1309
         workspace_api = WorkspaceApi(
1309
             current_user=self.user,
1310
             current_user=self.user,
1310
             session=self.session,
1311
             session=self.session,
1312
+            config=self.provider.app_config,
1311
         )
1313
         )
1312
         content_api = ContentApi(
1314
         content_api = ContentApi(
1313
             current_user=self.user,
1315
             current_user=self.user,

+ 5 - 2
tracim/models/contents.py Visa fil

251
                 return
251
                 return
252
         raise ContentTypeNotExist()
252
         raise ContentTypeNotExist()
253
 
253
 
254
-    def alias(self) -> typing.List[str]:
255
-        """ Get all alias of a content, useful for legacy code convertion"""
254
+    def get_slug_aliases(self) -> typing.List[str]:
255
+        """
256
+        Get all slug aliases of a content,
257
+        useful for legacy code convertion
258
+        """
256
         # TODO - G.M - 2018-07-05 - Remove this legacy compat code
259
         # TODO - G.M - 2018-07-05 - Remove this legacy compat code
257
         # when possible.
260
         # when possible.
258
         page_alias = [self.Page, self.PageLegacy]
261
         page_alias = [self.Page, self.PageLegacy]

+ 6 - 0
tracim/models/context_models.py Visa fil

128
             self,
128
             self,
129
             label: str,
129
             label: str,
130
             content_type: str,
130
             content_type: str,
131
+            parent_id: typing.Optional[int] = None,
131
     ) -> None:
132
     ) -> None:
132
         self.label = label
133
         self.label = label
133
         self.content_type = content_type
134
         self.content_type = content_type
135
+        self.parent_id = parent_id
134
 
136
 
135
 
137
 
136
 class CommentCreation(object):
138
 class CommentCreation(object):
507
         return self.revision.label
509
         return self.revision.label
508
 
510
 
509
     @property
511
     @property
512
+    def revision_type(self) -> str:
513
+        return self.revision.revision_type
514
+
515
+    @property
510
     def content_type(self) -> str:
516
     def content_type(self) -> str:
511
         content_type = ContentType(self.revision.type)
517
         content_type = ContentType(self.revision.type)
512
         if content_type:
518
         if content_type:

+ 113 - 0
tracim/tests/functional/test_contents.py Visa fil

1
 # -*- coding: utf-8 -*-
1
 # -*- coding: utf-8 -*-
2
+import transaction
3
+
4
+from tracim import models
5
+from tracim.lib.core.content import ContentApi
6
+from tracim.lib.core.workspace import WorkspaceApi
7
+from tracim.models import get_tm_session
8
+from tracim.models.contents import ContentTypeLegacy as ContentType
9
+from tracim.models.revision_protection import new_revision
2
 from tracim.tests import FunctionalTest
10
 from tracim.tests import FunctionalTest
3
 from tracim.tests import set_html_document_slug_to_legacy
11
 from tracim.tests import set_html_document_slug_to_legacy
4
 from tracim.fixtures.content import Content as ContentFixtures
12
 from tracim.fixtures.content import Content as ContentFixtures
314
         assert revision['status'] == 'open'
322
         assert revision['status'] == 'open'
315
         assert revision['workspace_id'] == 2
323
         assert revision['workspace_id'] == 2
316
         assert revision['revision_id'] == 6
324
         assert revision['revision_id'] == 6
325
+        assert revision['revision_type'] == 'creation'
317
         assert revision['sub_content_types']
326
         assert revision['sub_content_types']
318
         # TODO - G.M - 2018-06-173 - Test with real comments
327
         # TODO - G.M - 2018-06-173 - Test with real comments
319
         assert revision['comment_ids'] == []
328
         assert revision['comment_ids'] == []
335
         assert revision['status'] == 'open'
344
         assert revision['status'] == 'open'
336
         assert revision['workspace_id'] == 2
345
         assert revision['workspace_id'] == 2
337
         assert revision['revision_id'] == 7
346
         assert revision['revision_id'] == 7
347
+        assert revision['revision_type'] == 'edition'
338
         assert revision['sub_content_types']
348
         assert revision['sub_content_types']
339
         # TODO - G.M - 2018-06-173 - Test with real comments
349
         # TODO - G.M - 2018-06-173 - Test with real comments
340
         assert revision['comment_ids'] == []
350
         assert revision['comment_ids'] == []
356
         assert revision['status'] == 'open'
366
         assert revision['status'] == 'open'
357
         assert revision['workspace_id'] == 2
367
         assert revision['workspace_id'] == 2
358
         assert revision['revision_id'] == 27
368
         assert revision['revision_id'] == 27
369
+        assert revision['revision_type'] == 'edition'
359
         assert revision['sub_content_types']
370
         assert revision['sub_content_types']
360
         # TODO - G.M - 2018-06-173 - Test with real comments
371
         # TODO - G.M - 2018-06-173 - Test with real comments
361
         assert revision['comment_ids'] == []
372
         assert revision['comment_ids'] == []
697
         assert revision['workspace_id'] == 2
708
         assert revision['workspace_id'] == 2
698
         assert revision['revision_id'] == 8
709
         assert revision['revision_id'] == 8
699
         assert revision['sub_content_types']
710
         assert revision['sub_content_types']
711
+        assert revision['revision_type'] == 'creation'
700
         assert revision['comment_ids'] == [18, 19, 20]
712
         assert revision['comment_ids'] == [18, 19, 20]
701
         # TODO - G.M - 2018-06-173 - check date format
713
         # TODO - G.M - 2018-06-173 - check date format
702
         assert revision['created']
714
         assert revision['created']
716
         assert revision['status'] == 'open'
728
         assert revision['status'] == 'open'
717
         assert revision['workspace_id'] == 2
729
         assert revision['workspace_id'] == 2
718
         assert revision['revision_id'] == 26
730
         assert revision['revision_id'] == 26
731
+        assert revision['revision_type'] == 'edition'
719
         assert revision['sub_content_types']
732
         assert revision['sub_content_types']
720
         assert revision['comment_ids'] == []
733
         assert revision['comment_ids'] == []
721
         # TODO - G.M - 2018-06-173 - check date format
734
         # TODO - G.M - 2018-06-173 - check date format
725
         assert revision['author']['avatar_url'] is None
738
         assert revision['author']['avatar_url'] is None
726
         assert revision['author']['public_name'] == 'Bob i.'
739
         assert revision['author']['public_name'] == 'Bob i.'
727
 
740
 
741
+    def test_api__get_thread_revisions__ok_200__most_revision_type(self) -> None:
742
+        """
743
+        get threads revisions
744
+        """
745
+        dbsession = get_tm_session(self.session_factory, transaction.manager)
746
+        admin = dbsession.query(models.User) \
747
+            .filter(models.User.email == 'admin@admin.admin') \
748
+            .one()
749
+        workspace_api = WorkspaceApi(
750
+            current_user=admin,
751
+            session=dbsession,
752
+            config=self.app_config
753
+        )
754
+        business_workspace = workspace_api.get_one(1)
755
+        content_api = ContentApi(
756
+            current_user=admin,
757
+            session=dbsession,
758
+            config=self.app_config
759
+        )
760
+        tool_folder = content_api.get_one(1, content_type=ContentType.Any)
761
+        test_thread = content_api.create(
762
+            content_type=ContentType.Thread,
763
+            workspace=business_workspace,
764
+            parent=tool_folder,
765
+            label='Test Thread',
766
+            do_save=True,
767
+            do_notify=False,
768
+        )
769
+        with new_revision(
770
+           session=dbsession,
771
+           tm=transaction.manager,
772
+           content=test_thread,
773
+        ):
774
+            content_api.update_content(
775
+                test_thread,
776
+                new_label='test_thread_updated',
777
+                new_content='Just a test'
778
+            )
779
+        content_api.save(test_thread)
780
+        with new_revision(
781
+           session=dbsession,
782
+           tm=transaction.manager,
783
+           content=test_thread,
784
+        ):
785
+            content_api.archive(test_thread)
786
+        content_api.save(test_thread)
787
+
788
+        with new_revision(
789
+           session=dbsession,
790
+           tm=transaction.manager,
791
+           content=test_thread,
792
+        ):
793
+            content_api.unarchive(test_thread)
794
+        content_api.save(test_thread)
795
+
796
+        with new_revision(
797
+           session=dbsession,
798
+           tm=transaction.manager,
799
+           content=test_thread,
800
+        ):
801
+            content_api.delete(test_thread)
802
+        content_api.save(test_thread)
803
+
804
+        with new_revision(
805
+           session=dbsession,
806
+           tm=transaction.manager,
807
+           content=test_thread,
808
+        ):
809
+            content_api.undelete(test_thread)
810
+        content_api.save(test_thread)
811
+        dbsession.flush()
812
+        transaction.commit()
813
+        self.testapp.authorization = (
814
+            'Basic',
815
+            (
816
+                'admin@admin.admin',
817
+                'admin@admin.admin'
818
+            )
819
+        )
820
+        res = self.testapp.get(
821
+            '/api/v2/workspaces/1/threads/{}/revisions'.format(test_thread.content_id),  # nopep8
822
+            status=200
823
+        )
824
+        revisions = res.json_body
825
+        assert len(revisions) == 6
826
+        for revision in revisions:
827
+            revision['content_type'] == 'thread'
828
+            revision['workspace_id'] == 1
829
+            revision['content_id'] == test_thread.content_id
830
+        revision = revisions[0]
831
+        revision['revision_type'] == 'creation'
832
+        revision = revisions[1]
833
+        revision['revision_type'] == 'archiving'
834
+        revision = revisions[2]
835
+        revision['revision_type'] == 'unarchiving'
836
+        revision = revisions[3]
837
+        revision['revision_type'] == 'deletion'
838
+        revision = revisions[4]
839
+        revision['revision_type'] == 'undeletion'
840
+
728
     def test_api__set_thread_status__ok_200__nominal_case(self) -> None:
841
     def test_api__set_thread_status__ok_200__nominal_case(self) -> None:
729
         """
842
         """
730
         Set thread status
843
         Set thread status

+ 43 - 0
tracim/tests/functional/test_workspaces.py Visa fil

1081
         active_contents = self.testapp.get('/api/v2/workspaces/1/contents', params=params_active, status=200).json_body  # nopep8
1081
         active_contents = self.testapp.get('/api/v2/workspaces/1/contents', params=params_active, status=200).json_body  # nopep8
1082
         assert res.json_body in active_contents
1082
         assert res.json_body in active_contents
1083
 
1083
 
1084
+    def test_api__post_content_create_generic_content__ok_200__in_folder(self) -> None:  # nopep8
1085
+        """
1086
+        Create generic content in folder
1087
+        """
1088
+        self.testapp.authorization = (
1089
+            'Basic',
1090
+            (
1091
+                'admin@admin.admin',
1092
+                'admin@admin.admin'
1093
+            )
1094
+        )
1095
+        params = {
1096
+            'label': 'GenericCreatedContent',
1097
+            'content_type': 'markdownpage',
1098
+            'parent_id': 10,
1099
+        }
1100
+        res = self.testapp.post_json(
1101
+            '/api/v2/workspaces/1/contents',
1102
+            params=params,
1103
+            status=200
1104
+        )
1105
+        assert res
1106
+        assert res.json_body
1107
+        assert res.json_body['status'] == 'open'
1108
+        assert res.json_body['content_id']
1109
+        assert res.json_body['content_type'] == 'markdownpage'
1110
+        assert res.json_body['is_archived'] is False
1111
+        assert res.json_body['is_deleted'] is False
1112
+        assert res.json_body['workspace_id'] == 1
1113
+        assert res.json_body['slug'] == 'genericcreatedcontent'
1114
+        assert res.json_body['parent_id'] == 10
1115
+        assert res.json_body['show_in_ui'] is True
1116
+        assert res.json_body['sub_content_types']
1117
+        params_active = {
1118
+            'parent_id': 10,
1119
+            'show_archived': 0,
1120
+            'show_deleted': 0,
1121
+            'show_active': 1,
1122
+        }
1123
+        # INFO - G.M - 2018-06-165 - Verify if new content is correctly created
1124
+        active_contents = self.testapp.get('/api/v2/workspaces/1/contents', params=params_active, status=200).json_body  # nopep8
1125
+        assert res.json_body in active_contents
1126
+
1084
     def test_api__post_content_create_generic_content__err_400__empty_label(self) -> None:  # nopep8
1127
     def test_api__post_content_create_generic_content__err_400__empty_label(self) -> None:  # nopep8
1085
         """
1128
         """
1086
         Create generic content
1129
         Create generic content

+ 10 - 0
tracim/views/core_api/schemas.py Visa fil

23
 from tracim.models.context_models import ContentFilter
23
 from tracim.models.context_models import ContentFilter
24
 from tracim.models.context_models import LoginCredentials
24
 from tracim.models.context_models import LoginCredentials
25
 from tracim.models.data import UserRoleInWorkspace
25
 from tracim.models.data import UserRoleInWorkspace
26
+from tracim.models.data import ActionDescription
26
 
27
 
27
 
28
 
28
 class UserDigestSchema(marshmallow.Schema):
29
 class UserDigestSchema(marshmallow.Schema):
428
         example='html-documents',
429
         example='html-documents',
429
         validate=OneOf(ContentType.allowed_types_for_folding()),  # nopep8
430
         validate=OneOf(ContentType.allowed_types_for_folding()),  # nopep8
430
     )
431
     )
432
+    parent_id = marshmallow.fields.Integer(
433
+        example=35,
434
+        description='content_id of parent content, if content should be placed in a folder, this should be folder content_id.'
435
+    )
436
+
431
 
437
 
432
     @post_load
438
     @post_load
433
     def make_content_filter(self, data):
439
     def make_content_filter(self, data):
531
         example=12,
537
         example=12,
532
         validate=Range(min=1, error="Value must be greater than 0"),
538
         validate=Range(min=1, error="Value must be greater than 0"),
533
     )
539
     )
540
+    revision_type = marshmallow.fields.String(
541
+        example=ActionDescription.CREATION,
542
+        validate=OneOf(ActionDescription.allowed_values()),
543
+    )
534
     created = marshmallow.fields.DateTime(
544
     created = marshmallow.fields.DateTime(
535
         format=DATETIME_FORMAT,
545
         format=DATETIME_FORMAT,
536
         description='Content creation date',
546
         description='Content creation date',

+ 12 - 1
tracim/views/core_api/workspace_controller.py Visa fil

18
 from tracim.models.context_models import UserRoleWorkspaceInContext
18
 from tracim.models.context_models import UserRoleWorkspaceInContext
19
 from tracim.models.context_models import ContentInContext
19
 from tracim.models.context_models import ContentInContext
20
 from tracim.exceptions import EmptyLabelNotAllowed
20
 from tracim.exceptions import EmptyLabelNotAllowed
21
+from tracim.exceptions import ContentNotFound
21
 from tracim.exceptions import WorkspacesDoNotMatch
22
 from tracim.exceptions import WorkspacesDoNotMatch
23
+from tracim.exceptions import ParentNotFound
22
 from tracim.views.controllers import Controller
24
 from tracim.views.controllers import Controller
23
 from tracim.views.core_api.schemas import FilterContentQuerySchema
25
 from tracim.views.core_api.schemas import FilterContentQuerySchema
24
 from tracim.views.core_api.schemas import ContentMoveSchema
26
 from tracim.views.core_api.schemas import ContentMoveSchema
134
         api = ContentApi(
136
         api = ContentApi(
135
             current_user=request.current_user,
137
             current_user=request.current_user,
136
             session=request.dbsession,
138
             session=request.dbsession,
137
-            config=app_config,
139
+            config=app_config
138
         )
140
         )
141
+        parent = None
142
+        if creation_data.parent_id:
143
+            try:
144
+                parent = api.get_one(content_id=creation_data.parent_id, content_type=ContentType.Any)  # nopep8
145
+            except ContentNotFound as exc:
146
+                raise ParentNotFound(
147
+                    'Parent with content_id {} not found'.format(creation_data.parent_id)
148
+                ) from exc
139
         content = api.create(
149
         content = api.create(
140
             label=creation_data.label,
150
             label=creation_data.label,
141
             content_type=creation_data.content_type,
151
             content_type=creation_data.content_type,
142
             workspace=request.current_workspace,
152
             workspace=request.current_workspace,
153
+            parent=parent,
143
         )
154
         )
144
         api.save(content, ActionDescription.CREATION)
155
         api.save(content, ActionDescription.CREATION)
145
         content = api.get_content_in_context(content)
156
         content = api.get_content_in_context(content)