Browse Source

Add put and post for workspace endpoints #587

Guénaël Muller 6 years ago
parent
commit
560af8e809

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

@@ -13,6 +13,14 @@ from tracim.models.workspace_menu_entries import default_workspace_menu_entry
13 13
 from tracim.models.workspace_menu_entries import WorkspaceMenuEntry
14 14
 
15 15
 
16
+class MoveParams(object):
17
+    """
18
+    Json body params for move action
19
+    """
20
+    def __init__(self, new_parent_id: str):
21
+        self.new_parent_id = new_parent_id
22
+
23
+
16 24
 class LoginCredentials(object):
17 25
     """
18 26
     Login credentials model for login
@@ -23,22 +31,45 @@ class LoginCredentials(object):
23 31
         self.password = password
24 32
 
25 33
 
34
+class WorkspaceAndContentPath(object):
35
+    """
36
+    Paths params with workspace id and content_id
37
+    """
38
+    def __init__(self, workspace_id: int, content_id: int):
39
+        self.content_id = content_id
40
+        self.workspace_id = workspace_id
41
+
42
+
26 43
 class ContentFilter(object):
27 44
     """
28 45
     Content filter model
29 46
     """
30
-    def __init__(self,
31
-                 parent_id: int = None,
32
-                 show_archived: int = 0,
33
-                 show_deleted: int = 0,
34
-                 show_active: int = 1,
35
-                 ):
47
+    def __init__(
48
+            self,
49
+            parent_id: int = None,
50
+            show_archived: int = 0,
51
+            show_deleted: int = 0,
52
+            show_active: int = 1,
53
+    ):
36 54
         self.parent_id = parent_id
37 55
         self.show_archived = bool(show_archived)
38 56
         self.show_deleted = bool(show_deleted)
39 57
         self.show_active = bool(show_active)
40 58
 
41 59
 
60
+class ContentCreation(object):
61
+    """
62
+    Content creation model
63
+    """
64
+    def __init__(
65
+            self,
66
+            label: str,
67
+            content_type_slug: str,
68
+    ):
69
+        self.label = label
70
+        self.content_type_slug = content_type_slug
71
+
72
+
42 73
 class UserInContext(object):
43 74
     """
44 75
     Interface to get User data and User data related to context.

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

@@ -727,3 +727,136 @@ class TestWorkspaceContents(FunctionalTest):
727 727
         assert 'code' in res.json.keys()
728 728
         assert 'message' in res.json.keys()
729 729
         assert 'details' in res.json.keys()
