Browse Source

add thread endpoints

Guénaël Muller 6 years ago
parent
commit
caccbc00cc

+ 5 - 1
tracim/__init__.py View File

@@ -18,6 +18,7 @@ from tracim.lib.utils.authorization import TRACIM_DEFAULT_PERM
18 18
 from tracim.lib.webdav import WebdavAppFactory
19 19
 from tracim.views import BASE_API_V2
20 20
 from tracim.views.contents_api.html_document_controller import HTMLDocumentController  # nopep8
21
+from tracim.views.contents_api.threads_controller import ThreadController
21 22
 from tracim.views.core_api.session_controller import SessionController
22 23
 from tracim.views.core_api.system_controller import SystemController
23 24
 from tracim.views.core_api.user_controller import UserController
@@ -75,12 +76,15 @@ def web(global_config, **local_settings):
75 76
     workspace_controller = WorkspaceController()
76 77
     comment_controller = CommentController()
77 78
     html_document_controller = HTMLDocumentController()
79
+    thread_controller = ThreadController()
78 80
     configurator.include(session_controller.bind, route_prefix=BASE_API_V2)
79 81
     configurator.include(system_controller.bind, route_prefix=BASE_API_V2)
80 82
     configurator.include(user_controller.bind, route_prefix=BASE_API_V2)
81 83
     configurator.include(workspace_controller.bind, route_prefix=BASE_API_V2)
82 84
     configurator.include(comment_controller.bind, route_prefix=BASE_API_V2)
