Browse Source

Partial Support for get content in api (without full filter support)

Guénaël Muller 6 years ago
parent
commit
7034d8efd9

+ 12 - 1
tracim/lib/core/content.py View File

@@ -9,6 +9,8 @@ import transaction
9 9
 from sqlalchemy import func
10 10
 from sqlalchemy.orm import Query
11 11
 
12
+from tracim.models.context_models import ContentInContext
13
+
12 14
 __author__ = 'damien'
13 15
 
14 16
 import datetime
@@ -113,6 +115,7 @@ class ContentApi(object):
113 115
             show_archived: bool = False,
114 116
             show_deleted: bool = False,
115 117
             show_temporary: bool = False,
118
+            show_active: bool = True,
116 119
             all_content_in_treeview: bool = True,
117 120
             force_show_all_types: bool = False,
118 121
             disable_user_workspaces_filter: bool = False,
@@ -124,6 +127,7 @@ class ContentApi(object):
124 127
         self._show_archived = show_archived
125 128
         self._show_deleted = show_deleted
126 129
         self._show_temporary = show_temporary
130
+        self._show_active = show_active
127 131
         self._show_all_type_of_contents_in_treeview = all_content_in_treeview
128 132
         self._force_show_all_types = force_show_all_types
129 133
         self._disable_user_workspaces_filter = disable_user_workspaces_filter
@@ -156,6 +160,9 @@ class ContentApi(object):
156 160
             self._show_deleted = previous_show_deleted
157 161
             self._show_temporary = previous_show_temporary
158 162
 
163
+    def get_content_in_context(self, content: Content):
164
+        return ContentInContext(content, self._session, self._config)
165
+
159 166
     def get_revision_join(self) -> sqlalchemy.sql.elements.BooleanClauseList:
160 167
         """
161 168
         Return the Content/ContentRevision query join condition
@@ -243,6 +250,9 @@ class ContentApi(object):
243 250
         if not self._show_temporary:
244 251
             result = result.filter(Content.is_temporary==False)
245 252
 
253
+        if not self._show_active:
254
+            result = result.filter(Content.is_active==False)
255
+
246 256
         return result
247 257
 
248 258
     def __revisions_real_base_query(
@@ -686,8 +696,9 @@ class ContentApi(object):
686 696
 
687 697
         if parent_id:
688 698
             resultset = resultset.filter(Content.parent_id==parent_id)
689
-        if parent_id is False:
699
+        if parent_id == 0 or parent_id is False:
690 700
             resultset = resultset.filter(Content.parent_id == None)
701
+        # parent_id == None give all contents
691 702
 
692 703
         return resultset.all()
693 704
 

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

@@ -87,7 +87,7 @@ class ContentStatusLegacy(NewContentStatus):
87 87
     def __init__(self, slug: str):
88 88
         for status in CONTENT_DEFAULT_STATUS:
89 89
             if slug == status.slug:
90
-                super(ContentStatusLegacy).__init__(
90
+                super(ContentStatusLegacy, self).__init__(
91 91
                     slug=status.slug,
92 92
                     global_status=status.global_status,
93 93
                     label=status.label,
@@ -168,12 +168,22 @@ pagehtml_type = NewContentType(
168 168
     available_statuses=CONTENT_DEFAULT_STATUS,
169 169
 )
170 170
 
171
+# TODO - G.M - 31-05-2018 - Set Better folder params
172
+folder_type = NewContentType(
173
+    slug='folder',
174
+    icon=thread.icon,
175
+    hexcolor=thread.hexcolor,
176
+    label='Folder',
177
+    creation_label='Create collection of any documents',
178
+    available_statuses=CONTENT_DEFAULT_STATUS,
179
+)
171 180
 
172 181
 CONTENT_DEFAULT_TYPE = [
173 182
     thread_type,
174 183
     file_type,
175 184
     pagemarkdownplus_type,
176 185
     pagehtml_type,
186
+    folder_type,
177 187
 ]
178 188
 
179 189
 
@@ -195,7 +205,7 @@ class ContentTypeLegacy(NewContentType):
195 205
     def __init__(self, slug: str):
196 206
         for content_type in CONTENT_DEFAULT_TYPE:
197 207
             if slug == content_type.slug:
198
-                super(ContentTypeLegacy).__init__(
208
+                super(ContentTypeLegacy, self).__init__(
199 209
                     slug=content_type.slug,
200 210
                     icon=content_type.icon,
201 211
                     hexcolor=content_type.hexcolor,

+ 80 - 1
tracim/models/context_models.py View File

@@ -7,7 +7,7 @@ from sqlalchemy.orm import Session
7 7
 from tracim import CFG
8 8
 from tracim.models import User
9 9
 from tracim.models.auth import Profile
10
-from tracim.models.data import Workspace, UserRoleInWorkspace
10
+from tracim.models.data import Workspace, UserRoleInWorkspace, Content
11 11
 from tracim.models.workspace_menu_entries import default_workspace_menu_entry, \
12 12
     WorkspaceMenuEntry
13 13
 
@@ -22,6 +22,21 @@ class LoginCredentials(object):
22 22
         self.password = password
23 23
 
24 24
 
25
+class ContentFilter(object):
26
+    """
27
+    Content filter model
28
+    """
29
+    def __init__(self,
30
+                 parent_id: int = None,
31
+                 show_archived: int = 0,
32
+                 show_deleted: int = 0,
33
+                 show_active: int = 1,
34
+                 ):
35
+        self.parent_id = parent_id
36
+        self.show_archived = bool(show_archived)
37
+        self.show_deleted = bool(show_deleted)
38
+        self.show_active = bool(show_active)
39
+
25 40
 class UserInContext(object):
26 41
     """
