Browse Source

Merge pull request #91 from tracim/feature/598_comment_htmlpage_thread_contents_endpoints

Damien Accorsi 6 years ago
parent
commit
6056263e1b
No account linked to committer's email

+ 7 - 0
README.md View File

152
     # launch your favorite web-browser
152
     # launch your favorite web-browser
153
     firefox http://localhost:6543/api/v2/doc/
153
     firefox http://localhost:6543/api/v2/doc/
154
 
154
 
155
+## Roles, profile and access rights
156
+
157
+In Tracim, only some user can access to some informations, this is also true in
158
+Tracim REST API. you can check the [roles documentation](doc/roles.md) to check
159
+what a specific user can do.
160
+
161
+
155
 CI
162
 CI
156
 ---
163
 ---
157
 
164
 

+ 50 - 0
doc/roles.md View File

1
+# Introduction
2
+
3
+In Tracim, you have 2 system of "roles".
4
+
5
+One is global to whole tracim instance and is called "global profile" (Groups).
6
+The other is workspace related and is called "workspace role".
7
+
8
+## Global profile
9
+
10
+
11
+|                               | Normal User | Managers    | Admin   |
12
+|-------------------------------|-------------|-------------|---------|
13
+| participate to workspaces     |  yes        | yes         | yes     |
14
+| access to tracim apps         |  yes        | yes         | yes     |
15
+|-------------------------------|-------------|-------------|---------|
16
+| create workspace              |  no         | yes         | yes     |
17
+| invite user to tracim         |  no         | yes, if manager of a given workspace         | yes     |
18
+|-------------------------------|-------------|-------------|---------|
19
+| set user global profile rights|  no         | no          | yes     |
20
+| deactivate user               |  no         | no          | yes     |
21
+|-------------------------------|-------------|-------------|---------|
22
+| access to all user data (/users/{user_id} endpoints) |personal-only|personal-only| yes     |
23
+
24
+
25
+## Workspace Roles
26
+
27
+
28
+|                              | Reader | Contributor | Content Manager | Workspace Manager |
29
+|------------------------------|--------|-------------|-----------------|-------------------|
30
+| read content                 |  yes   | yes         | yes             | yes               |
31
+|------------------------------|--------|-------------|-----------------|-------------------|
32
+| create content               |  no    | yes         | yes             | yes               |
33
+| edit content                 |  no    | yes         | yes             | yes               |
34
+| copy content                 |  no    | yes         | yes             | yes               |
35
+| comments content             |  no    | yes         | yes             | yes               |
36
+| update content status        |  no    | yes         | yes             | yes               |
37
+-------------------------------|--------|-------------|-----------------|-------------------|
38
+| move content                 |  no    | no          | yes             | yes               |
39
+| archive content              |  no    | no          | yes             | yes               |
40
+| delete content               |  no    | no          | yes             | yes               |
41
+|------------------------------|--------|-------------|-----------------|-------------------|
42
+| edit workspace               |  no    | no          | no              | yes               |
43
+| invite users (to workspace)  |  no    | no          | no              | yes               |
44
+| set user workspace role      |  no    | no          | no              | yes               |
45
+| revoke users (from workspace)|  no    | no          | no              | yes               |
46
+|------------------------------|--------|-------------|-----------------|-------------------|
47
+| modify comments              |  no    | owner       | owner             | yes             |
48
+| delete comments              |  no    | owner       | owner             | yes             |
49
+ 
50
+ 

+ 10 - 0
tracim/__init__.py View File

17
 from tracim.lib.utils.authorization import TRACIM_DEFAULT_PERM
17
 from tracim.lib.utils.authorization import TRACIM_DEFAULT_PERM
18
 from tracim.lib.webdav import WebdavAppFactory
18
 from tracim.lib.webdav import WebdavAppFactory
19
 from tracim.views import BASE_API_V2
19
 from tracim.views import BASE_API_V2
20
+from tracim.views.contents_api.html_document_controller import HTMLDocumentController  # nopep8
21
+from tracim.views.contents_api.threads_controller import ThreadController
20
 from tracim.views.core_api.session_controller import SessionController
22
 from tracim.views.core_api.session_controller import SessionController
21
 from tracim.views.core_api.system_controller import SystemController
23
 from tracim.views.core_api.system_controller import SystemController
22
 from tracim.views.core_api.user_controller import UserController
24
 from tracim.views.core_api.user_controller import UserController
23
 from tracim.views.core_api.workspace_controller import WorkspaceController
25
 from tracim.views.core_api.workspace_controller import WorkspaceController
26
+from tracim.views.contents_api.comment_controller import CommentController
24
 from tracim.views.errors import ErrorSchema
27
 from tracim.views.errors import ErrorSchema
25
 from tracim.lib.utils.cors import add_cors_support
28
 from tracim.lib.utils.cors import add_cors_support
26
 
29
 
71
     system_controller = SystemController()
74
     system_controller = SystemController()
72
     user_controller = UserController()
75
     user_controller = UserController()
73
     workspace_controller = WorkspaceController()
76
     workspace_controller = WorkspaceController()
77
+    comment_controller = CommentController()
78
+    html_document_controller = HTMLDocumentController()
79
+    thread_controller = ThreadController()
74
     configurator.include(session_controller.bind, route_prefix=BASE_API_V2)
80
     configurator.include(session_controller.bind, route_prefix=BASE_API_V2)
75
     configurator.include(system_controller.bind, route_prefix=BASE_API_V2)
81
     configurator.include(system_controller.bind, route_prefix=BASE_API_V2)
76
     configurator.include(user_controller.bind, route_prefix=BASE_API_V2)
82
     configurator.include(user_controller.bind, route_prefix=BASE_API_V2)
77
     configurator.include(workspace_controller.bind, route_prefix=BASE_API_V2)
83
     configurator.include(workspace_controller.bind, route_prefix=BASE_API_V2)