83
-    configurator.include(html_document_controller.bind, route_prefix=BASE_API_V2)
85
+    configurator.include(html_document_controller.bind, route_prefix=BASE_API_V2)  # nopep8
86
+    configurator.include(thread_controller.bind, route_prefix=BASE_API_V2)
87
+
84 88
     hapic.add_documentation_view(
85 89
         '/api/v2/doc',
86 90
         'Tracim v2 API',

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

@@ -218,3 +218,219 @@ class TestHtmlDocuments(FunctionalTest):
218 218
         assert content['content_type'] == 'page'
219 219
         assert content['content_id'] == 6
220 220
         assert content['status'] == 'closed-deprecated'
221
+
222
+
223
+class TestThreads(FunctionalTest):
224
+    """
225
+    Tests for /api/v2/workspaces/{workspace_id}/threads/{content_id}
226
+    endpoint
227
+    """
228
+
229
+    fixtures = [BaseFixture, ContentFixtures]
230
+
231
+    def test_api__get_thread__err_400__wrong_content_type(self) -> None:
232
+        """
233
+        Get one html document of a content
234
+        """
235
+        self.testapp.authorization = (
236
+            'Basic',
237
+            (
238
+                'admin@admin.admin',
239
+                'admin@admin.admin'
240
+            )
241
+        )
242
+        res = self.testapp.get(
243
+            '/api/v2/workspaces/2/threads/6',
244
+            status=400
245
+        )   # nopep8
246
+
247
+    def test_api__get_thread__ok_200__nominal_case(self) -> None:
248
+        """
249
+        Get one html document of a content
250
+        """
251
+        self.testapp.authorization = (
252
+            'Basic',
253
+            (
254
+                'admin@admin.admin',
255
+                'admin@admin.admin'
256
+            )
257
+        )
258
+        res = self.testapp.get(
259
+            '/api/v2/workspaces/2/threads/7',
260
+            status=200
261
+        )   # nopep8
262
+        content = res.json_body
263
+        assert content['content_type'] == 'thread'
264
+        assert content['content_id'] == 7
265
+        assert content['is_archived'] is False
266
+        assert content['is_deleted'] is False
267
+        assert content['label'] == 'Best Cakes ?'
268
+        assert content['parent_id'] == 3
269
+        assert content['show_in_ui'] is True
270
+        assert content['slug'] == 'best-cakes'
271
+        assert content['status'] == 'open'
272
+        assert content['workspace_id'] == 2
273
+        assert content['current_revision_id'] == 8
274
+        # TODO - G.M - 2018-06-173 - check date format
275
+        assert content['created']
276
+        assert content['author']
277
+        assert content['author']['user_id'] == 1
278
+        assert content['author']['avatar_url'] is None
279
+        assert content['author']['public_name'] == 'Global manager'
280
+        # TODO - G.M - 2018-06-173 - check date format
281
+        assert content['modified']
282
+        assert content['last_modifier'] == content['author']
283
+        assert content['raw_content'] == 'What is the best cake ?'  # nopep8
284
+
285
+    def test_api__update_thread__ok_200__nominal_case(self) -> None:
286
+        """
287
+        Update(put) one html document of a content
288
+        """
289
+        self.testapp.authorization = (
290
+            'Basic',
291
+            (
292
+                'admin@admin.admin',
293
+                'admin@admin.admin'
294
+            )
295
+        )
296
+        params = {
297
+            'label': 'My New label',
298
+            'raw_content': '<p> Le nouveau contenu </p>',
299
+        }
300
+        res = self.testapp.put_json(
301
+            '/api/v2/workspaces/2/threads/7',
302
+            params=params,
303
+            status=200
304
+        )
305
+        content = res.json_body
306
+        assert content['content_type'] == 'thread'
307
+        assert content['content_id'] == 7
308
+        assert content['is_archived'] is False
309
+        assert content['is_deleted'] is False
310
+        assert content['label'] == 'My New label'
311
+        assert content['parent_id'] == 3
312
+        assert content['show_in_ui'] is True
313
+        assert content['slug'] == 'my-new-label'
314
+        assert content['status'] == 'open'
315
+        assert content['workspace_id'] == 2
316
+        assert content['current_revision_id'] == 26
317
+        # TODO - G.M - 2018-06-173 - check date format
318
+        assert content['created']
319
+        assert content['author']
320
+        assert content['author']['user_id'] == 1
321
+        assert content['author']['avatar_url'] is None
322
+        assert content['author']['public_name'] == 'Global manager'
323
+        # TODO - G.M - 2018-06-173 - check date format
324
+        assert content['modified']
325
+        assert content['last_modifier'] == content['author']
326
+        assert content['raw_content'] == '<p> Le nouveau contenu </p>'
327
+
328
+        res = self.testapp.get(
329
+            '/api/v2/workspaces/2/threads/7',
330
+            status=200
331
+        )   # nopep8
332
+        content = res.json_body
333
+        assert content['content_type'] == 'thread'
334
+        assert content['content_id'] == 7
335
+        assert content['is_archived'] is False
336
+        assert content['is_deleted'] is False
337
+        assert content['label'] == 'My New label'
338
+        assert content['parent_id'] == 3
339
+        assert content['show_in_ui'] is True
340
+        assert content['slug'] == 'my-new-label'
341
+        assert content['status'] == 'open'
342
+        assert content['workspace_id'] == 2
343
+        assert content['current_revision_id'] == 26
344
+        # TODO - G.M - 2018-06-173 - check date format
345
+        assert content['created']
346
+        assert content['author']
347
+        assert content['author']['user_id'] == 1
348
+        assert content['author']['avatar_url'] is None
349
+        assert content['author']['public_name'] == 'Global manager'
350
+        # TODO - G.M - 2018-06-173 - check date format
351
+        assert content['modified']
352
+        assert content['last_modifier'] == content['author']
353
+        assert content['raw_content'] == '<p> Le nouveau contenu </p>'
354
+
355
+    def test_api__get_thread_revisions__ok_200__nominal_case(
356
+            self
357
+    ) -> None:
358
+        """
359
+        Get one html document of a content
360
+        """
361
+        self.testapp.authorization = (
362
+            'Basic',
363
+            (
364
+                'admin@admin.admin',
365
+                'admin@admin.admin'
366
+            )
367
+        )
368
+        res = self.testapp.get(
369
+            '/api/v2/workspaces/2/threads/7/revisions',
370
+            status=200
371
+        )
372
+        revisions = res.json_body
373
+        assert len(revisions) == 1
374
+        revision = revisions[0]
375
+        assert revision['content_type'] == 'thread'
376
+        assert revision['content_id'] == 7
377
+        assert revision['is_archived'] is False
378
+        assert revision['is_deleted'] is False
379
+        assert revision['label'] == 'Best Cakes ?'
380
+        assert revision['parent_id'] == 3
381
+        assert revision['show_in_ui'] is True
382
+        assert revision['slug'] == 'best-cakes'
383
+        assert revision['status'] == 'open'
384
+        assert revision['workspace_id'] == 2
385
+        assert revision['revision_id'] == 8
386
+        assert revision['sub_content_types']
387
+        # TODO - G.M - 2018-06-173 - Test with real comments
388
+        assert revision['comments_ids'] == []
389
+        # TODO - G.M - 2018-06-173 - check date format
390
+        assert revision['created']
391
+        assert revision['author']
392
+        assert revision['author']['user_id'] == 1
393
+        assert revision['author']['avatar_url'] is None
394
+        assert revision['author']['public_name'] == 'Global manager'
395
+
396
+    def test_api__set_thread_status__ok_200__nominal_case(self) -> None:
397
+        """
398
+        Get one html document of a content
399
+        """
400
+        self.testapp.authorization = (
401
+            'Basic',
402
+            (
403
+                'admin@admin.admin',
404
+                'admin@admin.admin'
405
+            )
406
+        )
407
+        params = {
408
+            'status': 'closed-deprecated',
409
+        }
410
+
411
+        # before
412
+        res = self.testapp.get(
413
+            '/api/v2/workspaces/2/threads/7',
414
+            status=200
415
+        )   # nopep8
416
+        content = res.json_body
417
+        assert content['content_type'] == 'thread'
418
+        assert content['content_id'] == 7
419
+        assert content['status'] == 'open'
420
+
421
+        # set status
422
+        res = self.testapp.put_json(
423
+            '/api/v2/workspaces/2/threads/7/status',
424
+            params=params,
425
+            status=204
426
+        )
427
+
428
+        # after
429
+        res = self.testapp.get(
430
+            '/api/v2/workspaces/2/threads/7',
431
+            status=200
432
+        )   # nopep8
433
+        content = res.json_body
434
+        assert content['content_type'] == 'thread'
435
+        assert content['content_id'] == 7
436
+        assert content['status'] == 'closed-deprecated'

+ 197 - 0
tracim/views/contents_api/threads_controller.py View File

@@ -0,0 +1,197 @@
1
+# coding=utf-8
2
+import transaction
3
+from pyramid.config import Configurator
4
+
5
+from tracim.models.data import UserRoleInWorkspace
6
+
7
+try:  # Python 3.5+
8
+    from http import HTTPStatus
9
+except ImportError:
10
+    from http import client as HTTPStatus
11
+
12
+from tracim import TracimRequest
13
+from tracim.extensions import hapic
14
+from tracim.lib.core.content import ContentApi
15
+from tracim.views.controllers import Controller
16
+from tracim.views.core_api.schemas import ThreadContentSchema
17
+from tracim.views.core_api.schemas import ThreadRevisionSchema
18
+from tracim.views.core_api.schemas import SetContentStatusSchema
19
+from tracim.views.core_api.schemas import ThreadModifySchema
20
+from tracim.views.core_api.schemas import WorkspaceAndContentIdPathSchema
21
+from tracim.views.core_api.schemas import NoContentSchema
22
+from tracim.lib.utils.authorization import require_content_types
23
+from tracim.lib.utils.authorization import require_workspace_role
24
+from tracim.exceptions import WorkspaceNotFound, ContentTypeNotAllowed
25
+from tracim.exceptions import InsufficientUserProfile
26
+from tracim.exceptions import NotAuthenticated
27
+from tracim.exceptions import AuthenticationFailed
28
+from tracim.models.contents import ContentTypeLegacy as ContentType
29
+from tracim.models.contents import thread_type
30
+from tracim.models.revision_protection import new_revision
31
+
32
+THREAD_ENDPOINTS_TAG = 'Threads'
33
+
34
+
35
+class ThreadController(Controller):
36
+
37
+    @hapic.with_api_doc(tags=[THREAD_ENDPOINTS_TAG])
38
+    @hapic.handle_exception(NotAuthenticated, HTTPStatus.UNAUTHORIZED)
39
+    @hapic.handle_exception(InsufficientUserProfile, HTTPStatus.FORBIDDEN)
40
+    @hapic.handle_exception(WorkspaceNotFound, HTTPStatus.FORBIDDEN)
41
+    @hapic.handle_exception(AuthenticationFailed, HTTPStatus.BAD_REQUEST)
42
+    @hapic.handle_exception(ContentTypeNotAllowed, HTTPStatus.BAD_REQUEST)
43
+    @require_workspace_role(UserRoleInWorkspace.READER)
44
+    @require_content_types([thread_type])
45
+    @hapic.input_path(WorkspaceAndContentIdPathSchema())
46
+    @hapic.output_body(ThreadContentSchema())
47
+    def get_thread(self, context, request: TracimRequest, hapic_data=None):  # nopep8
48
+        """
49
+        Get thread content
50
+        """
51
+        app_config = request.registry.settings['CFG']
52
+        api = ContentApi(
53
+            current_user=request.current_user,
54
+            session=request.dbsession,
55
+            config=app_config,
56
+        )
57
+        content = api.get_one(
58
+            hapic_data.path.content_id,
59
+            content_type=ContentType.Any
60
+        )
61
+        return api.get_content_in_context(content)
62
+
63
+    @hapic.with_api_doc(tags=[THREAD_ENDPOINTS_TAG])
64
+    @hapic.handle_exception(NotAuthenticated, HTTPStatus.UNAUTHORIZED)
65
+    @hapic.handle_exception(InsufficientUserProfile, HTTPStatus.FORBIDDEN)
66
+    @hapic.handle_exception(WorkspaceNotFound, HTTPStatus.FORBIDDEN)
67
+    @hapic.handle_exception(AuthenticationFailed, HTTPStatus.BAD_REQUEST)
68
+    @require_workspace_role(UserRoleInWorkspace.CONTRIBUTOR)
69
+    @require_content_types([thread_type])
70
+    @hapic.input_path(WorkspaceAndContentIdPathSchema())
71
+    @hapic.input_body(ThreadModifySchema())
72
+    @hapic.output_body(ThreadContentSchema())
73
+    def update_thread(self, context, request: TracimRequest, hapic_data=None):
74
+        """
75
+        update thread
76
+        """
77
+        app_config = request.registry.settings['CFG']
78
+        api = ContentApi(
79
+            current_user=request.current_user,
80
+            session=request.dbsession,
81
+            config=app_config,
82
+        )
83
+        content = api.get_one(
84
+            hapic_data.path.content_id,
85
+            content_type=ContentType.Any
86
+        )
87
+        with new_revision(
88
+                session=request.dbsession,
89
+                tm=transaction.manager,
90
+                content=content
91
+        ):
92
+            api.update_content(
93
+                item=content,
94
+                new_label=hapic_data.body.label,
95
+                new_content=hapic_data.body.raw_content,
96
+
97
+            )
98
+            api.save(content)
99
+        return api.get_content_in_context(content)
100
+
101
+    @hapic.with_api_doc(tags=[THREAD_ENDPOINTS_TAG])
102
+    @hapic.handle_exception(NotAuthenticated, HTTPStatus.UNAUTHORIZED)
103
+    @hapic.handle_exception(InsufficientUserProfile, HTTPStatus.FORBIDDEN)
104
+    @hapic.handle_exception(WorkspaceNotFound, HTTPStatus.FORBIDDEN)
105
+    @hapic.handle_exception(AuthenticationFailed, HTTPStatus.BAD_REQUEST)
106
+    @require_workspace_role(UserRoleInWorkspace.READER)
107
+    @require_content_types([thread_type])
108
+    @hapic.input_path(WorkspaceAndContentIdPathSchema())
109
+    @hapic.output_body(ThreadRevisionSchema(many=True))
110
+    def get_thread_revisions(self, context, request: TracimRequest, hapic_data=None):  # nopep8
111
+        """
112
+        get thread revisions
113
+        """
114
+        app_config = request.registry.settings['CFG']
115
+        api = ContentApi(
116
+            current_user=request.current_user,
117
+            session=request.dbsession,
118
+            config=app_config,
119
+        )
120
+        content = api.get_one(
121
+            hapic_data.path.content_id,
122
+            content_type=ContentType.Any
123
+        )
124
+        revisions = content.revisions
125
+        return [
126
+            api.get_revision_in_context(revision)
127
+            for revision in revisions
128
+        ]
129
+
130
+    @hapic.with_api_doc(tags=[THREAD_ENDPOINTS_TAG])
131
+    @hapic.handle_exception(NotAuthenticated, HTTPStatus.UNAUTHORIZED)
132
+    @hapic.handle_exception(InsufficientUserProfile, HTTPStatus.FORBIDDEN)
133
+    @hapic.handle_exception(WorkspaceNotFound, HTTPStatus.FORBIDDEN)
134
+    @hapic.handle_exception(AuthenticationFailed, HTTPStatus.BAD_REQUEST)
135
+    @require_workspace_role(UserRoleInWorkspace.CONTRIBUTOR)
136
+    @require_content_types([thread_type])
137
+    @hapic.input_path(WorkspaceAndContentIdPathSchema())
138
+    @hapic.input_body(SetContentStatusSchema())
139
+    @hapic.output_body(NoContentSchema(), default_http_code=HTTPStatus.NO_CONTENT)  # nopep8
140
+    def set_thread_status(self, context, request: TracimRequest, hapic_data=None):  # nopep8
141
+        """
142
+        set thread status
143
+        """
144
+        app_config = request.registry.settings['CFG']
145
+        api = ContentApi(
146
+            current_user=request.current_user,
147
+            session=request.dbsession,
148
+            config=app_config,
149
+        )
150
+        content = api.get_one(
151
+            hapic_data.path.content_id,
152
+            content_type=ContentType.Any
153
+        )
154
+        with new_revision(
155
+                session=request.dbsession,
156
+                tm=transaction.manager,
157
+                content=content
158
+        ):
159
+            api.set_status(
160
+                content,
161
+                hapic_data.body.status,
162
+            )
163
+            api.save(content)
164
+        return
165
+
166
+    def bind(self, configurator: Configurator):
167
+        # Get thread
168
+        configurator.add_route(
169
+            'thread',
170
+            '/workspaces/{workspace_id}/threads/{content_id}',
171
+            request_method='GET'
172
+        )
173
+        configurator.add_view(self.get_thread, route_name='thread')  # nopep8
174
+
175
+        # update thread
176
+        configurator.add_route(
177
+            'update_thread',
178
+            '/workspaces/{workspace_id}/threads/{content_id}',
179
+            request_method='PUT'
180
+        )  # nopep8
181
+        configurator.add_view(self.update_thread, route_name='update_thread')  # nopep8
182
+
183
+        # get thread revisions
184
+        configurator.add_route(
185
+            'thread_revisions',
186
+            '/workspaces/{workspace_id}/threads/{content_id}/revisions',  # nopep8
187
+            request_method='GET'
188
+        )
189
+        configurator.add_view(self.get_thread_revisions, route_name='thread_revisions')  # nopep8
190
+
191
+        # get thread revisions
192
+        configurator.add_route(
193
+            'set_thread_status',
194
+            '/workspaces/{workspace_id}/threads/{content_id}/status',  # nopep8
195
+            request_method='PUT'
196
+        )
197
+        configurator.add_view(self.set_thread_status, route_name='set_thread_status')  # nopep8