27 42
     Interface to get User data and User data related to context.
@@ -211,3 +226,67 @@ class UserRoleWorkspaceInContext(object):
211 226
             self.dbsession,
212 227
             self.config
213 228
         )
229
+
230
+
231
+class ContentInContext(object):
232
+    """
233
+    Interface to get Content data and Content data related to context.
234
+    """
235
+
236
+    def __init__(self, content: Content, dbsession: Session, config: CFG):
237
+        self.content = content
238
+        self.dbsession = dbsession
239
+        self.config = config
240
+
241
+    # Default
242
+
243
+    @property
244
+    def id(self) -> int:
245
+        return self.content.content_id
246
+
247
+    @property
248
+    def parent_id(self) -> int:
249
+        return self.content.parent_id
250
+
251
+    @property
252
+    def workspace_id(self) -> int:
253
+        return self.content.workspace_id
254
+
255
+    @property
256
+    def label(self) -> str:
257
+        return self.content.label
258
+
259
+    @property
260
+    def content_type_slug(self) -> str:
261
+        return self.content.type
262
+
263
+    @property
264
+    def sub_content_type_slug(self) -> typing.List[str]:
265
+        return [type.slug for type in self.content.get_allowed_content_types()]
266
+
267
+    @property
268
+    def status_slug(self) -> str:
269
+        return self.content.status
270
+
271
+    @property
272
+    def is_archived(self):
273
+        return self.content.is_archived
274
+
275
+    @property
276
+    def is_deleted(self):
277
+        return self.content.is_deleted
278
+
279
+    # Context-related
280
+
281
+    @property
282
+    def show_in_ui(self):
283
+        # TODO - G.M - 31-05-2018 - Enable Show_in_ui params
284
+        # if false, then do not show content in the treeview.
285
+        # This may his maybe used for specific contents or for sub-contents.
286
+        # Default is True.
287
+        # In first version of the API, this field is always True
288
+        return True
289
+
290
+    @property
291
+    def slug(self):
292
+        return slugify(self.content.label)

+ 4 - 0
tracim/models/data.py View File

@@ -1148,6 +1148,10 @@ class Content(DeclarativeBase):
1148 1148
         return not self.is_archived and not self.is_deleted
1149 1149
 
1150 1150
     @property
1151
+    def is_active(self) -> bool:
1152
+        return self.is_editable
1153
+
1154
+    @property
1151 1155
     def depot_file(self) -> UploadedFile:
1152 1156
         return self.revision.depot_file
1153 1157
 

+ 1 - 1
tracim/tests/functional/test_system.py View File

@@ -131,7 +131,7 @@ class TestContentsTypesEndpoint(FunctionalTest):
131 131
         assert content_type['creation_label'] == 'Write a document'
132 132
         assert 'available_statuses' in content_type
133 133
         assert len(content_type['available_statuses']) == 4
134
-
134
+        # TODO - G.M - 31-05-2018 - Check Folder type
135 135
         # TODO - G.M - 29-05-2018 - Better check for available_statuses
136 136
 
137 137
     def test_api__get_content_types__err_401__unregistered_user(self):

+ 79 - 12
tracim/tests/functional/test_workspaces.py View File

@@ -219,7 +219,6 @@ class TestWorkspaceMembersEndpoint(FunctionalTest):
219 219
         assert 'details' in res.json.keys()
220 220
 
221 221
 
222
-@pytest.mark.xfail()
223 222
 class TestWorkspaceContents(FunctionalTest):