84
+    configurator.include(comment_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
+
78
     hapic.add_documentation_view(
88
     hapic.add_documentation_view(
79
         '/api/v2/doc',
89
         '/api/v2/doc',
80
         'Tracim v2 API',
90
         'Tracim v2 API',

+ 13 - 1
tracim/exceptions.py View File

69
     pass
69
     pass
70
 
70
 
71
 
71
 
72
-class InsufficientUserWorkspaceRole(TracimException):
72
+class InsufficientUserRoleInWorkspace(TracimException):
73
     pass
73
     pass
74
 
74
 
75
 
75
 
117
     pass
117
     pass
118
 
118
 
119
 
119
 
120
+class ContentNotFoundInTracimRequest(TracimException):
121
+    pass
122
+
123
+
124
+class ContentNotFound(TracimException):
125
+    pass
126
+
127
+
128
+class ContentTypeNotAllowed(TracimException):
129
+    pass
130
+
131
+
120
 class WorkspacesDoNotMatch(TracimException):
132
 class WorkspacesDoNotMatch(TracimException):
121
     pass
133
     pass

+ 72 - 3
tracim/fixtures/content.py View File

23
         bob = self._session.query(models.User) \
23
         bob = self._session.query(models.User) \
24
             .filter(models.User.email == 'bob@fsf.local') \
24
             .filter(models.User.email == 'bob@fsf.local') \
25
             .one()
25
             .one()
26
+        john_the_reader = self._session.query(models.User) \
27
+            .filter(models.User.email == 'john-the-reader@reader.local') \
28
+            .one()
29
+
26
         admin_workspace_api = WorkspaceApi(
30
         admin_workspace_api = WorkspaceApi(
27
             current_user=admin,
31
             current_user=admin,
28
             session=self._session,
32
             session=self._session,
38
             session=self._session,
42
             session=self._session,
39
             config=self._config
43
             config=self._config
40
         )
44
         )
45
+        bob_content_api = ContentApi(
46
+            current_user=bob,
47
+            session=self._session,
48
+            config=self._config
49
+        )
50
+        reader_content_api = ContentApi(
51
+            current_user=john_the_reader,
52
+            session=self._session,
53
+            config=self._config
54
+        )
41
         role_api = RoleApi(
55
         role_api = RoleApi(
42
             current_user=admin,
56
             current_user=admin,
43
             session=self._session,
57
             session=self._session,
68
             role_level=UserRoleInWorkspace.CONTENT_MANAGER,
82
             role_level=UserRoleInWorkspace.CONTENT_MANAGER,
69
             with_notif=False,
83
             with_notif=False,
70
         )
84
         )
85
+        role_api.create_one(
86
+            user=john_the_reader,
87
+            workspace=recipe_workspace,
88
+            role_level=UserRoleInWorkspace.READER,
89
+            with_notif=False,
90
+        )
71
         # Folders
91
         # Folders
72
 
92
 
73
         tool_workspace = content_api.create(
93
         tool_workspace = content_api.create(
112
             content_type=ContentType.Page,
132
             content_type=ContentType.Page,
113
             workspace=recipe_workspace,
133
             workspace=recipe_workspace,
114
             parent=dessert_folder,
134
             parent=dessert_folder,
115
-            label='Tiramisu Recipe',
135
+            label='Tiramisu Recipes!!!',
116
             do_save=True,
136
             do_save=True,
117
             do_notify=False,
137
             do_notify=False,
118
         )
138
         )
139
+        with new_revision(
140
+                session=self._session,
141
+                tm=transaction.manager,
142
+                content=tiramisu_page,
143
+        ):
144
+            content_api.update_content(
145
+                item=tiramisu_page,
146
+                new_content='<p>To cook a greet Tiramisu, you need many ingredients.</p>',  # nopep8
147
+                new_label='Tiramisu Recipes!!!',
148
+            )
149
+            content_api.save(tiramisu_page)
150
+
119
         best_cake_thread = content_api.create(
151
         best_cake_thread = content_api.create(
120
             content_type=ContentType.Thread,
152
             content_type=ContentType.Thread,
121
             workspace=recipe_workspace,
153
             workspace=recipe_workspace,
122
             parent=dessert_folder,
154
             parent=dessert_folder,
123
-            label='Best Cakes ?',
155
+            label='Best Cake',
124
             do_save=False,
156
             do_save=False,
125
             do_notify=False,
157
             do_notify=False,
126
         )
158
         )
127
-        best_cake_thread.description = 'What is the best cake ?'
159
+        best_cake_thread.description = 'Which is the best cake?'
128
         self._session.add(best_cake_thread)
160
         self._session.add(best_cake_thread)
129
         apple_pie_recipe = content_api.create(
161
         apple_pie_recipe = content_api.create(
130
             content_type=ContentType.File,
162
             content_type=ContentType.File,
246
             content_api.delete(bad_fruit_salad)
278
             content_api.delete(bad_fruit_salad)
247
         content_api.save(bad_fruit_salad)
279
         content_api.save(bad_fruit_salad)
248
 
280
 
281
+        content_api.create_comment(
282
+            parent=best_cake_thread,
283
+            content='<p>What is for you the best cake ever? </br> I personnally vote for Chocolate cupcake!</p>',  # nopep8
284
+            do_save=True,
285
+        )
286
+        bob_content_api.create_comment(
287
+            parent=best_cake_thread,
288
+            content='<p>What about Apple Pie? There are Awesome!</p>',
289
+            do_save=True,
290
+        )
291
+        reader_content_api.create_comment(
292
+            parent=best_cake_thread,
293
+            content='<p>You are right, but Kouign-amann are clearly better.</p>',
294
+            do_save=True,
295
+        )
296
+        with new_revision(
297
+                session=self._session,
298
+                tm=transaction.manager,
299
+                content=best_cake_thread,
300
+        ):
301
+            bob_content_api.update_content(
302
+                item=best_cake_thread,
303
+                new_content='What is the best cake?',
304
+                new_label='Best Cakes?',
305
+            )
306
+            bob_content_api.save(best_cake_thread)
249
 
307
 
308
+        with new_revision(
309
+                session=self._session,
310
+                tm=transaction.manager,
311
+                content=tiramisu_page,
312
+        ):
313
+            bob_content_api.update_content(
314
+                item=tiramisu_page,
315
+                new_content='<p>To cook a great Tiramisu, you need many ingredients.</p>',  # nopep8
316
+                new_label='Tiramisu Recipe',
317
+            )
318
+            bob_content_api.save(tiramisu_page)
250
         self._session.flush()
319
         self._session.flush()

+ 9 - 0
tracim/fixtures/users_and_groups.py View File

61
         bob.password = 'foobarbaz'
61
         bob.password = 'foobarbaz'
62
         self._session.add(bob)
62
         self._session.add(bob)
63
         g2.users.append(bob)
63
         g2.users.append(bob)
64
+
65
+        g1 = self._session.query(models.Group).\
66
+            filter(models.Group.group_name == 'users').one()
67
+        reader = models.User()
68
+        reader.display_name = 'John Reader'
69
+        reader.email = 'john-the-reader@reader.local'
70
+        reader.password = 'read'
71
+        self._session.add(reader)
72
+        g1.users.append(reader)

+ 26 - 8
tracim/lib/core/content.py View File

16
 from sqlalchemy.orm import aliased
16
 from sqlalchemy.orm import aliased
17
 from sqlalchemy.orm import joinedload
17
 from sqlalchemy.orm import joinedload
18
 from sqlalchemy.orm.attributes import get_history
18
 from sqlalchemy.orm.attributes import get_history
19
+from sqlalchemy.orm.exc import NoResultFound
19
 from sqlalchemy.orm.session import Session
20
 from sqlalchemy.orm.session import Session
20
 from sqlalchemy import desc
21
 from sqlalchemy import desc
21
 from sqlalchemy import distinct
22
 from sqlalchemy import distinct
25
 from tracim.lib.utils.utils import cmp_to_key
26
 from tracim.lib.utils.utils import cmp_to_key
26
 from tracim.lib.core.notifications import NotifierFactory
27
 from tracim.lib.core.notifications import NotifierFactory
27
 from tracim.exceptions import SameValueError
28
 from tracim.exceptions import SameValueError
29
+from tracim.exceptions import ContentNotFound
28
 from tracim.exceptions import WorkspacesDoNotMatch
30
 from tracim.exceptions import WorkspacesDoNotMatch
29
 from tracim.lib.utils.utils import current_date_for_filename
31
 from tracim.lib.utils.utils import current_date_for_filename
30
 from tracim.models.revision_protection import new_revision
32
 from tracim.models.revision_protection import new_revision
39
 from tracim.models.data import UserRoleInWorkspace
41
 from tracim.models.data import UserRoleInWorkspace
40
 from tracim.models.data import Workspace
42
 from tracim.models.data import Workspace
41
 from tracim.lib.utils.translation import fake_translator as _
43
 from tracim.lib.utils.translation import fake_translator as _
44
+from tracim.models.context_models import RevisionInContext
42
 from tracim.models.context_models import ContentInContext
45
 from tracim.models.context_models import ContentInContext
43
 
46
 
44
-
45
 __author__ = 'damien'
47
 __author__ = 'damien'
46
 
48
 
47
 
49
 
102
         ContentType.Comment,
104
         ContentType.Comment,
103
         ContentType.Thread,
105
         ContentType.Thread,
104
         ContentType.Page,
106
         ContentType.Page,
107
+        ContentType.PageLegacy,
105
         ContentType.MarkdownPage,
108
         ContentType.MarkdownPage,
106
     )
109
     )
107
 
110
 
158
             self._show_deleted = previous_show_deleted
161
             self._show_deleted = previous_show_deleted
159
             self._show_temporary = previous_show_temporary
162
             self._show_temporary = previous_show_temporary
160
 
163
 
161
-    def get_content_in_context(self, content: Content):
164
+    def get_content_in_context(self, content: Content) -> ContentInContext:
162
         return ContentInContext(content, self._session, self._config)
165
         return ContentInContext(content, self._session, self._config)
163
 
166
 
164
-    def get_revision_join(self) -> sqlalchemy.sql.elements.BooleanClauseList:
167
+    def get_revision_in_context(self, revision: ContentRevisionRO) -> RevisionInContext:  # nopep8
168
+        # TODO - G.M - 2018-06-173 - create revision in context object
169
+        return RevisionInContext(revision, self._session, self._config)
170
+    
171
+    def _get_revision_join(self) -> sqlalchemy.sql.elements.BooleanClauseList:
165
         """
172
         """
166
         Return the Content/ContentRevision query join condition
173
         Return the Content/ContentRevision query join condition
167
         :return: Content/ContentRevision query join condition
174
         :return: Content/ContentRevision query join condition
180
         :return: Content/ContentRevision Query
187
         :return: Content/ContentRevision Query
181
         """
188
         """
182
         return self._session.query(Content)\
189
         return self._session.query(Content)\
183
-            .join(ContentRevisionRO, self.get_revision_join())
190
+            .join(ContentRevisionRO, self._get_revision_join())
184
 
191
 
185
     @classmethod
192
     @classmethod
186
     def sort_tree_items(
193
     def sort_tree_items(
418
         item = Content()
425
         item = Content()
419
         item.owner = self._user
426
         item.owner = self._user
420
         item.parent = parent
427
         item.parent = parent
428
+        if parent and not workspace:
429
+            workspace = item.parent.workspace
421
         item.workspace = workspace
430
         item.workspace = workspace
422
         item.type = ContentType.Comment
431
         item.type = ContentType.Comment
423
         item.description = content
432
         item.description = content
449
 
458
 
450
         return content
459
         return content
451
 
460
 
452
-    def get_one(self, content_id: int, content_type: str, workspace: Workspace=None) -> Content:
461
+    def get_one(self, content_id: int, content_type: str, workspace: Workspace=None, parent: Content=None) -> Content:
453
 
462
 
454
         if not content_id:
463
         if not content_id:
455
             return None
464
             return None
456
 
465
 
457
-        if content_type==ContentType.Any:
458
-            return self._base_query(workspace).filter(Content.content_id==content_id).one()
466
+        base_request = self._base_query(workspace).filter(Content.content_id==content_id)
467
+
468
+        if content_type!=ContentType.Any:
469
+            base_request = base_request.filter(Content.type==content_type)
459
 
470
 
460
-        return self._base_query(workspace).filter(Content.content_id==content_id).filter(Content.type==content_type).one()
471
+        if parent:
472
+            base_request = base_request.filter(Content.parent_id==parent.content_id)  # nopep8
473
+
474
+        try:
475
+            content = base_request.one()
476
+        except NoResultFound as exc:
477
+            raise ContentNotFound('Content "{}" not found in database'.format(content_id)) from exc  # nopep8
478
+        return content
461
 
479
 
462
     def get_one_revision(self, revision_id: int = None) -> ContentRevisionRO:
480
     def get_one_revision(self, revision_id: int = None) -> ContentRevisionRO:
463
         """
481
         """

+ 74 - 15
tracim/lib/utils/authorization.py View File

1
 # -*- coding: utf-8 -*-
1
 # -*- coding: utf-8 -*-
2
+import typing
2
 from typing import TYPE_CHECKING
3
 from typing import TYPE_CHECKING
3
 import functools
4
 import functools
4
 from pyramid.interfaces import IAuthorizationPolicy
5
 from pyramid.interfaces import IAuthorizationPolicy
5
 from zope.interface import implementer
6
 from zope.interface import implementer
7
+
8
+from tracim.models.contents import NewContentType
9
+from tracim.models.context_models import ContentInContext
10
+
6
 try:
11
 try:
7
     from json.decoder import JSONDecodeError
12
     from json.decoder import JSONDecodeError
8
 except ImportError:  # python3.4
13
 except ImportError:  # python3.4
9
     JSONDecodeError = ValueError
14
     JSONDecodeError = ValueError
10
 
15
 
11
-from tracim.exceptions import InsufficientUserWorkspaceRole
16
+from tracim.models.contents import ContentTypeLegacy as ContentType
17
+from tracim.exceptions import InsufficientUserRoleInWorkspace
18
+from tracim.exceptions import ContentTypeNotAllowed
12
 from tracim.exceptions import InsufficientUserProfile
19
 from tracim.exceptions import InsufficientUserProfile
13
 if TYPE_CHECKING:
20
 if TYPE_CHECKING:
14
     from tracim import TracimRequest
21
     from tracim import TracimRequest
43
 # We prefer to use decorators
50
 # We prefer to use decorators
44
 
51
 
45
 
52
 
46
-def require_same_user_or_profile(group: int):
53
+def require_same_user_or_profile(group: int) -> typing.Callable:
47
     """
54
     """
48
     Decorator for view to restrict access of tracim request if candidate user
55
     Decorator for view to restrict access of tracim request if candidate user
49
     is distinct from authenticated user and not with high enough profile.
56
     is distinct from authenticated user and not with high enough profile.
51
     like Group.TIM_USER or Group.TIM_MANAGER
58
     like Group.TIM_USER or Group.TIM_MANAGER
52
     :return:
59
     :return:
53
     """
60
     """
54
-    def decorator(func):
61
+    def decorator(func: typing.Callable) -> typing.Callable:
55
         @functools.wraps(func)
62
         @functools.wraps(func)
56
-        def wrapper(self, context, request: 'TracimRequest'):
63
+        def wrapper(self, context, request: 'TracimRequest') -> typing.Callable:
57
             auth_user = request.current_user
64
             auth_user = request.current_user
58
             candidate_user = request.candidate_user
65
             candidate_user = request.candidate_user
59
             if auth_user.user_id == candidate_user.user_id or \
66
             if auth_user.user_id == candidate_user.user_id or \
64
     return decorator
71
     return decorator
65
 
72
 
66
 
73
 
67
-def require_profile(group: int):
74
+def require_profile(group: int) -> typing.Callable:
68
     """
75
     """
69
     Decorator for view to restrict access of tracim request if profile is
76
     Decorator for view to restrict access of tracim request if profile is
70
     not high enough
77
     not high enough
72
     like Group.TIM_USER or Group.TIM_MANAGER
79
     like Group.TIM_USER or Group.TIM_MANAGER
73
     :return:
80
     :return:
74
     """
81
     """
75
-    def decorator(func):
82
+    def decorator(func: typing.Callable) -> typing.Callable:
76
         @functools.wraps(func)
83
         @functools.wraps(func)
77
-        def wrapper(self, context, request: 'TracimRequest'):
84
+        def wrapper(self, context, request: 'TracimRequest') -> typing.Callable:
78
             user = request.current_user
85
             user = request.current_user
79
             if user.profile.id >= group:
86
             if user.profile.id >= group:
80
                 return func(self, context, request)
87
                 return func(self, context, request)
83
     return decorator
90
     return decorator
84
 
91
 
85
 
92
 
86
-def require_workspace_role(minimal_required_role: int):
93
+def require_workspace_role(minimal_required_role: int) -> typing.Callable:
87
     """
94
     """
88
     Restricts access to endpoint to minimal role or raise an exception.
95
     Restricts access to endpoint to minimal role or raise an exception.
89
     Check role for current_workspace.
96
     Check role for current_workspace.
91
     UserRoleInWorkspace.CONTRIBUTOR or UserRoleInWorkspace.READER
98
     UserRoleInWorkspace.CONTRIBUTOR or UserRoleInWorkspace.READER
92
     :return: decorator
99
     :return: decorator
93
     """
100
     """
94
-    def decorator(func):
101
+    def decorator(func: typing.Callable) -> typing.Callable:
95
         @functools.wraps(func)
102
         @functools.wraps(func)
96
-        def wrapper(self, context, request: 'TracimRequest'):
103
+        def wrapper(self, context, request: 'TracimRequest') -> typing.Callable:
97
             user = request.current_user
104
             user = request.current_user
98
             workspace = request.current_workspace
105
             workspace = request.current_workspace
99
             if workspace.get_user_role(user) >= minimal_required_role:
106
             if workspace.get_user_role(user) >= minimal_required_role:
100
                 return func(self, context, request)
107
                 return func(self, context, request)
101
-            raise InsufficientUserWorkspaceRole()
108
+            raise InsufficientUserRoleInWorkspace()
102
 
109
 
103
         return wrapper
110
         return wrapper
104
     return decorator
111
     return decorator
105
 
112
 
106
 
113
 
107
-def require_candidate_workspace_role(minimal_required_role: int):
114
+def require_candidate_workspace_role(minimal_required_role: int) -> typing.Callable:  # nopep8
108
     """
115
     """
109
     Restricts access to endpoint to minimal role or raise an exception.
116
     Restricts access to endpoint to minimal role or raise an exception.
110
     Check role for candidate_workspace.
117
     Check role for candidate_workspace.
112
     UserRoleInWorkspace.CONTRIBUTOR or UserRoleInWorkspace.READER
119
     UserRoleInWorkspace.CONTRIBUTOR or UserRoleInWorkspace.READER
113
     :return: decorator
120
     :return: decorator
114
     """
121
     """
115
-    def decorator(func):
122
+    def decorator(func: typing.Callable) -> typing.Callable:
116
 
123
 
117
-        def wrapper(self, context, request: 'TracimRequest'):
124
+        def wrapper(self, context, request: 'TracimRequest') -> typing.Callable:
118
             user = request.current_user
125
             user = request.current_user
119
             workspace = request.candidate_workspace
126
             workspace = request.candidate_workspace
120
 
127
 
121
             if workspace.get_user_role(user) >= minimal_required_role:
128
             if workspace.get_user_role(user) >= minimal_required_role:
122
                 return func(self, context, request)
129
                 return func(self, context, request)
123
-            raise InsufficientUserWorkspaceRole()
130
+            raise InsufficientUserRoleInWorkspace()
131
+
132
+        return wrapper
133
+    return decorator
134
+
135
+
136
+def require_content_types(content_types: typing.List['NewContentType']) -> typing.Callable:  # nopep8
137
+    """
138
+    Restricts access to specific file type or raise an exception.
139
+    Check role for candidate_workspace.
140
+    :param content_types: list of NewContentType object
141
+    :return: decorator
142
+    """
143
+    def decorator(func: typing.Callable) -> typing.Callable:
144
+        @functools.wraps(func)
145
+        def wrapper(self, context, request: 'TracimRequest') -> typing.Callable:
146
+            content = request.current_content
147
+            current_content_type_slug = ContentType(content.type).slug
148
+            content_types_slug = [content_type.slug for content_type in content_types]  # nopep8
149
+            if current_content_type_slug in content_types_slug:
150
+                return func(self, context, request)
151
+            raise ContentTypeNotAllowed()
152
+        return wrapper
153
+    return decorator
124
 
154
 
155
+
156
+def require_comment_ownership_or_role(
157
+        minimal_required_role_for_owner: int,
158
+        minimal_required_role_for_anyone: int,
159
+) -> typing.Callable:
160
+    """
161
+    Decorator for view to restrict access of tracim request if role is
162
+    not high enough and user is not owner of the current_content
163
+    :param minimal_required_role_for_owner: minimal role for owner
164
+    of current_content to access to this view
165
+    :param minimal_required_role_for_anyone: minimal role for anyone to
166
+    access to this view.
167
+    :return:
168
+    """
169
+    def decorator(func: typing.Callable) -> typing.Callable:
170
+        @functools.wraps(func)
171
+        def wrapper(self, context, request: 'TracimRequest') -> typing.Callable:
172
+            user = request.current_user
173
+            workspace = request.current_workspace
174
+            comment = request.current_comment
175
+            # INFO - G.M - 2018-06-178 - find minimal role required
176
+            if comment.owner.user_id == user.user_id:
177
+                minimal_required_role = minimal_required_role_for_owner
178
+            else:
179
+                minimal_required_role = minimal_required_role_for_anyone
180
+            # INFO - G.M - 2018-06-178 - normal role test
181
+            if workspace.get_user_role(user) >= minimal_required_role:
182
+                return func(self, context, request)
183
+            raise InsufficientUserRoleInWorkspace()
125
         return wrapper
184
         return wrapper
126
     return decorator
185
     return decorator

+ 141 - 14
tracim/lib/utils/request.py View File

2
 from pyramid.request import Request
2
 from pyramid.request import Request
3
 from sqlalchemy.orm.exc import NoResultFound
3
 from sqlalchemy.orm.exc import NoResultFound
4
 
4
 
5
-from tracim.exceptions import NotAuthenticated
5
+from tracim.exceptions import NotAuthenticated, ContentNotFound
6
+from tracim.exceptions import ContentNotFoundInTracimRequest
6
 from tracim.exceptions import WorkspaceNotFoundInTracimRequest
7
 from tracim.exceptions import WorkspaceNotFoundInTracimRequest
7
 from tracim.exceptions import UserNotFoundInTracimRequest
8
 from tracim.exceptions import UserNotFoundInTracimRequest
8
 from tracim.exceptions import UserDoesNotExist
9
 from tracim.exceptions import UserDoesNotExist
9
 from tracim.exceptions import WorkspaceNotFound
10
 from tracim.exceptions import WorkspaceNotFound
10
 from tracim.exceptions import ImmutableAttribute
11
 from tracim.exceptions import ImmutableAttribute
12
+from tracim.models.contents import ContentTypeLegacy as ContentType
13
+from tracim.lib.core.content import ContentApi
11
 from tracim.lib.core.user import UserApi
14
 from tracim.lib.core.user import UserApi
12
 from tracim.lib.core.workspace import WorkspaceApi
15
 from tracim.lib.core.workspace import WorkspaceApi
13
 from tracim.lib.utils.authorization import JSONDecodeError
16
 from tracim.lib.utils.authorization import JSONDecodeError
14
 
17
 
15
 from tracim.models import User
18
 from tracim.models import User
16
 from tracim.models.data import Workspace
19
 from tracim.models.data import Workspace
20
+from tracim.models.data import Content
17
 
21
 
18
 
22
 
19
 class TracimRequest(Request):
23
 class TracimRequest(Request):
35
             decode_param_names,
39
             decode_param_names,
36
             **kw
40
             **kw
37
         )
41
         )
42
+        # Current comment, found in request path
43
+        self._current_comment = None  # type: Content
44
+
45
+        # Current content, found in request path
46
+        self._current_content = None  # type: Content
47
+
38
         # Current workspace, found in request path
48
         # Current workspace, found in request path
39
         self._current_workspace = None  # type: Workspace
49
         self._current_workspace = None  # type: Workspace
40
 
50
 
60
         :return: Workspace of the request
70
         :return: Workspace of the request
61
         """
71
         """
62
         if self._current_workspace is None:
72
         if self._current_workspace is None:
63
-            self._current_workspace = self._get_current_workspace(self.current_user, self)
73
+            self._current_workspace = self._get_current_workspace(self.current_user, self)   # nopep8
64
         return self._current_workspace
74
         return self._current_workspace
65
 
75
 
66
     @current_workspace.setter
76
     @current_workspace.setter
93
             )
103
             )
94
         self._current_user = user
104
         self._current_user = user
95
 
105
 
106
+    @property
107
+    def current_content(self) -> Content:
108
+        """
109
+        Get current  content from path
110
+        """
111
+        if self._current_content is None:
112
+            self._current_content = self._get_current_content(
113
+                self.current_user,
114
+                self.current_workspace,
115
+                self
116
+                )
117
+        return self._current_content
118
+
119
+    @current_content.setter
120
+    def current_content(self, content: Content) -> None:
121
+        if self._current_content is not None:
122
+            raise ImmutableAttribute(
123
+                "Can't modify already setted current_content"
124
+            )
125
+        self._current_content = content
126
+
127
+    @property
128
+    def current_comment(self) -> Content:
129
+        """
130
+        Get current comment from path
131
+        """
132
+        if self._current_comment is None:
133
+            self._current_comment = self._get_current_comment(
134
+                self.current_user,
135
+                self.current_workspace,
136
+                self.current_content,
137
+                self
138
+                )
139
+        return self._current_comment
140
+
141
+    @current_comment.setter
142
+    def current_comment(self, content: Content) -> None:
143
+        if self._current_comment is not None:
144
+            raise ImmutableAttribute(
145
+                "Can't modify already setted current_content"
146
+            )
147
+        self._current_comment = content
96
     # TODO - G.M - 24-05-2018 - Find a better naming for this ?
148
     # TODO - G.M - 24-05-2018 - Find a better naming for this ?
149
+
97
     @property
150
     @property
98
     def candidate_user(self) -> User:
151
     def candidate_user(self) -> User:
99
         """
152
         """
132
         self._current_workspace = None
185
         self._current_workspace = None
133
         self.dbsession.close()
186
         self.dbsession.close()
134
 
187
 
135
-
136
     @candidate_user.setter
188
     @candidate_user.setter
137
     def candidate_user(self, user: User) -> None:
189
     def candidate_user(self, user: User) -> None:
138
         if self._candidate_user is not None:
190
         if self._candidate_user is not None:
144
     ###
196
     ###
145
     # Utils for TracimRequest
197
     # Utils for TracimRequest
146
     ###
198
     ###
199
+    def _get_current_comment(
200
+            self,
201
+            user: User,
202
+            workspace: Workspace,
203
+            content: Content,
204
+            request: 'TracimRequest'
205
+    ) -> Content:
206
+        """
207
+        Get current content from request
208
+        :param user: User who want to check the workspace
209
+        :param workspace: Workspace of the content
210
+        :param content: comment is related to this content
211
+        :param request: pyramid request
212
+        :return: current content
213
+        """
214
+        comment_id = ''
215
+        try:
216
+            if 'comment_id' in request.matchdict:
217
+                comment_id = int(request.matchdict['comment_id'])
218
+            if not comment_id:
219
+                raise ContentNotFoundInTracimRequest('No comment_id property found in request')  # nopep8
220
+            api = ContentApi(
221
+                current_user=user,
222
+                session=request.dbsession,
223
+                config=request.registry.settings['CFG']
224
+            )
225
+            comment = api.get_one(
226
+                comment_id,
227
+                content_type=ContentType.Comment,
228
+                workspace=workspace,
229
+                parent=content,
230
+            )
231
+        except JSONDecodeError as exc:
232
+            raise ContentNotFound('Invalid JSON content') from exc
233
+        except NoResultFound as exc:
234
+            raise ContentNotFound(
235
+                'Comment {} does not exist '
236
+                'or is not visible for this user'.format(comment_id)
237
+            ) from exc
238
+        return comment
239
+
240
+    def _get_current_content(
241
+            self,
242
+            user: User,
243
+            workspace: Workspace,
244
+            request: 'TracimRequest'
245
+    ) -> Content:
246
+        """
247
+        Get current content from request
248
+        :param user: User who want to check the workspace
249
+        :param workspace: Workspace of the content
250
+        :param request: pyramid request
251
+        :return: current content
252
+        """
253
+        content_id = ''
254
+        try:
255
+            if 'content_id' in request.matchdict:
256
+                content_id = int(request.matchdict['content_id'])
257
+            if not content_id:
258
+                raise ContentNotFoundInTracimRequest('No content_id property found in request')  # nopep8
259
+            api = ContentApi(
260
+                current_user=user,
261
+                session=request.dbsession,
262
+                config=request.registry.settings['CFG']
263
+            )
264
+            content = api.get_one(content_id=content_id, workspace=workspace, content_type=ContentType.Any)  # nopep8
265
+        except JSONDecodeError as exc:
266
+            raise ContentNotFound('Invalid JSON content') from exc
267
+        except NoResultFound as exc:
268
+            raise ContentNotFound(
269
+                'Content {} does not exist '
270
+                'or is not visible for this user'.format(content_id)
271
+            ) from exc
272
+        return content
147
 
273
 
148
     def _get_candidate_user(
274
     def _get_candidate_user(
149
             self,
275
             self,
156
         """
282
         """
157
         app_config = request.registry.settings['CFG']
283
         app_config = request.registry.settings['CFG']
158
         uapi = UserApi(None, session=request.dbsession, config=app_config)
284
         uapi = UserApi(None, session=request.dbsession, config=app_config)
159
-
285
+        login = ''
160
         try:
286
         try:
161
             login = None
287
             login = None
162
             if 'user_id' in request.matchdict:
288
             if 'user_id' in request.matchdict:
179
         """
305
         """
180
         app_config = request.registry.settings['CFG']
306
         app_config = request.registry.settings['CFG']
181
         uapi = UserApi(None, session=request.dbsession, config=app_config)
307
         uapi = UserApi(None, session=request.dbsession, config=app_config)
308
+        login = ''
182
         try:
309
         try:
183
             login = request.authenticated_userid
310
             login = request.authenticated_userid
184
             if not login:
311
             if not login:
204
             if 'workspace_id' in request.matchdict:
331
             if 'workspace_id' in request.matchdict:
205
                 workspace_id = request.matchdict['workspace_id']
332
                 workspace_id = request.matchdict['workspace_id']
206
             if not workspace_id:
333
             if not workspace_id:
207
-                raise WorkspaceNotFoundInTracimRequest('No workspace_id property found in request')
334
+                raise WorkspaceNotFoundInTracimRequest('No workspace_id property found in request')  # nopep8
208
             wapi = WorkspaceApi(
335
             wapi = WorkspaceApi(
209
                 current_user=user,
336
                 current_user=user,
210
                 session=request.dbsession,
337
                 session=request.dbsession,
211
                 config=request.registry.settings['CFG']
338
                 config=request.registry.settings['CFG']
212
             )
339
             )
213
             workspace = wapi.get_one(workspace_id)
340
             workspace = wapi.get_one(workspace_id)
214
-        except JSONDecodeError:
215
-            raise WorkspaceNotFound('Bad json body')
216
-        except NoResultFound:
341
+        except JSONDecodeError as exc:
342
+            raise WorkspaceNotFound('Invalid JSON content') from exc
343
+        except NoResultFound as exc:
217
             raise WorkspaceNotFound(
344
             raise WorkspaceNotFound(
218
                 'Workspace {} does not exist '
345
                 'Workspace {} does not exist '
219
                 'or is not visible for this user'.format(workspace_id)
346
                 'or is not visible for this user'.format(workspace_id)
220
-            )
347
+            ) from exc
221
         return workspace
348
         return workspace
222
 
349
 
223
     def _get_candidate_workspace(
350
     def _get_candidate_workspace(
236
             if 'new_workspace_id' in request.json_body:
363
             if 'new_workspace_id' in request.json_body:
237
                 workspace_id = request.json_body['new_workspace_id']
364
                 workspace_id = request.json_body['new_workspace_id']
238
             if not workspace_id:
365
             if not workspace_id:
239
-                raise WorkspaceNotFoundInTracimRequest('No new_workspace_id property found in body')
366
+                raise WorkspaceNotFoundInTracimRequest('No new_workspace_id property found in body')  # nopep8
240
             wapi = WorkspaceApi(
367
             wapi = WorkspaceApi(
241
                 current_user=user,
368
                 current_user=user,
242
                 session=request.dbsession,
369
                 session=request.dbsession,
243
                 config=request.registry.settings['CFG']
370
                 config=request.registry.settings['CFG']
244
             )
371
             )
245
             workspace = wapi.get_one(workspace_id)
372
             workspace = wapi.get_one(workspace_id)
246
-        except JSONDecodeError:
247
-            raise WorkspaceNotFound('Bad json body')
248
-        except NoResultFound:
373
+        except JSONDecodeError as exc:
374
+            raise WorkspaceNotFound('Invalid JSON content') from exc
375
+        except NoResultFound as exc:
249
             raise WorkspaceNotFound(
376
             raise WorkspaceNotFound(
250
                 'Workspace {} does not exist '
377
                 'Workspace {} does not exist '
251
                 'or is not visible for this user'.format(workspace_id)
378
                 'or is not visible for this user'.format(workspace_id)
252
-            )
379
+            ) from exc
253
         return workspace
380
         return workspace

+ 1 - 0
tracim/lib/utils/utils.py View File

5
 
5
 
6
 from tracim.config import CFG
6
 from tracim.config import CFG
7
 
7
 
8
+DATETIME_FORMAT = '%Y-%m-%dT%H:%M:%SZ'
8
 DEFAULT_WEBDAV_CONFIG_FILE = "wsgidav.conf"
9
 DEFAULT_WEBDAV_CONFIG_FILE = "wsgidav.conf"
9
 DEFAULT_TRACIM_CONFIG_FILE = "development.ini"
10
 DEFAULT_TRACIM_CONFIG_FILE = "development.ini"
10
 
11
 

+ 6 - 6
tracim/models/applications.py View File

41
 calendar = Application(
41
 calendar = Application(
42
     label='Calendar',
42
     label='Calendar',
43
     slug='calendar',
43
     slug='calendar',
44
-    fa_icon='calendar-alt',
44
+    fa_icon='calendar',
45
     hexcolor='#757575',
45
     hexcolor='#757575',
46
     is_active=True,
46
     is_active=True,
47
     config={},
47
     config={},
72
 markdownpluspage = Application(
72
 markdownpluspage = Application(
73
     label='Markdown Plus Documents',  # TODO - G.M - 24-05-2018 - Check label
73
     label='Markdown Plus Documents',  # TODO - G.M - 24-05-2018 - Check label
74
     slug='contents/markdownpluspage',
74
     slug='contents/markdownpluspage',
75
-    fa_icon='file-code',
75
+    fa_icon='file-code-o',
76
     hexcolor='#f12d2d',
76
     hexcolor='#f12d2d',
77
     is_active=True,
77
     is_active=True,
78
     config={},
78
     config={},
79
     main_route='/#/workspaces/{workspace_id}/contents?type=markdownpluspage',
79
     main_route='/#/workspaces/{workspace_id}/contents?type=markdownpluspage',
80
 )
80
 )
81
 
81
 
82
-htmlpage = Application(
82
+html_documents = Application(
83
     label='Text Documents',  # TODO - G.M - 24-05-2018 - Check label
83
     label='Text Documents',  # TODO - G.M - 24-05-2018 - Check label
84
-    slug='contents/htmlpage',
84
+    slug='contents/html-documents',
85
     fa_icon='file-text-o',
85
     fa_icon='file-text-o',
86
     hexcolor='#3f52e3',
86
     hexcolor='#3f52e3',
87
     is_active=True,
87
     is_active=True,
88
     config={},
88
     config={},
89
-    main_route='/#/workspaces/{workspace_id}/contents?type=htmlpage',
89
+    main_route='/#/workspaces/{workspace_id}/contents?type=html-documents',
90
 )
90
 )
91
 # TODO - G.M - 08-06-2018 - This is hardcoded lists of app, make this dynamic.
91
 # TODO - G.M - 08-06-2018 - This is hardcoded lists of app, make this dynamic.
92
 # List of applications
92
 # List of applications
93
 applications = [
93
 applications = [
94
-    htmlpage,
94
+    html_documents,
95
     markdownpluspage,
95
     markdownpluspage,
96
     _file,
96
     _file,
97
     thread,
97
     thread,

+ 52 - 20
tracim/models/contents.py View File

2
 import typing
2
 import typing
3
 from enum import Enum
3
 from enum import Enum
4
 
4
 
5
-from tracim.exceptions import ContentStatusNotExist, ContentTypeNotExist
6
-from tracim.models.applications import htmlpage, _file, thread, markdownpluspage
5
+from tracim.exceptions import ContentTypeNotExist
6
+from tracim.exceptions import ContentStatusNotExist
7
+from tracim.models.applications import html_documents
8
+from tracim.models.applications import _file
9
+from tracim.models.applications import thread
10
+from tracim.models.applications import markdownpluspage
7
 
11
 
8
 
12
 
9
 ####
13
 ####
38
     slug='open',
42
     slug='open',
39
     global_status=GlobalStatus.OPEN.value,
43
     global_status=GlobalStatus.OPEN.value,
40
     label='Open',
44
     label='Open',
41
-    fa_icon='fa-square-o',
42
-    hexcolor='#000FF',
45
+    fa_icon='square-o',
46
+    hexcolor='#3f52e3',
43
 )
47
 )
44
 
48
 
45
 closed_validated_status = NewContentStatus(
49
 closed_validated_status = NewContentStatus(
46
     slug='closed-validated',
50
     slug='closed-validated',
47
     global_status=GlobalStatus.CLOSED.value,
51
     global_status=GlobalStatus.CLOSED.value,
48
     label='Validated',
52
     label='Validated',
49
-    fa_icon='fa-check-square-o',
50
-    hexcolor='#000FF',
53
+    fa_icon='check-square-o',
54
+    hexcolor='#008000',
51
 )
55
 )
52
 
56
 
53
 closed_unvalidated_status = NewContentStatus(
57
 closed_unvalidated_status = NewContentStatus(
54
     slug='closed-unvalidated',
58
     slug='closed-unvalidated',
55
     global_status=GlobalStatus.CLOSED.value,
59
     global_status=GlobalStatus.CLOSED.value,
56
     label='Cancelled',
60
     label='Cancelled',
57
-    fa_icon='fa-close',
58
-    hexcolor='#000FF',
61
+    fa_icon='close',
62
+    hexcolor='#f63434',
59
 )
63
 )
60
 
64
 
61
 closed_deprecated_status = NewContentStatus(
65
 closed_deprecated_status = NewContentStatus(
62
     slug='closed-deprecated',
66
     slug='closed-deprecated',
63
     global_status=GlobalStatus.CLOSED.value,
67
     global_status=GlobalStatus.CLOSED.value,
64
     label='Deprecated',
68
     label='Deprecated',
65
-    fa_icon='fa-warning',
66
-    hexcolor='#000FF',
69
+    fa_icon='warning',
70
+    hexcolor='#ababab',
67
 )
71
 )
68
 
72
 
69
 
73
 
159
     available_statuses=CONTENT_DEFAULT_STATUS,
163
     available_statuses=CONTENT_DEFAULT_STATUS,
160
 )
164
 )
161
 
165
 
162
-htmlpage_type = NewContentType(
163
-    slug='page',
164
-    fa_icon=htmlpage.fa_icon,
165
-    hexcolor=htmlpage.hexcolor,
166
+html_documents_type = NewContentType(
167
+    slug='html-documents',
168
+    fa_icon=html_documents.fa_icon,
169
+    hexcolor=html_documents.hexcolor,
166
     label='Text Document',
170
     label='Text Document',
167
     creation_label='Write a document',
171
     creation_label='Write a document',
168
     available_statuses=CONTENT_DEFAULT_STATUS,
172
     available_statuses=CONTENT_DEFAULT_STATUS,
182
     thread_type,
186
     thread_type,
183
     file_type,
187
     file_type,
184
     markdownpluspage_type,
188
     markdownpluspage_type,
185
-    htmlpage_type,
189
+    html_documents_type,
186
     folder_type,
190
     folder_type,
187
 ]
191
 ]
188
 
192
 
193
+# TODO - G.M - 31-05-2018 - Set Better Event params
194
+event_type = NewContentType(
195
+    slug='event',
196
+    fa_icon=thread.fa_icon,
197
+    hexcolor=thread.hexcolor,
198
+    label='Event',
199
+    creation_label='Event',
200
+    available_statuses=CONTENT_DEFAULT_STATUS,
201
+)
202
+
203
+# TODO - G.M - 31-05-2018 - Set Better Event params
204
+comment_type = NewContentType(
205
+    slug='comment',
206
+    fa_icon=thread.fa_icon,
207
+    hexcolor=thread.hexcolor,
208
+    label='Comment',
209
+    creation_label='Comment',
210
+    available_statuses=CONTENT_DEFAULT_STATUS,
211
+)
212
+
213
+CONTENT_DEFAULT_TYPE_SPECIAL = [
214
+    event_type,
215
+    comment_type,
216
+]
217
+
218
+ALL_CONTENTS_DEFAULT_TYPES = CONTENT_DEFAULT_TYPE + CONTENT_DEFAULT_TYPE_SPECIAL
219
+
189
 
220
 
190
 class ContentTypeLegacy(NewContentType):
221
 class ContentTypeLegacy(NewContentType):
191
     """
222
     """
200
 
231
 
201
     File = file_type.slug
232
     File = file_type.slug
202
     Thread = thread_type.slug
233
     Thread = thread_type.slug
203
-    Page = htmlpage_type.slug
234
+    Page = html_documents_type.slug
235
+    PageLegacy = 'page'
204
     MarkdownPage = markdownpluspage_type.slug
236
     MarkdownPage = markdownpluspage_type.slug
205
 
237
 
206
     def __init__(self, slug: str):
238
     def __init__(self, slug: str):
207
-        for content_type in CONTENT_DEFAULT_TYPE:
239
+        if slug == 'page':
240
+            slug = ContentTypeLegacy.Page
241
+        for content_type in ALL_CONTENTS_DEFAULT_TYPES:
208
             if slug == content_type.slug:
242
             if slug == content_type.slug:
209
                 super(ContentTypeLegacy, self).__init__(
243
                 super(ContentTypeLegacy, self).__init__(
210
                     slug=content_type.slug,
244
                     slug=content_type.slug,
223
 
257
 
224
     @classmethod
258
     @classmethod
225
     def allowed_types(cls) -> typing.List[str]:
259
     def allowed_types(cls) -> typing.List[str]:
226
-        contents_types = [status.slug for status in CONTENT_DEFAULT_TYPE]
227
-        contents_types.extend([cls.Folder, cls.Event, cls.Comment])
260
+        contents_types = [status.slug for status in ALL_CONTENTS_DEFAULT_TYPES]
228
         return contents_types
261
         return contents_types
229
 
262
 
230
     @classmethod
263
     @classmethod
232
         # This method is used for showing only "main"
265
         # This method is used for showing only "main"
233
         # types in the left-side treeview
266
         # types in the left-side treeview
234
         contents_types = [status.slug for status in CONTENT_DEFAULT_TYPE]
267
         contents_types = [status.slug for status in CONTENT_DEFAULT_TYPE]
235
-        contents_types.extend([cls.Folder])
236
         return contents_types
268
         return contents_types
237
 
269
 
238
     # TODO - G.M - 30-05-2018 - This method don't do anything.
270
     # TODO - G.M - 30-05-2018 - This method don't do anything.

+ 269 - 11
tracim/models/context_models.py View File

8
 from tracim.models import User
8
 from tracim.models import User
9
 from tracim.models.auth import Profile
9
 from tracim.models.auth import Profile
10
 from tracim.models.data import Content
10
 from tracim.models.data import Content
11
+from tracim.models.data import ContentRevisionRO
11
 from tracim.models.data import Workspace, UserRoleInWorkspace
12
 from tracim.models.data import Workspace, UserRoleInWorkspace
12
 from tracim.models.workspace_menu_entries import default_workspace_menu_entry
13
 from tracim.models.workspace_menu_entries import default_workspace_menu_entry
13
 from tracim.models.workspace_menu_entries import WorkspaceMenuEntry
14
 from tracim.models.workspace_menu_entries import WorkspaceMenuEntry
15
+from tracim.models.contents import ContentTypeLegacy as ContentType
14
 
16
 
15
 
17
 
16
 class MoveParams(object):
18
 class MoveParams(object):
17
     """
19
     """
18
-    Json body params for move action
20
+    Json body params for move action model
19
     """
21
     """
20
-    def __init__(self, new_parent_id: str, new_workspace_id: str = None):
22
+    def __init__(self, new_parent_id: str, new_workspace_id: str = None) -> None:  # nopep8
21
         self.new_parent_id = new_parent_id
23
         self.new_parent_id = new_parent_id
22
         self.new_workspace_id = new_workspace_id
24
         self.new_workspace_id = new_workspace_id
23
 
25
 
24
 
26
 
25
 class LoginCredentials(object):
27
 class LoginCredentials(object):
26
     """
28
     """
27
-    Login credentials model for login
29
+    Login credentials model for login model
28
     """
30
     """
29
 
31
 
30
-    def __init__(self, email: str, password: str):
32
+    def __init__(self, email: str, password: str) -> None:
31
         self.email = email
33
         self.email = email
32
         self.password = password
34
         self.password = password
33
 
35
 
34
 
36
 
35
 class WorkspaceAndContentPath(object):
37
 class WorkspaceAndContentPath(object):
36
     """
38
     """
37
-    Paths params with workspace id and content_id
39
+    Paths params with workspace id and content_id model
38
     """
40
     """
39
-    def __init__(self, workspace_id: int, content_id: int):
41
+    def __init__(self, workspace_id: int, content_id: int) -> None:
40
         self.content_id = content_id
42
         self.content_id = content_id
41
         self.workspace_id = workspace_id
43
         self.workspace_id = workspace_id
42
 
44
 
43
 
45
 
46
+class CommentPath(object):
47
+    """
48
+    Paths params with workspace id and content_id and comment_id model
49
+    """
50
+    def __init__(
51
+        self,
52
+        workspace_id: int,
53
+        content_id: int,
54
+        comment_id: int
55
+    ) -> None:
56
+        self.content_id = content_id
57
+        self.workspace_id = workspace_id
58
+        self.comment_id = comment_id
59
+
60
+
44
 class ContentFilter(object):
61
 class ContentFilter(object):
45
     """
62
     """
46
     Content filter model
63
     Content filter model
51
             show_archived: int = 0,
68
             show_archived: int = 0,
52
             show_deleted: int = 0,
69
             show_deleted: int = 0,
53
             show_active: int = 1,
70
             show_active: int = 1,
54
-    ):
71
+    ) -> None:
55
         self.parent_id = parent_id
72
         self.parent_id = parent_id
56
         self.show_archived = bool(show_archived)
73
         self.show_archived = bool(show_archived)
57
         self.show_deleted = bool(show_deleted)
74
         self.show_deleted = bool(show_deleted)
66
             self,
83
             self,
67
             label: str,
84
             label: str,
68
             content_type: str,
85
             content_type: str,
69
-    ):
86
+    ) -> None:
70
         self.label = label
87
         self.label = label
71
         self.content_type = content_type
88
         self.content_type = content_type
72
 
89
 
73
 
90
 
91
+class CommentCreation(object):
92
+    """
93
+    Comment creation model
94
+    """
95
+    def __init__(
96
+            self,
97
+            raw_content: str,
98
+    ) -> None:
99
+        self.raw_content = raw_content
100
+
101
+
102
+class SetContentStatus(object):
103
+    """
104
+    Set content status
105
+    """
106
+    def __init__(
107
+            self,
108
+            status: str,
109
+    ) -> None:
110
+        self.status = status
111
+
112
+
113
+class HTMLDocumentUpdate(object):
114
+    """
115
+    Html Document update model
116
+    """
117
+    def __init__(
118
+            self,
119
+            label: str,
120
+            raw_content: str,
121
+    ) -> None:
122
+        self.label = label
123
+        self.raw_content = raw_content
124
+
125
+
126
+class ThreadUpdate(object):
127
+    """
128
+    Thread update model
129
+    """
130
+    def __init__(
131
+            self,
132
+            label: str,
133
+            raw_content: str,
134
+    ) -> None:
135
+        self.label = label
136
+        self.raw_content = raw_content
137
+
138
+
74
 class UserInContext(object):
139
 class UserInContext(object):
75
     """
140
     """
76
     Interface to get User data and User data related to context.
141
     Interface to get User data and User data related to context.
306
 
371
 
307
     @property
372
     @property
308
     def content_type(self) -> str:
373
     def content_type(self) -> str:
309
-        return self.content.type
374
+        content_type = ContentType(self.content.type)
375
+        return content_type.slug
310
 
376
 
311
     @property
377
     @property
312
     def sub_content_types(self) -> typing.List[str]:
378
     def sub_content_types(self) -> typing.List[str]:
313
-        return [type.slug for type in self.content.get_allowed_content_types()]
379
+        return [_type.slug for _type in self.content.get_allowed_content_types()]  # nopep8
314
 
380
 
315
     @property
381
     @property
316
     def status(self) -> str:
382
     def status(self) -> str:
324
     def is_deleted(self):
390
     def is_deleted(self):
325
         return self.content.is_deleted
391
         return self.content.is_deleted
326
 
392
 
327
-    # Context-related
393
+    @property
394
+    def raw_content(self):
395
+        return self.content.description
396
+
397
+    @property
398
+    def author(self):
399
+        return UserInContext(
400
+            dbsession=self.dbsession,
401
+            config=self.config,
402
+            user=self.content.first_revision.owner
403
+        )
404
+
405
+    @property
406
+    def current_revision_id(self):
407
+        return self.content.revision_id
408
+
409
+    @property
410
+    def created(self):
411
+        return self.content.created
412
+
413
+    @property
414
+    def modified(self):
415
+        return self.updated
416
+
417
+    @property
418
+    def updated(self):
419
+        return self.content.updated
328
 
420
 
329
     @property
421
     @property
422
+    def last_modifier(self):
423
+        return UserInContext(
424
+            dbsession=self.dbsession,
425
+            config=self.config,
426
+            user=self.content.last_revision.owner
427
+        )
428
+
429
+    # Context-related
430
+    @property
330
     def show_in_ui(self):
431
     def show_in_ui(self):
331
         # TODO - G.M - 31-05-2018 - Enable Show_in_ui params
432
         # TODO - G.M - 31-05-2018 - Enable Show_in_ui params
332
         # if false, then do not show content in the treeview.
433
         # if false, then do not show content in the treeview.
338
     @property
439
     @property
339
     def slug(self):
440
     def slug(self):
340
         return slugify(self.content.label)
441
         return slugify(self.content.label)
442
+
443
+
444
+class RevisionInContext(object):
445
+    """
446
+    Interface to get Content data and Content data related to context.
447
+    """
448
+
449
+    def __init__(self, content_revision: ContentRevisionRO, dbsession: Session, config: CFG):
450
+        assert content_revision is not None
451
+        self.revision = content_revision
452
+        self.dbsession = dbsession
453
+        self.config = config
454
+
455
+    # Default
456
+    @property
457
+    def content_id(self) -> int:
458
+        return self.revision.content_id
459
+
460
+    @property
461
+    def id(self) -> int:
462
+        return self.content_id
463
+
464
+    @property
465
+    def parent_id(self) -> int:
466
+        """
467
+        Return parent_id of the content
468
+        """
469
+        return self.revision.parent_id
470
+
471
+    @property
472
+    def workspace_id(self) -> int:
473
+        return self.revision.workspace_id
474
+
475
+    @property
476
+    def label(self) -> str:
477
+        return self.revision.label
478
+
479
+    @property
480
+    def content_type(self) -> str:
481
+        content_type = ContentType(self.revision.type)
482
+        if content_type:
483
+            return content_type.slug
484
+        else:
485
+            return None
486
+
487
+    @property
488
+    def sub_content_types(self) -> typing.List[str]:
489
+        return [_type.slug for _type
490
+                in self.revision.node.get_allowed_content_types()]
491
+
492
+    @property
493
+    def status(self) -> str:
494
+        return self.revision.status
495
+
496
+    @property
497
+    def is_archived(self) -> bool:
498
+        return self.revision.is_archived
499
+
500
+    @property
501
+    def is_deleted(self) -> bool:
502
+        return self.revision.is_deleted
503
+
504
+    @property
505
+    def raw_content(self) -> str:
506
+        return self.revision.description
507
+
508
+    @property
509
+    def author(self) -> UserInContext:
510
+        return UserInContext(
511
+            dbsession=self.dbsession,
512
+            config=self.config,
513
+            user=self.revision.owner
514
+        )
515
+
516
+    @property
517
+    def revision_id(self) -> int:
518
+        return self.revision.revision_id
519
+
520
+    @property
521
+    def created(self) -> datetime:
522
+        return self.updated
523
+
524
+    @property
525
+    def modified(self) -> datetime:
526
+        return self.updated
527
+
528
+    @property
529
+    def updated(self) -> datetime:
530
+        return self.revision.updated
531
+
532
+    @property
533
+    def next_revision(self) -> typing.Optional[ContentRevisionRO]:
534
+        """
535
+        Get next revision (later revision)
536
+        :return: next_revision
537
+        """
538
+        next_revision = None
539
+        revisions = self.revision.node.revisions
540
+        # INFO - G.M - 2018-06-177 - Get revisions more recent that
541
+        # current one
542
+        next_revisions = [
543
+            revision for revision in revisions
544
+            if revision.revision_id > self.revision.revision_id
545
+        ]
546
+        if next_revisions:
547
+            # INFO - G.M - 2018-06-177 -sort revisions by date
548
+            sorted_next_revisions = sorted(
549
+                next_revisions,
550
+                key=lambda revision: revision.updated
551
+            )
552
+            # INFO - G.M - 2018-06-177 - return only next revision
553
+            return sorted_next_revisions[0]
554
+        else:
555
+            return None
556
+
557
+    @property
558
+    def comment_ids(self) -> typing.List[int]:
559
+        """
560
+        Get list of ids of all current revision related comments
561
+        :return: list of comments ids
562
+        """
563
+        comments = self.revision.node.get_comments()
564
+        # INFO - G.M - 2018-06-177 - Get comments more recent than revision.
565
+        revision_comments = [
566
+            comment for comment in comments
567
+            if comment.created > self.revision.updated
568
+        ]
569
+        if self.next_revision:
570
+            # INFO - G.M - 2018-06-177 - if there is a revision more recent
571
+            # than current remove comments from theses rev (comments older
572
+            # than next_revision.)
573
+            revision_comments = [
574
+                comment for comment in revision_comments
575
+                if comment.created < self.next_revision.updated
576
+            ]
577
+        sorted_revision_comments = sorted(
578
+            revision_comments,
579
+            key=lambda revision: revision.created
580
+        )
581
+        comment_ids = []
582
+        for comment in sorted_revision_comments:
583
+            comment_ids.append(comment.content_id)
584
+        return comment_ids
585
+
586
+    # Context-related
587
+    @property
588
+    def show_in_ui(self) -> bool:
589
+        # TODO - G.M - 31-05-2018 - Enable Show_in_ui params
590
+        # if false, then do not show content in the treeview.
591
+        # This may his maybe used for specific contents or for sub-contents.
592
+        # Default is True.
593
+        # In first version of the API, this field is always True
594
+        return True
595
+
596
+    @property
597
+    def slug(self) -> str:
598
+        return slugify(self.revision.label)

+ 21 - 15
tracim/models/data.py View File

235
 
235
 
236
     # TODO - G.M - 10-04-2018 - [Cleanup] Drop this
236
     # TODO - G.M - 10-04-2018 - [Cleanup] Drop this
237
     _ICONS = {
237
     _ICONS = {
238
-        'archiving': 'fa fa-archive',
239
-        'content-comment': 'fa-comment-o',
240
-        'creation': 'fa-magic',
241
-        'deletion': 'fa-trash',
242
-        'edition': 'fa-edit',
243
-        'revision': 'fa-history',
244
-        'status-update': 'fa-random',
245
-        'unarchiving': 'fa-file-archive-o',
246
-        'undeletion': 'fa-trash-o',
247
-        'move': 'fa-arrows',
248
-        'copy': 'fa-files-o',
238
+        'archiving': 'archive',
239
+        'content-comment': 'comment-o',
240
+        'creation': 'magic',
241
+        'deletion': 'trash',
242
+        'edition': 'edit',
243
+        'revision': 'history',
244
+        'status-update': 'random',
245
+        'unarchiving': 'file-archive-o',
246
+        'undeletion': 'trash-o',
247
+        'move': 'arrows',
248
+        'copy': 'files-o',
249
     }
249
     }
250
     #
250
     #
251
     # _LABELS = {
251
     # _LABELS = {
598
 
598
 
599
     revision_id = Column(Integer, primary_key=True)
599
     revision_id = Column(Integer, primary_key=True)
600
     content_id = Column(Integer, ForeignKey('content.id'), nullable=False)
600
     content_id = Column(Integer, ForeignKey('content.id'), nullable=False)
601
+    # TODO - G.M - 2018-06-177 - [author] Owner should be renamed "author"
601
     owner_id = Column(Integer, ForeignKey('users.user_id'), nullable=True)
602
     owner_id = Column(Integer, ForeignKey('users.user_id'), nullable=True)
602
 
603
 
603
     label = Column(Unicode(1024), unique=False, nullable=False)
604
     label = Column(Unicode(1024), unique=False, nullable=False)
631
     parent = relationship("Content", foreign_keys=[parent_id], back_populates="children_revisions")
632
     parent = relationship("Content", foreign_keys=[parent_id], back_populates="children_revisions")
632
 
633
 
633
     node = relationship("Content", foreign_keys=[content_id], back_populates="revisions")
634
     node = relationship("Content", foreign_keys=[content_id], back_populates="revisions")
635
+    # TODO - G.M - 2018-06-177 - [author] Owner should be renamed "author"
634
     owner = relationship('User', remote_side=[User.user_id])
636
     owner = relationship('User', remote_side=[User.user_id])
635
 
637
 
636
     """ List of column copied when make a new revision from another """
638
     """ List of column copied when make a new revision from another """
788
             file_extension,
790
             file_extension,
789
         )
791
         )
790
 
792
 
791
-
793
+# TODO - G.M - 2018-06-177 - [author] Owner should be renamed "author"
792
 Index('idx__content_revisions__owner_id', ContentRevisionRO.owner_id)
794
 Index('idx__content_revisions__owner_id', ContentRevisionRO.owner_id)
793
 Index('idx__content_revisions__parent_id', ContentRevisionRO.parent_id)
795
 Index('idx__content_revisions__parent_id', ContentRevisionRO.parent_id)
794
 
796
 
810
     # QUERY CONTENTS
812
     # QUERY CONTENTS
811
 
813
 
812
     To query contents you will need to join your content query with ContentRevisionRO. Join
814
     To query contents you will need to join your content query with ContentRevisionRO. Join
813
-    condition is available at tracim.lib.content.ContentApi#get_revision_join:
815
+    condition is available at tracim.lib.content.ContentApi#_get_revision_join:
814
 
816
 
815
-    content = DBSession.query(Content).join(ContentRevisionRO, ContentApi.get_revision_join())
817
+    content = DBSession.query(Content).join(ContentRevisionRO, ContentApi._get_revision_join())
816
                   .filter(Content.label == 'foo')
818
                   .filter(Content.label == 'foo')
817
                   .one()
819
                   .one()
818
 
820
 
867
     def revision_id(cls) -> InstrumentedAttribute:
869
     def revision_id(cls) -> InstrumentedAttribute:
868
         return ContentRevisionRO.revision_id
870
         return ContentRevisionRO.revision_id
869
 
871
 
872
+    # TODO - G.M - 2018-06-177 - [author] Owner should be renamed "author"
873
+    # and should be author of first revision.
870
     @hybrid_property
874
     @hybrid_property
871
     def owner_id(self) -> int:
875
     def owner_id(self) -> int:
872
         return self.revision.owner_id
876
         return self.revision.owner_id
1113
     def node(cls) -> InstrumentedAttribute:
1117
     def node(cls) -> InstrumentedAttribute:
1114
         return ContentRevisionRO.node
1118
         return ContentRevisionRO.node
1115
 
1119
 
1120
+    # TODO - G.M - 2018-06-177 - [author] Owner should be renamed "author"
1121
+    # and should be author of first revision.
1116
     @hybrid_property
1122
     @hybrid_property
1117
     def owner(self) -> User:
1123
     def owner(self) -> User:
1118
         return self.revision.owner
1124
         return self.revision.owner
1482
             delta_from_datetime = datetime.utcnow()
1488
             delta_from_datetime = datetime.utcnow()
1483
 
1489
 
1484
         delta = delta_from_datetime - self.created
1490
         delta = delta_from_datetime - self.created
1485
-        
1491
+
1486
         if delta.days > 0:
1492
         if delta.days > 0:
1487
             if delta.days >= 365:
1493
             if delta.days >= 365:
1488
                 aff = '%d year%s ago' % (delta.days/365, 's' if delta.days/365>=2 else '')
1494
                 aff = '%d year%s ago' % (delta.days/365, 's' if delta.days/365>=2 else '')

+ 2 - 2
tracim/models/workspace_menu_entries.py View File

29
   label='Dashboard',
29
   label='Dashboard',
30
   route='/#/workspaces/{workspace_id}/dashboard',
30
   route='/#/workspaces/{workspace_id}/dashboard',
31
   hexcolor='#252525',
31
   hexcolor='#252525',
32
-  fa_icon="",
32
+  fa_icon="signal",
33
 )
