Ver código fonte

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

Damien Accorsi 6 anos atrás
pai
commit
6056263e1b
Nenhuma conta conectada ao e-mail do autor de commit

+ 7 - 0
README.md Ver arquivo

@@ -152,6 +152,13 @@ For example, with default config:
152 152
     # launch your favorite web-browser
153 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 162
 CI
156 163
 ---
157 164
 

+ 50 - 0
doc/roles.md Ver arquivo

@@ -0,0 +1,50 @@
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 Ver arquivo

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

+ 13 - 1
tracim/exceptions.py Ver arquivo

@@ -69,7 +69,7 @@ class WorkspaceNotFoundInTracimRequest(NotFound):
69 69
     pass
70 70
 
71 71
 
72
-class InsufficientUserWorkspaceRole(TracimException):
72
+class InsufficientUserRoleInWorkspace(TracimException):
73 73
     pass
74 74
 
75 75
 
@@ -117,5 +117,17 @@ class UserNotFoundInTracimRequest(TracimException):
117 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 132
 class WorkspacesDoNotMatch(TracimException):
121 133
     pass

+ 72 - 3
tracim/fixtures/content.py Ver arquivo

@@ -23,6 +23,10 @@ class Content(Fixture):
23 23
         bob = self._session.query(models.User) \
24 24
             .filter(models.User.email == 'bob@fsf.local') \