224 223
     """
225 224
     Tests for /api/v2/workspaces/{workspace_id}/contents endpoint
@@ -240,7 +239,40 @@ class TestWorkspaceContents(FunctionalTest):
240 239
         )
241 240
         res = self.testapp.get('/api/v2/workspaces/1/contents', status=200).json_body   # nopep8
242 241
         # TODO - G.M - 30-05-2018 - Check this test
243
-        raise NotImplementedError()
242
+        assert len(res) == 3
243
+        content = res[0]
244
+        assert content['id'] == 1
245
+        assert content['is_archived'] is False
246
+        assert content['is_deleted'] is False
247
+        assert content['label'] == 'Tools'
248
+        assert content['parent_id'] is None
249
+        assert content['show_in_ui'] == True
250
+        assert content['slug'] == 'tools'
251
+        assert content['status_slug'] == 'open'
252
+        assert set(content['sub_content_type_slug']) == set(['thread', 'page', 'folder', 'file'])
253
+        assert content['workspace_id'] == 1
254
+        content = res[1]
255
+        assert content['id'] == 2
256
+        assert content['is_archived'] is False
257
+        assert content['is_deleted'] is False
258
+        assert content['label'] == 'Menus'
259
+        assert content['parent_id'] is None
260
+        assert content['show_in_ui'] == True
261
+        assert content['slug'] == 'menus'
262
+        assert content['status_slug'] == 'open'
263
+        assert set(content['sub_content_type_slug']) == set(['thread', 'page', 'folder', 'file'])
264
+        assert content['workspace_id'] == 1
265
+        content = res[2]
266
+        assert content['id'] == 11
267
+        assert content['is_archived'] is False
268
+        assert content['is_deleted'] is False
269
+        assert content['label'] == 'Current Menu'
270
+        assert content['parent_id'] == 2
271
+        assert content['show_in_ui'] == True
272
+        assert content['slug'] == 'current-menu'
273
+        assert content['status_slug'] == 'open'
274
+        assert set(content['sub_content_type_slug']) == set(['thread', 'page', 'folder', 'file'])
275
+        assert content['workspace_id'] == 1
244 276
 
245 277
     # Root related
246 278
 
@@ -267,8 +299,31 @@ class TestWorkspaceContents(FunctionalTest):
267 299
             params=params,
268 300
         ).json_body  # nopep8
269 301
         # TODO - G.M - 30-05-2018 - Check this test
270
-        raise NotImplementedError()
271
-
302
+        assert len(res) == 2
303
+        content = res[0]
304
+        assert content['id'] == 1
305
+        assert content['is_archived'] is False
306
+        assert content['is_deleted'] is False
307
+        assert content['label'] == 'Tools'
308
+        assert content['parent_id'] is None
309
+        assert content['show_in_ui'] == True
310
+        assert content['slug'] == 'tools'
311
+        assert content['status_slug'] == 'open'
312
+        assert set(content['sub_content_type_slug']) == set(['thread', 'page', 'folder', 'file'])
313
+        assert content['workspace_id'] == 1
314
+        content = res[1]
315
+        assert content['id'] == 2
316
+        assert content['is_archived'] is False
317
+        assert content['is_deleted'] is False
318
+        assert content['label'] == 'Menus'
319
+        assert content['parent_id'] is None
320
+        assert content['show_in_ui'] == True
321
+        assert content['slug'] == 'menus'
322
+        assert content['status_slug'] == 'open'
323
+        assert set(content['sub_content_type_slug']) == set(['thread', 'page', 'folder', 'file'])
324
+        assert content['workspace_id'] == 1
325
+
326
+    @pytest.mark.xfail()
272 327
     def test_api__get_workspace_content__ok_200__get_only_active_root_content(self):
273 328
         """
274 329
         Check obtain workspace root active contents
@@ -294,6 +349,7 @@ class TestWorkspaceContents(FunctionalTest):
294 349
         # TODO - G.M - 30-05-2018 - Check this test
295 350
         raise NotImplementedError()
296 351
 
352
+    @pytest.mark.xfail()
297 353
     def test_api__get_workspace_content__ok_200__get_only_archived_root_content(self):
298 354
         """
299 355
         Check obtain workspace root archived contents
@@ -319,6 +375,7 @@ class TestWorkspaceContents(FunctionalTest):
319 375
         # TODO - G.M - 30-05-2018 - Check this test
320 376
         raise NotImplementedError()
321 377
 
378
+    @pytest.mark.xfail()
322 379
     def test_api__get_workspace_content__ok_200__get_only_deleted_root_content(self):
323 380
         """