33
 )
34
 all_content_menu_entry = WorkspaceMenuEntry(
34
 all_content_menu_entry = WorkspaceMenuEntry(
35
   slug="contents/all",
35
   slug="contents/all",
36
   label="All Contents",
36
   label="All Contents",
37
   route="/#/workspaces/{workspace_id}/contents",
37
   route="/#/workspaces/{workspace_id}/contents",
38
   hexcolor="#fdfdfd",
38
   hexcolor="#fdfdfd",
39
-  fa_icon="",
39
+  fa_icon="th",
40
 )
40
 )
41
 
41
 
42
 # TODO - G.M - 08-06-2018 - This is hardcoded default menu entry,
42
 # TODO - G.M - 08-06-2018 - This is hardcoded default menu entry,

+ 29 - 4
tracim/tests/__init__.py View File

8
 from pyramid import testing
8
 from pyramid import testing
9
 from sqlalchemy.exc import IntegrityError
9
 from sqlalchemy.exc import IntegrityError
10
 
10
 
11
-from tracim.command.database import InitializeDBCommand
12
 from tracim.lib.core.content import ContentApi
11
 from tracim.lib.core.content import ContentApi
13
 from tracim.lib.core.workspace import WorkspaceApi
12
 from tracim.lib.core.workspace import WorkspaceApi