25 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 30
         admin_workspace_api = WorkspaceApi(
27 31
             current_user=admin,
28 32
             session=self._session,
@@ -38,6 +42,16 @@ class Content(Fixture):
38 42
             session=self._session,
39 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 55
         role_api = RoleApi(
42 56
             current_user=admin,
43 57
             session=self._session,
@@ -68,6 +82,12 @@ class Content(Fixture):
68 82
             role_level=UserRoleInWorkspace.CONTENT_MANAGER,
69 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 91
         # Folders
72 92
 
73 93
         tool_workspace = content_api.create(
@@ -112,19 +132,31 @@ class Content(Fixture):
112 132
             content_type=ContentType.Page,
113 133
             workspace=recipe_workspace,
114 134
             parent=dessert_folder,
115
-            label='Tiramisu Recipe',
135
+            label='Tiramisu Recipes!!!',
116 136
             do_save=True,
117 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 151
         best_cake_thread = content_api.create(
120 152
             content_type=ContentType.Thread,
121 153
             workspace=recipe_workspace,
122 154
             parent=dessert_folder,
123
-            label='Best Cakes ?',
155
+            label='Best Cake',
124 156
             do_save=False,
125 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 160
         self._session.add(best_cake_thread)
129 161
         apple_pie_recipe = content_api.create(
130 162
             content_type=ContentType.File,
@@ -246,5 +278,42 @@ class Content(Fixture):
246 278
             content_api.delete(bad_fruit_salad)
247 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 319
         self._session.flush()

+ 9 - 0
tracim/fixtures/users_and_groups.py Ver arquivo

@@ -61,3 +61,12 @@ class Test(Fixture):
61 61
         bob.password = 'foobarbaz'
62 62
         self._session.add(bob)
63 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 Ver arquivo

@@ -16,6 +16,7 @@ import sqlalchemy
16 16
 from sqlalchemy.orm import aliased
17 17
 from sqlalchemy.orm import joinedload
18 18
 from sqlalchemy.orm.attributes import get_history
19
+from sqlalchemy.orm.exc import NoResultFound
19 20
 from sqlalchemy.orm.session import Session
20 21
 from sqlalchemy import desc
21 22
 from sqlalchemy import distinct
@@ -25,6 +26,7 @@ from sqlalchemy.sql.elements import and_
25 26
 from tracim.lib.utils.utils import cmp_to_key
26 27
 from tracim.lib.core.notifications import NotifierFactory
27 28
 from tracim.exceptions import SameValueError
29
+from tracim.exceptions import ContentNotFound
28 30
 from tracim.exceptions import WorkspacesDoNotMatch
29 31
 from tracim.lib.utils.utils import current_date_for_filename
30 32
 from tracim.models.revision_protection import new_revision
@@ -39,9 +41,9 @@ from tracim.models.data import RevisionReadStatus
39 41
 from tracim.models.data import UserRoleInWorkspace
40 42
 from tracim.models.data import Workspace
41 43
 from tracim.lib.utils.translation import fake_translator as _
44
+from tracim.models.context_models import RevisionInContext
42 45
 from tracim.models.context_models import ContentInContext
43 46
 
44
-
45 47
 __author__ = 'damien'
46 48
 
47 49
 
@@ -102,6 +104,7 @@ class ContentApi(object):
102 104
         ContentType.Comment,
103 105
         ContentType.Thread,
104 106
         ContentType.Page,
107
+        ContentType.PageLegacy,
105 108
         ContentType.MarkdownPage,
106 109
     )
107 110
 
@@ -158,10 +161,14 @@ class ContentApi(object):
158 161
             self._show_deleted = previous_show_deleted
159 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 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 173
         Return the Content/ContentRevision query join condition
167 174
         :return: Content/ContentRevision query join condition
@@ -180,7 +187,7 @@ class ContentApi(object):
180 187
         :return: Content/ContentRevision Query
181 188
         """
182 189
         return self._session.query(Content)\
183
-            .join(ContentRevisionRO, self.get_revision_join())
190
+            .join(ContentRevisionRO, self._get_revision_join())
184 191
 
185 192
     @classmethod
186 193
     def sort_tree_items(
@@ -418,6 +425,8 @@ class ContentApi(object):
418 425
         item = Content()
419 426
         item.owner = self._user
420 427
         item.parent = parent
428
+        if parent and not workspace:
429
+            workspace = item.parent.workspace
421 430
         item.workspace = workspace
422 431
         item.type = ContentType.Comment
423 432
         item.description = content
@@ -449,15 +458,24 @@ class ContentApi(object):
449 458
 
450 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 463
         if not content_id:
455 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 480
     def get_one_revision(self, revision_id: int = None) -> ContentRevisionRO:
463 481
         """

+ 74 - 15
tracim/lib/utils/authorization.py Ver arquivo

@@ -1,14 +1,21 @@
1 1
 # -*- coding: utf-8 -*-
2
+import typing
2 3
 from typing import TYPE_CHECKING
3 4
 import functools
4 5
 from pyramid.interfaces import IAuthorizationPolicy
5 6
 from zope.interface import implementer
7
+
8
+from tracim.models.contents import NewContentType
9
+from tracim.models.context_models import ContentInContext
10
+
6 11
 try:
7 12
     from json.decoder import JSONDecodeError
8 13
 except ImportError:  # python3.4
9 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 19
 from tracim.exceptions import InsufficientUserProfile
13 20
 if TYPE_CHECKING:
14 21
     from tracim import TracimRequest
@@ -43,7 +50,7 @@ class AcceptAllAuthorizationPolicy(object):
43 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 55
     Decorator for view to restrict access of tracim request if candidate user
49 56
     is distinct from authenticated user and not with high enough profile.
@@ -51,9 +58,9 @@ def require_same_user_or_profile(group: int):
51 58
     like Group.TIM_USER or Group.TIM_MANAGER
52 59
     :return:
53 60
     """
54
-    def decorator(func):
61
+    def decorator(func: typing.Callable) -> typing.Callable:
55 62
         @functools.wraps(func)
56
-        def wrapper(self, context, request: 'TracimRequest'):
63
+        def wrapper(self, context, request: 'TracimRequest') -> typing.Callable:
57 64
             auth_user = request.current_user
58 65
             candidate_user = request.candidate_user
59 66
             if auth_user.user_id == candidate_user.user_id or \
@@ -64,7 +71,7 @@ def require_same_user_or_profile(group: int):
64 71
     return decorator
65 72
 
66 73
 
67
-def require_profile(group: int):
74
+def require_profile(group: int) -> typing.Callable:
68 75
     """
69 76
     Decorator for view to restrict access of tracim request if profile is
70 77
     not high enough
@@ -72,9 +79,9 @@ def require_profile(group: int):
72 79
     like Group.TIM_USER or Group.TIM_MANAGER
73 80
     :return:
74 81
     """
75
-    def decorator(func):
82
+    def decorator(func: typing.Callable) -> typing.Callable:
76 83
         @functools.wraps(func)
77
-        def wrapper(self, context, request: 'TracimRequest'):
84
+        def wrapper(self, context, request: 'TracimRequest') -> typing.Callable:
78 85
             user = request.current_user
79 86
             if user.profile.id >= group:
80 87
                 return func(self, context, request)
@@ -83,7 +90,7 @@ def require_profile(group: int):
83 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 95
     Restricts access to endpoint to minimal role or raise an exception.
89 96
     Check role for current_workspace.
@@ -91,20 +98,20 @@ def require_workspace_role(minimal_required_role: int):
91 98
     UserRoleInWorkspace.CONTRIBUTOR or UserRoleInWorkspace.READER
92 99
     :return: decorator
93 100
     """
94
-    def decorator(func):
101
+    def decorator(func: typing.Callable) -> typing.Callable:
95 102
         @functools.wraps(func)
96
-        def wrapper(self, context, request: 'TracimRequest'):
103
+        def wrapper(self, context, request: 'TracimRequest') -> typing.Callable:
97 104
             user = request.current_user
98 105
             workspace = request.current_workspace
99 106
             if workspace.get_user_role(user) >= minimal_required_role:
100 107
                 return func(self, context, request)
101
-            raise InsufficientUserWorkspaceRole()
108
+            raise InsufficientUserRoleInWorkspace()
102 109
 
103 110
         return wrapper
104 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 116
     Restricts access to endpoint to minimal role or raise an exception.
110 117
     Check role for candidate_workspace.
@@ -112,15 +119,67 @@ def require_candidate_workspace_role(minimal_required_role: int):
112 119
     UserRoleInWorkspace.CONTRIBUTOR or UserRoleInWorkspace.READER
113 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 125
             user = request.current_user
119 126
             workspace = request.candidate_workspace
120 127
 
121 128
             if workspace.get_user_role(user) >= minimal_required_role:
122 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 184
         return wrapper
126 185
     return decorator

+ 141 - 14
tracim/lib/utils/request.py Ver arquivo

@@ -2,18 +2,22 @@
2 2
 from pyramid.request import Request
3 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 7
 from tracim.exceptions import WorkspaceNotFoundInTracimRequest
7 8
 from tracim.exceptions import UserNotFoundInTracimRequest
8 9
 from tracim.exceptions import UserDoesNotExist
9 10
 from tracim.exceptions import WorkspaceNotFound
10 11
 from tracim.exceptions import ImmutableAttribute
12
+from tracim.models.contents import ContentTypeLegacy as ContentType
13
+from tracim.lib.core.content import ContentApi
11 14
 from tracim.lib.core.user import UserApi
12 15
 from tracim.lib.core.workspace import WorkspaceApi
13 16
 from tracim.lib.utils.authorization import JSONDecodeError
14 17
 
15 18
 from tracim.models import User
16 19
 from tracim.models.data import Workspace
20
+from tracim.models.data import Content
17 21
 
18 22
 
19 23
 class TracimRequest(Request):
@@ -35,6 +39,12 @@ class TracimRequest(Request):
35 39
             decode_param_names,
36 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 48
         # Current workspace, found in request path
39 49
         self._current_workspace = None  # type: Workspace
40 50
 
@@ -60,7 +70,7 @@ class TracimRequest(Request):
60 70
         :return: Workspace of the request
61 71
         """
62 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 74
         return self._current_workspace
65 75
 
66 76
     @current_workspace.setter
@@ -93,7 +103,50 @@ class TracimRequest(Request):
93 103
             )
94 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 148
     # TODO - G.M - 24-05-2018 - Find a better naming for this ?
149
+
97 150
     @property
98 151
     def candidate_user(self) -> User:
99 152
         """
@@ -132,7 +185,6 @@ class TracimRequest(Request):
132 185
         self._current_workspace = None
133 186
         self.dbsession.close()
134 187
 
135
-
136 188
     @candidate_user.setter
137 189
     def candidate_user(self, user: User) -> None:
138 190
         if self._candidate_user is not None:
@@ -144,6 +196,80 @@ class TracimRequest(Request):
144 196
     ###
145 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 274
     def _get_candidate_user(
149 275
             self,
@@ -156,7 +282,7 @@ class TracimRequest(Request):
156 282
         """
157 283
         app_config = request.registry.settings['CFG']
158 284
         uapi = UserApi(None, session=request.dbsession, config=app_config)
159
-
285
+        login = ''
160 286
         try:
161 287
             login = None
162 288
             if 'user_id' in request.matchdict:
@@ -179,6 +305,7 @@ class TracimRequest(Request):
179 305
         """
180 306
         app_config = request.registry.settings['CFG']
181 307
         uapi = UserApi(None, session=request.dbsession, config=app_config)
308
+        login = ''
182 309
         try:
183 310
             login = request.authenticated_userid
184 311
             if not login:
@@ -204,20 +331,20 @@ class TracimRequest(Request):
204 331
             if 'workspace_id' in request.matchdict:
205 332
                 workspace_id = request.matchdict['workspace_id']
206 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 335
             wapi = WorkspaceApi(
209 336
                 current_user=user,
210 337
                 session=request.dbsession,
211 338
                 config=request.registry.settings['CFG']
212 339
             )
213 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 344
             raise WorkspaceNotFound(
218 345
                 'Workspace {} does not exist '
219 346
                 'or is not visible for this user'.format(workspace_id)
220
-            )
347
+            ) from exc
221 348
         return workspace
222 349
 
223 350
     def _get_candidate_workspace(
@@ -236,18 +363,18 @@ class TracimRequest(Request):
236 363
             if 'new_workspace_id' in request.json_body:
237 364
                 workspace_id = request.json_body['new_workspace_id']
238 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 367
             wapi = WorkspaceApi(
241 368
                 current_user=user,
242 369
                 session=request.dbsession,
243 370
                 config=request.registry.settings['CFG']
244 371
             )
245 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 376
             raise WorkspaceNotFound(
250 377
                 'Workspace {} does not exist '
251 378
                 'or is not visible for this user'.format(workspace_id)
252
-            )
379
+            ) from exc
253 380
         return workspace

+ 1 - 0
tracim/lib/utils/utils.py Ver arquivo

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

+ 6 - 6
tracim/models/applications.py Ver arquivo

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

+ 52 - 20
tracim/models/contents.py Ver arquivo

@@ -2,8 +2,12 @@
2 2
 import typing
3 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,32 +42,32 @@ open_status = NewContentStatus(
38 42
     slug='open',
39 43
     global_status=GlobalStatus.OPEN.value,
40 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 49
 closed_validated_status = NewContentStatus(
46 50
     slug='closed-validated',
47 51
     global_status=GlobalStatus.CLOSED.value,
48 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 57
 closed_unvalidated_status = NewContentStatus(
54 58
     slug='closed-unvalidated',
55 59
     global_status=GlobalStatus.CLOSED.value,
56 60
     label='Cancelled',
57
-    fa_icon='fa-close',
58
-    hexcolor='#000FF',
61
+    fa_icon='close',
62
+    hexcolor='#f63434',
59 63
 )
60 64
 
61 65
 closed_deprecated_status = NewContentStatus(
62 66
     slug='closed-deprecated',
63 67
     global_status=GlobalStatus.CLOSED.value,
64 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,10 +163,10 @@ markdownpluspage_type = NewContentType(
159 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 170
     label='Text Document',
167 171
     creation_label='Write a document',
168 172
     available_statuses=CONTENT_DEFAULT_STATUS,
@@ -182,10 +186,37 @@ CONTENT_DEFAULT_TYPE = [
182 186
     thread_type,
183 187
     file_type,
184 188
     markdownpluspage_type,
185
-    htmlpage_type,
189
+    html_documents_type,
186 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 221
 class ContentTypeLegacy(NewContentType):
191 222
     """
@@ -200,11 +231,14 @@ class ContentTypeLegacy(NewContentType):
200 231
 
201 232
     File = file_type.slug
202 233
     Thread = thread_type.slug
203
-    Page = htmlpage_type.slug
234
+    Page = html_documents_type.slug
235
+    PageLegacy = 'page'
204 236
     MarkdownPage = markdownpluspage_type.slug
205 237
 
206 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 242
             if slug == content_type.slug:
209 243
                 super(ContentTypeLegacy, self).__init__(
210 244
                     slug=content_type.slug,
@@ -223,8 +257,7 @@ class ContentTypeLegacy(NewContentType):
223 257
 
224 258
     @classmethod
225 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 261
         return contents_types
229 262
 
230 263
     @classmethod
@@ -232,7 +265,6 @@ class ContentTypeLegacy(NewContentType):
232 265
         # This method is used for showing only "main"
233 266
         # types in the left-side treeview
234 267
         contents_types = [status.slug for status in CONTENT_DEFAULT_TYPE]
235
-        contents_types.extend([cls.Folder])
236 268
         return contents_types
237 269
 
238 270
     # TODO - G.M - 30-05-2018 - This method don't do anything.

+ 269 - 11
tracim/models/context_models.py Ver arquivo

@@ -8,39 +8,56 @@ from tracim import CFG
8 8
 from tracim.models import User
9 9
 from tracim.models.auth import Profile
10 10
 from tracim.models.data import Content
11
+from tracim.models.data import ContentRevisionRO
11 12
 from tracim.models.data import Workspace, UserRoleInWorkspace
12 13
 from tracim.models.workspace_menu_entries import default_workspace_menu_entry
13 14
 from tracim.models.workspace_menu_entries import WorkspaceMenuEntry
15
+from tracim.models.contents import ContentTypeLegacy as ContentType
14 16
 
15 17
 
16 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 23
         self.new_parent_id = new_parent_id
22 24
         self.new_workspace_id = new_workspace_id
23 25
 
24 26
 
25 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 33
         self.email = email
32 34
         self.password = password
33 35
 
34 36
 
35 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 42
         self.content_id = content_id
41 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 61
 class ContentFilter(object):
45 62
     """
46 63
     Content filter model
@@ -51,7 +68,7 @@ class ContentFilter(object):
51 68
             show_archived: int = 0,
52 69
             show_deleted: int = 0,
53 70
             show_active: int = 1,
54
-    ):
71
+    ) -> None:
55 72
         self.parent_id = parent_id
56 73
         self.show_archived = bool(show_archived)
57 74
         self.show_deleted = bool(show_deleted)
@@ -66,11 +83,59 @@ class ContentCreation(object):
66 83
             self,
67 84
             label: str,
68 85
             content_type: str,
69
-    ):
86
+    ) -> None:
70 87
         self.label = label
71 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 139
 class UserInContext(object):
75 140
     """
76 141
     Interface to get User data and User data related to context.
@@ -306,11 +371,12 @@ class ContentInContext(object):
306 371
 
307 372
     @property
308 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 377
     @property
312 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 381
     @property
316 382
     def status(self) -> str:
@@ -324,9 +390,44 @@ class ContentInContext(object):
324 390
     def is_deleted(self):
325 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 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 431
     def show_in_ui(self):
331 432
         # TODO - G.M - 31-05-2018 - Enable Show_in_ui params
332 433
         # if false, then do not show content in the treeview.
@@ -338,3 +439,160 @@ class ContentInContext(object):
338 439
     @property
339 440
     def slug(self):
340 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 Ver arquivo

@@ -235,17 +235,17 @@ class ActionDescription(object):
235 235
 
236 236
     # TODO - G.M - 10-04-2018 - [Cleanup] Drop this
237 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 251
     # _LABELS = {
@@ -598,6 +598,7 @@ class ContentRevisionRO(DeclarativeBase):
598 598
 
599 599
     revision_id = Column(Integer, primary_key=True)
600 600
     content_id = Column(Integer, ForeignKey('content.id'), nullable=False)
601
+    # TODO - G.M - 2018-06-177 - [author] Owner should be renamed "author"
601 602
     owner_id = Column(Integer, ForeignKey('users.user_id'), nullable=True)
602 603
 
603 604
     label = Column(Unicode(1024), unique=False, nullable=False)
@@ -631,6 +632,7 @@ class ContentRevisionRO(DeclarativeBase):
631 632
     parent = relationship("Content", foreign_keys=[parent_id], back_populates="children_revisions")
632 633
 
633 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 636
     owner = relationship('User', remote_side=[User.user_id])
635 637
 
636 638
     """ List of column copied when make a new revision from another """
@@ -788,7 +790,7 @@ class ContentRevisionRO(DeclarativeBase):
788 790
             file_extension,
789 791
         )
790 792
 
791
-
793
+# TODO - G.M - 2018-06-177 - [author] Owner should be renamed "author"
792 794
 Index('idx__content_revisions__owner_id', ContentRevisionRO.owner_id)
793 795
 Index('idx__content_revisions__parent_id', ContentRevisionRO.parent_id)
794 796
 
@@ -810,9 +812,9 @@ class Content(DeclarativeBase):
810 812
     # QUERY CONTENTS
811 813
 
812 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 818
                   .filter(Content.label == 'foo')
817 819
                   .one()
818 820
 
@@ -867,6 +869,8 @@ class Content(DeclarativeBase):
867 869
     def revision_id(cls) -> InstrumentedAttribute:
868 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 874
     @hybrid_property
871 875
     def owner_id(self) -> int:
872 876
         return self.revision.owner_id
@@ -1113,6 +1117,8 @@ class Content(DeclarativeBase):
1113 1117
     def node(cls) -> InstrumentedAttribute:
1114 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 1122
     @hybrid_property
1117 1123
     def owner(self) -> User:
1118 1124
         return self.revision.owner
@@ -1482,7 +1488,7 @@ class VirtualEvent(object):
1482 1488
             delta_from_datetime = datetime.utcnow()
1483 1489
 
1484 1490
         delta = delta_from_datetime - self.created
1485
-        
1491
+
1486 1492
         if delta.days > 0:
1487 1493
             if delta.days >= 365:
1488 1494
                 aff = '%d year%s ago' % (delta.days/365, 's' if delta.days/365>=2 else '')

+ 2 - 2
tracim/models/workspace_menu_entries.py Ver arquivo

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

+ 29 - 4
tracim/tests/__init__.py Ver arquivo

@@ -8,12 +8,15 @@ from depot.manager import DepotManager
8 8
 from pyramid import testing
9 9
 from sqlalchemy.exc import IntegrityError
10 10
 
11
-from tracim.command.database import InitializeDBCommand
12 11
 from tracim.lib.core.content import ContentApi
13 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 20
 from tracim.models.data import Content
18 21
 from tracim.lib.utils.logger import logger
19 22
 from tracim.fixtures import FixturesLoader
@@ -28,6 +31,28 @@ def eq_(a, b, msg=None):
28 31
     # TODO - G.M - 05-04-2018 - Remove this when all old nose code is removed
29 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 57
 class FunctionalTest(unittest.TestCase):
33 58
 

+ 284 - 0
tracim/tests/functional/test_comments.py Ver arquivo

@@ -0,0 +1,284 @@
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 Ver arquivo

@@ -0,0 +1,546 @@
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 Ver arquivo

@@ -162,8 +162,8 @@ class TestNotificationsSync(MailHogTest):
162 162
         assert headers['From'][0] == '"Bob i. via Tracim" <test_user_from+3@localhost>'  # nopep8
163 163
         assert headers['To'][0] == 'Global manager <admin@admin.admin>'
164 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 169
 class TestNotificationsAsync(MailHogTest):
@@ -263,5 +263,5 @@ class TestNotificationsAsync(MailHogTest):
263 263
         assert headers['From'][0] == '"Bob i. via Tracim" <test_user_from+3@localhost>'  # nopep8
264 264
         assert headers['To'][0] == 'Global manager <admin@admin.admin>'
265 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 Ver arquivo

@@ -59,14 +59,14 @@ class TestLoginEndpoint(FunctionalTest):
59 59
         assert res.json_body['caldav_url'] is None
60 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 63
         params = {
64 64
             'email': 'admin@admin.admin',
65 65
             'password': 'bad_password',
66 66
         }
67 67
         res = self.testapp.post_json(
68 68
             '/api/v2/sessions/login',
69
-            status=400,
69
+            status=403,
70 70
             params=params,
71 71
         )
72 72
         assert isinstance(res.json, dict)
@@ -74,14 +74,14 @@ class TestLoginEndpoint(FunctionalTest):
74 74
         assert 'message' in res.json.keys()
75 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 78
         params = {
79 79
             'email': 'unknown_user@unknown.unknown',
80 80
             'password': 'bad_password',
81 81
         }
82 82
         res = self.testapp.post_json(
83 83
             '/api/v2/sessions/login',
84
-            status=400,
84
+            status=403,
85 85
             params=params,
86 86
         )
87 87
         assert isinstance(res.json, dict)

+ 5 - 5
tracim/tests/functional/test_system.py Ver arquivo

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

+ 6 - 6
tracim/tests/functional/test_user.py Ver arquivo

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

+ 96 - 32
tracim/tests/functional/test_workspaces.py Ver arquivo

@@ -3,6 +3,7 @@
3 3
 Tests for /api/v2/workspaces subpath endpoints.
4 4
 """
5 5
 from tracim.tests import FunctionalTest
6
+from tracim.tests import set_html_document_slug_to_legacy
6 7
 from tracim.fixtures.content import Content as ContentFixtures
7 8
 from tracim.fixtures.users_and_groups import Base as BaseFixture
8 9
 
@@ -38,19 +39,19 @@ class TestWorkspaceEndpoint(FunctionalTest):
38 39
         assert sidebar_entry['label'] == 'Dashboard'
39 40
         assert sidebar_entry['route'] == '/#/workspaces/1/dashboard'  # nopep8
40 41
         assert sidebar_entry['hexcolor'] == "#252525"
41
-        assert sidebar_entry['fa_icon'] == ""
42
+        assert sidebar_entry['fa_icon'] == "signal"
42 43
 
43 44
         sidebar_entry = workspace['sidebar_entries'][1]
44 45
         assert sidebar_entry['slug'] == 'contents/all'
45 46
         assert sidebar_entry['label'] == 'All Contents'
46 47
         assert sidebar_entry['route'] == "/#/workspaces/1/contents"  # nopep8
47 48
         assert sidebar_entry['hexcolor'] == "#fdfdfd"
48
-        assert sidebar_entry['fa_icon'] == ""
49
+        assert sidebar_entry['fa_icon'] == "th"
49 50
 
50 51
         sidebar_entry = workspace['sidebar_entries'][2]
51
-        assert sidebar_entry['slug'] == 'contents/htmlpage'
52
+        assert sidebar_entry['slug'] == 'contents/html-documents'
52 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 55
         assert sidebar_entry['hexcolor'] == "#3f52e3"
55 56
         assert sidebar_entry['fa_icon'] == "file-text-o"
56 57
 
@@ -59,7 +60,7 @@ class TestWorkspaceEndpoint(FunctionalTest):
59 60
         assert sidebar_entry['label'] == 'Markdown Plus Documents'
60 61
         assert sidebar_entry['route'] == "/#/workspaces/1/contents?type=markdownpluspage"    # nopep8
61 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 65
         sidebar_entry = workspace['sidebar_entries'][4]
65 66
         assert sidebar_entry['slug'] == 'contents/files'
@@ -80,7 +81,7 @@ class TestWorkspaceEndpoint(FunctionalTest):
80 81
         assert sidebar_entry['label'] == 'Calendar'
81 82
         assert sidebar_entry['route'] == "/#/workspaces/1/calendar"  # nopep8
82 83
         assert sidebar_entry['hexcolor'] == "#757575"
83
-        assert sidebar_entry['fa_icon'] == "calendar-alt"
84
+        assert sidebar_entry['fa_icon'] == "calendar"
84 85
 
85 86
     def test_api__get_workspace__err_403__unallowed_user(self) -> None:
86 87
         """
@@ -247,7 +248,7 @@ class TestWorkspaceContents(FunctionalTest):
247 248
         assert content['show_in_ui'] is True
248 249
         assert content['slug'] == 'tools'
249 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 252
         assert content['workspace_id'] == 1
252 253
         content = res[1]
253 254
         assert content['content_id'] == 2
@@ -258,7 +259,7 @@ class TestWorkspaceContents(FunctionalTest):
258 259
         assert content['show_in_ui'] is True
259 260
         assert content['slug'] == 'menus'
260 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 263
         assert content['workspace_id'] == 1
263 264
         content = res[2]
264 265
         assert content['content_id'] == 11
@@ -269,10 +270,73 @@ class TestWorkspaceContents(FunctionalTest):
269 270
         assert content['show_in_ui'] is True
270 271
         assert content['slug'] == 'current-menu'
271 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 274
         assert content['workspace_id'] == 1
274 275
 
275 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 341
     def test_api__get_workspace_content__ok_200__get_all_root_content(self):
278 342
         """
@@ -299,7 +363,7 @@ class TestWorkspaceContents(FunctionalTest):
299 363
         # TODO - G.M - 30-05-2018 - Check this test
300 364
         assert len(res) == 4
301 365
         content = res[1]
302
-        assert content['content_type'] == 'page'
366
+        assert content['content_type'] == 'html-documents'
303 367
         assert content['content_id'] == 15
304 368
         assert content['is_archived'] is False
305 369
         assert content['is_deleted'] is False
@@ -308,11 +372,11 @@ class TestWorkspaceContents(FunctionalTest):
308 372
         assert content['show_in_ui'] is True
309 373
         assert content['slug'] == 'new-fruit-salad'
310 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 376
         assert content['workspace_id'] == 3
313 377
 
314 378
         content = res[2]
315
-        assert content['content_type'] == 'page'
379
+        assert content['content_type'] == 'html-documents'
316 380
         assert content['content_id'] == 16
317 381
         assert content['is_archived'] is True
318 382
         assert content['is_deleted'] is False
@@ -321,11 +385,11 @@ class TestWorkspaceContents(FunctionalTest):
321 385
         assert content['show_in_ui'] is True
322 386
         assert content['slug'].startswith('fruit-salad')
323 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 389
         assert content['workspace_id'] == 3
326 390
 
327 391
         content = res[3]
328
-        assert content['content_type'] == 'page'
392
+        assert content['content_type'] == 'html-documents'
329 393
         assert content['content_id'] == 17
330 394
         assert content['is_archived'] is False
331 395
         assert content['is_deleted'] is True
@@ -334,7 +398,7 @@ class TestWorkspaceContents(FunctionalTest):
334 398
         assert content['show_in_ui'] is True
335 399
         assert content['slug'].startswith('bad-fruit-salad')
336 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 402
         assert content['workspace_id'] == 3
339 403
 
340 404
     def test_api__get_workspace_content__ok_200__get_only_active_root_content(self):  # nopep8
@@ -362,7 +426,7 @@ class TestWorkspaceContents(FunctionalTest):
362 426
         # TODO - G.M - 30-05-2018 - Check this test
363 427
         assert len(res) == 2
364 428
         content = res[1]
365
-        assert content['content_type'] == 'page'
429
+        assert content['content_type'] == 'html-documents'
366 430
         assert content['content_id'] == 15
367 431
         assert content['is_archived'] is False
368 432
         assert content['is_deleted'] is False
@@ -371,7 +435,7 @@ class TestWorkspaceContents(FunctionalTest):
371 435
         assert content['show_in_ui'] is True
372 436
         assert content['slug'] == 'new-fruit-salad'
373 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 439
         assert content['workspace_id'] == 3
376 440
 
377 441
     def test_api__get_workspace_content__ok_200__get_only_archived_root_content(self):  # nopep8
@@ -398,7 +462,7 @@ class TestWorkspaceContents(FunctionalTest):
398 462
         ).json_body   # nopep8
399 463
         assert len(res) == 1
400 464
         content = res[0]
401
-        assert content['content_type'] == 'page'
465
+        assert content['content_type'] == 'html-documents'
402 466
         assert content['content_id'] == 16
403 467
         assert content['is_archived'] is True
404 468
         assert content['is_deleted'] is False
@@ -407,7 +471,7 @@ class TestWorkspaceContents(FunctionalTest):
407 471
         assert content['show_in_ui'] is True
408 472
         assert content['slug'].startswith('fruit-salad')
409 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 475
         assert content['workspace_id'] == 3
412 476
 
413 477
     def test_api__get_workspace_content__ok_200__get_only_deleted_root_content(self):  # nopep8
@@ -436,7 +500,7 @@ class TestWorkspaceContents(FunctionalTest):
436 500
 
437 501
         assert len(res) == 1
438 502
         content = res[0]
439
-        assert content['content_type'] == 'page'
503
+        assert content['content_type'] == 'html-documents'
440 504
         assert content['content_id'] == 17
441 505
         assert content['is_archived'] is False
442 506
         assert content['is_deleted'] is True
@@ -445,7 +509,7 @@ class TestWorkspaceContents(FunctionalTest):
445 509
         assert content['show_in_ui'] is True
446 510
         assert content['slug'].startswith('bad-fruit-salad')
447 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 513
         assert content['workspace_id'] == 3
450 514
 
451 515
     def test_api__get_workspace_content__ok_200__get_nothing_root_content(self):
@@ -500,7 +564,7 @@ class TestWorkspaceContents(FunctionalTest):
500 564
         ).json_body   # nopep8
501 565
         assert len(res) == 3
502 566
         content = res[0]
503
-        assert content['content_type'] == 'page'
567
+        assert content['content_type'] == 'html-documents'
504 568
         assert content['content_id'] == 12
505 569
         assert content['is_archived'] is False
506 570
         assert content['is_deleted'] is False
@@ -509,11 +573,11 @@ class TestWorkspaceContents(FunctionalTest):
509 573
         assert content['show_in_ui'] is True
510 574
         assert content['slug'] == 'new-fruit-salad'
511 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 577
         assert content['workspace_id'] == 2
514 578
 
515 579
         content = res[1]
516
-        assert content['content_type'] == 'page'
580
+        assert content['content_type'] == 'html-documents'
517 581
         assert content['content_id'] == 13
518 582
         assert content['is_archived'] is True
519 583
         assert content['is_deleted'] is False
@@ -522,11 +586,11 @@ class TestWorkspaceContents(FunctionalTest):
522 586
         assert content['show_in_ui'] is True
523 587
         assert content['slug'].startswith('fruit-salad')
524 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 590
         assert content['workspace_id'] == 2
527 591
 
528 592
         content = res[2]
529
-        assert content['content_type'] == 'page'
593
+        assert content['content_type'] == 'html-documents'
530 594
         assert content['content_id'] == 14
531 595
         assert content['is_archived'] is False
532 596
         assert content['is_deleted'] is True
@@ -535,7 +599,7 @@ class TestWorkspaceContents(FunctionalTest):
535 599
         assert content['show_in_ui'] is True
536 600
         assert content['slug'].startswith('bad-fruit-salad')
537 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 603
         assert content['workspace_id'] == 2
540 604
 
541 605
     def test_api__get_workspace_content__ok_200__get_only_active_folder_content(self):  # nopep8
@@ -571,7 +635,7 @@ class TestWorkspaceContents(FunctionalTest):
571 635
         assert content['show_in_ui'] is True
572 636
         assert content['slug'] == 'new-fruit-salad'
573 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 639
         assert content['workspace_id'] == 2
576 640
 
577 641
     def test_api__get_workspace_content__ok_200__get_only_archived_folder_content(self):  # nopep8
@@ -598,7 +662,7 @@ class TestWorkspaceContents(FunctionalTest):
598 662
         ).json_body   # nopep8
599 663
         assert len(res) == 1
600 664
         content = res[0]
601
-        assert content['content_type'] == 'page'
665
+        assert content['content_type'] == 'html-documents'
602 666
         assert content['content_id'] == 13
603 667
         assert content['is_archived'] is True
604 668
         assert content['is_deleted'] is False
@@ -607,7 +671,7 @@ class TestWorkspaceContents(FunctionalTest):
607 671
         assert content['show_in_ui'] is True
608 672
         assert content['slug'].startswith('fruit-salad')
609 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 675
         assert content['workspace_id'] == 2
612 676
 
613 677
     def test_api__get_workspace_content__ok_200__get_only_deleted_folder_content(self):  # nopep8
@@ -635,7 +699,7 @@ class TestWorkspaceContents(FunctionalTest):
635 699
 
636 700
         assert len(res) == 1
637 701
         content = res[0]
638
-        assert content['content_type'] == 'page'
702
+        assert content['content_type'] == 'html-documents'
639 703
         assert content['content_id'] == 14
640 704
         assert content['is_archived'] is False
641 705
         assert content['is_deleted'] is True
@@ -644,7 +708,7 @@ class TestWorkspaceContents(FunctionalTest):
644 708
         assert content['show_in_ui'] is True
645 709
         assert content['slug'].startswith('bad-fruit-salad')
646 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 712
         assert content['workspace_id'] == 2
649 713
 
650 714
     def test_api__get_workspace_content__ok_200__get_nothing_folder_content(self):  # nopep8

+ 2 - 2
tracim/tests/library/test_content_api.py Ver arquivo

@@ -2083,7 +2083,7 @@ class TestContentApi(DefaultTest):
2083 2083
             tm=transaction.manager,
2084 2084
             content=p2,
2085 2085
         ):
2086
-            p2.description = 'What\'s up ?'
2086
+            p2.description = 'What\'s up?'
2087 2087
 
2088 2088
         api.save(p1)
2089 2089
         api.save(p2)
@@ -2096,7 +2096,7 @@ class TestContentApi(DefaultTest):
2096 2096
         eq_(2, self.session.query(ContentRevisionRO).filter(ContentRevisionRO.label == 'this is dummy label content').count())
2097 2097
         eq_(1, self.session.query(ContentRevisionRO).filter(ContentRevisionRO.description == 'This is some amazing test').count())
2098 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 2101
         res = api.search(['dummy', 'jon'])
2102 2102
         eq_(2, len(res.all()))

+ 8 - 8
tracim/tests/library/test_webdav.py Ver arquivo

@@ -253,8 +253,8 @@ class TestWebDav(StandardTest):
253 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 258
                 content_names,
259 259
         )