324 381
          Check obtain workspace root deleted contents
@@ -368,7 +425,7 @@ class TestWorkspaceContents(FunctionalTest):
368 425
             params=params,
369 426
         ).json_body  # nopep8
370 427
         # TODO - G.M - 30-05-2018 - Check this test
371
-        raise NotImplementedError()
428
+        assert res == []
372 429
 
373 430
     # Folder related
374 431
 
@@ -395,8 +452,20 @@ class TestWorkspaceContents(FunctionalTest):
395 452
             params=params,
396 453
         ).json_body   # nopep8
397 454
         # TODO - G.M - 30-05-2018 - Check this test
398
-        raise NotImplementedError()
399
-
455
+        assert len(res) == 1
456
+        content = res[0]
457
+        assert content['id'] == 11
458
+        assert content['is_archived'] is False
459
+        assert content['is_deleted'] is False
460
+        assert content['label'] == 'Current Menu'
461
+        assert content['parent_id'] == 2
462
+        assert content['show_in_ui'] == True
463
+        assert content['slug'] == 'current-menu'
464
+        assert content['status_slug'] == 'open'
465
+        assert set(content['sub_content_type_slug']) == set(['thread', 'page', 'folder', 'file'])
466
+        assert content['workspace_id'] == 1
467
+
468
+    @pytest.mark.xfail()
400 469
     def test_api__get_workspace_content__ok_200__get_only_active_folder_content(self):
401 470
         """
402 471
          Check obtain workspace folder active contents
@@ -422,6 +491,7 @@ class TestWorkspaceContents(FunctionalTest):
422 491
         # TODO - G.M - 30-05-2018 - Check this test
423 492
         raise NotImplementedError()
424 493
 
494
+    @pytest.mark.xfail()
425 495
     def test_api__get_workspace_content__ok_200__get_only_archived_folder_content(self):
426 496
         """
427 497
          Check obtain workspace folder archived contents
@@ -447,6 +517,7 @@ class TestWorkspaceContents(FunctionalTest):
447 517
         # TODO - G.M - 30-05-2018 - Check this test
448 518
         raise NotImplementedError()
449 519
 
520
+    @pytest.mark.xfail()
450 521
     def test_api__get_workspace_content__ok_200__get_only_deleted_folder_content(self):