14
-from tracim.models import get_engine, DeclarativeBase, get_session_factory, \
15
-    get_tm_session
16
-from tracim.models.data import Workspace, ContentType
13
+from tracim.models import get_engine
14
+from tracim.models import DeclarativeBase
15
+from tracim.models import get_session_factory
16
+from tracim.models import get_tm_session
17
+from tracim.models.data import Workspace
18
+from tracim.models.data import ContentType
19
+from tracim.models.data import ContentRevisionRO
17
 from tracim.models.data import Content
20
 from tracim.models.data import Content
18
 from tracim.lib.utils.logger import logger
21
 from tracim.lib.utils.logger import logger
19
 from tracim.fixtures import FixturesLoader
22
 from tracim.fixtures import FixturesLoader
28
     # TODO - G.M - 05-04-2018 - Remove this when all old nose code is removed
31
     # TODO - G.M - 05-04-2018 - Remove this when all old nose code is removed
29
     assert a == b, msg or "%r != %r" % (a, b)
32
     assert a == b, msg or "%r != %r" % (a, b)
30
 
33
 
34
+# TODO - G.M - 2018-06-179 - Refactor slug change function
35
+#  as a kind of pytest fixture ?
36
+
37
+
38
+def set_html_document_slug_to_legacy(session_factory) -> None:
39
+    """
40
+    Simple function to help some functional test. This modify "html-documents"
41
+    type content in database to legacy "page" slug.
42
+    :param session_factory: session factory of the test
43
+    :return: Nothing.
44
+    """
45
+    dbsession = get_tm_session(
46
+        session_factory,
47
+        transaction.manager
48
+    )
49
+    content_query = dbsession.query(ContentRevisionRO).filter(ContentRevisionRO.type == 'page').filter(ContentRevisionRO.content_id == 6)  # nopep8
50
+    assert content_query.count() == 0
51
+    html_documents_query = dbsession.query(ContentRevisionRO).filter(ContentRevisionRO.type == 'html-documents')  # nopep8
52
+    html_documents_query.update({ContentRevisionRO.type: 'page'})
53
+    transaction.commit()
54
+    assert content_query.count() > 0
55
+
31
 
56
 
32
 class FunctionalTest(unittest.TestCase):
57
 class FunctionalTest(unittest.TestCase):
33
 
58
 

+ 284 - 0
tracim/tests/functional/test_comments.py View File

1
+# -*- coding: utf-8 -*-
2
+from tracim.tests import FunctionalTest
3
+from tracim.fixtures.content import Content as ContentFixtures
4
+from tracim.fixtures.users_and_groups import Base as BaseFixture
5
+
6
+
7
+class TestCommentsEndpoint(FunctionalTest):
8
+    """
9
+    Tests for /api/v2/workspaces/{workspace_id}/contents/{content_id}/comments
10
+    endpoint
11
+    """
12
+
13
+    fixtures = [BaseFixture, ContentFixtures]
14
+
15
+    def test_api__get_contents_comments__ok_200__nominal_case(self) -> None:
16
+        """
17
+        Get alls comments of a content
18
+        """
19
+        self.testapp.authorization = (
20
+            'Basic',
21
+            (
22
+                'admin@admin.admin',
23
+                'admin@admin.admin'
24
+            )
25
+        )
26
+        res = self.testapp.get('/api/v2/workspaces/2/contents/7/comments', status=200)   # nopep8
27
+        assert len(res.json_body) == 3
28
+        comment = res.json_body[0]
29
+        assert comment['content_id'] == 18
30
+        assert comment['parent_id'] == 7
31
+        assert comment['raw_content'] == '<p>What is for you the best cake ever? </br> I personnally vote for Chocolate cupcake!</p>'  # nopep8
32
+        assert comment['author']
33
+        assert comment['author']['user_id'] == 1
34
+        # TODO - G.M - 2018-06-172 - [avatar] setup avatar url
35
+        assert comment['author']['avatar_url'] == None
36
+        assert comment['author']['public_name'] == 'Global manager'
37
+
38
+        comment = res.json_body[1]
39
+        assert comment['content_id'] == 19
40
+        assert comment['parent_id'] == 7
41
+        assert comment['raw_content'] == '<p>What about Apple Pie? There are Awesome!</p>'  # nopep8
42
+        assert comment['author']
43
+        assert comment['author']['user_id'] == 3
44
+        # TODO - G.M - 2018-06-172 - [avatar] setup avatar url
45
+        assert comment['author']['avatar_url'] == None
46
+        assert comment['author']['public_name'] == 'Bob i.'
47
+        # TODO - G.M - 2018-06-179 - better check for datetime
48
+        assert comment['created']
49
+
50
+        comment = res.json_body[2]
51
+        assert comment['content_id'] == 20
52
+        assert comment['parent_id'] == 7
53
+        assert comment['raw_content'] == '<p>You are right, but Kouign-amann are clearly better.</p>'  # nopep8
54
+        assert comment['author']
55
+        assert comment['author']['user_id'] == 4
56
+        # TODO - G.M - 2018-06-172 - [avatar] setup avatar url
57
+        assert comment['author']['avatar_url'] == None
58
+        assert comment['author']['public_name'] == 'John Reader'
59
+        # TODO - G.M - 2018-06-179 - better check for datetime
60
+        assert comment['created']
61
+
62
+    def test_api__post_content_comment__ok_200__nominal_case(self) -> None:
63
+        """
64
+        Get alls comments of a content
65
+        """
66
+        self.testapp.authorization = (
67
+            'Basic',
68
+            (
69
+                'admin@admin.admin',
70
+                'admin@admin.admin'
71
+            )
72
+        )
73
+        params = {
74
+            'raw_content': 'I strongly disagree, Tiramisu win!'
75
+        }
76
+        res = self.testapp.post_json(
77
+            '/api/v2/workspaces/2/contents/7/comments',
78
+            params=params,
79
+            status=200
80
+        )
81
+        comment = res.json_body
82
+        assert comment['content_id']
83
+        assert comment['parent_id'] == 7
84
+        assert comment['raw_content'] == 'I strongly disagree, Tiramisu win!'
85
+        assert comment['author']
86
+        assert comment['author']['user_id'] == 1
87
+        # TODO - G.M - 2018-06-172 - [avatar] setup avatar url
88
+        assert comment['author']['avatar_url'] is None
89
+        assert comment['author']['public_name'] == 'Global manager'
90
+        # TODO - G.M - 2018-06-179 - better check for datetime
91
+        assert comment['created']
92
+
93
+        res = self.testapp.get('/api/v2/workspaces/2/contents/7/comments', status=200)  # nopep8
94
+        assert len(res.json_body) == 4
95
+        assert comment == res.json_body[3]
96
+
97
+    def test_api__delete_content_comment__ok_200__user_is_owner_and_workspace_manager(self) -> None:  # nopep8
98
+        """
99
+        delete comment (user is workspace_manager and owner)
100
+        """
101
+        self.testapp.authorization = (
102
+            'Basic',
103
+            (
104
+                'admin@admin.admin',
105
+                'admin@admin.admin'
106
+            )
107
+        )
108
+        res = self.testapp.get('/api/v2/workspaces/2/contents/7/comments', status=200)
109
+        assert len(res.json_body) == 3
110
+        comment = res.json_body[0]
111
+        assert comment['content_id'] == 18
112
+        assert comment['parent_id'] == 7
113
+        assert comment['raw_content'] == '<p>What is for you the best cake ever? </br> I personnally vote for Chocolate cupcake!</p>'   # nopep8
114
+        assert comment['author']
115
+        assert comment['author']['user_id'] == 1
116
+        # TODO - G.M - 2018-06-172 - [avatar] setup avatar url
117
+        assert comment['author']['avatar_url'] is None
118
+        assert comment['author']['public_name'] == 'Global manager'
119
+        # TODO - G.M - 2018-06-179 - better check for datetime
120
+        assert comment['created']
121
+
122
+        res = self.testapp.delete(
123
+            '/api/v2/workspaces/2/contents/7/comments/18',
124
+            status=204
125
+        )
126
+        res = self.testapp.get('/api/v2/workspaces/2/contents/7/comments', status=200)
127
+        assert len(res.json_body) == 2
128
+        assert not [content for content in res.json_body if content['content_id'] == 18]  # nopep8
129
+
130
+    def test_api__delete_content_comment__ok_200__user_is_workspace_manager(self) -> None:  # nopep8
131
+        """
132
+        delete comment (user is workspace_manager)
133
+        """
134
+        self.testapp.authorization = (
135
+            'Basic',
136
+            (
137
+                'admin@admin.admin',
138
+                'admin@admin.admin'
139
+            )
140
+        )
141
+        res = self.testapp.get('/api/v2/workspaces/2/contents/7/comments', status=200)
142
+        assert len(res.json_body) == 3
143
+        comment = res.json_body[1]
144
+        assert comment['content_id'] == 19
145
+        assert comment['parent_id'] == 7
146
+        assert comment['raw_content'] == '<p>What about Apple Pie? There are Awesome!</p>'   # nopep8
147
+        assert comment['author']
148
+        assert comment['author']['user_id'] == 3
149
+        # TODO - G.M - 2018-06-172 - [avatar] setup avatar url
150
+        assert comment['author']['avatar_url'] is None
151
+        assert comment['author']['public_name'] == 'Bob i.'
152
+        # TODO - G.M - 2018-06-179 - better check for datetime
153
+        assert comment['created']
154
+
155
+        res = self.testapp.delete(
156
+            '/api/v2/workspaces/2/contents/7/comments/19',
157
+            status=204
158
+        )
159
+        res = self.testapp.get('/api/v2/workspaces/2/contents/7/comments', status=200)
160
+        assert len(res.json_body) == 2
161
+        assert not [content for content in res.json_body if content['content_id'] == 19]  # nopep8
162
+
163
+    def test_api__delete_content_comment__ok_200__user_is_owner_and_content_manager(self) -> None:   # nopep8
164
+        """
165
+        delete comment (user is content-manager and owner)
166
+        """
167
+        self.testapp.authorization = (
168
+            'Basic',
169
+            (
170
+                'admin@admin.admin',
171
+                'admin@admin.admin'
172
+            )
173
+        )
174
+        res = self.testapp.get('/api/v2/workspaces/2/contents/7/comments', status=200)
175
+        assert len(res.json_body) == 3
176
+        comment = res.json_body[1]
177
+        assert comment['content_id'] == 19
178
+        assert comment['parent_id'] == 7
179
+        assert comment['raw_content'] == '<p>What about Apple Pie? There are Awesome!</p>'   # nopep8
180
+        assert comment['author']
181
+        assert comment['author']['user_id'] == 3
182
+        # TODO - G.M - 2018-06-172 - [avatar] setup avatar url
183
+        assert comment['author']['avatar_url'] is None
184
+        assert comment['author']['public_name'] == 'Bob i.'
185
+        # TODO - G.M - 2018-06-179 - better check for datetime
186
+        assert comment['created']
187
+
188
+        res = self.testapp.delete(
189
+            '/api/v2/workspaces/2/contents/7/comments/19',
190
+            status=204
191
+        )
192
+        res = self.testapp.get('/api/v2/workspaces/2/contents/7/comments', status=200)
193
+        assert len(res.json_body) == 2
194
+        assert not [content for content in res.json_body if content['content_id'] == 19]  # nopep8
195
+
196
+    def test_api__delete_content_comment__err_403__user_is_content_manager(self) -> None:  # nopep8
197
+        """
198
+        delete comment (user is content-manager)
199
+        """
200
+        self.testapp.authorization = (
201
+            'Basic',
202
+            (
203
+                'bob@fsf.local',
204
+                'foobarbaz'
205
+            )
206
+        )
207
+        res = self.testapp.get('/api/v2/workspaces/2/contents/7/comments', status=200)  #  nopep8
208
+        assert len(res.json_body) == 3
209
+        comment = res.json_body[2]
210
+        assert comment['content_id'] == 20
211
+        assert comment['parent_id'] == 7
212
+        assert comment['raw_content'] == '<p>You are right, but Kouign-amann are clearly better.</p>'   # nopep8
213
+        assert comment['author']
214
+        assert comment['author']['user_id'] == 4
215
+        # TODO - G.M - 2018-06-172 - [avatar] setup avatar url
216
+        assert comment['author']['avatar_url'] is None
217
+        assert comment['author']['public_name'] == 'John Reader'
218
+        # TODO - G.M - 2018-06-179 - better check for datetime
219
+        assert comment['created']
220
+
221
+        res = self.testapp.delete(
222
+            '/api/v2/workspaces/2/contents/7/comments/20',
223
+            status=403
224
+        )
225
+
226
+    def test_api__delete_content_comment__err_403__user_is_owner_and_reader(self) -> None:
227
+        """
228
+        delete comment (user is reader and owner)
229
+        """
230
+        self.testapp.authorization = (
231
+            'Basic',
232
+            (
233
+                'bob@fsf.local',
234
+                'foobarbaz'
235
+            )
236
+        )
237
+        res = self.testapp.get('/api/v2/workspaces/2/contents/7/comments', status=200)
238
+        assert len(res.json_body) == 3
239
+        comment = res.json_body[2]
240
+        assert comment['content_id'] == 20
241
+        assert comment['parent_id'] == 7
242
+        assert comment['raw_content'] == '<p>You are right, but Kouign-amann are clearly better.</p>'   # nopep8
243
+        assert comment['author']
244
+        assert comment['author']['user_id'] == 4
245
+        # TODO - G.M - 2018-06-172 - [avatar] setup avatar url
246
+        assert comment['author']['avatar_url'] is None
247
+        assert comment['author']['public_name'] == 'John Reader'
248
+        # TODO - G.M - 2018-06-179 - better check for datetime
249
+        assert comment['created']
250
+
251
+        res = self.testapp.delete(
252
+            '/api/v2/workspaces/2/contents/7/comments/20',
253
+            status=403
254
+        )
255
+
256
+    def test_api__delete_content_comment__err_403__user_is_reader(self) -> None:
257
+        """
258
+        delete comment (user is reader)
259
+        """
260
+        self.testapp.authorization = (
261
+            'Basic',
262
+            (
263
+                'bob@fsf.local',
264
+                'foobarbaz'
265
+            )
266
+        )
267
+        res = self.testapp.get('/api/v2/workspaces/2/contents/7/comments', status=200)
268
+        assert len(res.json_body) == 3
269
+        comment = res.json_body[2]
270
+        assert comment['content_id'] == 20
271
+        assert comment['parent_id'] == 7
272
+        assert comment['raw_content'] == '<p>You are right, but Kouign-amann are clearly better.</p>'   # nopep8
273
+        assert comment['author']
274
+        assert comment['author']['user_id'] == 4
275
+        # TODO - G.M - 2018-06-172 - [avatar] setup avatar url
276
+        assert comment['author']['avatar_url'] is None
277
+        assert comment['author']['public_name'] == 'John Reader'
278
+        # TODO - G.M - 2018-06-179 - better check for datetime
279
+        assert comment['created']
280
+
281
+        res = self.testapp.delete(
282
+            '/api/v2/workspaces/2/contents/7/comments/20',
283
+            status=403
284
+        )

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