260 260
         assert 'Apple_Pie.txt' in content_names,\
@@ -313,7 +313,7 @@ class TestWebDav(StandardTest):
313 313
         eq_(
314 314
             True,
315 315
             content_pie.is_deleted,
316
-            msg='Content should be deleted !'
316
+            msg='Content should be deleted!'
317 317
         )
318 318
 
319 319
         result = provider.getResourceInst(
@@ -392,7 +392,7 @@ class TestWebDav(StandardTest):
392 392
         eq_(
393 393
             False,
394 394
             content_new_file.is_deleted,
395
-            msg='Content should not be deleted !'
395
+            msg='Content should not be deleted!'
396 396
         )
397 397
         content_new_file_id = content_new_file.content_id
398 398
 
@@ -407,7 +407,7 @@ class TestWebDav(StandardTest):
407 407
         eq_(
408 408
             True,
409 409
             content_pie.is_deleted,
410
-            msg='Content should be deleted !'
410
+            msg='Content should be deleted!'
411 411
         )
412 412
 
413 413
         result = provider.getResourceInst(
@@ -441,7 +441,7 @@ class TestWebDav(StandardTest):
441 441
         eq_(
442 442
             True,
443 443
             content_pie.is_deleted,
444
-            msg='Content should be deleted !'
444
+            msg='Content should be deleted!'
445 445
         )
446 446
 
447 447
         # And an other file exist for this name
@@ -450,12 +450,12 @@ class TestWebDav(StandardTest):
450 450
             .order_by(Content.revision_id.desc()) \
451 451
             .first()
452 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 455
         eq_(
456 456
             False,
457 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 461
     def test_unit__rename_content__ok(self):

+ 0 - 0
tracim/views/contents_api/__init__.py Ver arquivo


+ 166 - 0
tracim/views/contents_api/comment_controller.py Ver arquivo

@@ -0,0 +1,166 @@
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 Ver arquivo

@@ -0,0 +1,212 @@
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 Ver arquivo

@@ -0,0 +1,205 @@
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 Ver arquivo

@@ -3,31 +3,52 @@ import marshmallow
3 3
 from marshmallow import post_load
4 4
 from marshmallow.validate import OneOf
5 5
 
6
+from tracim.lib.utils.utils import DATETIME_FORMAT
6 7
 from tracim.models.auth import Profile
7 8
 from tracim.models.contents import CONTENT_DEFAULT_TYPE
8 9
 from tracim.models.contents import CONTENT_DEFAULT_STATUS
9 10
 from tracim.models.contents import GlobalStatus
10 11
 from tracim.models.contents import open_status
11 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 16
 from tracim.models.context_models import MoveParams
13 17
 from tracim.models.context_models import WorkspaceAndContentPath
14 18
 from tracim.models.context_models import ContentFilter
15 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 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 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 37
     public_name = marshmallow.fields.String(
27 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 50
     created = marshmallow.fields.DateTime(
30
-        format='%Y-%m-%dT%H:%M:%SZ',
51
+        format=DATETIME_FORMAT,
31 52
         description='User account creation date',
32 53
     )
33 54
     is_active = marshmallow.fields.Bool(
@@ -46,13 +67,6 @@ class UserSchema(marshmallow.Schema):
46 67
         example="/api/v2/calendar/user/3.ics/",
47 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 70
     profile = marshmallow.fields.String(
57 71
         attribute='profile',
58 72
         validate=OneOf(Profile._NAME),
@@ -62,7 +76,6 @@ class UserSchema(marshmallow.Schema):
62 76
     class Meta:
63 77
         description = 'User account of Tracim'
64 78
 
65
-
66 79
 # Path Schemas
67 80
 
68 81
 
@@ -78,12 +91,26 @@ class ContentIdPathSchema(marshmallow.Schema):
78 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 98
     @post_load
83 99
     def make_path_object(self, data):
84 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 114
 class FilterContentQuerySchema(marshmallow.Schema):
88 115
     parent_id = marshmallow.fields.Int(
89 116
         example=2,
@@ -116,6 +143,7 @@ class FilterContentQuerySchema(marshmallow.Schema):
116 143
                     'The reason for this parameter to exist is for example '
117 144
                     'to allow to show only archived documents'
118 145
     )
146
+
119 147
     @post_load
120 148
     def make_content_filter(self, data):
121 149
         return ContentFilter(**data)
@@ -309,7 +337,7 @@ class ContentCreationSchema(marshmallow.Schema):
309 337
         description='Title of the content to create'
310 338
     )
311 339
     content_type = marshmallow.fields.String(
312
-        example='htmlpage',
340
+        example='html-documents',
313 341
         validate=OneOf([content.slug for content in CONTENT_DEFAULT_TYPE]),
314 342
     )
315 343
 
@@ -331,11 +359,11 @@ class ContentDigestSchema(marshmallow.Schema):
331 359
     )
332 360
     label = marshmallow.fields.Str(example='Intervention Report 12')
333 361
     content_type = marshmallow.fields.Str(
334
-        example='htmlpage',
362
+        example='html-documents',
335 363
         validate=OneOf([content.slug for content in CONTENT_DEFAULT_TYPE]),
336 364
     )
337 365
     sub_content_types = marshmallow.fields.List(
338
-        marshmallow.fields.Str,
366
+        marshmallow.fields.String(),
339 367
         description='list of content types allowed as sub contents. '
340 368
                     'This field is required for folder contents, '
341 369
                     'set it to empty list in other cases'
@@ -355,3 +383,129 @@ class ContentDigestSchema(marshmallow.Schema):
355 383
                     'for sub-contents. Default is True. '
356 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 Ver arquivo

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

+ 16 - 18
tracim/views/core_api/workspace_controller.py Ver arquivo

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