730
+
731
+    def test_api__post_content_create_generic_content__ok_200__nominal_case(self) -> None:
732
+        """
733
+        Create generic content
734
+        """
735
+        self.testapp.authorization = (
736
+            'Basic',
737
+            (
738
+                'admin@admin.admin',
739
+                'admin@admin.admin'
740
+            )
741
+        )
742
+        params = {
743
+            'label': 'GenericCreatedContent',
744
+            'content_type_slug': 'markdownpage',
745
+        }
746
+        res = self.testapp.post_json(
747
+            '/api/v2/workspaces/1/contents',
748
+            params=params,
749
+            status=200
750
+        )
751
+        assert res
752
+        assert res.json_body
753
+        assert res.json_body['status_slug'] == 'open'
754
+        assert res.json_body['id']
755
+        assert res.json_body['content_type_slug'] == 'markdownpage'
756
+        assert res.json_body['is_archived'] is False
757
+        assert res.json_body['is_deleted'] is False
758
+        assert res.json_body['workspace_id'] == 1
759
+        assert res.json_body['slug'] == 'genericcreatedcontent'
760
+        assert res.json_body['parent_id'] is None
761
+        assert res.json_body['show_in_ui'] is True
762
+        assert res.json_body['sub_content_type_slug']
763
+
764
+    def test_api_put_move_content__ok_200__nominal_case(self):
765
+        """
766
+        Move content
767
+        """
768
+        self.testapp.authorization = (
769
+            'Basic',
770
+            (
771
+                'admin@admin.admin',
772
+                'admin@admin.admin'
773
+            )
774
+        )
775
+        params = {
776
+            'new_parent_id': '4',  # Salads
777
+        }
778
+        # TODO - G.M - 2018-06-163 - Check content
779
+        res = self.testapp.put_json(
780
+            # INFO - G.M - 2018-06-163 - move Apple_Pie
781
+            # from Desserts to Salads subfolder of workspace Recipes
782
+            '/api/v2/workspaces/2/contents/8/move',
783
+            params=params,
784
+            status=200
785
+        )
786
+        # TODO - G.M - 2018-06-163 - Recheck content
787
+
788
+    def test_api_put_delete_content__ok_200__nominal_case(self):
789
+        """
790
+        Move content
791
+        """
792
+        self.testapp.authorization = (
793
+            'Basic',
794
+            (
795
+                'admin@admin.admin',
796
+                'admin@admin.admin'
797
+            )
798
+        )
799
+        # TODO - G.M - 2018-06-163 - Check content
800
+        res = self.testapp.put_json(
801
+            # INFO - G.M - 2018-06-163 - delete Apple_Pie
802
+            '/api/v2/workspaces/2/contents/8/delete',
803
+            status=200
804
+        )
805
+        # TODO - G.M - 2018-06-163 - Recheck content
806
+
807
+    def test_api_put_archive_content__ok_200__nominal_case(self):
808
+        """
809
+        archive content
810
+        """
811
+        self.testapp.authorization = (
812
+            'Basic',
813
+            (
814
+                'admin@admin.admin',
815
+                'admin@admin.admin'
816
+            )
817
+        )
818
+        # TODO - G.M - 2018-06-163 - Check content
819
+        res = self.testapp.put_json(
820
+            # INFO - G.M - 2018-06-163 - archive Apple_Pie
821
+            '/api/v2/workspaces/2/contents/8/archive',
822
+            status=200
823
+        )
824
+        # TODO - G.M - 2018-06-163 - Recheck content
825
+
826
+    def test_api_put_undelete_content__ok_200__nominal_case(self):
827
+        """
828
+        Delete content
829
+        """
830
+        self.testapp.authorization = (
831
+            'Basic',
832
+            (
833
+                'bob@fsf.local',
834
+                'foobarbaz'
835
+            )
836
+        )
837
+        # TODO - G.M - 2018-06-163 - Check content
838
+        res = self.testapp.put_json(
839
+            # INFO - G.M - 2018-06-163 - delete Apple_Pie
840
+            '/api/v2/workspaces/2/contents/14/undelete',
841
+            status=200
842
+        )
843
+        # TODO - G.M - 2018-06-163 - Recheck content
844
+
845
+    def test_api_put_unarchive_content__ok_200__nominal_case(self):
846
+        """
847
+        Delete content
848
+        """
849
+        self.testapp.authorization = (
850
+            'Basic',
851
+            (
852
+                'bob@fsf.local',
853
+                'foobarbaz'
854
+            )
855
+        )
856
+        # TODO - G.M - 2018-06-163 - Check content
857
+        res = self.testapp.put_json(
858
+            # INFO - G.M - 2018-06-163 - delete Apple_Pie
859
+            '/api/v2/workspaces/2/contents/13/unarchive',
860
+            status=200
861
+        )
862
+        # TODO - G.M - 2018-06-163 - Recheck content

+ 24 - 6
tracim/views/core_api/schemas.py View File

@@ -4,8 +4,15 @@ from marshmallow import post_load
4 4
 from marshmallow.validate import OneOf
5 5
 
6 6
 from tracim.models.auth import Profile