1
+# -*- coding: utf-8 -*-
2
+from tracim.tests import FunctionalTest
3
+from tracim.tests import set_html_document_slug_to_legacy
4
+from tracim.fixtures.content import Content as ContentFixtures
5
+from tracim.fixtures.users_and_groups import Base as BaseFixture
6
+
7
+
8
+class TestHtmlDocuments(FunctionalTest):
9
+    """
10
+    Tests for /api/v2/workspaces/{workspace_id}/html-documents/{content_id}
11
+    endpoint
12
+    """
13
+
14
+    fixtures = [BaseFixture, ContentFixtures]
15
+
16
+    def test_api__get_html_document__err_400__wrong_content_type(self) -> None:
17
+        """
18
+        Get one html document of a content
19
+        """
20
+        self.testapp.authorization = (
21
+            'Basic',
22
+            (
23
+                'admin@admin.admin',
24
+                'admin@admin.admin'
25
+            )
26
+        )
27
+        res = self.testapp.get(
28
+            '/api/v2/workspaces/2/html-documents/7',
29
+            status=400
30
+        )   # nopep8
31
+
32
+    def test_api__get_html_document__ok_200__legacy_slug(self) -> None:
33
+        """
34
+        Get one html document of a content
35
+        """
36
+        self.testapp.authorization = (
37
+            'Basic',
38
+            (
39
+                'admin@admin.admin',
40
+                'admin@admin.admin'
41
+            )
42
+        )
43
+        set_html_document_slug_to_legacy(self.session_factory)
44
+        res = self.testapp.get(
45
+            '/api/v2/workspaces/2/html-documents/6',
46
+            status=200
47
+        )   # nopep8
48
+        content = res.json_body
49
+        assert content['content_type'] == 'html-documents'
50
+        assert content['content_id'] == 6
51
+        assert content['is_archived'] is False
52
+        assert content['is_deleted'] is False
53
+        assert content['label'] == 'Tiramisu Recipe'
54
+        assert content['parent_id'] == 3
55
+        assert content['show_in_ui'] is True
56
+        assert content['slug'] == 'tiramisu-recipe'
57
+        assert content['status'] == 'open'
58
+        assert content['workspace_id'] == 2
59
+        assert content['current_revision_id'] == 27
60
+        # TODO - G.M - 2018-06-173 - check date format
61
+        assert content['created']
62
+        assert content['author']
63
+        assert content['author']['user_id'] == 1
64
+        assert content['author']['avatar_url'] is None
65
+        assert content['author']['public_name'] == 'Global manager'
66
+        # TODO - G.M - 2018-06-173 - check date format
67
+        assert content['modified']
68
+        assert content['last_modifier'] != content['author']
69
+        assert content['last_modifier']['user_id'] == 3
70
+        assert content['last_modifier']['public_name'] == 'Bob i.'
71
+        assert content['last_modifier']['avatar_url'] is None
72
+        assert content['raw_content'] == '<p>To cook a great Tiramisu, you need many ingredients.</p>'  # nopep8
73
+
74
+    def test_api__get_html_document__ok_200__nominal_case(self) -> None:
75
+        """
76
+        Get one html document of a content
77
+        """
78
+        self.testapp.authorization = (
79
+            'Basic',
80
+            (
81
+                'admin@admin.admin',
82
+                'admin@admin.admin'
83
+            )
84
+        )
85
+        res = self.testapp.get(
86
+            '/api/v2/workspaces/2/html-documents/6',
87
+            status=200
88
+        )   # nopep8
89
+        content = res.json_body
90
+        assert content['content_type'] == 'html-documents'
91
+        assert content['content_id'] == 6
92
+        assert content['is_archived'] is False
93
+        assert content['is_deleted'] is False
94
+        assert content['label'] == 'Tiramisu Recipe'
95
+        assert content['parent_id'] == 3
96
+        assert content['show_in_ui'] is True
97
+        assert content['slug'] == 'tiramisu-recipe'
98
+        assert content['status'] == 'open'
99
+        assert content['workspace_id'] == 2
100
+        assert content['current_revision_id'] == 27
101
+        # TODO - G.M - 2018-06-173 - check date format
102
+        assert content['created']
103
+        assert content['author']
104
+        assert content['author']['user_id'] == 1
105
+        assert content['author']['avatar_url'] is None
106
+        assert content['author']['public_name'] == 'Global manager'
107
+        # TODO - G.M - 2018-06-173 - check date format
108
+        assert content['modified']
109
+        assert content['last_modifier'] != content['author']
110
+        assert content['last_modifier']['user_id'] == 3
111
+        assert content['last_modifier']['public_name'] == 'Bob i.'
112
+        assert content['last_modifier']['avatar_url'] is None
113
+        assert content['raw_content'] == '<p>To cook a great Tiramisu, you need many ingredients.</p>'  # nopep8
114
+
115
+    def test_api__update_html_document__ok_200__nominal_case(self) -> None:
116
+        """
117
+        Update(put) one html document of a content
118
+        """
119
+        self.testapp.authorization = (
120
+            'Basic',
121
+            (
122
+                'admin@admin.admin',
123
+                'admin@admin.admin'
124
+            )
125
+        )
126
+        params = {
127
+            'label': 'My New label',
128
+            'raw_content': '<p> Le nouveau contenu </p>',
129
+        }
130
+        res = self.testapp.put_json(
131
+            '/api/v2/workspaces/2/html-documents/6',
132
+            params=params,
133
+            status=200
134
+        )
135
+        content = res.json_body
136
+        assert content['content_type'] == 'html-documents'
137
+        assert content['content_id'] == 6
138
+        assert content['is_archived'] is False
139
+        assert content['is_deleted'] is False
140
+        assert content['label'] == 'My New label'
141
+        assert content['parent_id'] == 3
142
+        assert content['show_in_ui'] is True
143
+        assert content['slug'] == 'my-new-label'
144
+        assert content['status'] == 'open'
145
+        assert content['workspace_id'] == 2
146
+        assert content['current_revision_id'] == 28
147
+        # TODO - G.M - 2018-06-173 - check date format
148
+        assert content['created']
149
+        assert content['author']
150
+        assert content['author']['user_id'] == 1
151
+        assert content['author']['avatar_url'] is None
152
+        assert content['author']['public_name'] == 'Global manager'
153
+        # TODO - G.M - 2018-06-173 - check date format
154
+        assert content['modified']
155
+        assert content['last_modifier'] == content['author']
156
+        assert content['raw_content'] == '<p> Le nouveau contenu </p>'
157
+
158
+        res = self.testapp.get(
159
+            '/api/v2/workspaces/2/html-documents/6',
160
+            status=200
161
+        )   # nopep8
162
+        content = res.json_body
163
+        assert content['content_type'] == 'html-documents'
164
+        assert content['content_id'] == 6
165
+        assert content['is_archived'] is False
166
+        assert content['is_deleted'] is False
167
+        assert content['label'] == 'My New label'
168
+        assert content['parent_id'] == 3
169
+        assert content['show_in_ui'] is True
170
+        assert content['slug'] == 'my-new-label'
171
+        assert content['status'] == 'open'
172
+        assert content['workspace_id'] == 2
173
+        assert content['current_revision_id'] == 28
174
+        # TODO - G.M - 2018-06-173 - check date format
175
+        assert content['created']
176
+        assert content['author']
177
+        assert content['author']['user_id'] == 1
178
+        assert content['author']['avatar_url'] is None
179
+        assert content['author']['public_name'] == 'Global manager'
180
+        # TODO - G.M - 2018-06-173 - check date format
181
+        assert content['modified']
182
+        assert content['last_modifier'] == content['author']
183
+        assert content['raw_content'] == '<p> Le nouveau contenu </p>'
184
+
185
+    def test_api__get_html_document_revisions__ok_200__nominal_case(
186
+            self
187
+    ) -> None:
188
+        """
189
+        Get one html document of a content
190
+        """
191
+        self.testapp.authorization = (
192
+            'Basic',
193
+            (
194
+                'admin@admin.admin',
195
+                'admin@admin.admin'
196
+            )
197
+        )
198
+        res = self.testapp.get(
199
+            '/api/v2/workspaces/2/html-documents/6/revisions',
200
+            status=200
201
+        )
202
+        revisions = res.json_body
203
+        assert len(revisions) == 3
204
+        revision = revisions[0]
205
+        assert revision['content_type'] == 'html-documents'
206
+        assert revision['content_id'] == 6
207
+        assert revision['is_archived'] is False
208
+        assert revision['is_deleted'] is False
209
+        assert revision['label'] == 'Tiramisu Recipes!!!'
210
+        assert revision['parent_id'] == 3
211
+        assert revision['show_in_ui'] is True
212
+        assert revision['slug'] == 'tiramisu-recipes'
213
+        assert revision['status'] == 'open'
214
+        assert revision['workspace_id'] == 2
215
+        assert revision['revision_id'] == 6
216
+        assert revision['sub_content_types']
217
+        # TODO - G.M - 2018-06-173 - Test with real comments
218
+        assert revision['comment_ids'] == []
219
+        # TODO - G.M - 2018-06-173 - check date format
220
+        assert revision['created']
221
+        assert revision['author']
222
+        assert revision['author']['user_id'] == 1
223
+        assert revision['author']['avatar_url'] is None
224
+        assert revision['author']['public_name'] == 'Global manager'
225
+        revision = revisions[1]
226
+        assert revision['content_type'] == 'html-documents'
227
+        assert revision['content_id'] == 6
228
+        assert revision['is_archived'] is False
229
+        assert revision['is_deleted'] is False
230
+        assert revision['label'] == 'Tiramisu Recipes!!!'
231
+        assert revision['parent_id'] == 3
232
+        assert revision['show_in_ui'] is True
233
+        assert revision['slug'] == 'tiramisu-recipes'
234
+        assert revision['status'] == 'open'
235
+        assert revision['workspace_id'] == 2
236
+        assert revision['revision_id'] == 7
237
+        assert revision['sub_content_types']
238
+        # TODO - G.M - 2018-06-173 - Test with real comments
239
+        assert revision['comment_ids'] == []
240
+        # TODO - G.M - 2018-06-173 - check date format
241
+        assert revision['created']
242
+        assert revision['author']
243
+        assert revision['author']['user_id'] == 1
244
+        assert revision['author']['avatar_url'] is None
245
+        assert revision['author']['public_name'] == 'Global manager'
246
+        revision = revisions[2]
247
+        assert revision['content_type'] == 'html-documents'
248
+        assert revision['content_id'] == 6
249
+        assert revision['is_archived'] is False
250
+        assert revision['is_deleted'] is False
251
+        assert revision['label'] == 'Tiramisu Recipe'
252
+        assert revision['parent_id'] == 3
253
+        assert revision['show_in_ui'] is True
254
+        assert revision['slug'] == 'tiramisu-recipe'
255
+        assert revision['status'] == 'open'
256
+        assert revision['workspace_id'] == 2
257
+        assert revision['revision_id'] == 27
258
+        assert revision['sub_content_types']
259
+        # TODO - G.M - 2018-06-173 - Test with real comments
260
+        assert revision['comment_ids'] == []
261
+        # TODO - G.M - 2018-06-173 - check date format
262
+        assert revision['created']
263
+        assert revision['author']
264
+        assert revision['author']['user_id'] == 3
265
+        assert revision['author']['avatar_url'] is None
266
+        assert revision['author']['public_name'] == 'Bob i.'
267
+
268
+    def test_api__set_html_document_status__ok_200__nominal_case(self) -> None:
269
+        """
270
+        Get one html document of a content
271
+        """
272
+        self.testapp.authorization = (
273
+            'Basic',
274
+            (
275
+                'admin@admin.admin',
276
+                'admin@admin.admin'
277
+            )
278
+        )
279
+        params = {
280
+            'status': 'closed-deprecated',
281
+        }
282
+
283
+        # before
284
+        res = self.testapp.get(
285
+            '/api/v2/workspaces/2/html-documents/6',
286
+            status=200
287
+        )   # nopep8
288
+        content = res.json_body
289
+        assert content['content_type'] == 'html-documents'
290
+        assert content['content_id'] == 6
291
+        assert content['status'] == 'open'
292
+
293
+        # set status
294
+        res = self.testapp.put_json(
295
+            '/api/v2/workspaces/2/html-documents/6/status',
296
+            params=params,
297
+            status=204
298
+        )
299
+
300
+        # after
301
+        res = self.testapp.get(
302
+            '/api/v2/workspaces/2/html-documents/6',
303
+            status=200
304
+        )   # nopep8
305
+        content = res.json_body
306
+        assert content['content_type'] == 'html-documents'
307
+        assert content['content_id'] == 6
308
+        assert content['status'] == 'closed-deprecated'
309
+
310
+
311
+class TestThreads(FunctionalTest):
312
+    """
313
+    Tests for /api/v2/workspaces/{workspace_id}/threads/{content_id}
314
+    endpoint
315
+    """
316
+
317
+    fixtures = [BaseFixture, ContentFixtures]
318
+
319
+    def test_api__get_thread__err_400__wrong_content_type(self) -> None:
320
+        """
321
+        Get one html document of a content
322
+        """
323
+        self.testapp.authorization = (
324
+            'Basic',
325
+            (
326
+                'admin@admin.admin',
327
+                'admin@admin.admin'
328
+            )
329
+        )
330
+        res = self.testapp.get(
331
+            '/api/v2/workspaces/2/threads/6',
332
+            status=400
333
+        )   # nopep8
334
+
335
+    def test_api__get_thread__ok_200__nominal_case(self) -> None:
336
+        """
337
+        Get one html document of a content
338
+        """
339
+        self.testapp.authorization = (
340
+            'Basic',
341
+            (
342
+                'admin@admin.admin',
343
+                'admin@admin.admin'
344
+            )
345
+        )
346
+        res = self.testapp.get(
347
+            '/api/v2/workspaces/2/threads/7',
348
+            status=200
349
+        )   # nopep8
350
+        content = res.json_body
351
+        assert content['content_type'] == 'thread'
352
+        assert content['content_id'] == 7
353
+        assert content['is_archived'] is False
354
+        assert content['is_deleted'] is False
355
+        assert content['label'] == 'Best Cakes?'
356
+        assert content['parent_id'] == 3
357
+        assert content['show_in_ui'] is True
358
+        assert content['slug'] == 'best-cakes'
359
+        assert content['status'] == 'open'
360
+        assert content['workspace_id'] == 2
361
+        assert content['current_revision_id'] == 26
362
+        # TODO - G.M - 2018-06-173 - check date format
363
+        assert content['created']
364
+        assert content['author']
365
+        assert content['author']['user_id'] == 1
366
+        assert content['author']['avatar_url'] is None
367
+        assert content['author']['public_name'] == 'Global manager'
368
+        # TODO - G.M - 2018-06-173 - check date format
369
+        assert content['modified']
370
+        assert content['last_modifier'] != content['author']
371
+        assert content['last_modifier']['user_id'] == 3
372
+        assert content['last_modifier']['public_name'] == 'Bob i.'
373
+        assert content['last_modifier']['avatar_url'] is None
374
+        assert content['raw_content'] == 'What is the best cake?'  # nopep8
375
+
376
+    def test_api__update_thread__ok_200__nominal_case(self) -> None:
377
+        """
378
+        Update(put) one html document of a content
379
+        """
380
+        self.testapp.authorization = (
381
+            'Basic',
382
+            (
383
+                'admin@admin.admin',
384
+                'admin@admin.admin'
385
+            )
386
+        )
387
+        params = {
388
+            'label': 'My New label',
389
+            'raw_content': '<p> Le nouveau contenu </p>',
390
+        }
391
+        res = self.testapp.put_json(
392
+            '/api/v2/workspaces/2/threads/7',
393
+            params=params,
394
+            status=200
395
+        )
396
+        content = res.json_body
397
+        assert content['content_type'] == 'thread'
398
+        assert content['content_id'] == 7
399
+        assert content['is_archived'] is False
400
+        assert content['is_deleted'] is False
401
+        assert content['label'] == 'My New label'
402
+        assert content['parent_id'] == 3
403
+        assert content['show_in_ui'] is True
404
+        assert content['slug'] == 'my-new-label'
405
+        assert content['status'] == 'open'
406
+        assert content['workspace_id'] == 2
407
+        assert content['current_revision_id'] == 28
408
+        # TODO - G.M - 2018-06-173 - check date format
409
+        assert content['created']
410
+        assert content['author']
411
+        assert content['author']['user_id'] == 1
412
+        assert content['author']['avatar_url'] is None
413
+        assert content['author']['public_name'] == 'Global manager'
414
+        # TODO - G.M - 2018-06-173 - check date format
415
+        assert content['modified']
416
+        assert content['last_modifier'] == content['author']
417
+        assert content['raw_content'] == '<p> Le nouveau contenu </p>'
418
+
419
+        res = self.testapp.get(
420
+            '/api/v2/workspaces/2/threads/7',
421
+            status=200
422
+        )   # nopep8
423
+        content = res.json_body
424
+        assert content['content_type'] == 'thread'
425
+        assert content['content_id'] == 7
426
+        assert content['is_archived'] is False
427
+        assert content['is_deleted'] is False
428
+        assert content['label'] == 'My New label'
429
+        assert content['parent_id'] == 3
430
+        assert content['show_in_ui'] is True
431
+        assert content['slug'] == 'my-new-label'
432
+        assert content['status'] == 'open'
433
+        assert content['workspace_id'] == 2
434
+        assert content['current_revision_id'] == 28
435
+        # TODO - G.M - 2018-06-173 - check date format
436
+        assert content['created']
437
+        assert content['author']
438
+        assert content['author']['user_id'] == 1
439
+        assert content['author']['avatar_url'] is None
440
+        assert content['author']['public_name'] == 'Global manager'
441
+        # TODO - G.M - 2018-06-173 - check date format
442
+        assert content['modified']
443
+        assert content['last_modifier'] == content['author']
444
+        assert content['raw_content'] == '<p> Le nouveau contenu </p>'
445
+
446
+    def test_api__get_thread_revisions__ok_200__nominal_case(
447
+            self
448
+    ) -> None:
449
+        """
450
+        Get one html document of a content
451
+        """
452
+        self.testapp.authorization = (
453
+            'Basic',
454
+            (
455
+                'admin@admin.admin',
456
+                'admin@admin.admin'
457
+            )
458
+        )
459
+        res = self.testapp.get(
460
+            '/api/v2/workspaces/2/threads/7/revisions',
461
+            status=200
462
+        )
463
+        revisions = res.json_body
464
+        assert len(revisions) == 2
465
+        revision = revisions[0]
466
+        assert revision['content_type'] == 'thread'
467
+        assert revision['content_id'] == 7
468
+        assert revision['is_archived'] is False
469
+        assert revision['is_deleted'] is False
470
+        assert revision['label'] == 'Best Cake'
471
+        assert revision['parent_id'] == 3
472
+        assert revision['show_in_ui'] is True
473
+        assert revision['slug'] == 'best-cake'
474
+        assert revision['status'] == 'open'
475
+        assert revision['workspace_id'] == 2
476
+        assert revision['revision_id'] == 8
477
+        assert revision['sub_content_types']
478
+        assert revision['comment_ids'] == [18, 19, 20]
479
+        # TODO - G.M - 2018-06-173 - check date format
480
+        assert revision['created']
481
+        assert revision['author']
482
+        assert revision['author']['user_id'] == 1
483
+        assert revision['author']['avatar_url'] is None
484
+        assert revision['author']['public_name'] == 'Global manager'
485
+        revision = revisions[1]
486
+        assert revision['content_type'] == 'thread'
487
+        assert revision['content_id'] == 7
488
+        assert revision['is_archived'] is False
489
+        assert revision['is_deleted'] is False
490
+        assert revision['label'] == 'Best Cakes?'
491
+        assert revision['parent_id'] == 3
492
+        assert revision['show_in_ui'] is True
493
+        assert revision['slug'] == 'best-cakes'
494
+        assert revision['status'] == 'open'
495
+        assert revision['workspace_id'] == 2
496
+        assert revision['revision_id'] == 26
497
+        assert revision['sub_content_types']
498
+        assert revision['comment_ids'] == []
499
+        # TODO - G.M - 2018-06-173 - check date format
500
+        assert revision['created']
501
+        assert revision['author']
502
+        assert revision['author']['user_id'] == 3
503
+        assert revision['author']['avatar_url'] is None
504
+        assert revision['author']['public_name'] == 'Bob i.'
505
+
506
+    def test_api__set_thread_status__ok_200__nominal_case(self) -> None:
507
+        """
508
+        Get one html document of a content
509
+        """
510
+        self.testapp.authorization = (
511
+            'Basic',
512
+            (
513
+                'admin@admin.admin',
514
+                'admin@admin.admin'
515
+            )
516
+        )
517
+        params = {
518
+            'status': 'closed-deprecated',
519
+        }
520
+
521
+        # before
522
+        res = self.testapp.get(
523
+            '/api/v2/workspaces/2/threads/7',
524
+            status=200
525
+        )   # nopep8
526
+        content = res.json_body
527
+        assert content['content_type'] == 'thread'
528
+        assert content['content_id'] == 7
529
+        assert content['status'] == 'open'
530
+
531
+        # set status
532
+        res = self.testapp.put_json(
533
+            '/api/v2/workspaces/2/threads/7/status',
534
+            params=params,
535
+            status=204
536
+        )
537
+
538
+        # after
539
+        res = self.testapp.get(
540
+            '/api/v2/workspaces/2/threads/7',
541
+            status=200
542
+        )   # nopep8
543
+        content = res.json_body
544
+        assert content['content_type'] == 'thread'
545
+        assert content['content_id'] == 7
546
+        assert content['status'] == 'closed-deprecated'

+ 4 - 4
tracim/tests/functional/test_mail_notification.py View File

162
         assert headers['From'][0] == '"Bob i. via Tracim" <test_user_from+3@localhost>'  # nopep8
162
         assert headers['From'][0] == '"Bob i. via Tracim" <test_user_from+3@localhost>'  # nopep8
163
         assert headers['To'][0] == 'Global manager <admin@admin.admin>'
163
         assert headers['To'][0] == 'Global manager <admin@admin.admin>'
164
         assert headers['Subject'][0] == '[TRACIM] [Recipes] file1 (Open)'
164
         assert headers['Subject'][0] == '[TRACIM] [Recipes] file1 (Open)'
165
-        assert headers['References'][0] == 'test_user_refs+19@localhost'
166
-        assert headers['Reply-to'][0] == '"Bob i. & all members of Recipes" <test_user_reply+19@localhost>'  # nopep8
165
+        assert headers['References'][0] == 'test_user_refs+22@localhost'
166
+        assert headers['Reply-to'][0] == '"Bob i. & all members of Recipes" <test_user_reply+22@localhost>'  # nopep8
167
 
167
 
168
 
168
 
169
 class TestNotificationsAsync(MailHogTest):
169
 class TestNotificationsAsync(MailHogTest):
263
         assert headers['From'][0] == '"Bob i. via Tracim" <test_user_from+3@localhost>'  # nopep8
263
         assert headers['From'][0] == '"Bob i. via Tracim" <test_user_from+3@localhost>'  # nopep8
264
         assert headers['To'][0] == 'Global manager <admin@admin.admin>'
264
         assert headers['To'][0] == 'Global manager <admin@admin.admin>'
265
         assert headers['Subject'][0] == '[TRACIM] [Recipes] file1 (Open)'
265
         assert headers['Subject'][0] == '[TRACIM] [Recipes] file1 (Open)'
266
-        assert headers['References'][0] == 'test_user_refs+19@localhost'
267
-        assert headers['Reply-to'][0] == '"Bob i. & all members of Recipes" <test_user_reply+19@localhost>'  # nopep8
266
+        assert headers['References'][0] == 'test_user_refs+22@localhost'
267
+        assert headers['Reply-to'][0] == '"Bob i. & all members of Recipes" <test_user_reply+22@localhost>'  # nopep8

+ 4 - 4
tracim/tests/functional/test_session.py View File

59
         assert res.json_body['caldav_url'] is None
59
         assert res.json_body['caldav_url'] is None
60
         assert res.json_body['avatar_url'] is None
60
         assert res.json_body['avatar_url'] is None
61
 
61
 
62
-    def test_api__try_login_enpoint__err_400__bad_password(self):
62
+    def test_api__try_login_enpoint__err_403__bad_password(self):
63
         params = {
63
         params = {
64
             'email': 'admin@admin.admin',
64
             'email': 'admin@admin.admin',
65
             'password': 'bad_password',
65
             'password': 'bad_password',
66
         }
66
         }
67
         res = self.testapp.post_json(
67
         res = self.testapp.post_json(
68
             '/api/v2/sessions/login',
68
             '/api/v2/sessions/login',
69
-            status=400,
69
+            status=403,
70
             params=params,
70
             params=params,
71
         )
71
         )
72
         assert isinstance(res.json, dict)
72
         assert isinstance(res.json, dict)
74
         assert 'message' in res.json.keys()
74
         assert 'message' in res.json.keys()
75
         assert 'details' in res.json.keys()
75
         assert 'details' in res.json.keys()
76
 
76
 
77
-    def test_api__try_login_enpoint__err_400__unregistered_user(self):
77
+    def test_api__try_login_enpoint__err_403__unregistered_user(self):
78
         params = {
78
         params = {
79
             'email': 'unknown_user@unknown.unknown',
79
             'email': 'unknown_user@unknown.unknown',
80
             'password': 'bad_password',
80
             'password': 'bad_password',
81
         }
81
         }
82
         res = self.testapp.post_json(
82
         res = self.testapp.post_json(
83
             '/api/v2/sessions/login',
83
             '/api/v2/sessions/login',
84
-            status=400,
84
+            status=403,
85
             params=params,
85
             params=params,
86
         )
86
         )
87
         assert isinstance(res.json, dict)
87
         assert isinstance(res.json, dict)

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

25
         res = res.json_body
25
         res = res.json_body
26
         application = res[0]
26
         application = res[0]
27
         assert application['label'] == "Text Documents"
27
         assert application['label'] == "Text Documents"
28
-        assert application['slug'] == 'contents/htmlpage'
28
+        assert application['slug'] == 'contents/html-documents'
29
         assert application['fa_icon'] == 'file-text-o'
29
         assert application['fa_icon'] == 'file-text-o'
30
         assert application['hexcolor'] == '#3f52e3'
30
         assert application['hexcolor'] == '#3f52e3'
31
         assert application['is_active'] is True
31
         assert application['is_active'] is True
33
         application = res[1]
33
         application = res[1]
34
         assert application['label'] == "Markdown Plus Documents"
34
         assert application['label'] == "Markdown Plus Documents"
35
         assert application['slug'] == 'contents/markdownpluspage'
35
         assert application['slug'] == 'contents/markdownpluspage'
36
-        assert application['fa_icon'] == 'file-code'
36
+        assert application['fa_icon'] == 'file-code-o'
37
         assert application['hexcolor'] == '#f12d2d'
37
         assert application['hexcolor'] == '#f12d2d'
38
         assert application['is_active'] is True
38
         assert application['is_active'] is True
39
         assert 'config' in application
39
         assert 'config' in application
54
         application = res[4]
54
         application = res[4]
55
         assert application['label'] == "Calendar"
55
         assert application['label'] == "Calendar"
56
         assert application['slug'] == 'calendar'
56
         assert application['slug'] == 'calendar'
57
-        assert application['fa_icon'] == 'calendar-alt'
57
+        assert application['fa_icon'] == 'calendar'
58
         assert application['hexcolor'] == '#757575'
58
         assert application['hexcolor'] == '#757575'
59
         assert application['is_active'] is True
59
         assert application['is_active'] is True
60
         assert 'config' in application
60
         assert 'config' in application
116
 
116
 
117
         content_type = res[2]
117
         content_type = res[2]
118
         assert content_type['slug'] == 'markdownpage'
118
         assert content_type['slug'] == 'markdownpage'
119
-        assert content_type['fa_icon'] == 'file-code'
119
+        assert content_type['fa_icon'] == 'file-code-o'
120
         assert content_type['hexcolor'] == '#f12d2d'
120
         assert content_type['hexcolor'] == '#f12d2d'
121
         assert content_type['label'] == 'Rich Markdown File'
121
         assert content_type['label'] == 'Rich Markdown File'
122
         assert content_type['creation_label'] == 'Create a Markdown document'
122
         assert content_type['creation_label'] == 'Create a Markdown document'
124
         assert len(content_type['available_statuses']) == 4
124
         assert len(content_type['available_statuses']) == 4
125
 
125
 
126
         content_type = res[3]
126
         content_type = res[3]
127
-        assert content_type['slug'] == 'page'
127
+        assert content_type['slug'] == 'html-documents'
128
         assert content_type['fa_icon'] == 'file-text-o'
128
         assert content_type['fa_icon'] == 'file-text-o'