451 522
         """
452 523
          Check obtain workspace folder deleted contents
@@ -496,7 +567,7 @@ class TestWorkspaceContents(FunctionalTest):
496 567
             params=params,
497 568
         ).json_body   # nopep8
498 569
         # TODO - G.M - 30-05-2018 - Check this test
499
-        raise NotImplementedError()
570
+        assert res == []
500 571
 
501 572
     # Error case
502 573
 
@@ -552,7 +623,3 @@ class TestWorkspaceContents(FunctionalTest):
552 623
         assert 'code' in res.json.keys()
553 624
         assert 'message' in res.json.keys()
554 625
         assert 'details' in res.json.keys()
555
-
556
-    def test_api_get_workspace_content__err_404__parent_id_does_not_exist(self):
557
-        # TODO - G.M - 30-05-2018 - Check this test
558
-        raise NotImplementedError()

+ 7 - 7
tracim/views/core_api/schemas.py View File

@@ -5,7 +5,7 @@ from marshmallow.validate import OneOf
5 5
 
6 6
 from tracim.models.auth import Profile
7 7
 from tracim.models.contents import CONTENT_DEFAULT_TYPE, GlobalStatus, CONTENT_DEFAULT_STATUS
8
-from tracim.models.context_models import LoginCredentials
8
+from tracim.models.context_models import LoginCredentials, ContentFilter
9 9
 from tracim.models.data import UserRoleInWorkspace
10 10
 
11 11
 
@@ -85,8 +85,7 @@ class WorkspaceAndContentIdPathSchema(WorkspaceIdPathSchema, ContentIdPathSchema
85 85
     pass
86 86
 
87 87
 
88
-class FilterContentPathSchema(marshmallow.Schema):
89
-    workspace_id = marshmallow.fields.Int(example=4, required=True)
88
+class FilterContentQuerySchema(marshmallow.Schema):
90 89
     parent_id = workspace_id = marshmallow.fields.Int(
91 90
         example=2,
92 91
         default=None,
@@ -118,7 +117,9 @@ class FilterContentPathSchema(marshmallow.Schema):
118 117
                     'The reason for this parameter to exist is for example '
119 118
                     'to allow to show only archived documents'
120 119
     )
121
-
120
+    @post_load
121
+    def make_content_filter(self, data):
122
+        return ContentFilter(**data)
122 123
 ###
123 124
 
124 125
 
@@ -325,9 +326,8 @@ class ContentDigestSchema(marshmallow.Schema):
325 326
         example='htmlpage',
326 327
         validate=OneOf([content.slug for content in CONTENT_DEFAULT_TYPE]),
327 328
     )
328
-    sub_content_type_slug = marshmallow.fields.Nested(
329
-        ContentTypeSchema(only=('slug',)),
330
-        many=True,
329
+    sub_content_type_slug = marshmallow.fields.List(
330
+        marshmallow.fields.Str,
331 331
         description='list of content types allowed as sub contents. '
332 332
                     'This field is required for folder contents, '
333 333
                     'set it to empty list in other cases'

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

@@ -3,10 +3,11 @@ import typing
3 3
 from pyramid.config import Configurator
4 4
 from sqlalchemy.orm.exc import NoResultFound
5 5
 
6
+from tracim.lib.core.content import ContentApi
6 7
 from tracim.lib.core.userworkspace import RoleApi
7 8
 from tracim.lib.utils.authorization import require_workspace_role
8 9
 from tracim.models.context_models import WorkspaceInContext, \
9
-    UserRoleWorkspaceInContext
10
+    UserRoleWorkspaceInContext, ContentInContext
10 11
 from tracim.models.data import UserRoleInWorkspace
11 12
 
12 13
 try:  # Python 3.5+
@@ -21,7 +22,9 @@ from tracim.lib.core.user import UserApi
21 22
 from tracim.lib.core.workspace import WorkspaceApi
22 23
 from tracim.views.controllers import Controller
23 24
 from tracim.views.core_api.schemas import WorkspaceSchema, UserSchema, \
24
-    WorkspaceIdPathSchema, WorkspaceMemberSchema
25
+    WorkspaceIdPathSchema, WorkspaceMemberSchema, \
26
+    WorkspaceAndContentIdPathSchema, FilterContentQuerySchema, \
27
+    ContentDigestSchema
25 28
 
26 29
 
27 30
 class WorkspaceController(Controller):
@@ -73,14 +76,55 @@ class WorkspaceController(Controller):
73 76
             for user_role in rapi.get_all_for_workspace(request.current_workspace)
74 77
         ]
75 78
 
79
+    @hapic.with_api_doc()
80
+    @hapic.handle_exception(NotAuthentificated, HTTPStatus.UNAUTHORIZED)
81
+    @hapic.handle_exception(InsufficientUserProfile, HTTPStatus.FORBIDDEN)
82
+    @hapic.handle_exception(WorkspaceNotFound, HTTPStatus.FORBIDDEN)
83
+    @require_workspace_role(UserRoleInWorkspace.READER)
84
+    @hapic.input_path(WorkspaceIdPathSchema())
85
+    @hapic.input_query(FilterContentQuerySchema())
86
+    @hapic.output_body(ContentDigestSchema(many=True))
87
+    def workspace_content(
88
+            self,
89
+            context,
90
+            request: TracimRequest,
91
+            hapic_data=None,
92
+    ) -> typing.List[ContentInContext]:
93
+        """
94
+        return list of contents found in the workspace
95
+        """
96
+        #hapic_data.query=
97
+        app_config = request.registry.settings['CFG']
98
+        content_filter = hapic_data.query
99
+        api = ContentApi(
100
+            current_user=request.current_user,
101
+            session=request.dbsession,
102
+            config=app_config,
103
+            show_archived=content_filter.show_archived,
104
+            show_deleted=content_filter.show_deleted,
105
+            show_active=content_filter.show_active,
106
+        )
107
+        contents = api.get_all(
108
+            parent_id=content_filter.parent_id,
109
+            workspace=request.current_workspace,
110
+        )
111
+        contents = [
112
+            api.get_content_in_context(content) for content in contents
113
+        ]
114
+        return contents
115
+
76 116
     def bind(self, configurator: Configurator) -> None:
77 117
         """
78 118
         Create all routes and views using pyramid configurator
79 119
         for this controller
80 120
         """
81 121
 
82
-        # Applications
122
+        # Workspace
83 123
         configurator.add_route('workspace', '/workspaces/{workspace_id}', request_method='GET')  # nopep8
84 124
         configurator.add_view(self.workspace, route_name='workspace')
125
+        # Workspace Members (Roles)
85 126
         configurator.add_route('workspace_members', '/workspaces/{workspace_id}/members', request_method='GET')  # nopep8
86 127
         configurator.add_view(self.workspaces_members, route_name='workspace_members')  # nopep8
128
+        # Workspace Content
129
+        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