7
-from tracim.models.contents import CONTENT_DEFAULT_TYPE, GlobalStatus, CONTENT_DEFAULT_STATUS
8
-from tracim.models.context_models import LoginCredentials, ContentFilter
7
+from tracim.models.contents import CONTENT_DEFAULT_TYPE
8
+from tracim.models.contents import CONTENT_DEFAULT_STATUS
9
+from tracim.models.contents import GlobalStatus
10
+from tracim.models.contents import open_status
11
+from tracim.models.context_models import ContentCreation
12
+from tracim.models.context_models import MoveParams
13
+from tracim.models.context_models import WorkspaceAndContentPath
14
+from tracim.models.context_models import ContentFilter
15
+from tracim.models.context_models import LoginCredentials
9 16
 from tracim.models.data import UserRoleInWorkspace
10 17
 
11 18
 
@@ -82,7 +89,9 @@ class ContentIdPathSchema(marshmallow.Schema):
82 89
 
83 90
 
84 91
 class WorkspaceAndContentIdPathSchema(WorkspaceIdPathSchema, ContentIdPathSchema):
85
-    pass
92
+    @post_load
93
+    def make_path_object(self, data):
94
+        return WorkspaceAndContentPath(**data)
86 95
 
87 96
 
88 97
 class FilterContentQuerySchema(marshmallow.Schema):
@@ -292,6 +301,10 @@ class ContentMoveSchema(marshmallow.Schema):
292 301
         description='id of the new parent content id.'
293 302
     )
294 303
 
304
+    @post_load
305
+    def make_move_params(self, data):
306
+        return MoveParams(**data)
307
+
295 308
 
296 309
 class ContentCreationSchema(marshmallow.Schema):
297 310
     label = marshmallow.fields.String(
@@ -300,9 +313,13 @@ class ContentCreationSchema(marshmallow.Schema):
300 313
     )
301 314
     content_type_slug = marshmallow.fields.String(
302 315
         example='htmlpage',
303
-        validate=OneOf(CONTENT_DEFAULT_TYPE),
316
+        validate=OneOf([content.slug for content in CONTENT_DEFAULT_TYPE]),
304 317
     )
305 318
 
319
+    @post_load
320
+    def make_content_filter(self, data):
321
+        return ContentCreation(**data)
322
+
306 323
 
307 324
 class ContentDigestSchema(marshmallow.Schema):
308 325
     id = marshmallow.fields.Int(example=6)
@@ -330,9 +347,10 @@ class ContentDigestSchema(marshmallow.Schema):
330 347
         example='closed-deprecated',
331 348
         validate=OneOf([status.slug for status in CONTENT_DEFAULT_STATUS]),
332 349
         description='this slug is found in content_type available statuses',
350
+        default=open_status
333 351
     )
334
-    is_archived = marshmallow.fields.Bool(example=False)
335
-    is_deleted = marshmallow.fields.Bool(example=False)
352
+    is_archived = marshmallow.fields.Bool(example=False, default=False)
353
+    is_deleted = marshmallow.fields.Bool(example=False, default=False)
336 354
     show_in_ui = marshmallow.fields.Bool(
337 355
         example=True,
338 356
         description='if false, then do not show content in the treeview. '

+ 237 - 3
tracim/views/core_api/workspace_controller.py View File

@@ -1,7 +1,7 @@
1 1
 import typing
2 2
 
3
+import transaction
3 4
 from pyramid.config import Configurator
4
-
5 5
 try:  # Python 3.5+
6 6
     from http import HTTPStatus
7 7
 except ImportError:
@@ -12,7 +12,7 @@ from tracim.lib.core.workspace import WorkspaceApi
12 12
 from tracim.lib.core.content import ContentApi
13 13
 from tracim.lib.core.userworkspace import RoleApi
14 14
 from tracim.lib.utils.authorization import require_workspace_role
15
-from tracim.models.data import UserRoleInWorkspace
15
+from tracim.models.data import UserRoleInWorkspace, ActionDescription
16 16
 from tracim.models.context_models import UserRoleWorkspaceInContext
17 17
 from tracim.models.context_models import ContentInContext
18 18
 from tracim.exceptions import NotAuthentificated
@@ -20,10 +20,16 @@ from tracim.exceptions import InsufficientUserProfile
20 20
 from tracim.exceptions import WorkspaceNotFound
21 21
 from tracim.views.controllers import Controller
22 22
 from tracim.views.core_api.schemas import FilterContentQuerySchema
23
+from tracim.views.core_api.schemas import ContentMoveSchema
24
+from tracim.views.core_api.schemas import NoContentSchema
25
+from tracim.views.core_api.schemas import ContentCreationSchema
26
+from tracim.views.core_api.schemas import WorkspaceAndContentIdPathSchema
23 27
 from tracim.views.core_api.schemas import ContentDigestSchema
24 28
 from tracim.views.core_api.schemas import WorkspaceSchema
25 29
 from tracim.views.core_api.schemas import WorkspaceIdPathSchema
26 30
 from tracim.views.core_api.schemas import WorkspaceMemberSchema
31
+from tracim.models.data import ContentType
32
+from tracim.models.revision_protection import new_revision
27 33
 
28 34
 
29 35
 class WorkspaceController(Controller):
@@ -113,6 +119,218 @@ class WorkspaceController(Controller):
113 119
         ]