129
         assert content_type['hexcolor'] == '#3f52e3'
129
         assert content_type['hexcolor'] == '#3f52e3'
130
         assert content_type['label'] == 'Text Document'
130
         assert content_type['label'] == 'Text Document'

+ 6 - 6
tracim/tests/functional/test_user.py View File

38
         assert sidebar_entry['label'] == 'Dashboard'
38
         assert sidebar_entry['label'] == 'Dashboard'
39
         assert sidebar_entry['route'] == '/#/workspaces/1/dashboard'  # nopep8
39
         assert sidebar_entry['route'] == '/#/workspaces/1/dashboard'  # nopep8
40
         assert sidebar_entry['hexcolor'] == "#252525"
40
         assert sidebar_entry['hexcolor'] == "#252525"
41
-        assert sidebar_entry['fa_icon'] == ""
41
+        assert sidebar_entry['fa_icon'] == "signal"
42
 
42
 
43
         sidebar_entry = workspace['sidebar_entries'][1]
43
         sidebar_entry = workspace['sidebar_entries'][1]
44
         assert sidebar_entry['slug'] == 'contents/all'
44
         assert sidebar_entry['slug'] == 'contents/all'
45
         assert sidebar_entry['label'] == 'All Contents'
45
         assert sidebar_entry['label'] == 'All Contents'
46
         assert sidebar_entry['route'] == "/#/workspaces/1/contents"  # nopep8
46
         assert sidebar_entry['route'] == "/#/workspaces/1/contents"  # nopep8
47
         assert sidebar_entry['hexcolor'] == "#fdfdfd"
47
         assert sidebar_entry['hexcolor'] == "#fdfdfd"
48
-        assert sidebar_entry['fa_icon'] == ""
48
+        assert sidebar_entry['fa_icon'] == "th"
49
 
49
 
50
         sidebar_entry = workspace['sidebar_entries'][2]
50
         sidebar_entry = workspace['sidebar_entries'][2]
51
-        assert sidebar_entry['slug'] == 'contents/htmlpage'
51
+        assert sidebar_entry['slug'] == 'contents/html-documents'
52
         assert sidebar_entry['label'] == 'Text Documents'
52
         assert sidebar_entry['label'] == 'Text Documents'
53
-        assert sidebar_entry['route'] == '/#/workspaces/1/contents?type=htmlpage'  # nopep8
53
+        assert sidebar_entry['route'] == '/#/workspaces/1/contents?type=html-documents'  # nopep8
54
         assert sidebar_entry['hexcolor'] == "#3f52e3"
54
         assert sidebar_entry['hexcolor'] == "#3f52e3"
55
         assert sidebar_entry['fa_icon'] == "file-text-o"
55
         assert sidebar_entry['fa_icon'] == "file-text-o"
56
 
56
 
59
         assert sidebar_entry['label'] == 'Markdown Plus Documents'
59
         assert sidebar_entry['label'] == 'Markdown Plus Documents'
60
         assert sidebar_entry['route'] == "/#/workspaces/1/contents?type=markdownpluspage"    # nopep8
60
         assert sidebar_entry['route'] == "/#/workspaces/1/contents?type=markdownpluspage"    # nopep8
61
         assert sidebar_entry['hexcolor'] == "#f12d2d"
61
         assert sidebar_entry['hexcolor'] == "#f12d2d"
62
-        assert sidebar_entry['fa_icon'] == "file-code"
62
+        assert sidebar_entry['fa_icon'] == "file-code-o"
63
 
63
 
64
         sidebar_entry = workspace['sidebar_entries'][4]
64
         sidebar_entry = workspace['sidebar_entries'][4]
65
         assert sidebar_entry['slug'] == 'contents/files'
65
         assert sidebar_entry['slug'] == 'contents/files'
80
         assert sidebar_entry['label'] == 'Calendar'
80
         assert sidebar_entry['label'] == 'Calendar'
81
         assert sidebar_entry['route'] == "/#/workspaces/1/calendar"  # nopep8
81
         assert sidebar_entry['route'] == "/#/workspaces/1/calendar"  # nopep8
82
         assert sidebar_entry['hexcolor'] == "#757575"
82
         assert sidebar_entry['hexcolor'] == "#757575"
83
-        assert sidebar_entry['fa_icon'] == "calendar-alt"
83
+        assert sidebar_entry['fa_icon'] == "calendar"
84
 
84
 
85
     def test_api__get_user_workspaces__err_403__unallowed_user(self):
85
     def test_api__get_user_workspaces__err_403__unallowed_user(self):
86
         """
86
         """

+ 96 - 32
tracim/tests/functional/test_workspaces.py View File

3
 Tests for /api/v2/workspaces subpath endpoints.
3
 Tests for /api/v2/workspaces subpath endpoints.
4
 """
4
 """
5
 from tracim.tests import FunctionalTest
5
 from tracim.tests import FunctionalTest
6
+from tracim.tests import set_html_document_slug_to_legacy
6
 from tracim.fixtures.content import Content as ContentFixtures
7
 from tracim.fixtures.content import Content as ContentFixtures
7
 from tracim.fixtures.users_and_groups import Base as BaseFixture
8
 from tracim.fixtures.users_and_groups import Base as BaseFixture
8
 
9
 
38
         assert sidebar_entry['label'] == 'Dashboard'
39
         assert sidebar_entry['label'] == 'Dashboard'
39
         assert sidebar_entry['route'] == '/#/workspaces/1/dashboard'  # nopep8
40
         assert sidebar_entry['route'] == '/#/workspaces/1/dashboard'  # nopep8
40
         assert sidebar_entry['hexcolor'] == "#252525"
41
         assert sidebar_entry['hexcolor'] == "#252525"
41
-        assert sidebar_entry['fa_icon'] == ""
42
+        assert sidebar_entry['fa_icon'] == "signal"
42
 
43
 
43
         sidebar_entry = workspace['sidebar_entries'][1]
44
         sidebar_entry = workspace['sidebar_entries'][1]
44
         assert sidebar_entry['slug'] == 'contents/all'
45
         assert sidebar_entry['slug'] == 'contents/all'
45
         assert sidebar_entry['label'] == 'All Contents'
46
         assert sidebar_entry['label'] == 'All Contents'
46
         assert sidebar_entry['route'] == "/#/workspaces/1/contents"  # nopep8
47
         assert sidebar_entry['route'] == "/#/workspaces/1/contents"  # nopep8
47
         assert sidebar_entry['hexcolor'] == "#fdfdfd"
48
         assert sidebar_entry['hexcolor'] == "#fdfdfd"
48
-        assert sidebar_entry['fa_icon'] == ""
49
+        assert sidebar_entry['fa_icon'] == "th"
49
 
50
 
50
         sidebar_entry = workspace['sidebar_entries'][2]
51
         sidebar_entry = workspace['sidebar_entries'][2]
51
-        assert sidebar_entry['slug'] == 'contents/htmlpage'
52
+        assert sidebar_entry['slug'] == 'contents/html-documents'
52
         assert sidebar_entry['label'] == 'Text Documents'
53
         assert sidebar_entry['label'] == 'Text Documents'
53
-        assert sidebar_entry['route'] == '/#/workspaces/1/contents?type=htmlpage'  # nopep8
54
+        assert sidebar_entry['route'] == '/#/workspaces/1/contents?type=html-documents'  # nopep8
54
         assert sidebar_entry['hexcolor'] == "#3f52e3"
55
         assert sidebar_entry['hexcolor'] == "#3f52e3"
55
         assert sidebar_entry['fa_icon'] == "file-text-o"
56
         assert sidebar_entry['fa_icon'] == "file-text-o"
56
 
57
 
59
         assert sidebar_entry['label'] == 'Markdown Plus Documents'
60
         assert sidebar_entry['label'] == 'Markdown Plus Documents'
60
         assert sidebar_entry['route'] == "/#/workspaces/1/contents?type=markdownpluspage"    # nopep8
61
         assert sidebar_entry['route'] == "/#/workspaces/1/contents?type=markdownpluspage"    # nopep8
61
         assert sidebar_entry['hexcolor'] == "#f12d2d"
62
         assert sidebar_entry['hexcolor'] == "#f12d2d"
62
-        assert sidebar_entry['fa_icon'] == "file-code"
63
+        assert sidebar_entry['fa_icon'] == "file-code-o"
63
 
64
 
64
         sidebar_entry = workspace['sidebar_entries'][4]
65
         sidebar_entry = workspace['sidebar_entries'][4]
65
         assert sidebar_entry['slug'] == 'contents/files'
66
         assert sidebar_entry['slug'] == 'contents/files'
80
         assert sidebar_entry['label'] == 'Calendar'
81
         assert sidebar_entry['label'] == 'Calendar'
81
         assert sidebar_entry['route'] == "/#/workspaces/1/calendar"  # nopep8
82
         assert sidebar_entry['route'] == "/#/workspaces/1/calendar"  # nopep8
82
         assert sidebar_entry['hexcolor'] == "#757575"
83
         assert sidebar_entry['hexcolor'] == "#757575"
83
-        assert sidebar_entry['fa_icon'] == "calendar-alt"
84
+        assert sidebar_entry['fa_icon'] == "calendar"
84
 
85
 
85
     def test_api__get_workspace__err_403__unallowed_user(self) -> None:
86
     def test_api__get_workspace__err_403__unallowed_user(self) -> None:
86
         """
87
         """
247
         assert content['show_in_ui'] is True
248
         assert content['show_in_ui'] is True
248
         assert content['slug'] == 'tools'
249
         assert content['slug'] == 'tools'
249
         assert content['status'] == 'open'
250
         assert content['status'] == 'open'
250
-        assert set(content['sub_content_types']) == {'thread', 'page', 'folder', 'file'}  # nopep8
251
+        assert set(content['sub_content_types']) == {'thread', 'html-documents', 'folder', 'file'}  # nopep8
251
         assert content['workspace_id'] == 1
252
         assert content['workspace_id'] == 1
252
         content = res[1]
253
         content = res[1]
253
         assert content['content_id'] == 2
254
         assert content['content_id'] == 2
258
         assert content['show_in_ui'] is True
259
         assert content['show_in_ui'] is True
259
         assert content['slug'] == 'menus'
260
         assert content['slug'] == 'menus'
260
         assert content['status'] == 'open'
261
         assert content['status'] == 'open'
261
-        assert set(content['sub_content_types']) == {'thread', 'page', 'folder', 'file'}  # nopep8
262
+        assert set(content['sub_content_types']) == {'thread', 'html-documents', 'folder', 'file'}  # nopep8
262
         assert content['workspace_id'] == 1
263
         assert content['workspace_id'] == 1
263
         content = res[2]
264
         content = res[2]
264
         assert content['content_id'] == 11
265
         assert content['content_id'] == 11
269
         assert content['show_in_ui'] is True
270
         assert content['show_in_ui'] is True
270
         assert content['slug'] == 'current-menu'
271
         assert content['slug'] == 'current-menu'
271
         assert content['status'] == 'open'
272
         assert content['status'] == 'open'
272
-        assert set(content['sub_content_types']) == {'thread', 'page', 'folder', 'file'}  # nopep8
273
+        assert set(content['sub_content_types']) == {'thread', 'html-documents', 'folder', 'file'}  # nopep8
273
         assert content['workspace_id'] == 1
274
         assert content['workspace_id'] == 1
274
 
275
 
275
     # Root related
276
     # Root related
277
+    def test_api__get_workspace_content__ok_200__get_all_root_content__legacy_html_slug(self):
278
+        """
279
+        Check obtain workspace all root contents
280
+        """
281
+        set_html_document_slug_to_legacy(self.session_factory)
282
+        params = {
283
+            'parent_id': 0,
284
+            'show_archived': 1,
285
+            'show_deleted': 1,
286
+            'show_active': 1,
287
+        }
288
+        self.testapp.authorization = (
289
+            'Basic',
290
+            (
291
+                'bob@fsf.local',
292
+                'foobarbaz'
293
+            )
294
+        )
295
+        res = self.testapp.get(
296
+            '/api/v2/workspaces/3/contents',
297
+            status=200,
298
+            params=params,
299
+        ).json_body  # nopep8
300
+        # TODO - G.M - 30-05-2018 - Check this test
301
+        assert len(res) == 4
302
+        content = res[1]
303
+        assert content['content_type'] == 'html-documents'
304
+        assert content['content_id'] == 15
305
+        assert content['is_archived'] is False
306
+        assert content['is_deleted'] is False
307
+        assert content['label'] == 'New Fruit Salad'
308
+        assert content['parent_id'] is None
309
+        assert content['show_in_ui'] is True
310
+        assert content['slug'] == 'new-fruit-salad'
311
+        assert content['status'] == 'open'
312
+        assert set(content['sub_content_types']) == {'thread', 'html-documents', 'folder', 'file'}  # nopep8
313
+        assert content['workspace_id'] == 3
314
+
315
+        content = res[2]
316
+        assert content['content_type'] == 'html-documents'
317
+        assert content['content_id'] == 16
318
+        assert content['is_archived'] is True
319
+        assert content['is_deleted'] is False
320
+        assert content['label'].startswith('Fruit Salad')
321
+        assert content['parent_id'] is None
322
+        assert content['show_in_ui'] is True
323
+        assert content['slug'].startswith('fruit-salad')
324
+        assert content['status'] == 'open'
325
+        assert set(content['sub_content_types']) == {'thread', 'html-documents', 'folder', 'file'}  # nopep8
326
+        assert content['workspace_id'] == 3
327
+
328
+        content = res[3]
329
+        assert content['content_type'] == 'html-documents'
330
+        assert content['content_id'] == 17
331
+        assert content['is_archived'] is False
332
+        assert content['is_deleted'] is True
333
+        assert content['label'].startswith('Bad Fruit Salad')
334
+        assert content['parent_id'] is None
335
+        assert content['show_in_ui'] is True
336
+        assert content['slug'].startswith('bad-fruit-salad')
337
+        assert content['status'] == 'open'
338
+        assert set(content['sub_content_types']) == {'thread', 'html-documents', 'folder', 'file'}  # nopep8
339
+        assert content['workspace_id'] == 3
276
 
340
 
277
     def test_api__get_workspace_content__ok_200__get_all_root_content(self):
341
     def test_api__get_workspace_content__ok_200__get_all_root_content(self):
278
         """
342
         """
299
         # TODO - G.M - 30-05-2018 - Check this test
363
         # TODO - G.M - 30-05-2018 - Check this test
300
         assert len(res) == 4
364
         assert len(res) == 4
301
         content = res[1]
365
         content = res[1]
302
-        assert content['content_type'] == 'page'
366
+        assert content['content_type'] == 'html-documents'
303
         assert content['content_id'] == 15
367
         assert content['content_id'] == 15
304
         assert content['is_archived'] is False
368
         assert content['is_archived'] is False
305
         assert content['is_deleted'] is False
369
         assert content['is_deleted'] is False
308
         assert content['show_in_ui'] is True
372
         assert content['show_in_ui'] is True
309
         assert content['slug'] == 'new-fruit-salad'
373
         assert content['slug'] == 'new-fruit-salad'
310
         assert content['status'] == 'open'
374
         assert content['status'] == 'open'
311
-        assert set(content['sub_content_types']) == {'thread', 'page', 'folder', 'file'}  # nopep8
375
+        assert set(content['sub_content_types']) == {'thread', 'html-documents', 'folder', 'file'}  # nopep8
312
         assert content['workspace_id'] == 3
376
         assert content['workspace_id'] == 3
313
 
377
 
314
         content = res[2]
378
         content = res[2]
315
-        assert content['content_type'] == 'page'
379
+        assert content['content_type'] == 'html-documents'
316
         assert content['content_id'] == 16
380
         assert content['content_id'] == 16
317
         assert content['is_archived'] is True
381
         assert content['is_archived'] is True
318
         assert content['is_deleted'] is False
382
         assert content['is_deleted'] is False
321
         assert content['show_in_ui'] is True
385
         assert content['show_in_ui'] is True
322
         assert content['slug'].startswith('fruit-salad')
386
         assert content['slug'].startswith('fruit-salad')
323
         assert content['status'] == 'open'
387
         assert content['status'] == 'open'
324
-        assert set(content['sub_content_types']) == {'thread', 'page', 'folder', 'file'}  # nopep8
388
+        assert set(content['sub_content_types']) == {'thread', 'html-documents', 'folder', 'file'}  # nopep8
325
         assert content['workspace_id'] == 3
389
         assert content['workspace_id'] == 3
326
 
390
 
327
         content = res[3]
391
         content = res[3]
328
-        assert content['content_type'] == 'page'
392
+        assert content['content_type'] == 'html-documents'
329
         assert content['content_id'] == 17
393
         assert content['content_id'] == 17
330
         assert content['is_archived'] is False
394
         assert content['is_archived'] is False
331
         assert content['is_deleted'] is True
395
         assert content['is_deleted'] is True
334
         assert content['show_in_ui'] is True
398
         assert content['show_in_ui'] is True
335
         assert content['slug'].startswith('bad-fruit-salad')
399
         assert content['slug'].startswith('bad-fruit-salad')
336
         assert content['status'] == 'open'
400
         assert content['status'] == 'open'
337
-        assert set(content['sub_content_types']) == {'thread', 'page', 'folder', 'file'}  # nopep8
401
+        assert set(content['sub_content_types']) == {'thread', 'html-documents', 'folder', 'file'}  # nopep8
338
         assert content['workspace_id'] == 3
402
         assert content['workspace_id'] == 3
339
 
403
 
340
     def test_api__get_workspace_content__ok_200__get_only_active_root_content(self):  # nopep8
404
     def test_api__get_workspace_content__ok_200__get_only_active_root_content(self):  # nopep8
362
         # TODO - G.M - 30-05-2018 - Check this test
426
         # TODO - G.M - 30-05-2018 - Check this test
363
         assert len(res) == 2
427
         assert len(res) == 2
364
         content = res[1]
428
         content = res[1]
365
-        assert content['content_type'] == 'page'
429
+        assert content['content_type'] == 'html-documents'
366
         assert content['content_id'] == 15
430
         assert content['content_id'] == 15
367
         assert content['is_archived'] is False
431
         assert content['is_archived'] is False
368
         assert content['is_deleted'] is False
432
         assert content['is_deleted'] is False
371
         assert content['show_in_ui'] is True
435
         assert content['show_in_ui'] is True
372
         assert content['slug'] == 'new-fruit-salad'
436
         assert content['slug'] == 'new-fruit-salad'
373
         assert content['status'] == 'open'
437
         assert content['status'] == 'open'
374
-        assert set(content['sub_content_types']) == {'thread', 'page', 'folder', 'file'}  # nopep8
438
+        assert set(content['sub_content_types']) == {'thread', 'html-documents', 'folder', 'file'}  # nopep8
375
         assert content['workspace_id'] == 3
439
         assert content['workspace_id'] == 3
376
 
440
 
377
     def test_api__get_workspace_content__ok_200__get_only_archived_root_content(self):  # nopep8
441
     def test_api__get_workspace_content__ok_200__get_only_archived_root_content(self):  # nopep8
398
         ).json_body   # nopep8
462
         ).json_body   # nopep8
399
         assert len(res) == 1
463
         assert len(res) == 1
400
         content = res[0]
464
         content = res[0]
401
-        assert content['content_type'] == 'page'
465
+        assert content['content_type'] == 'html-documents'
402
         assert content['content_id'] == 16
466
         assert content['content_id'] == 16
403
         assert content['is_archived'] is True
467
         assert content['is_archived'] is True
404
         assert content['is_deleted'] is False
468
         assert content['is_deleted'] is False
407
         assert content['show_in_ui'] is True
471
         assert content['show_in_ui'] is True
408
         assert content['slug'].startswith('fruit-salad')
472
         assert content['slug'].startswith('fruit-salad')
409
         assert content['status'] == 'open'
473
         assert content['status'] == 'open'
410
-        assert set(content['sub_content_types']) == {'thread', 'page', 'folder', 'file'}  # nopep8
474
+        assert set(content['sub_content_types']) == {'thread', 'html-documents', 'folder', 'file'}  # nopep8
411
         assert content['workspace_id'] == 3
475
         assert content['workspace_id'] == 3
412
 
476
 
413
     def test_api__get_workspace_content__ok_200__get_only_deleted_root_content(self):  # nopep8
477
     def test_api__get_workspace_content__ok_200__get_only_deleted_root_content(self):  # nopep8
436
 
500
 
437
         assert len(res) == 1
501
         assert len(res) == 1
438
         content = res[0]
502
         content = res[0]
439
-        assert content['content_type'] == 'page'
503
+        assert content['content_type'] == 'html-documents'
440
         assert content['content_id'] == 17
504
         assert content['content_id'] == 17
441
         assert content['is_archived'] is False
505
         assert content['is_archived'] is False
442
         assert content['is_deleted'] is True
506
         assert content['is_deleted'] is True
445
         assert content['show_in_ui'] is True
509
         assert content['show_in_ui'] is True
446
         assert content['slug'].startswith('bad-fruit-salad')
510
         assert content['slug'].startswith('bad-fruit-salad')
447
         assert content['status'] == 'open'
511
         assert content['status'] == 'open'
448
-        assert set(content['sub_content_types']) == {'thread', 'page', 'folder', 'file'}  # nopep8
512
+        assert set(content['sub_content_types']) == {'thread', 'html-documents', 'folder', 'file'}  # nopep8
449
         assert content['workspace_id'] == 3
513
         assert content['workspace_id'] == 3
450
 
514
 
451
     def test_api__get_workspace_content__ok_200__get_nothing_root_content(self):
515
     def test_api__get_workspace_content__ok_200__get_nothing_root_content(self):
500
         ).json_body   # nopep8
564
         ).json_body   # nopep8
501
         assert len(res) == 3
565
         assert len(res) == 3
502
         content = res[0]
566
         content = res[0]
503
-        assert content['content_type'] == 'page'
567
+        assert content['content_type'] == 'html-documents'
504
         assert content['content_id'] == 12
568
         assert content['content_id'] == 12
505
         assert content['is_archived'] is False
569
         assert content['is_archived'] is False
506
         assert content['is_deleted'] is False
570
         assert content['is_deleted'] is False
509
         assert content['show_in_ui'] is True
573
         assert content['show_in_ui'] is True
510
         assert content['slug'] == 'new-fruit-salad'
574
         assert content['slug'] == 'new-fruit-salad'
511
         assert content['status'] == 'open'
575
         assert content['status'] == 'open'
512
-        assert set(content['sub_content_types']) == {'thread', 'page', 'folder', 'file'}  # nopep8
576
+        assert set(content['sub_content_types']) == {'thread', 'html-documents', 'folder', 'file'}  # nopep8
513
         assert content['workspace_id'] == 2
