Browse Source

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

Guénaël Muller 5 years ago
parent
commit
ab70a84771

+ 1 - 1
README.md View File

@@ -105,7 +105,7 @@ to stop them:
105 105
 
106 106
 run tracim_backend web api:
107 107
 
108
-    pserve developement.ini
108
+    pserve development.ini
109 109
 
110 110
 run wsgidav server:
111 111
 

+ 3 - 0
tracim/exceptions.py View File

@@ -162,3 +162,6 @@ class EmptyLabelNotAllowed(EmptyValueNotAllowed):
162 162
 
163 163
 class EmptyCommentContentNotAllowed(EmptyValueNotAllowed):
164 164
     pass
165
+
166
+class ParentNotFound(NotFound):
167
+    pass

+ 6 - 2
tracim/lib/core/content.py View File

@@ -746,7 +746,7 @@ class ContentApi(object):
746 746
             # INFO - G.M - 2018-07-05 - convert with
747 747
             #  content type object to support legacy slug
748 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 751
         if parent_id:
752 752
             resultset = resultset.filter(Content.parent_id==parent_id)
@@ -1032,7 +1032,11 @@ class ContentApi(object):
1032 1032
         self.save(item, do_notify=False)
1033 1033
 
1034 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 1040
                 self.move_recursively(child, item, new_workspace)
1037 1041
         return
1038 1042
 

+ 50 - 8
tracim/lib/webdav/dav_provider.py View File

@@ -70,7 +70,12 @@ class Provider(DAVProvider):
70 70
 
71 71
         # If the requested path is the root, then we return a RootResource resource