114 120
         return contents
115 121
 
122
+    @hapic.with_api_doc()
123
+    @hapic.handle_exception(NotAuthentificated, HTTPStatus.UNAUTHORIZED)
124
+    @hapic.handle_exception(InsufficientUserProfile, HTTPStatus.FORBIDDEN)
125
+    @hapic.handle_exception(WorkspaceNotFound, HTTPStatus.FORBIDDEN)
126
+    @require_workspace_role(UserRoleInWorkspace.CONTRIBUTOR)
127
+    @hapic.input_path(WorkspaceIdPathSchema())
128
+    @hapic.input_body(ContentCreationSchema())
129
+    @hapic.output_body(ContentDigestSchema())
130
+    def create_generic_empty_content(
131
+            self,
132
+            context,
133
+            request: TracimRequest,
134
+            hapic_data=None,
135
+    ) -> typing.List[ContentInContext]:
136
+        """
137
+        create a generic empty content
138
+        """
139
+        app_config = request.registry.settings['CFG']
140
+        creation_data = hapic_data.body
141
+        api = ContentApi(
142
+            current_user=request.current_user,
143
+            session=request.dbsession,
144
+            config=app_config,
145
+        )
146
+        content = api.create(
147
+            label=creation_data.label,
148
+            content_type=creation_data.content_type_slug,
149
+            workspace=request.current_workspace,
150
+        )
151
+        api.save(content, ActionDescription.CREATION)
152
+        content = api.get_content_in_context(content)
153
+        return content
154
+
155
+    @hapic.with_api_doc()
156
+    @hapic.handle_exception(NotAuthentificated, HTTPStatus.UNAUTHORIZED)
157
+    @hapic.handle_exception(InsufficientUserProfile, HTTPStatus.FORBIDDEN)
158
+    @hapic.handle_exception(WorkspaceNotFound, HTTPStatus.FORBIDDEN)
159
+    @require_workspace_role(UserRoleInWorkspace.CONTRIBUTOR)
160
+    @hapic.input_path(WorkspaceAndContentIdPathSchema())
161
+    @hapic.input_body(ContentMoveSchema())
162
+    @hapic.output_body(NoContentSchema())
163
+    def move_content(
164
+            self,
165
+            context,
166
+            request: TracimRequest,
167
+            hapic_data=None,
168
+    ) -> typing.List[ContentInContext]:
169
+        """
170
+        move a content
171
+        """
172
+        app_config = request.registry.settings['CFG']
173
+        path_data = hapic_data.path
174
+        move_data = hapic_data.body
175
+        api = ContentApi(
176
+            current_user=request.current_user,
177
+            session=request.dbsession,
178
+            config=app_config,
179
+        )
180
+        content = api.get_one(
181
+            path_data.content_id,
182
+            content_type=ContentType.Any
183
+        )
184
+        new_parent = api.get_one(
185
+            move_data.new_parent_id, content_type=ContentType.Any
186
+        )
187
+        with new_revision(
188
+                session=request.dbsession,
189
+                tm=transaction.manager,
190
+                content=content
191
+        ):
192
+            api.move(content, new_parent=new_parent)
193
+        return
194
+
195
+    @hapic.with_api_doc()
196
+    @hapic.handle_exception(NotAuthentificated, HTTPStatus.UNAUTHORIZED)
197
+    @hapic.handle_exception(InsufficientUserProfile, HTTPStatus.FORBIDDEN)
198
+    @hapic.handle_exception(WorkspaceNotFound, HTTPStatus.FORBIDDEN)
199
+    @require_workspace_role(UserRoleInWorkspace.CONTRIBUTOR)
200
+    @hapic.input_path(WorkspaceAndContentIdPathSchema())
201
+    @hapic.output_body(NoContentSchema())
202
+    def delete_content(
203
+            self,
204
+            context,
205
+            request: TracimRequest,
206
+            hapic_data=None,
207
+    ) -> typing.List[ContentInContext]:
208
+        """
209
+        delete a content
210
+        """
211
+        app_config = request.registry.settings['CFG']
212
+        path_data = hapic_data.path
213
+        api = ContentApi(
214
+            current_user=request.current_user,
215
+            session=request.dbsession,
216
+            config=app_config,
217
+        )
218
+        content = api.get_one(
219
+            path_data.content_id,
220
+            content_type=ContentType.Any
221
+        )
222
+        with new_revision(
223
+                session=request.dbsession,
224
+                tm=transaction.manager,
225
+                content=content
226
+        ):
227
+            api.delete(content)
228
+        return
229
+
230
+    @hapic.with_api_doc()
231
+    @hapic.handle_exception(NotAuthentificated, HTTPStatus.UNAUTHORIZED)
232
+    @hapic.handle_exception(InsufficientUserProfile, HTTPStatus.FORBIDDEN)
233
+    @hapic.handle_exception(WorkspaceNotFound, HTTPStatus.FORBIDDEN)
234
+    @require_workspace_role(UserRoleInWorkspace.CONTRIBUTOR)
235
+    @hapic.input_path(WorkspaceAndContentIdPathSchema())
236
+    @hapic.output_body(NoContentSchema())
237
+    def undelete_content(
238
+            self,
239
+            context,
240
+            request: TracimRequest,
241
+            hapic_data=None,
242
+    ) -> typing.List[ContentInContext]:
243
+        """
244
+        undelete a content
245
+        """
246
+        app_config = request.registry.settings['CFG']
247
+        path_data = hapic_data.path
248
+        api = ContentApi(
249
+            current_user=request.current_user,
250
+            session=request.dbsession,
251
+            config=app_config,
252
+            show_deleted=True,
253
+        )
254
+        content = api.get_one(
255
+            path_data.content_id,
256
+            content_type=ContentType.Any
257
+        )
258
+        with new_revision(
259
+                session=request.dbsession,
260
+                tm=transaction.manager,
261
+                content=content
262
+        ):
263
+            api.undelete(content)
264
+        return
265
+
266
+    @hapic.with_api_doc()
267
+    @hapic.handle_exception(NotAuthentificated, HTTPStatus.UNAUTHORIZED)
268
+    @hapic.handle_exception(InsufficientUserProfile, HTTPStatus.FORBIDDEN)
269
+    @hapic.handle_exception(WorkspaceNotFound, HTTPStatus.FORBIDDEN)
270
+    @require_workspace_role(UserRoleInWorkspace.CONTRIBUTOR)
271
+    @hapic.input_path(WorkspaceAndContentIdPathSchema())
272
+    @hapic.output_body(NoContentSchema())
273
+    def archive_content(
274
+            self,
275
+            context,
276
+            request: TracimRequest,
277
+            hapic_data=None,
278
+    ) -> typing.List[ContentInContext]:
279
+        """
280
+        archive a content
281
+        """
282
+        app_config = request.registry.settings['CFG']
283
+        path_data = hapic_data.path
284
+        api = ContentApi(
285
+            current_user=request.current_user,
286
+            session=request.dbsession,
287
+            config=app_config,
288
+        )
289
+        content = api.get_one(path_data.content_id, content_type=ContentType.Any)
290
+        with new_revision(
291
+                session=request.dbsession,
292
+                tm=transaction.manager,
293
+                content=content
294
+        ):
295
+            api.archive(content)
296
+        return
297
+
298
+    @hapic.with_api_doc()
299
+    @hapic.handle_exception(NotAuthentificated, HTTPStatus.UNAUTHORIZED)
300
+    @hapic.handle_exception(InsufficientUserProfile, HTTPStatus.FORBIDDEN)
301
+    @hapic.handle_exception(WorkspaceNotFound, HTTPStatus.FORBIDDEN)
302
+    @require_workspace_role(UserRoleInWorkspace.CONTRIBUTOR)
303
+    @hapic.input_path(WorkspaceAndContentIdPathSchema())
304
+    @hapic.output_body(NoContentSchema())
305
+    def unarchive_content(
306
+            self,
307
+            context,
308
+            request: TracimRequest,
309
+            hapic_data=None,
310
+    ) -> typing.List[ContentInContext]:
311
+        """
312
+        unarchive a content
313
+        """
314
+        app_config = request.registry.settings['CFG']
315
+        path_data = hapic_data.path
316
+        api = ContentApi(
317
+            current_user=request.current_user,
318
+            session=request.dbsession,
319
+            config=app_config,
320
+            show_archived=True,
321
+        )
322
+        content = api.get_one(
323
+            path_data.content_id,
324
+            content_type=ContentType.Any
325
+        )
326
+        with new_revision(
327
+                session=request.dbsession,
328
+                tm=transaction.manager,
329
+                content=content
330
+        ):
331
+            api.unarchive(content)
332
+        return
333
+
116 334
     def bind(self, configurator: Configurator) -> None:
117 335
         """
118 336
         Create all routes and views using
@@ -127,4 +345,20 @@ class WorkspaceController(Controller):
127 345
         configurator.add_view(self.workspaces_members, route_name='workspace_members')  # nopep8
128 346
         # Workspace Content
129 347
         configurator.add_route('workspace_content', '/workspaces/{workspace_id}/contents', request_method='GET')  # nopep8
130
-        configurator.add_view(self.workspace_content, route_name='workspace_content')  # nopep8
348
+        configurator.add_view(self.workspace_content, route_name='workspace_content')  # nopep8
349
+        # Create Generic Content
350
+        configurator.add_route('create_generic_content', '/workspaces/{workspace_id}/contents', request_method='POST')  # nopep8
351
+        configurator.add_view(self.create_generic_empty_content, route_name='create_generic_content')  # nopep8
352
+        # Move Content
353
+        configurator.add_route('move_content', '/workspaces/{workspace_id}/contents/{content_id}/move', request_method='PUT')  # nopep8
354
+        configurator.add_view(self.move_content, route_name='move_content')  # nopep8
355
+        # Delete/Undelete Content
356
+        configurator.add_route('delete_content', '/workspaces/{workspace_id}/contents/{content_id}/delete', request_method='PUT')  # nopep8
357
+        configurator.add_view(self.delete_content, route_name='delete_content')  # nopep8
358
+        configurator.add_route('undelete_content', '/workspaces/{workspace_id}/contents/{content_id}/undelete', request_method='PUT')  # nopep8
359
+        configurator.add_view(self.undelete_content, route_name='undelete_content')  # nopep8
360
+        # # Archive/Unarchive Content
361
+        configurator.add_route('archive_content', '/workspaces/{workspace_id}/contents/{content_id}/archive', request_method='PUT')  # nopep8
362
+        configurator.add_view(self.archive_content, route_name='archive_content')  # nopep8
363
+        configurator.add_route('unarchive_content', '/workspaces/{workspace_id}/contents/{content_id}/unarchive', request_method='PUT')  # nopep8
364
+        configurator.add_view(self.unarchive_content, route_name='unarchive_content')  # nopep8