577
         assert content['workspace_id'] == 2
514
 
578
 
515
         content = res[1]
579
         content = res[1]
516
-        assert content['content_type'] == 'page'
580
+        assert content['content_type'] == 'html-documents'
517
         assert content['content_id'] == 13
581
         assert content['content_id'] == 13
518
         assert content['is_archived'] is True
582
         assert content['is_archived'] is True
519
         assert content['is_deleted'] is False
583
         assert content['is_deleted'] is False
522
         assert content['show_in_ui'] is True
586
         assert content['show_in_ui'] is True
523
         assert content['slug'].startswith('fruit-salad')
587
         assert content['slug'].startswith('fruit-salad')
524
         assert content['status'] == 'open'
588
         assert content['status'] == 'open'
525
-        assert set(content['sub_content_types']) == {'thread', 'page', 'folder', 'file'}  # nopep8
589
+        assert set(content['sub_content_types']) == {'thread', 'html-documents', 'folder', 'file'}  # nopep8
526
         assert content['workspace_id'] == 2
590
         assert content['workspace_id'] == 2
527
 
591
 
528
         content = res[2]
592
         content = res[2]
529
-        assert content['content_type'] == 'page'
593
+        assert content['content_type'] == 'html-documents'
530
         assert content['content_id'] == 14
594
         assert content['content_id'] == 14
531
         assert content['is_archived'] is False
595
         assert content['is_archived'] is False
532
         assert content['is_deleted'] is True
596
         assert content['is_deleted'] is True
535
         assert content['show_in_ui'] is True
599
         assert content['show_in_ui'] is True
536
         assert content['slug'].startswith('bad-fruit-salad')
600
         assert content['slug'].startswith('bad-fruit-salad')
537
         assert content['status'] == 'open'
601
         assert content['status'] == 'open'
538
-        assert set(content['sub_content_types']) == {'thread', 'page', 'folder', 'file'}  # nopep8
602
+        assert set(content['sub_content_types']) == {'thread', 'html-documents', 'folder', 'file'}  # nopep8
539
         assert content['workspace_id'] == 2
603
         assert content['workspace_id'] == 2
540
 
604
 
541
     def test_api__get_workspace_content__ok_200__get_only_active_folder_content(self):  # nopep8
605
     def test_api__get_workspace_content__ok_200__get_only_active_folder_content(self):  # nopep8
571
         assert content['show_in_ui'] is True
635
         assert content['show_in_ui'] is True
572
         assert content['slug'] == 'new-fruit-salad'
636
         assert content['slug'] == 'new-fruit-salad'
573
         assert content['status'] == 'open'
637
         assert content['status'] == 'open'
574
-        assert set(content['sub_content_types']) == {'thread', 'page', 'folder', 'file'}  # nopep8
638
+        assert set(content['sub_content_types']) == {'thread', 'html-documents', 'folder', 'file'}  # nopep8
575
         assert content['workspace_id'] == 2
639
         assert content['workspace_id'] == 2
576
 
640
 
577
     def test_api__get_workspace_content__ok_200__get_only_archived_folder_content(self):  # nopep8
641
     def test_api__get_workspace_content__ok_200__get_only_archived_folder_content(self):  # nopep8
598
         ).json_body   # nopep8
662
         ).json_body   # nopep8
599
         assert len(res) == 1
663
         assert len(res) == 1
600
         content = res[0]
664
         content = res[0]
601
-        assert content['content_type'] == 'page'
665
+        assert content['content_type'] == 'html-documents'
602
         assert content['content_id'] == 13
666
         assert content['content_id'] == 13
603
         assert content['is_archived'] is True
667
         assert content['is_archived'] is True
604
         assert content['is_deleted'] is False
668
         assert content['is_deleted'] is False
607
         assert content['show_in_ui'] is True
671
         assert content['show_in_ui'] is True
608
         assert content['slug'].startswith('fruit-salad')
672
         assert content['slug'].startswith('fruit-salad')
609
         assert content['status'] == 'open'
673
         assert content['status'] == 'open'
610
-        assert set(content['sub_content_types']) == {'thread', 'page', 'folder', 'file'}  # nopep8
674
+        assert set(content['sub_content_types']) == {'thread', 'html-documents', 'folder', 'file'}  # nopep8
611
         assert content['workspace_id'] == 2
675
         assert content['workspace_id'] == 2
612
 
676
 
613
     def test_api__get_workspace_content__ok_200__get_only_deleted_folder_content(self):  # nopep8
677
     def test_api__get_workspace_content__ok_200__get_only_deleted_folder_content(self):  # nopep8
635
 
699
 
636
         assert len(res) == 1
700
         assert len(res) == 1
637
         content = res[0]
701
         content = res[0]
638
-        assert content['content_type'] == 'page'
702
+        assert content['content_type'] == 'html-documents'
639
         assert content['content_id'] == 14
703
         assert content['content_id'] == 14
640
         assert content['is_archived'] is False
704
         assert content['is_archived'] is False
641
         assert content['is_deleted'] is True
705
         assert content['is_deleted'] is True
644
         assert content['show_in_ui'] is True
708
         assert content['show_in_ui'] is True
645
         assert content['slug'].startswith('bad-fruit-salad')
709
         assert content['slug'].startswith('bad-fruit-salad')
646
         assert content['status'] == 'open'
710
         assert content['status'] == 'open'
647
-        assert set(content['sub_content_types']) == {'thread', 'page', 'folder', 'file'}  # nopep8
711
+        assert set(content['sub_content_types']) == {'thread', 'html-documents', 'folder', 'file'}  # nopep8
648
         assert content['workspace_id'] == 2
712
         assert content['workspace_id'] == 2
649
 
713
 
650
     def test_api__get_workspace_content__ok_200__get_nothing_folder_content(self):  # nopep8
714
     def test_api__get_workspace_content__ok_200__get_nothing_folder_content(self):  # nopep8

+ 2 - 2
tracim/tests/library/test_content_api.py View File

2083
             tm=transaction.manager,
2083
             tm=transaction.manager,
2084
             content=p2,
2084
             content=p2,
2085
         ):
2085
         ):
2086
-            p2.description = 'What\'s up ?'
2086
+            p2.description = 'What\'s up?'
2087
 
2087
 
2088
         api.save(p1)
2088
         api.save(p1)
2089
         api.save(p2)
2089
         api.save(p2)
2096
         eq_(2, self.session.query(ContentRevisionRO).filter(ContentRevisionRO.label == 'this is dummy label content').count())
2096
         eq_(2, self.session.query(ContentRevisionRO).filter(ContentRevisionRO.label == 'this is dummy label content').count())
2097
         eq_(1, self.session.query(ContentRevisionRO).filter(ContentRevisionRO.description == 'This is some amazing test').count())
2097
         eq_(1, self.session.query(ContentRevisionRO).filter(ContentRevisionRO.description == 'This is some amazing test').count())
2098
         eq_(2, self.session.query(ContentRevisionRO).filter(ContentRevisionRO.label == 'Hey ! Jon !').count())
2098
         eq_(2, self.session.query(ContentRevisionRO).filter(ContentRevisionRO.label == 'Hey ! Jon !').count())
2099
-        eq_(1, self.session.query(ContentRevisionRO).filter(ContentRevisionRO.description == 'What\'s up ?').count())
2099
+        eq_(1, self.session.query(ContentRevisionRO).filter(ContentRevisionRO.description == 'What\'s up?').count())
2100
 
2100
 
2101
         res = api.search(['dummy', 'jon'])
2101
         res = api.search(['dummy', 'jon'])
2102
         eq_(2, len(res.all()))
2102
         eq_(2, len(res.all()))

+ 8 - 8
tracim/tests/library/test_webdav.py View File

253
                 content_names,
253
                 content_names,
254
         )
254
         )
255
 
255
 
256
-        assert 'Best Cakes ʔ.html' in content_names,\
257
-            'Best Cakes ʔ.html should be in names ({0})'.format(
256
+        assert 'Best Cakesʔ.html' in content_names,\
257
+            'Best Cakesʔ.html should be in names ({0})'.format(
258
                 content_names,
258
                 content_names,
259
         )
259
         )
260
         assert 'Apple_Pie.txt' in content_names,\
260
         assert 'Apple_Pie.txt' in content_names,\
313
         eq_(
313
         eq_(
314
             True,
314
             True,
315
             content_pie.is_deleted,
315
             content_pie.is_deleted,
316
-            msg='Content should be deleted !'
316
+            msg='Content should be deleted!'
317
         )
317
         )
318
 
318
 