72 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 80
         workspace_api = WorkspaceApi(
76 81
             current_user=user,
@@ -111,10 +116,24 @@ class Provider(DAVProvider):
111 116
 
112 117
         # Easy cases : path either end with /.deleted, /.archived or /.history, then we return corresponding resources
113 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 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 138
         if path.endswith(SpecialFolderExtension.History) and self._show_history:
120 139
             is_deleted_folder = re.search(r'/\.deleted/\.history$', path) is not None
@@ -124,7 +143,15 @@ class Provider(DAVProvider):
124 143
                 else HistoryType.Archived if is_archived_folder \
125 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 156
         # Now that's more complicated, we're trying to find out if the path end with /.history/file_name
130 157
         is_history_file_folder = re.search(r'/\.history/([^/]+)$', path) is not None
@@ -133,9 +160,10 @@ class Provider(DAVProvider):
133 160
             return resources.HistoryFileFolderResource(
134 161
                 path=path,
135 162
                 environ=environ,
136
-                content=content
163
+                user=user,
164
+                content=content,
165
+                session=session,
137 166
             )
138
-
139 167
         # And here next step :
140 168
         is_history_file = re.search(r'/\.history/[^/]+/\((\d+) - [a-zA-Z]+\) .+', path) is not None
141 169
 
@@ -147,9 +175,23 @@ class Provider(DAVProvider):
147 175
             content = self.get_content_from_revision(content_revision, content_api)
148 176
 
149 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 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 196
         # And if we're still going, the client is asking for a standard Folder/File/Page/Thread so we check the type7
155 197
         # and return the corresponding resource

+ 2 - 0
tracim/lib/webdav/resources.py View File

@@ -516,6 +516,7 @@ class FolderResource(WorkspaceResource):
516 516
         workspace_api = WorkspaceApi(
517 517
             current_user=self.user,
518 518
             session=self.session,
519
+            config=self.provider.app_config,
519 520
         )
520 521
         workspace = self.provider.get_workspace_from_path(
521 522
             normpath(destpath), workspace_api
@@ -1308,6 +1309,7 @@ class FileResource(DAVNonCollection):
1308 1309
         workspace_api = WorkspaceApi(
1309 1310
             current_user=self.user,
1310 1311
             session=self.session,
1312
+            config=self.provider.app_config,
1311 1313
         )
1312 1314
         content_api = ContentApi(
1313 1315
             current_user=self.user,

+ 5 - 2
tracim/models/contents.py View File

@@ -251,8 +251,11 @@ class ContentTypeLegacy(NewContentType):
251 251
                 return
252 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 259
         # TODO - G.M - 2018-07-05 - Remove this legacy compat code
257 260
         # when possible.
258 261
         page_alias = [self.Page, self.PageLegacy]

+ 6 - 0
tracim/models/context_models.py View File

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

+ 113 - 0
tracim/tests/functional/test_contents.py View File

@@ -1,4 +1,12 @@
1 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 10
 from tracim.tests import FunctionalTest
3 11
 from tracim.tests import set_html_document_slug_to_legacy
4 12
 from tracim.fixtures.content import Content as ContentFixtures
@@ -314,6 +322,7 @@ class TestHtmlDocuments(FunctionalTest):
314 322
         assert revision['status'] == 'open'
315 323
         assert revision['workspace_id'] == 2
316 324
         assert revision['revision_id'] == 6
325
+        assert revision['revision_type'] == 'creation'
317 326
         assert revision['sub_content_types']
318 327
         # TODO - G.M - 2018-06-173 - Test with real comments
319 328
         assert revision['comment_ids'] == []
@@ -335,6 +344,7 @@ class TestHtmlDocuments(FunctionalTest):
335 344
         assert revision['status'] == 'open'
336 345
         assert revision['workspace_id'] == 2
337 346
         assert revision['revision_id'] == 7
347
+        assert revision['revision_type'] == 'edition'
338 348
         assert revision['sub_content_types']
339 349
         # TODO - G.M - 2018-06-173 - Test with real comments
340 350
         assert revision['comment_ids'] == []
@@ -356,6 +366,7 @@ class TestHtmlDocuments(FunctionalTest):
356 366
         assert revision['status'] == 'open'
357 367
         assert revision['workspace_id'] == 2
358 368
         assert revision['revision_id'] == 27
369
+        assert revision['revision_type'] == 'edition'
359 370
         assert revision['sub_content_types']
360 371
         # TODO - G.M - 2018-06-173 - Test with real comments
361 372
         assert revision['comment_ids'] == []
@@ -697,6 +708,7 @@ class TestThreads(FunctionalTest):
697 708
         assert revision['workspace_id'] == 2
698 709
         assert revision['revision_id'] == 8
699 710
         assert revision['sub_content_types']
711
+        assert revision['revision_type'] == 'creation'
700 712
         assert revision['comment_ids'] == [18, 19, 20]
701 713
         # TODO - G.M - 2018-06-173 - check date format
702 714
         assert revision['created']
@@ -716,6 +728,7 @@ class TestThreads(FunctionalTest):
716 728
         assert revision['status'] == 'open'
717 729
         assert revision['workspace_id'] == 2
718 730
         assert revision['revision_id'] == 26
731
+        assert revision['revision_type'] == 'edition'
719 732
         assert revision['sub_content_types']
720 733
         assert revision['comment_ids'] == []
721 734
         # TODO - G.M - 2018-06-173 - check date format
@@ -725,6 +738,106 @@ class TestThreads(FunctionalTest):
725 738
         assert revision['author']['avatar_url'] is None
726 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 841
     def test_api__set_thread_status__ok_200__nominal_case(self) -> None:
729 842
         """
730 843
         Set thread status

+ 43 - 0
tracim/tests/functional/test_workspaces.py View File

@@ -1081,6 +1081,49 @@ class TestWorkspaceContents(FunctionalTest):
1081 1081
         active_contents = self.testapp.get('/api/v2/workspaces/1/contents', params=params_active, status=200).json_body  # nopep8
1082 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 1127
     def test_api__post_content_create_generic_content__err_400__empty_label(self) -> None:  # nopep8
1085 1128
         """
1086 1129
         Create generic content

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

@@ -23,6 +23,7 @@ from tracim.models.context_models import WorkspaceAndContentPath
23 23
 from tracim.models.context_models import ContentFilter
24 24
 from tracim.models.context_models import LoginCredentials
25 25
 from tracim.models.data import UserRoleInWorkspace
26
+from tracim.models.data import ActionDescription
26 27
 
27 28
 
28 29
 class UserDigestSchema(marshmallow.Schema):
@@ -428,6 +429,11 @@ class ContentCreationSchema(marshmallow.Schema):
428 429
         example='html-documents',
429 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 438
     @post_load
433 439
     def make_content_filter(self, data):
@@ -531,6 +537,10 @@ class RevisionSchema(ContentDigestSchema):
531 537
         example=12,
532 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 544
     created = marshmallow.fields.DateTime(
535 545
         format=DATETIME_FORMAT,
536 546
         description='Content creation date',

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

@@ -18,7 +18,9 @@ from tracim.models.data import ActionDescription
18 18
 from tracim.models.context_models import UserRoleWorkspaceInContext
19 19
 from tracim.models.context_models import ContentInContext
20 20
 from tracim.exceptions import EmptyLabelNotAllowed
21
+from tracim.exceptions import ContentNotFound
21 22
 from tracim.exceptions import WorkspacesDoNotMatch
23
+from tracim.exceptions import ParentNotFound
22 24
 from tracim.views.controllers import Controller
23 25
 from tracim.views.core_api.schemas import FilterContentQuerySchema
24 26
 from tracim.views.core_api.schemas import ContentMoveSchema
@@ -134,12 +136,21 @@ class WorkspaceController(Controller):
134 136
         api = ContentApi(
135 137
             current_user=request.current_user,
136 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 149
         content = api.create(
140 150
             label=creation_data.label,
141 151
             content_type=creation_data.content_type,
142 152
             workspace=request.current_workspace,
153
+            parent=parent,
143 154
         )
144 155
         api.save(content, ActionDescription.CREATION)
145 156
         content = api.get_content_in_context(content)