319
         result = provider.getResourceInst(
319
         result = provider.getResourceInst(
392
         eq_(
392
         eq_(
393
             False,
393
             False,
394
             content_new_file.is_deleted,
394
             content_new_file.is_deleted,
395
-            msg='Content should not be deleted !'
395
+            msg='Content should not be deleted!'
396
         )
396
         )
397
         content_new_file_id = content_new_file.content_id
397
         content_new_file_id = content_new_file.content_id
398
 
398
 
407
         eq_(
407
         eq_(
408
             True,
408
             True,
409
             content_pie.is_deleted,
409
             content_pie.is_deleted,
410
-            msg='Content should be deleted !'
410
+            msg='Content should be deleted!'
411
         )
411
         )
412
 
412
 
413
         result = provider.getResourceInst(
413
         result = provider.getResourceInst(
441
         eq_(
441
         eq_(
442
             True,
442
             True,
443
             content_pie.is_deleted,
443
             content_pie.is_deleted,
444
-            msg='Content should be deleted !'
444
+            msg='Content should be deleted!'
445
         )
445
         )
446
 
446
 
447
         # And an other file exist for this name
447
         # And an other file exist for this name
450
             .order_by(Content.revision_id.desc()) \
450
             .order_by(Content.revision_id.desc()) \
451
             .first()
451
             .first()
452
         assert content_new_new_file.content_id != content_new_file_id,\
452
         assert content_new_new_file.content_id != content_new_file_id,\
453
-            'Contents ids should not be same !'
453
+            'Contents ids should not be same!'
454
 
454
 
455
         eq_(
455
         eq_(
456
             False,
456
             False,
457
             content_new_new_file.is_deleted,
457
             content_new_new_file.is_deleted,
458
-            msg='Content should not be deleted !'
458
+            msg='Content should not be deleted!'
459
         )
459
         )
460
 
460
 
461
     def test_unit__rename_content__ok(self):
461
     def test_unit__rename_content__ok(self):

+ 0 - 0
tracim/views/contents_api/__init__.py View File


+ 166 - 0
tracim/views/contents_api/comment_controller.py View File

1
+# coding=utf-8
2
+import transaction
3
+from pyramid.config import Configurator
4
+
5
+try:  # Python 3.5+
6
+    from http import HTTPStatus
7
+except ImportError:
8
+    from http import client as HTTPStatus
9
+
10
+from tracim import TracimRequest
11
+from tracim.extensions import hapic
12
+from tracim.lib.core.content import ContentApi
13
+from tracim.lib.core.workspace import WorkspaceApi
14
+from tracim.lib.utils.authorization import require_workspace_role
15
+from tracim.lib.utils.authorization import require_comment_ownership_or_role
16
+from tracim.views.controllers import Controller
17
+from tracim.views.core_api.schemas import CommentSchema
18
+from tracim.views.core_api.schemas import CommentsPathSchema
19
+from tracim.views.core_api.schemas import SetCommentSchema
20
+from tracim.views.core_api.schemas import WorkspaceAndContentIdPathSchema
21
+from tracim.views.core_api.schemas import NoContentSchema
22
+from tracim.exceptions import WorkspaceNotFound
23
+from tracim.exceptions import InsufficientUserRoleInWorkspace
24
+from tracim.exceptions import NotAuthenticated
25
+from tracim.exceptions import AuthenticationFailed
26
+from tracim.models.contents import ContentTypeLegacy as ContentType
27
+from tracim.models.revision_protection import new_revision
28
+from tracim.models.data import UserRoleInWorkspace
29
+
30
+COMMENT_ENDPOINTS_TAG = 'Comments'
31
+
32
+
33
+class CommentController(Controller):
34
+
35
+    @hapic.with_api_doc(tags=[COMMENT_ENDPOINTS_TAG])
36
+    @hapic.handle_exception(NotAuthenticated, HTTPStatus.UNAUTHORIZED)
37
+    @hapic.handle_exception(InsufficientUserRoleInWorkspace, HTTPStatus.FORBIDDEN)
38
+    @hapic.handle_exception(WorkspaceNotFound, HTTPStatus.FORBIDDEN)
39
+    @hapic.handle_exception(AuthenticationFailed, HTTPStatus.FORBIDDEN)
40
+    @require_workspace_role(UserRoleInWorkspace.READER)
41
+    @hapic.input_path(WorkspaceAndContentIdPathSchema())
42
+    @hapic.output_body(CommentSchema(many=True),)
43
+    def content_comments(self, context, request: TracimRequest, hapic_data=None):
44
+        """
45
+        Get all comments related to a content in asc order (first is the oldest)
46
+        """
47
+
48
+        # login = hapic_data.body
49
+        app_config = request.registry.settings['CFG']
50
+        api = ContentApi(
51
+            current_user=request.current_user,
52
+            session=request.dbsession,
53
+            config=app_config,
54
+        )
55
+        content = api.get_one(
56
+            hapic_data.path.content_id,
57
+            content_type=ContentType.Any
58
+        )
59
+        comments = content.get_comments()
60
+        comments.sort(key=lambda comment: comment.created)
61
+        return [api.get_content_in_context(comment)
62
+                for comment in comments
63
+        ]
64
+
65
+    @hapic.with_api_doc(tags=[COMMENT_ENDPOINTS_TAG])
66
+    @hapic.handle_exception(NotAuthenticated, HTTPStatus.UNAUTHORIZED)
67
+    @hapic.handle_exception(InsufficientUserRoleInWorkspace, HTTPStatus.FORBIDDEN)
68
+    @hapic.handle_exception(WorkspaceNotFound, HTTPStatus.FORBIDDEN)
69
+    @hapic.handle_exception(AuthenticationFailed, HTTPStatus.FORBIDDEN)
70
+    @require_workspace_role(UserRoleInWorkspace.CONTRIBUTOR)
71
+    @hapic.input_path(WorkspaceAndContentIdPathSchema())
72
+    @hapic.input_body(SetCommentSchema())
73
+    @hapic.output_body(CommentSchema(),)
74
+    def add_comment(self, context, request: TracimRequest, hapic_data=None):
75
+        """
76
+        Add new comment
77
+        """
78
+        # login = hapic_data.body
79
+        app_config = request.registry.settings['CFG']
80
+        api = ContentApi(
81
+            current_user=request.current_user,
82
+            session=request.dbsession,
83
+            config=app_config,
84
+        )
85
+        content = api.get_one(
86
+            hapic_data.path.content_id,
87
+            content_type=ContentType.Any
88
+        )
89
+        comment = api.create_comment(
90
+            content.workspace,
91
+            content,
92
+            hapic_data.body.raw_content,
93
+            do_save=True,
94
+        )
95
+        return api.get_content_in_context(comment)
96
+
97
+    @hapic.with_api_doc(tags=[COMMENT_ENDPOINTS_TAG])
98
+    @hapic.handle_exception(NotAuthenticated, HTTPStatus.UNAUTHORIZED)
99
+    @hapic.handle_exception(InsufficientUserRoleInWorkspace, HTTPStatus.FORBIDDEN)
100
+    @hapic.handle_exception(WorkspaceNotFound, HTTPStatus.FORBIDDEN)
101
+    @hapic.handle_exception(AuthenticationFailed, HTTPStatus.FORBIDDEN)
102
+    @require_comment_ownership_or_role(
103
+        minimal_required_role_for_anyone=UserRoleInWorkspace.WORKSPACE_MANAGER,
104
+        minimal_required_role_for_owner=UserRoleInWorkspace.CONTRIBUTOR,
105
+    )
106
+    @hapic.input_path(CommentsPathSchema())
107
+    @hapic.output_body(NoContentSchema(), default_http_code=HTTPStatus.NO_CONTENT)  # nopep8
108
+    def delete_comment(self, context, request: TracimRequest, hapic_data=None):
109
+        """
110
+        Delete comment
111
+        """
112
+        app_config = request.registry.settings['CFG']
113
+        api = ContentApi(
114
+            current_user=request.current_user,
115
+            session=request.dbsession,
116
+            config=app_config,
117
+        )
118
+        wapi = WorkspaceApi(
119
+            current_user=request.current_user,
120
+            session=request.dbsession,
121
+            config=app_config,
122
+        )
123
+        workspace = wapi.get_one(hapic_data.path.workspace_id)
124
+        parent = api.get_one(
125
+            hapic_data.path.content_id,
126
+            content_type=ContentType.Any,
127
+            workspace=workspace
128
+        )
129
+        comment = api.get_one(
130
+            hapic_data.path.comment_id,
131
+            content_type=ContentType.Comment,
132
+            workspace=workspace,
133
+            parent=parent,
134
+        )
135
+        with new_revision(
136
+                session=request.dbsession,
137
+                tm=transaction.manager,
138
+                content=comment
139
+        ):
140
+            api.delete(comment)
141
+        return
142
+
143
+    def bind(self, configurator: Configurator):
144
+        # Get comments
145
+        configurator.add_route(
146
+            'content_comments',
147
+            '/workspaces/{workspace_id}/contents/{content_id}/comments',
148
+            request_method='GET'
149
+        )
150
+        configurator.add_view(self.content_comments, route_name='content_comments')
151
+
152
+        # Add comments
153
+        configurator.add_route(
154
+            'add_comment',
155
+            '/workspaces/{workspace_id}/contents/{content_id}/comments',
156
+            request_method='POST'
157
+        )  # nopep8
158
+        configurator.add_view(self.add_comment, route_name='add_comment')
159
+
160
+        # delete comments
161
+        configurator.add_route(
162
+            'delete_comment',
163
+            '/workspaces/{workspace_id}/contents/{content_id}/comments/{comment_id}',  # nopep8
164
+            request_method='DELETE'
165
+        )
166
+        configurator.add_view(self.delete_comment, route_name='delete_comment')

+ 212 - 0
tracim/views/contents_api/html_document_controller.py View File

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

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

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

+ 172 - 18
tracim/views/core_api/schemas.py View File

3
 from marshmallow import post_load
3
 from marshmallow import post_load
4
 from marshmallow.validate import OneOf
4
 from marshmallow.validate import OneOf
5
 
5
 
6
+from tracim.lib.utils.utils import DATETIME_FORMAT
6
 from tracim.models.auth import Profile
7
 from tracim.models.auth import Profile
7
 from tracim.models.contents import CONTENT_DEFAULT_TYPE
8
 from tracim.models.contents import CONTENT_DEFAULT_TYPE
8
 from tracim.models.contents import CONTENT_DEFAULT_STATUS
9
 from tracim.models.contents import CONTENT_DEFAULT_STATUS
9
 from tracim.models.contents import GlobalStatus
10
 from tracim.models.contents import GlobalStatus
10
 from tracim.models.contents import open_status
11
 from tracim.models.contents import open_status
11
 from tracim.models.context_models import ContentCreation
12
 from tracim.models.context_models import ContentCreation
13
+from tracim.models.context_models import SetContentStatus
14
+from tracim.models.context_models import CommentCreation
15
+from tracim.models.context_models import CommentPath
12
 from tracim.models.context_models import MoveParams
16
 from tracim.models.context_models import MoveParams
13
 from tracim.models.context_models import WorkspaceAndContentPath
17
 from tracim.models.context_models import WorkspaceAndContentPath
14
 from tracim.models.context_models import ContentFilter
18
 from tracim.models.context_models import ContentFilter
15
 from tracim.models.context_models import LoginCredentials
19
 from tracim.models.context_models import LoginCredentials
20
+from tracim.models.context_models import HTMLDocumentUpdate
21
+from tracim.models.context_models import ThreadUpdate
16
 from tracim.models.data import UserRoleInWorkspace
22
 from tracim.models.data import UserRoleInWorkspace
17
 
23
 
18
 
24
 
19
-class UserSchema(marshmallow.Schema):
20
-
25
+class UserDigestSchema(marshmallow.Schema):
26
+    """
27
+    Simple user schema
28
+    """
21
     user_id = marshmallow.fields.Int(dump_only=True, example=3)
29
     user_id = marshmallow.fields.Int(dump_only=True, example=3)
22
-    email = marshmallow.fields.Email(
23
-        required=True,
24
-        example='suri.cate@algoo.fr'
30
+    avatar_url = marshmallow.fields.Url(
31
+        allow_none=True,
32
+        example="/api/v2/assets/avatars/suri-cate.jpg",
33
+        description="avatar_url is the url to the image file. "
34
+                    "If no avatar, then set it to null "
35
+                    "(and frontend will interpret this with a default avatar)",
25
     )
36
     )
26
     public_name = marshmallow.fields.String(
37
     public_name = marshmallow.fields.String(
27
         example='Suri Cate',
38
         example='Suri Cate',
28
     )
39
     )
40
+
41
+
42
+class UserSchema(UserDigestSchema):
43
+    """
44
+    Complete user schema
45
+    """
46
+    email = marshmallow.fields.Email(
47
+        required=True,
48
+        example='suri.cate@algoo.fr'
49
+    )
29
     created = marshmallow.fields.DateTime(
50
     created = marshmallow.fields.DateTime(
30
-        format='%Y-%m-%dT%H:%M:%SZ',
51
+        format=DATETIME_FORMAT,
31
         description='User account creation date',
52
         description='User account creation date',
32
     )
53
     )
33
     is_active = marshmallow.fields.Bool(
54
     is_active = marshmallow.fields.Bool(
46
         example="/api/v2/calendar/user/3.ics/",
67
         example="/api/v2/calendar/user/3.ics/",
47
         description="The url for calendar CalDAV direct access",
68
         description="The url for calendar CalDAV direct access",
48
     )
69
     )
49
-    avatar_url = marshmallow.fields.Url(
50
-        allow_none=True,
51
-        example="/api/v2/assets/avatars/suri-cate.jpg",
52
-        description="avatar_url is the url to the image file. "
53
-                    "If no avatar, then set it to null "
54
-                    "(and frontend will interpret this with a default avatar)",
55
-    )
56
     profile = marshmallow.fields.String(
70
     profile = marshmallow.fields.String(
57
         attribute='profile',
71
         attribute='profile',
58
         validate=OneOf(Profile._NAME),
72
         validate=OneOf(Profile._NAME),
62
     class Meta:
76
     class Meta:
63
         description = 'User account of Tracim'
77
         description = 'User account of Tracim'
64
 
78
 
65
-
66
 # Path Schemas
79
 # Path Schemas
67
 
80
 
68
 
81
 
78
     content_id = marshmallow.fields.Int(example=6, required=True)
91
     content_id = marshmallow.fields.Int(example=6, required=True)
79
 
92
 
80
 
93
 
81
-class WorkspaceAndContentIdPathSchema(WorkspaceIdPathSchema, ContentIdPathSchema):
94
+class WorkspaceAndContentIdPathSchema(
95
+    WorkspaceIdPathSchema,
96
+    ContentIdPathSchema
97
+):
82
     @post_load
98
     @post_load
83
     def make_path_object(self, data):
99
     def make_path_object(self, data):
84
         return WorkspaceAndContentPath(**data)
100
         return WorkspaceAndContentPath(**data)
85
 
101
 
86
 
102
 
103
+class CommentsPathSchema(WorkspaceAndContentIdPathSchema):
104
+    comment_id = marshmallow.fields.Int(
105
+        example=6,
106
+        description='id of a comment related to content content_id',
107
+        required=True
108
+    )
109
+    @post_load
110
+    def make_path_object(self, data):
111
+        return CommentPath(**data)
112
+
113
+
87
 class FilterContentQuerySchema(marshmallow.Schema):
114
 class FilterContentQuerySchema(marshmallow.Schema):
88
     parent_id = marshmallow.fields.Int(
115
     parent_id = marshmallow.fields.Int(
89
         example=2,
116
         example=2,
116
                     'The reason for this parameter to exist is for example '
143
                     'The reason for this parameter to exist is for example '
117
                     'to allow to show only archived documents'
144
                     'to allow to show only archived documents'
118
     )
145
     )
146
+
119
     @post_load
147
     @post_load
120
     def make_content_filter(self, data):
148
     def make_content_filter(self, data):
121
         return ContentFilter(**data)
149
         return ContentFilter(**data)
309
         description='Title of the content to create'
337
         description='Title of the content to create'
310
     )
338
     )
311
     content_type = marshmallow.fields.String(
339
     content_type = marshmallow.fields.String(
312
-        example='htmlpage',
340
+        example='html-documents',
313
         validate=OneOf([content.slug for content in CONTENT_DEFAULT_TYPE]),
341
         validate=OneOf([content.slug for content in CONTENT_DEFAULT_TYPE]),
314
     )
342
     )
315
 
343
 
331
     )
359
     )
332
     label = marshmallow.fields.Str(example='Intervention Report 12')
360
     label = marshmallow.fields.Str(example='Intervention Report 12')
333
     content_type = marshmallow.fields.Str(
361
     content_type = marshmallow.fields.Str(
334
-        example='htmlpage',
362
+        example='html-documents',
335
         validate=OneOf([content.slug for content in CONTENT_DEFAULT_TYPE]),
363
         validate=OneOf([content.slug for content in CONTENT_DEFAULT_TYPE]),
336
     )
364
     )
337
     sub_content_types = marshmallow.fields.List(
365
     sub_content_types = marshmallow.fields.List(
338
-        marshmallow.fields.Str,
366
+        marshmallow.fields.String(),
339
         description='list of content types allowed as sub contents. '
367
         description='list of content types allowed as sub contents. '
340
                     'This field is required for folder contents, '
368
                     'This field is required for folder contents, '
341
                     'set it to empty list in other cases'
369
                     'set it to empty list in other cases'
355
                     'for sub-contents. Default is True. '
383
                     'for sub-contents. Default is True. '
356
                     'In first version of the API, this field is always True',
384
                     'In first version of the API, this field is always True',
357
     )
385
     )
386
+
387
+
388
+#####
389
+# Content
390
+#####
391
+
392
+class ContentSchema(ContentDigestSchema):
393
+    current_revision_id = marshmallow.fields.Int(example=12)
394
+    created = marshmallow.fields.DateTime(
395
+        format=DATETIME_FORMAT,
396
+        description='Content creation date',
397
+    )
398
+    author = marshmallow.fields.Nested(UserDigestSchema)
399
+    modified = marshmallow.fields.DateTime(
400
+        format=DATETIME_FORMAT,
401
+        description='date of last modification of content',
402
+    )
403
+    last_modifier = marshmallow.fields.Nested(UserDigestSchema)
404
+
405
+
406
+class ThreadContentSchema(ContentSchema):
407
+    content_type = marshmallow.fields.Str(
408
+        example='thread',
409
+        validate=OneOf([content.slug for content in CONTENT_DEFAULT_TYPE]),
410
+    )
411
+    raw_content = marshmallow.fields.String('Description of Thread')
412
+
413
+
414
+class HtmlDocumentContentSchema(ContentSchema):
415
+    content_type = marshmallow.fields.Str(
416
+        example='html-documents',
417
+        validate=OneOf([content.slug for content in CONTENT_DEFAULT_TYPE]),
418
+    )
419
+    raw_content = marshmallow.fields.String('<p>Html page Content!</p>')
420
+
421
+#####
422
+# Revision
423
+#####
424
+
425
+
426
+class RevisionSchema(ContentDigestSchema):
427
+    comment_ids = marshmallow.fields.List(marshmallow.fields.Int(example=4))
428
+    revision_id = marshmallow.fields.Int(example=12)
429
+    created = marshmallow.fields.DateTime(
430
+        format=DATETIME_FORMAT,
431
+        description='Content creation date',
432
+    )
433
+    author = marshmallow.fields.Nested(UserDigestSchema)
434
+
435
+
436
+class ThreadRevisionSchema(RevisionSchema):
437
+    content_type = marshmallow.fields.Str(
438
+        example='thread',
439
+        validate=OneOf([content.slug for content in CONTENT_DEFAULT_TYPE]),
440
+    )
441
+    raw_content = marshmallow.fields.String('Description of Thread')
442
+
443
+
444
+class HtmlDocumentRevisionSchema(RevisionSchema):
445
+    content_type = marshmallow.fields.Str(
446
+        example='html-documents',
447
+        validate=OneOf([content.slug for content in CONTENT_DEFAULT_TYPE]),
448
+    )
449
+    raw_content = marshmallow.fields.String('<p>Html page Content!</p>')
450
+
451
+
452
+####
453
+
454
+class CommentSchema(marshmallow.Schema):
455
+    content_id = marshmallow.fields.Int(example=6)
456
+    parent_id = marshmallow.fields.Int(example=34)
457
+    raw_content = marshmallow.fields.String(
458
+        example='<p>This is just an html comment !</p>'
459
+    )
460
+    author = marshmallow.fields.Nested(UserDigestSchema)
461
+    created = marshmallow.fields.DateTime(
462
+        format=DATETIME_FORMAT,
463
+        description='comment creation date',
464
+    )
465
+
466
+
467
+class ContentModifySchema(marshmallow.Schema):
468
+    label = marshmallow.fields.String(
469
+        example='contract for client XXX',
470
+        description='New title of the content'
471
+    )
472
+
473
+
474
+class HtmlDocumentModifySchema(ContentModifySchema):
475
+    raw_content = marshmallow.fields.String('<p>Html page Content!</p>')
476
+
477
+    @post_load
478
+    def html_document_update(self, data):
479
+        return HTMLDocumentUpdate(**data)
480
+
481
+
482
+class ThreadModifySchema(ContentModifySchema):
483
+    raw_content = marshmallow.fields.String('Description of Thread')
484
+
485
+    @post_load
486
+    def thread_update(self, data):
487
+        return ThreadUpdate(**data)
488
+
489
+
490
+class SetCommentSchema(marshmallow.Schema):
491
+    raw_content = marshmallow.fields.String(
492
+        example='<p>This is just an html comment !</p>'
493
+    )
494
+
495
+    @post_load
496
+    def create_comment(self, data):
497
+        return CommentCreation(**data)
498
+
499
+
500
+class SetContentStatusSchema(marshmallow.Schema):
501
+    status = marshmallow.fields.Str(
502
+        example='closed-deprecated',
503
+        validate=OneOf([status.slug for status in CONTENT_DEFAULT_STATUS]),
504
+        description='this slug is found in content_type available statuses',
505
+        default=open_status,
506
+        required=True,
507
+    )
508
+
509
+    @post_load
510
+    def set_status(self, data):
511
+        return SetContentStatus(**data)

+ 1 - 1
tracim/views/core_api/session_controller.py View File

24
     @hapic.with_api_doc(tags=[SESSION_ENDPOINTS_TAG])
24
     @hapic.with_api_doc(tags=[SESSION_ENDPOINTS_TAG])
25
     @hapic.input_headers(LoginOutputHeaders())
25
     @hapic.input_headers(LoginOutputHeaders())
26
     @hapic.input_body(BasicAuthSchema())
26
     @hapic.input_body(BasicAuthSchema())
27
-    @hapic.handle_exception(AuthenticationFailed, HTTPStatus.BAD_REQUEST)
27
+    @hapic.handle_exception(AuthenticationFailed, HTTPStatus.FORBIDDEN)
28
     # TODO - G.M - 17-04-2018 - fix output header ?
28
     # TODO - G.M - 17-04-2018 - fix output header ?
29
     # @hapic.output_headers()
29
     # @hapic.output_headers()
30
     @hapic.output_body(UserSchema(),)
30
     @hapic.output_body(UserSchema(),)

+ 16 - 18
tracim/views/core_api/workspace_controller.py View File

17
 from tracim.models.data import ActionDescription
17
 from tracim.models.data import ActionDescription
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 NotAuthenticated, InsufficientUserWorkspaceRole
20
+from tracim.exceptions import NotAuthenticated, InsufficientUserRoleInWorkspace
21
 from tracim.exceptions import WorkspaceNotFoundInTracimRequest
21
 from tracim.exceptions import WorkspaceNotFoundInTracimRequest
22
 from tracim.exceptions import WorkspacesDoNotMatch
22
 from tracim.exceptions import WorkspacesDoNotMatch
23
-from tracim.exceptions import InsufficientUserProfile
24
 from tracim.exceptions import WorkspaceNotFound
23
 from tracim.exceptions import WorkspaceNotFound
25
 from tracim.views.controllers import Controller
24
 from tracim.views.controllers import Controller
26
 from tracim.views.core_api.schemas import FilterContentQuerySchema
25
 from tracim.views.core_api.schemas import FilterContentQuerySchema
42
 
41
 
43
     @hapic.with_api_doc(tags=[WORKSPACE_ENDPOINTS_TAG])
42
     @hapic.with_api_doc(tags=[WORKSPACE_ENDPOINTS_TAG])
44
     @hapic.handle_exception(NotAuthenticated, HTTPStatus.UNAUTHORIZED)
43
     @hapic.handle_exception(NotAuthenticated, HTTPStatus.UNAUTHORIZED)
45
-    @hapic.handle_exception(InsufficientUserProfile, HTTPStatus.FORBIDDEN)
44
+    @hapic.handle_exception(InsufficientUserRoleInWorkspace, HTTPStatus.FORBIDDEN)
46
     @hapic.handle_exception(WorkspaceNotFound, HTTPStatus.FORBIDDEN)
45
     @hapic.handle_exception(WorkspaceNotFound, HTTPStatus.FORBIDDEN)
47
     @require_workspace_role(UserRoleInWorkspace.READER)
46
     @require_workspace_role(UserRoleInWorkspace.READER)
48
     @hapic.input_path(WorkspaceIdPathSchema())
47
     @hapic.input_path(WorkspaceIdPathSchema())
62
 
61
 
63
     @hapic.with_api_doc(tags=[WORKSPACE_ENDPOINTS_TAG])
62
     @hapic.with_api_doc(tags=[WORKSPACE_ENDPOINTS_TAG])
64
     @hapic.handle_exception(NotAuthenticated, HTTPStatus.UNAUTHORIZED)
63
     @hapic.handle_exception(NotAuthenticated, HTTPStatus.UNAUTHORIZED)
65
-    @hapic.handle_exception(InsufficientUserProfile, HTTPStatus.FORBIDDEN)
64
+    @hapic.handle_exception(InsufficientUserRoleInWorkspace, HTTPStatus.FORBIDDEN)
66
     @hapic.handle_exception(WorkspaceNotFound, HTTPStatus.FORBIDDEN)
65
     @hapic.handle_exception(WorkspaceNotFound, HTTPStatus.FORBIDDEN)
67
     @require_workspace_role(UserRoleInWorkspace.READER)
66
     @require_workspace_role(UserRoleInWorkspace.READER)
68
     @hapic.input_path(WorkspaceIdPathSchema())
67
     @hapic.input_path(WorkspaceIdPathSchema())
91
 
90
 
92
     @hapic.with_api_doc(tags=[WORKSPACE_ENDPOINTS_TAG])
91
     @hapic.with_api_doc(tags=[WORKSPACE_ENDPOINTS_TAG])
93
     @hapic.handle_exception(NotAuthenticated, HTTPStatus.UNAUTHORIZED)
92
     @hapic.handle_exception(NotAuthenticated, HTTPStatus.UNAUTHORIZED)
94
-    @hapic.handle_exception(InsufficientUserProfile, HTTPStatus.FORBIDDEN)
93
+    @hapic.handle_exception(InsufficientUserRoleInWorkspace, HTTPStatus.FORBIDDEN)
95
     @hapic.handle_exception(WorkspaceNotFound, HTTPStatus.FORBIDDEN)
94
     @hapic.handle_exception(WorkspaceNotFound, HTTPStatus.FORBIDDEN)
96
     @require_workspace_role(UserRoleInWorkspace.READER)
95
     @require_workspace_role(UserRoleInWorkspace.READER)
97
     @hapic.input_path(WorkspaceIdPathSchema())
96
     @hapic.input_path(WorkspaceIdPathSchema())
127
 
126
 
128
     @hapic.with_api_doc(tags=[WORKSPACE_ENDPOINTS_TAG])
127
     @hapic.with_api_doc(tags=[WORKSPACE_ENDPOINTS_TAG])
129
     @hapic.handle_exception(NotAuthenticated, HTTPStatus.UNAUTHORIZED)
128
     @hapic.handle_exception(NotAuthenticated, HTTPStatus.UNAUTHORIZED)
130
-    @hapic.handle_exception(InsufficientUserProfile, HTTPStatus.FORBIDDEN)
129
+    @hapic.handle_exception(InsufficientUserRoleInWorkspace, HTTPStatus.FORBIDDEN)
131
     @hapic.handle_exception(WorkspaceNotFound, HTTPStatus.FORBIDDEN)
130
     @hapic.handle_exception(WorkspaceNotFound, HTTPStatus.FORBIDDEN)
132
     @require_workspace_role(UserRoleInWorkspace.CONTRIBUTOR)
131
     @require_workspace_role(UserRoleInWorkspace.CONTRIBUTOR)
133
     @hapic.input_path(WorkspaceIdPathSchema())
132
     @hapic.input_path(WorkspaceIdPathSchema())
160
 
159
 
161
     @hapic.with_api_doc(tags=[WORKSPACE_ENDPOINTS_TAG])
160
     @hapic.with_api_doc(tags=[WORKSPACE_ENDPOINTS_TAG])
162
     @hapic.handle_exception(NotAuthenticated, HTTPStatus.UNAUTHORIZED)
161
     @hapic.handle_exception(NotAuthenticated, HTTPStatus.UNAUTHORIZED)
163
-    @hapic.handle_exception(InsufficientUserProfile, HTTPStatus.FORBIDDEN)
164
     @hapic.handle_exception(WorkspaceNotFound, HTTPStatus.FORBIDDEN)
162
     @hapic.handle_exception(WorkspaceNotFound, HTTPStatus.FORBIDDEN)
165
-    @hapic.handle_exception(InsufficientUserWorkspaceRole, HTTPStatus.FORBIDDEN)
163
+    @hapic.handle_exception(InsufficientUserRoleInWorkspace, HTTPStatus.FORBIDDEN)
166
     @hapic.handle_exception(WorkspacesDoNotMatch, HTTPStatus.BAD_REQUEST)
164
     @hapic.handle_exception(WorkspacesDoNotMatch, HTTPStatus.BAD_REQUEST)
167
-    @require_workspace_role(UserRoleInWorkspace.CONTRIBUTOR)
168
-    @require_candidate_workspace_role(UserRoleInWorkspace.CONTRIBUTOR)
165
+    @require_workspace_role(UserRoleInWorkspace.CONTENT_MANAGER)
166
+    @require_candidate_workspace_role(UserRoleInWorkspace.CONTENT_MANAGER)
169
     @hapic.input_path(WorkspaceAndContentIdPathSchema())
167
     @hapic.input_path(WorkspaceAndContentIdPathSchema())
170
     @hapic.input_body(ContentMoveSchema())
168
     @hapic.input_body(ContentMoveSchema())
171
     @hapic.output_body(ContentDigestSchema())
169
     @hapic.output_body(ContentDigestSchema())
216
 
214
 
217
     @hapic.with_api_doc(tags=[WORKSPACE_ENDPOINTS_TAG])
215
     @hapic.with_api_doc(tags=[WORKSPACE_ENDPOINTS_TAG])
218
     @hapic.handle_exception(NotAuthenticated, HTTPStatus.UNAUTHORIZED)
216
     @hapic.handle_exception(NotAuthenticated, HTTPStatus.UNAUTHORIZED)
219
-    @hapic.handle_exception(InsufficientUserProfile, HTTPStatus.FORBIDDEN)
217
+    @hapic.handle_exception(InsufficientUserRoleInWorkspace, HTTPStatus.FORBIDDEN)
220
     @hapic.handle_exception(WorkspaceNotFound, HTTPStatus.FORBIDDEN)
218
     @hapic.handle_exception(WorkspaceNotFound, HTTPStatus.FORBIDDEN)
221
-    @require_workspace_role(UserRoleInWorkspace.CONTRIBUTOR)
219
+    @require_workspace_role(UserRoleInWorkspace.CONTENT_MANAGER)
222
     @hapic.input_path(WorkspaceAndContentIdPathSchema())
220
     @hapic.input_path(WorkspaceAndContentIdPathSchema())
223
     @hapic.output_body(NoContentSchema(), default_http_code=HTTPStatus.NO_CONTENT)  # nopep8
221
     @hapic.output_body(NoContentSchema(), default_http_code=HTTPStatus.NO_CONTENT)  # nopep8
224
     def delete_content(
222
     def delete_content(
251
 
249
 
252
     @hapic.with_api_doc(tags=[WORKSPACE_ENDPOINTS_TAG])
250
     @hapic.with_api_doc(tags=[WORKSPACE_ENDPOINTS_TAG])
253
     @hapic.handle_exception(NotAuthenticated, HTTPStatus.UNAUTHORIZED)
251
     @hapic.handle_exception(NotAuthenticated, HTTPStatus.UNAUTHORIZED)
254
-    @hapic.handle_exception(InsufficientUserProfile, HTTPStatus.FORBIDDEN)
252
+    @hapic.handle_exception(InsufficientUserRoleInWorkspace, HTTPStatus.FORBIDDEN)
255
     @hapic.handle_exception(WorkspaceNotFound, HTTPStatus.FORBIDDEN)
253
     @hapic.handle_exception(WorkspaceNotFound, HTTPStatus.FORBIDDEN)
256
-    @require_workspace_role(UserRoleInWorkspace.CONTRIBUTOR)
254
+    @require_workspace_role(UserRoleInWorkspace.CONTENT_MANAGER)
257
     @hapic.input_path(WorkspaceAndContentIdPathSchema())
255
     @hapic.input_path(WorkspaceAndContentIdPathSchema())
258
     @hapic.output_body(NoContentSchema(), default_http_code=HTTPStatus.NO_CONTENT)  # nopep8
256
     @hapic.output_body(NoContentSchema(), default_http_code=HTTPStatus.NO_CONTENT)  # nopep8
259
     def undelete_content(
257
     def undelete_content(
287
 
285
 
288
     @hapic.with_api_doc(tags=[WORKSPACE_ENDPOINTS_TAG])
286
     @hapic.with_api_doc(tags=[WORKSPACE_ENDPOINTS_TAG])
289
     @hapic.handle_exception(NotAuthenticated, HTTPStatus.UNAUTHORIZED)
287
     @hapic.handle_exception(NotAuthenticated, HTTPStatus.UNAUTHORIZED)
290
-    @hapic.handle_exception(InsufficientUserProfile, HTTPStatus.FORBIDDEN)
288
+    @hapic.handle_exception(InsufficientUserRoleInWorkspace, HTTPStatus.FORBIDDEN)
291
     @hapic.handle_exception(WorkspaceNotFound, HTTPStatus.FORBIDDEN)
289
     @hapic.handle_exception(WorkspaceNotFound, HTTPStatus.FORBIDDEN)
292
-    @require_workspace_role(UserRoleInWorkspace.CONTRIBUTOR)
290
+    @require_workspace_role(UserRoleInWorkspace.CONTENT_MANAGER)
293
     @hapic.input_path(WorkspaceAndContentIdPathSchema())
291
     @hapic.input_path(WorkspaceAndContentIdPathSchema())
294
     @hapic.output_body(NoContentSchema(), default_http_code=HTTPStatus.NO_CONTENT)  # nopep8
292
     @hapic.output_body(NoContentSchema(), default_http_code=HTTPStatus.NO_CONTENT)  # nopep8
295
     def archive_content(
293
     def archive_content(
319
 
317
 
320
     @hapic.with_api_doc(tags=[WORKSPACE_ENDPOINTS_TAG])
318
     @hapic.with_api_doc(tags=[WORKSPACE_ENDPOINTS_TAG])
321
     @hapic.handle_exception(NotAuthenticated, HTTPStatus.UNAUTHORIZED)
319
     @hapic.handle_exception(NotAuthenticated, HTTPStatus.UNAUTHORIZED)
322
-    @hapic.handle_exception(InsufficientUserProfile, HTTPStatus.FORBIDDEN)
320
+    @hapic.handle_exception(InsufficientUserRoleInWorkspace, HTTPStatus.FORBIDDEN)
323
     @hapic.handle_exception(WorkspaceNotFound, HTTPStatus.FORBIDDEN)
321
     @hapic.handle_exception(WorkspaceNotFound, HTTPStatus.FORBIDDEN)
324
-    @require_workspace_role(UserRoleInWorkspace.CONTRIBUTOR)
322
+    @require_workspace_role(UserRoleInWorkspace.CONTENT_MANAGER)
325
     @hapic.input_path(WorkspaceAndContentIdPathSchema())
323
     @hapic.input_path(WorkspaceAndContentIdPathSchema())
326
     @hapic.output_body(NoContentSchema(), default_http_code=HTTPStatus.NO_CONTENT)  # nopep8
324
     @hapic.output_body(NoContentSchema(), default_http_code=HTTPStatus.NO_CONTENT)  # nopep8
327
     def unarchive_content(
325
     def unarchive_content(