Browse Source

Merge pull request #80 from tracim/feature/578_add_api_for_workspace/apps

Bastien Sevajol 6 years ago
parent
commit
fa2ead5b8f
No account linked to committer's email

+ 1 - 0
setup.py View File

@@ -34,6 +34,7 @@ requires = [
34 34
     # others
35 35
     'filedepot',
36 36
     'babel',
37
+    'python-slugify',
37 38
     # mail-notifier
38 39
     'mako',
39 40
     'lxml',

+ 11 - 2
tracim/__init__.py View File

@@ -18,6 +18,9 @@ from tracim.lib.utils.authorization import TRACIM_DEFAULT_PERM
18 18
 from tracim.lib.webdav import WebdavAppFactory
19 19
 from tracim.views import BASE_API_V2
20 20
 from tracim.views.core_api.session_controller import SessionController
21
+from tracim.views.core_api.system_controller import SystemController
22
+from tracim.views.core_api.user_controller import UserController
23
+from tracim.views.core_api.workspace_controller import WorkspaceController
21 24
 from tracim.views.errors import ErrorSchema
22 25
 from tracim.lib.utils.cors import add_cors_support
23 26
 
@@ -64,8 +67,14 @@ def web(global_config, **local_settings):
64 67
     context.handle_exception(OperationalError, 500)
65 68
     context.handle_exception(Exception, 500)
66 69
     # Add controllers
67
-    session_api = SessionController()
68
-    configurator.include(session_api.bind, route_prefix=BASE_API_V2)
70
+    session_controller = SessionController()
71
+    system_controller = SystemController()
72
+    user_controller = UserController()
73
+    workspace_controller = WorkspaceController()
74
+    configurator.include(session_controller.bind, route_prefix=BASE_API_V2)
75
+    configurator.include(system_controller.bind, route_prefix=BASE_API_V2)
76
+    configurator.include(user_controller.bind, route_prefix=BASE_API_V2)
77
+    configurator.include(workspace_controller.bind, route_prefix=BASE_API_V2)
69 78
     hapic.add_documentation_view(
70 79
         '/api/v2/doc',
71 80
         'Tracim v2 API',

+ 1 - 0
tracim/command/user.py View File

@@ -165,6 +165,7 @@ class UserCommand(AppContextCommand):
165 165
         self._group_api = GroupApi(
166 166
             current_user=None,
167 167
             session=self._session,
168
+            config=self._app_config,
168 169
         )
169 170
         user = self._proceed_user(parsed_args)
170 171
         self._proceed_groups(user, parsed_args)

+ 6 - 2
tracim/exceptions.py View File

@@ -61,7 +61,7 @@ class SameValueError(ValueError):
61 61
     pass
62 62
 
63 63
 
64
-class NotAuthentificated(TracimException):
64
+class NotAuthenticated(TracimException):
65 65
     pass
66 66
 
67 67
 
@@ -93,7 +93,11 @@ class WrongUserPassword(TracimException):
93 93
     pass
94 94
 
95 95
 
96
-class UserNotExist(TracimException):
96
+class UserDoesNotExist(TracimException):
97
+    pass
98
+
99
+
100
+class UserNotFoundInTracimRequest(TracimException):
97 101
     pass
98 102
 
99 103
 

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

@@ -24,10 +24,12 @@ class Content(Fixture):
24 24
         admin_workspace_api = WorkspaceApi(
25 25
             current_user=admin,
26 26
             session=self._session,
27
+            config=self._config,
27 28
         )
28 29
         bob_workspace_api = WorkspaceApi(
29 30
             current_user=bob,
30 31
             session=self._session,
32
+            config=self._config
31 33
         )
32 34
         content_api = ContentApi(
33 35
             current_user=admin,
@@ -37,12 +39,25 @@ class Content(Fixture):
37 39
         role_api = RoleApi(
38 40
             current_user=admin,
39 41
             session=self._session,
42
+            config=self._config,
40 43
         )
41 44
 
42 45
         # Workspaces
43
-        w1 = admin_workspace_api.create_workspace('w1', save_now=True)
44
-        w2 = bob_workspace_api.create_workspace('w2', save_now=True)
45
-        w3 = admin_workspace_api.create_workspace('w3', save_now=True)
46
+        w1 = admin_workspace_api.create_workspace(
47
+            'w1',
48
+            description='This is a workspace',
49
+            save_now=True
50
+        )
51
+        w2 = bob_workspace_api.create_workspace(
52
+            'w2',
53
+            description='A great workspace',
54
+            save_now=True
55
+        )
56
+        w3 = admin_workspace_api.create_workspace(
57
+            'w3',
58
+            description='Just another workspace',
59
+            save_now=True
60
+        )
46 61
 
47 62
         # Workspaces roles
48 63
         role_api.create_one(

+ 4 - 0
tracim/lib/core/group.py View File

@@ -1,6 +1,8 @@
1 1
 # -*- coding: utf-8 -*-
2 2
 import typing
3 3
 
4
+from tracim import CFG
5
+
4 6
 __author__ = 'damien'
5 7
 
6 8
 from tracim.models.auth import Group, User
@@ -14,9 +16,11 @@ class GroupApi(object):
14 16
             self,
15 17
             session: Session,
16 18
             current_user: typing.Optional[User],
19
+            config: CFG
17 20
     ):
18 21
         self._user = current_user
19 22
         self._session = session
23
+        self._config = config
20 24
 
21 25
     def _base_query(self) -> Query:
22 26
         return self._session.query(Group)

+ 16 - 7
tracim/lib/core/user.py View File

@@ -11,8 +11,9 @@ from sqlalchemy.orm import Session
11 11
 
12 12
 from tracim import CFG
13 13
 from tracim.models.auth import User
14
+from tracim.models.auth import Group
14 15
 from sqlalchemy.orm.exc import NoResultFound
15
-from tracim.exceptions import WrongUserPassword, UserNotExist
16
+from tracim.exceptions import WrongUserPassword, UserDoesNotExist
16 17
 from tracim.exceptions import AuthenticationFailed
17 18
 from tracim.models.context_models import UserInContext
18 19
 
@@ -49,7 +50,11 @@ class UserApi(object):
49 50
         """
50 51
         Get one user by user id
51 52
         """
52
-        return self._base_query().filter(User.user_id == user_id).one()
53
+        try:
54
+            user = self._base_query().filter(User.user_id == user_id).one()
55
+        except NoResultFound as exc:
56
+            raise UserDoesNotExist('User "{}" not found in database'.format(user_id)) from exc  # nopep8
57
+        return user
53 58
 
54 59
     def get_one_by_email(self, email: str) -> User:
55 60
         """
@@ -57,7 +62,11 @@ class UserApi(object):
57 62
         :param email: Email of the user
58 63
         :return: one user
59 64
         """
60
-        return self._base_query().filter(User.email == email).one()
65
+        try:
66
+            user = self._base_query().filter(User.email == email).one()
67
+        except NoResultFound as exc:
68
+            raise UserDoesNotExist('User "{}" not found in database'.format(email)) from exc  # nopep8
69
+        return user
61 70
 
62 71
     # FIXME - G.M - 24-04-2018 - Duplicate method with get_one.
63 72
     def get_one_by_id(self, id: int) -> User:
@@ -68,7 +77,7 @@ class UserApi(object):
68 77
         Get current_user
69 78
         """
70 79
         if not self._user:
71
-            raise UserNotExist()
80
+            raise UserDoesNotExist('There is no current user')
72 81
         return self._user
73 82
 
74 83
     def get_all(self) -> typing.Iterable[User]:
@@ -97,9 +106,9 @@ class UserApi(object):
97 106
             if user.validate_password(password):
98 107
                 return user
99 108
             else:
100
-                raise WrongUserPassword()
101
-        except (WrongUserPassword, NoResultFound):
102
-            raise AuthenticationFailed()
109
+                raise WrongUserPassword('User "{}" password is incorrect'.format(email))  # nopep8
110
+        except (WrongUserPassword, UserDoesNotExist) as exc:
111
+            raise AuthenticationFailed('User "{}" authentication failed'.format(email)) from exc  # nopep8
103 112
 
104 113
     # Actions
105 114
 

+ 50 - 20
tracim/lib/core/userworkspace.py View File

@@ -1,9 +1,13 @@
1 1
 # -*- coding: utf-8 -*-
2 2
 import typing
3 3
 
4
+from tracim import CFG
5
+from tracim.models.context_models import UserRoleWorkspaceInContext
6
+
4 7
 __author__ = 'damien'
5 8
 
6 9
 from sqlalchemy.orm import Session
10
+from sqlalchemy.orm import Query
7 11
 from tracim.models.auth import User
8 12
 from tracim.models.data import Workspace
9 13
 from tracim.models.data import UserRoleInWorkspace
@@ -38,6 +42,21 @@ class RoleApi(object):
38 42
         ],
39 43
     }
40 44
 
45
+    def get_user_role_workspace_with_context(
46
+            self,
47
+            user_role: UserRoleInWorkspace
48
+    ) -> UserRoleWorkspaceInContext:
49
+        """
50
+        Return WorkspaceInContext object from Workspace
51
+        """
52
+        assert self._config
53
+        workspace = UserRoleWorkspaceInContext(
54
+            user_role=user_role,
55
+            dbsession=self._session,
56
+            config=self._config,
57
+        )
58
+        return workspace
59
+
41 60
     @classmethod
42 61
     def role_can_read_member_role(cls, reader_role: int, tested_role: int) \
43 62
             -> bool:
@@ -56,11 +75,17 @@ class RoleApi(object):
56 75
 
57 76
         return role
58 77
 
59
-    def __init__(self, session: Session, current_user: typing.Optional[User]):
78
+    def __init__(
79
+        self,
80
+        session: Session,
81
+        current_user: typing.Optional[User],
82
+        config: CFG,
83
+    )-> None:
60 84
         self._session = session
61 85
         self._user = current_user
86
+        self._config = config
62 87
 
63
-    def _get_one_rsc(self, user_id, workspace_id):
88
+    def _get_one_rsc(self, user_id: int, workspace_id: int) -> Query:
64 89
         """
65 90
         :param user_id:
66 91
         :param workspace_id:
@@ -70,16 +95,16 @@ class RoleApi(object):
70 95
             filter(UserRoleInWorkspace.workspace_id == workspace_id).\
71 96
             filter(UserRoleInWorkspace.user_id == user_id)
72 97
 
73
-    def get_one(self, user_id, workspace_id):
98
+    def get_one(self, user_id: int, workspace_id: int) -> UserRoleInWorkspace:
74 99
         return self._get_one_rsc(user_id, workspace_id).one()
75 100
 
76 101
     def create_one(
77
-            self,
78
-            user: User,
79
-            workspace: Workspace,
80
-            role_level: int,
81
-            with_notif: bool,
82
-            flush: bool=True
102
+        self,
103
+        user: User,
104
+        workspace: Workspace,
105
+        role_level: int,
106
+        with_notif: bool,
107
+        flush: bool=True
83 108
     ) -> UserRoleInWorkspace:
84 109
         role = self.create_role()
85 110
         role.user_id = user.user_id
@@ -90,34 +115,39 @@ class RoleApi(object):
90 115
             self._session.flush()
91 116
         return role
92 117
 
93
-    def delete_one(self, user_id, workspace_id, flush=True):
118
+    def delete_one(self, user_id: int, workspace_id: int, flush=True) -> None:
94 119
         self._get_one_rsc(user_id, workspace_id).delete()
95 120
         if flush:
96 121
             self._session.flush()
97 122
 
98
-    def _get_all_for_user(self, user_id):
123
+    def _get_all_for_user(self, user_id) -> typing.List[UserRoleInWorkspace]:
99 124
         return self._session.query(UserRoleInWorkspace)\
100 125
             .filter(UserRoleInWorkspace.user_id == user_id)
101 126
 
102
-    def get_all_for_user(self, user_id):
103
-        return self._get_all_for_user(user_id).all()
127
+    def get_all_for_user(self, user: User) -> typing.List[UserRoleInWorkspace]:
128
+        return self._get_all_for_user(user.user_id).all()
104 129
 
105 130
     def get_all_for_user_order_by_workspace(
106
-            self,
107
-            user_id: int
108
-    ) -> UserRoleInWorkspace:
131
+        self,
132
+        user_id: int
133
+    ) -> typing.List[UserRoleInWorkspace]:
109 134
         return self._get_all_for_user(user_id)\
110 135
             .join(UserRoleInWorkspace.workspace).order_by(Workspace.label).all()
111 136
 
112
-    def get_all_for_workspace(self, workspace_id):
137
+    def get_all_for_workspace(
138
+        self,
139
+        workspace:Workspace
140
+    ) -> typing.List[UserRoleInWorkspace]:
113 141
         return self._session.query(UserRoleInWorkspace)\
114
-            .filter(UserRoleInWorkspace.workspace_id == workspace_id).all()
142
+            .filter(UserRoleInWorkspace.workspace_id==workspace.workspace_id)\
143
+            .all()
115 144
 
116
-    def save(self, role: UserRoleInWorkspace):
145
+    def save(self, role: UserRoleInWorkspace) -> None:
117 146
         self._session.flush()
118 147
 
148
+    # TODO - G.M - 07-06-2018 - [Cleanup] Check if this method is already needed
119 149
     @classmethod
120
-    def get_roles_for_select_field(cls):
150
+    def get_roles_for_select_field(cls) -> typing.List[RoleType]:
121 151
         """
122 152
 
123 153
         :return: list of DictLikeClass instances representing available Roles

+ 20 - 0
tracim/lib/core/workspace.py View File

@@ -3,11 +3,14 @@ import typing
3 3
 
4 4
 from sqlalchemy.orm import Query
5 5
 from sqlalchemy.orm import Session
6
+
7
+from tracim import CFG
6 8
 from tracim.lib.utils.translation import fake_translator as _
7 9
 
8 10
 from tracim.lib.core.userworkspace import RoleApi
9 11
 from tracim.models.auth import Group
10 12
 from tracim.models.auth import User
13
+from tracim.models.context_models import WorkspaceInContext
11 14
 from tracim.models.data import UserRoleInWorkspace
12 15
 from tracim.models.data import Workspace
13 16
 
@@ -20,6 +23,7 @@ class WorkspaceApi(object):
20 23
             self,
21 24
             session: Session,
22 25
             current_user: User,
26
+            config: CFG,
23 27
             force_role: bool=False
24 28
     ):
25 29
         """
@@ -28,6 +32,7 @@ class WorkspaceApi(object):
28 32
         """
29 33
         self._session = session
30 34
         self._user = current_user
35
+        self._config = config
31 36
         self._force_role = force_role
32 37
 
33 38
     def _base_query_without_roles(self):
@@ -42,6 +47,20 @@ class WorkspaceApi(object):
42 47
             filter(UserRoleInWorkspace.user_id == self._user.user_id).\
43 48
             filter(Workspace.is_deleted == False)
44 49
 
50
+    def get_workspace_with_context(
51
+            self,
52
+            workspace: Workspace
53
+    ) -> WorkspaceInContext:
54
+        """
55
+        Return WorkspaceInContext object from Workspace
56
+        """
57
+        workspace = WorkspaceInContext(
58
+            workspace=workspace,
59
+            dbsession=self._session,
60
+            config=self._config,
61
+        )
62
+        return workspace
63
+
45 64
     def create_workspace(
46 65
             self,
47 66
             label: str='',
@@ -62,6 +81,7 @@ class WorkspaceApi(object):
62 81
         role_api = RoleApi(
63 82
             session=self._session,
64 83
             current_user=self._user,
84
+            config=self._config
65 85
         )
66 86
 
67 87
         role = role_api.create_one(

+ 1 - 0
tracim/lib/mail_notifier/notifier.py View File

@@ -238,6 +238,7 @@ class EmailManager(object):
238 238
         notifiable_roles = WorkspaceApi(
239 239
             current_user=user,
240 240
             session=self.session,
241
+            config=self.config,
241 242
         ).get_notifiable_roles(content.workspace)
242 243
 
243 244
         if len(notifiable_roles) <= 0:

+ 2 - 1
tracim/lib/utils/authentification.py View File

@@ -4,6 +4,7 @@ from pyramid.request import Request
4 4
 from sqlalchemy.orm.exc import NoResultFound
5 5
 
6 6
 from tracim import TracimRequest
7
+from tracim.exceptions import UserDoesNotExist
7 8
 from tracim.lib.core.user import UserApi
8 9
 from tracim.models import User
9 10
 
@@ -50,6 +51,6 @@ def _get_basic_auth_unsafe_user(
50 51
         if not login:
51 52
             return None
52 53
         user = uapi.get_one_by_email(login)
53
-    except NoResultFound:
54
+    except UserDoesNotExist:
54 55
         return None
55 56
     return user

+ 22 - 2
tracim/lib/utils/authorization.py View File

@@ -44,7 +44,27 @@ class AcceptAllAuthorizationPolicy(object):
44 44
 # We prefer to use decorators
45 45
 
46 46
 
47
-def require_profile(group):
47
+def require_same_user_or_profile(group: int):
48
+    """
49
+    Decorator for view to restrict access of tracim request if candidate user
50
+    is distinct from authenticated user and not with high enough profile.
51
+    :param group: value from Group Object
52
+    like Group.TIM_USER or Group.TIM_MANAGER
53
+    :return:
54
+    """
55
+    def decorator(func):
56
+        def wrapper(self, context, request: 'TracimRequest'):
57
+            auth_user = request.current_user
58
+            candidate_user = request.candidate_user
59
+            if auth_user.user_id == candidate_user.user_id or \
60
+                    auth_user.profile.id >= group:
61
+                return func(self, context, request)
62
+            raise InsufficientUserProfile()
63
+        return wrapper
64
+    return decorator
65
+
66
+
67
+def require_profile(group: int):
48 68
     """
49 69
     Decorator for view to restrict access of tracim request if profile is
50 70
     not high enough
@@ -62,7 +82,7 @@ def require_profile(group):
62 82
     return decorator
63 83
 
64 84
 
65
-def require_workspace_role(minimal_required_role):
85
+def require_workspace_role(minimal_required_role: int):
66 86
     """
67 87
     Decorator for view to restrict access of tracim request if role
68 88
     is not high enough

+ 116 - 53
tracim/lib/utils/request.py View File

@@ -1,8 +1,10 @@
1
-import typing
1
+# -*- coding: utf-8 -*-
2 2
 from pyramid.request import Request
3 3
 from sqlalchemy.orm.exc import NoResultFound
4 4
 
5
-from tracim.exceptions import NotAuthentificated
5
+from tracim.exceptions import NotAuthenticated
6
+from tracim.exceptions import UserNotFoundInTracimRequest
7
+from tracim.exceptions import UserDoesNotExist
6 8
 from tracim.exceptions import WorkspaceNotFound
7 9
 from tracim.exceptions import ImmutableAttribute
8 10
 from tracim.lib.core.user import UserApi
@@ -32,8 +34,16 @@ class TracimRequest(Request):
32 34
             decode_param_names,
33 35
             **kw
34 36
         )
37
+        # Current workspace, found by request headers or content
35 38
         self._current_workspace = None  # type: Workspace
39
+
40
+        # Authenticated user
36 41
         self._current_user = None  # type: User
42
+
43
+        # User found from request headers, content, distinct from authenticated
44
+        # user
45
+        self._candidate_user = None  # type: User
46
+
37 47
         # INFO - G.M - 18-05-2018 - Close db at the end of the request
38 48
         self.add_finished_callback(self._cleanup)
39 49
 
@@ -46,7 +56,7 @@ class TracimRequest(Request):
46 56
         :return: Workspace of the request
47 57
         """
48 58
         if self._current_workspace is None:
49
-            self.current_workspace = get_workspace(self.current_user, self)
59
+            self.current_workspace = self._get_workspace(self.current_user, self)
50 60
         return self._current_workspace
51 61
 
52 62
     @current_workspace.setter
@@ -64,8 +74,11 @@ class TracimRequest(Request):
64 74
 
65 75
     @property
66 76
     def current_user(self) -> User:
77
+        """
78
+        Get user from authentication mecanism.
79
+        """
67 80
         if self._current_user is None:
68
-            self.current_user = get_safe_user(self)
81
+            self.current_user = self._get_auth_safe_user(self)
69 82
         return self._current_user
70 83
 
71 84
     @current_user.setter
@@ -76,6 +89,19 @@ class TracimRequest(Request):
76 89
             )
77 90
         self._current_user = user
78 91
 
92
+    # TODO - G.M - 24-05-2018 - Find a better naming for this ?
93
+    @property
94
+    def candidate_user(self) -> User:
95
+        """
96
+        Get user from headers/body request. This user is not
97
+        the one found by authentication mecanism. This user
98
+        can help user to know about who one page is about in
99
+        a similar way as current_workspace.
100
+        """
101
+        if self._candidate_user is None:
102
+            self.candidate_user = self._get_candidate_user(self)
103
+        return self._candidate_user
104
+
79 105
     def _cleanup(self, request: 'TracimRequest') -> None:
80 106
         """
81 107
         Close dbsession at the end of the request in order to avoid exception
@@ -89,53 +115,90 @@ class TracimRequest(Request):
89 115
         self._current_workspace = None
90 116
         self.dbsession.close()
91 117
 
92
-###
93
-# Utils for TracimRequest
94
-###
95 118
 
96
-def get_safe_user(
97
-        request: TracimRequest,
98
-) -> User:
99
-    """
100
-    Get current pyramid authenticated user from request
101
-    :param request: pyramid request
102
-    :return: current authenticated user
103
-    """
104
-    app_config = request.registry.settings['CFG']
105
-    uapi = UserApi(None, session=request.dbsession, config=app_config)
106
-    try:
107
-        login = request.authenticated_userid
108
-        if not login:
109
-            raise NotAuthentificated('not authenticated user_id,'
110
-                                     'Failed Authentification ?')
111
-        user = uapi.get_one_by_email(login)
112
-    except NoResultFound:
113
-        raise NotAuthentificated('User not found')
114
-    return user
115
-
116
-
117
-def get_workspace(
118
-        user: User,
119
-        request: TracimRequest
120
-) -> Workspace:
121
-    """
122
-    Get current workspace from request
123
-    :param user: User who want to check the workspace
124
-    :param request: pyramid request
125
-    :return: current workspace
126
-    """
127
-    workspace_id = ''
128
-    try:
129
-        if 'workspace_id' not in request.json_body:
130
-            raise WorkspaceNotFound('No workspace_id param in json body')
131
-        workspace_id = request.json_body['workspace_id']
132
-        wapi = WorkspaceApi(current_user=user, session=request.dbsession)
133
-        workspace = wapi.get_one(workspace_id)
134
-    except JSONDecodeError:
135
-        raise WorkspaceNotFound('Bad json body')
136
-    except NoResultFound:
137
-        raise WorkspaceNotFound(
138
-            'Workspace {} does not exist '
139
-            'or is not visible for this user'.format(workspace_id)
140
-        )
141
-    return workspace
119
+    @candidate_user.setter
120
+    def candidate_user(self, user: User) -> None:
121
+        if self._candidate_user is not None:
122
+            raise ImmutableAttribute(
123
+                "Can't modify already setted candidate_user"
124
+            )
125
+        self._candidate_user = user
126
+
127
+    ###
128
+    # Utils for TracimRequest
129
+    ###
130
+
131
+    def _get_candidate_user(
132
+            self,
133
+            request: 'TracimRequest',
134
+    ) -> User:
135
+        """
136
+        Get candidate user
137
+        :param request: pyramid request
138
+        :return: user found from header/body
139
+        """
140
+        app_config = request.registry.settings['CFG']
141
+        uapi = UserApi(None, session=request.dbsession, config=app_config)
142
+
143
+        try:
144
+            login = None
145
+            if 'user_id' in request.matchdict:
146
+                login = request.matchdict['user_id']
147
+            if not login:
148
+                raise UserNotFoundInTracimRequest('You request a candidate user but the context not permit to found one')  # nopep8
149
+            user = uapi.get_one(login)
150
+        except UserNotFoundInTracimRequest as exc:
151
+            raise UserDoesNotExist('User {} not found'.format(login)) from exc
152
+        return user
153
+
154
+    def _get_auth_safe_user(
155
+            self,
156
+            request: 'TracimRequest',
157
+    ) -> User:
158
+        """
159
+        Get current pyramid authenticated user from request
160
+        :param request: pyramid request
161
+        :return: current authenticated user
162
+        """
163
+        app_config = request.registry.settings['CFG']
164
+        uapi = UserApi(None, session=request.dbsession, config=app_config)
165
+        try:
166
+            login = request.authenticated_userid
167
+            if not login:
168
+                raise UserNotFoundInTracimRequest('You request a current user but the context not permit to found one')  # nopep8
169
+            user = uapi.get_one_by_email(login)
170
+        except (UserDoesNotExist, UserNotFoundInTracimRequest) as exc:
171
+            raise NotAuthenticated('User {} not found'.format(login)) from exc
172
+        return user
173
+
174
+    def _get_workspace(
175
+            self,
176
+            user: User,
177
+            request: 'TracimRequest'
178
+    ) -> Workspace:
179
+        """
180
+        Get current workspace from request
181
+        :param user: User who want to check the workspace
182
+        :param request: pyramid request
183
+        :return: current workspace
184
+        """
185
+        workspace_id = ''
186
+        try:
187
+            if 'workspace_id' in request.matchdict:
188
+                workspace_id = request.matchdict['workspace_id']
189
+            if not workspace_id:
190
+                raise WorkspaceNotFound('No workspace_id property found in request')
191
+            wapi = WorkspaceApi(
192
+                current_user=user,
193
+                session=request.dbsession,
194
+                config=request.registry.settings['CFG']
195
+            )
196
+            workspace = wapi.get_one(workspace_id)
197
+        except JSONDecodeError:
198
+            raise WorkspaceNotFound('Bad json body')
199
+        except NoResultFound:
200
+            raise WorkspaceNotFound(
201
+                'Workspace {} does not exist '
202
+                'or is not visible for this user'.format(workspace_id)
203
+            )
204
+        return workspace

+ 10 - 2
tracim/lib/webdav/dav_provider.py View File

@@ -72,7 +72,11 @@ class Provider(DAVProvider):
72 72
         if path == root_path:
73 73
             return resources.RootResource(path, environ, user=user, session=session)
74 74
 
75
-        workspace_api = WorkspaceApi(current_user=user, session=session)
75
+        workspace_api = WorkspaceApi(
76
+            current_user=user,
77
+            session=session,
78
+            config=self.app_config,
79
+        )
76 80
         workspace = self.get_workspace_from_path(path, workspace_api)
77 81
 
78 82
         # If the request path is in the form root/name, then we return a WorkspaceResource resource
@@ -194,7 +198,11 @@ class Provider(DAVProvider):
194 198
 
195 199
         workspace = self.get_workspace_from_path(
196 200
             path,
197
-            WorkspaceApi(current_user=user, session=session)
201
+            WorkspaceApi(
202
+                current_user=user,
203
+                session=session,
204
+                config=self.app_config,
205
+            )
198 206
         )
199 207
 
200 208
         if parent_path == root_path or workspace is None:

+ 2 - 2
tracim/lib/webdav/design.py View File

@@ -164,7 +164,7 @@ def designPage(content: data.Content, content_revision: data.ContentRevisionRO)
164 164
                     <td>%s</td>
165 165
                 </tr>
166 166
                 ''' % ('warning' if event.id == content_revision.revision_id else '',
167
-                       event.type.icon,
167
+                       event.type.fa_icon,
168 168
                        label,
169 169
                        date,
170 170
                        event.owner.display_name,
@@ -282,7 +282,7 @@ def designThread(content: data.Content, content_revision: data.ContentRevisionRO
282 282
                         </div>
283 283
                     </div>
284 284
                     ''' % ('warning' if t.id == content_revision.revision_id else '',
285
-                           t.type.icon,
285
+                           t.type.fa_icon,
286 286
                            t.owner.display_name,
287 287
                            t.create_readable_date(),
288 288
                            label,

+ 3 - 1
tracim/lib/webdav/resources.py View File

@@ -90,7 +90,8 @@ class RootResource(DAVCollection):
90 90
         self.workspace_api = WorkspaceApi(
91 91
             current_user=self.user,
92 92
             session=session,
93
-            force_role=True
93
+            force_role=True,
94
+            config=self.provider.app_config
94 95
         )
95 96
 
96 97
     def __repr__(self) -> str:
@@ -1254,6 +1255,7 @@ class FileResource(DAVNonCollection):
1254 1255
             workspace_api = WorkspaceApi(
1255 1256
                 current_user=self.user,
1256 1257
                 session=self.session,
1258
+                config=self.provider.app_config,
1257 1259
                 )
1258 1260
             content_api = ContentApi(
1259 1261
                 current_user=self.user,

+ 99 - 0
tracim/models/applications.py View File

@@ -0,0 +1,99 @@
1
+# coding=utf-8
2
+import typing
3
+
4
+
5
+class Application(object):
6
+    """
7
+    Application class with data needed for frontend
8
+    """
9
+    def __init__(
10
+            self,
11
+            label: str,
12
+            slug: str,
13
+            fa_icon: str,
14
+            hexcolor: str,
15
+            is_active: bool,
16
+            config: typing.Dict[str, str],
17
+            main_route: str,
18
+    ) -> None:
19
+        """
20
+        @param label: public label of application
21
+        @param slug: identifier of application
22
+        @param icon: font awesome icon class
23
+        @param hexcolor: hexa color of application main color
24
+        @param is_active: True if application enable, False if inactive
25
+        @param config: a dict with eventual application config
26
+        @param main_route: the route of the frontend "home" screen of
27
+        the application. For exemple, if you have an application
28
+        called "calendar", the main route will be something
29
+        like /#/workspace/{wid}/calendar.
30
+        """
31
+        self.label = label
32
+        self.slug = slug
33
+        self.fa_icon = fa_icon
34
+        self.hexcolor = hexcolor
35
+        self.is_active = is_active
36
+        self.config = config
37
+        self.main_route = main_route
38
+
39
+
40
+# default apps
41
+calendar = Application(
42
+    label='Calendar',
43
+    slug='calendar',
44
+    fa_icon='calendar-alt',
45
+    hexcolor='#757575',
46
+    is_active=True,
47
+    config={},
48
+    main_route='/#/workspaces/{workspace_id}/calendar',
49
+)
50
+
51
+thread = Application(
52
+    label='Threads',
53
+    slug='contents/threads',
54
+    fa_icon='comments-o',
55
+    hexcolor='#ad4cf9',
56
+    is_active=True,
57
+    config={},
58
+    main_route='/#/workspaces/{workspace_id}/contents?type=thread',
59
+
60
+)
61
+
62
+_file = Application(
63
+    label='Files',
64
+    slug='contents/files',
65
+    fa_icon='paperclip',
66
+    hexcolor='#FF9900',
67
+    is_active=True,
68
+    config={},
69
+    main_route='/#/workspaces/{workspace_id}/contents?type=file',
70
+)
71
+
72
+markdownpluspage = Application(
73
+    label='Markdown Plus Documents',  # TODO - G.M - 24-05-2018 - Check label
74
+    slug='contents/markdownpluspage',
75
+    fa_icon='file-code',
76
+    hexcolor='#f12d2d',
77
+    is_active=True,
78
+    config={},
79
+    main_route='/#/workspaces/{workspace_id}/contents?type=markdownpluspage',
80
+)
81
+
82
+htmlpage = Application(
83
+    label='Text Documents',  # TODO - G.M - 24-05-2018 - Check label
84
+    slug='contents/htmlpage',
85
+    fa_icon='file-text-o',
86
+    hexcolor='#3f52e3',
87
+    is_active=True,
88
+    config={},
89
+    main_route='/#/workspaces/{workspace_id}/contents?type=htmlpage',
90
+)
91
+# TODO - G.M - 08-06-2018 - This is hardcoded lists of app, make this dynamic.
92
+# List of applications
93
+applications = [
94
+    htmlpage,
95
+    markdownpluspage,
96
+    _file,
97
+    thread,
98
+    calendar,
99
+]

+ 13 - 4
tracim/models/auth.py View File

@@ -91,10 +91,19 @@ class Group(DeclarativeBase):
91 91
 class Profile(object):
92 92
     """This model is the "max" group associated to a given user."""
93 93
 
94
-    _NAME = [Group.TIM_NOBODY_GROUPNAME,
95
-             Group.TIM_USER_GROUPNAME,
96
-             Group.TIM_MANAGER_GROUPNAME,
97
-             Group.TIM_ADMIN_GROUPNAME]
94
+    _NAME = [
95
+        Group.TIM_NOBODY_GROUPNAME,
96
+        Group.TIM_USER_GROUPNAME,
97
+        Group.TIM_MANAGER_GROUPNAME,
98
+        Group.TIM_ADMIN_GROUPNAME,
99
+    ]
100
+
101
+    _IDS = [
102
+        Group.TIM_NOBODY,
103
+        Group.TIM_USER,
104
+        Group.TIM_MANAGER,
105
+        Group.TIM_ADMIN,
106
+    ]
98 107
 
99 108
     # TODO - G.M - 18-04-2018 [Cleanup] Drop this
100 109
     # _LABEL = [l_('Nobody'),

+ 137 - 0
tracim/models/context_models.py View File

@@ -2,10 +2,14 @@
2 2
 import typing
3 3
 from datetime import datetime
4 4
 
5
+from slugify import slugify
5 6
 from sqlalchemy.orm import Session
6 7
 from tracim import CFG
7 8
 from tracim.models import User
8 9
 from tracim.models.auth import Profile
10
+from tracim.models.data import Workspace, UserRoleInWorkspace
11
+from tracim.models.workspace_menu_entries import default_workspace_menu_entry
12
+from tracim.models.workspace_menu_entries import WorkspaceMenuEntry
9 13
 
10 14
 
11 15
 class LoginCredentials(object):
@@ -74,3 +78,136 @@ class UserInContext(object):
74 78
     def avatar_url(self) -> typing.Optional[str]:
75 79
         # TODO - G-M - 20-04-2018 - [Avatar] Add user avatar feature
76 80
         return None
81
+
82
+
83
+class WorkspaceInContext(object):
84
+    """
85
+    Interface to get Workspace data and Workspace data related to context.
86
+    """
87
+
88
+    def __init__(self, workspace: Workspace, dbsession: Session, config: CFG):
89
+        self.workspace = workspace
90
+        self.dbsession = dbsession
91
+        self.config = config
92
+
93
+    @property
94
+    def workspace_id(self) -> int:
95
+        """
96
+        numeric id of the workspace.
97
+        """
98
+        return self.workspace.workspace_id
99
+
100
+    @property
101
+    def id(self) -> int:
102
+        """
103
+        alias of workspace_id
104
+        """
105
+        return self.workspace_id
106
+
107
+    @property
108
+    def label(self) -> str:
109
+        """
110
+        get workspace label
111
+        """
112
+        return self.workspace.label
113
+
114
+    @property
115
+    def description(self) -> str:
116
+        """
117
+        get workspace description
118
+        """
119
+        return self.workspace.description
120
+
121
+    @property
122
+    def slug(self) -> str:
123
+        """
124
+        get workspace slug
125
+        """
126
+        return slugify(self.workspace.label)
127
+
128
+    @property
129
+    def sidebar_entries(self) -> typing.List[WorkspaceMenuEntry]:
130
+        """
131
+        get sidebar entries, those depends on activated apps.
132
+        """
133
+        # TODO - G.M - 22-05-2018 - Rework on this in
134
+        # order to not use hardcoded list
135
+        # list should be able to change (depending on activated/disabled
136
+        # apps)
137
+        return default_workspace_menu_entry(self.workspace)
138
+
139
+
140
+class UserRoleWorkspaceInContext(object):
141
+    """
142
+    Interface to get UserRoleInWorkspace data and related content
143
+
144
+    """
145
+    def __init__(
146
+            self,
147
+            user_role: UserRoleInWorkspace,
148
+            dbsession: Session,
149
+            config: CFG,
150
+    )-> None:
151
+        self.user_role = user_role
152
+        self.dbsession = dbsession
153
+        self.config = config
154
+
155
+    @property
156
+    def user_id(self) -> int:
157
+        """
158
+        User who has the role has this id
159
+        :return: user id as integer
160
+        """
161
+        return self.user_role.user_id
162
+
163
+    @property
164
+    def workspace_id(self) -> int:
165
+        """
166
+        This role apply only on the workspace with this workspace_id
167
+        :return: workspace id as integer
168
+        """
169
+        return self.user_role.workspace_id
170
+
171
+    # TODO - G.M - 23-05-2018 - Check the API spec for this this !
172
+
173
+    @property
174
+    def role_id(self) -> int:
175
+        """
176
+        role as int id, each value refer to a different role.
177
+        """
178
+        return self.user_role.role
179
+
180
+    @property
181
+    def role_slug(self) -> str:
182
+        """
183
+        simple name of the role of the user.
184
+        can be anything from UserRoleInWorkspace SLUG, like
185
+        'not_applicable', 'reader',
186
+        'contributor', 'content_manager', 'workspace_manager'
187
+        :return: user workspace role as slug.
188
+        """
189
+        return UserRoleInWorkspace.SLUG[self.user_role.role]
190
+
191
+    @property
192
+    def user(self) -> UserInContext:
193
+        """
194
+        User who has this role, with context data
195
+        :return: UserInContext object
196
+        """
197
+        return UserInContext(
198
+            self.user_role.user,
199
+            self.dbsession,
200
+            self.config
201
+        )
202
+
203
+    @property
204
+    def workspace(self) -> WorkspaceInContext:
205
+        """
206
+        Workspace related to this role, with his context data
207
+        :return: WorkspaceInContext object
208
+        """
209
+        return WorkspaceInContext(
210
+            self.user_role.workspace,
211
+            self.dbsession,
212
+            self.config
213
+        )

+ 33 - 10
tracim/models/data.py View File

@@ -130,13 +130,21 @@ class UserRoleInWorkspace(DeclarativeBase):
130 130
     CONTENT_MANAGER = 4
131 131
     WORKSPACE_MANAGER = 8
132 132
 
133
-    # TODO - G.M - 10-04-2018 - [Cleanup] Drop this
133
+    SLUG = {
134
+        NOT_APPLICABLE: 'not-applicable',
135
+        READER: 'reader',
136
+        CONTRIBUTOR: 'contributor',
137
+        CONTENT_MANAGER: 'content-manager',
138
+        WORKSPACE_MANAGER: 'workspace-manager',
139
+    }
140
+
134 141
     LABEL = dict()
135 142
     LABEL[0] = l_('N/A')
136 143
     LABEL[1] = l_('Reader')
137 144
     LABEL[2] = l_('Contributor')
138 145
     LABEL[4] = l_('Content Manager')
139 146
     LABEL[8] = l_('Workspace Manager')
147
+    # TODO - G.M - 10-04-2018 - [Cleanup] Drop this
140 148
     #
141 149
     # STYLE = dict()
142 150
     # STYLE[0] = ''
@@ -154,7 +162,7 @@ class UserRoleInWorkspace(DeclarativeBase):
154 162
     #
155 163
     #
156 164
     # @property
157
-    # def icon(self):
165
+    # def fa_icon(self):
158 166
     #     return UserRoleInWorkspace.ICON[self.role]
159 167
     #
160 168
     # @property
@@ -166,7 +174,10 @@ class UserRoleInWorkspace(DeclarativeBase):
166 174
         return UserRoleInWorkspace.LABEL[self.role]
167 175
 
168 176
     @classmethod
169
-    def get_all_role_values(self):
177
+    def get_all_role_values(cls) -> typing.List[int]:
178
+        """
179
+        Return all valid role value
180
+        """
170 181
         return [
171 182
             UserRoleInWorkspace.READER,
172 183
             UserRoleInWorkspace.CONTRIBUTOR,
@@ -174,11 +185,22 @@ class UserRoleInWorkspace(DeclarativeBase):
174 185
             UserRoleInWorkspace.WORKSPACE_MANAGER
175 186
         ]
176 187
 
188
+    @classmethod
189
+    def get_all_role_slug(cls) -> typing.List[str]:
190
+        """
191
+        Return all valid role slug
192
+        """
193
+        # INFO - G.M - 25-05-2018 - Be carefull, as long as this method
194
+        # and get_all_role_values are both used for API, this method should
195
+        # return item in the same order as get_all_role_values
196
+        return [cls.SLUG[value] for value in cls.get_all_role_values()]
197
+
198
+
177 199
 class RoleType(object):
178 200
     def __init__(self, role_id):
179 201
         self.role_type_id = role_id
180 202
         # TODO - G.M - 10-04-2018 - [Cleanup] Drop this
181
-        # self.icon = UserRoleInWorkspace.ICON[role_id]
203
+        # self.fa_icon = UserRoleInWorkspace.ICON[role_id]
182 204
         # self.role_label = UserRoleInWorkspace.LABEL[role_id]
183 205
         # self.css_style = UserRoleInWorkspace.STYLE[role_id]
184 206
 
@@ -243,11 +265,12 @@ class ActionDescription(object):
243 265
     def __init__(self, id):
244 266
         assert id in ActionDescription.allowed_values()
245 267
         self.id = id
246
-        # FIXME - G.M - 17-04-2018 - Label and icon needed for webdav
268
+        # FIXME - G.M - 17-04-2018 - Label and fa_icon needed for webdav
247 269
         #  design template,
248 270
         # find a way to not rely on this.
249 271
         self.label = self.id
250
-        self.icon = ActionDescription._ICONS[id]
272
+        self.fa_icon = ActionDescription._ICONS[id]
273
+        #self.icon = self.fa_icon
251 274
         # TODO - G.M - 10-04-2018 - [Cleanup] Drop this
252 275
         # self.label = ActionDescription._LABELS[id]
253 276
 
@@ -321,7 +344,7 @@ class ContentStatus(object):
321 344
         self.id = id
322 345
         self.label = self.id
323 346
         # TODO - G.M - 10-04-2018 - [Cleanup] Drop this
324
-        # self.icon = ContentStatus._ICONS[id]
347
+        # self.fa_icon = ContentStatus._ICONS[id]
325 348
         # self.css = ContentStatus._CSS[id]
326 349
         #
327 350
         # if type==ContentType.Thread:
@@ -498,7 +521,7 @@ class ContentType(object):
498 521
     def __init__(self, type):
499 522
         self.id = type
500 523
         # TODO - G.M - 10-04-2018 - [Cleanup] Drop this
501
-        # self.icon = ContentType._CSS_ICONS[type]
524
+        # self.fa_icon = ContentType._CSS_ICONS[type]
502 525
         # self.color = ContentType._CSS_COLORS[type]  # deprecated
503 526
         # self.css = ContentType._CSS_COLORS[type]
504 527
         # self.label = ContentType._LABEL[type]
@@ -508,7 +531,7 @@ class ContentType(object):
508 531
         return dict(id=self.type,
509 532
                     type=self.type,
510 533
                     # TODO - G.M - 10-04-2018 - [Cleanup] Drop this
511
-                    # icon=self.icon,
534
+                    # fa_icon=self.fa_icon,
512 535
                     # color=self.color,
513 536
                     # label=self.label,
514 537
                     priority=self.priority)
@@ -1435,7 +1458,7 @@ class VirtualEvent(object):
1435 1458
         assert hasattr(type, 'id')
1436 1459
         # TODO - G.M - 10-04-2018 - [Cleanup] Drop this
1437 1460
         # assert hasattr(type, 'css')
1438
-        # assert hasattr(type, 'icon')
1461
+        # assert hasattr(type, 'fa_icon')
1439 1462
         # assert hasattr(type, 'label')
1440 1463
 
1441 1464
     def created_as_delta(self, delta_from_datetime:datetime=None):

+ 71 - 0
tracim/models/workspace_menu_entries.py View File

@@ -0,0 +1,71 @@
1
+# coding=utf-8
2
+import typing
3
+from copy import copy
4
+
5
+from tracim.models.applications import applications
6
+from tracim.models.data import Workspace
7
+
8
+
9
+class WorkspaceMenuEntry(object):
10
+    """
11
+    Application class with data needed for frontend
12
+    """
13
+    def __init__(
14
+            self,
15
+            label: str,
16
+            slug: str,
17
+            fa_icon: str,
18
+            hexcolor: str,
19
+            route: str,
20
+    ) -> None:
21
+        self.slug = slug
22
+        self.label = label
23
+        self.route = route
24
+        self.hexcolor = hexcolor
25
+        self.fa_icon = fa_icon
26
+
27
+dashboard_menu_entry = WorkspaceMenuEntry(
28
+  slug='dashboard',
29
+  label='Dashboard',
30
+  route='/#/workspaces/{workspace_id}/dashboard',
31
+  hexcolor='#252525',
32
+  fa_icon="",
33
+)
34
+all_content_menu_entry = WorkspaceMenuEntry(
35
+  slug="contents/all",
36
+  label="All Contents",
37
+  route="/#/workspaces/{workspace_id}/contents",
38
+  hexcolor="#fdfdfd",
39
+  fa_icon="",
40
+)
41
+
42
+# TODO - G.M - 08-06-2018 - This is hardcoded default menu entry,
43
+#  of app, make this dynamic (and loaded from application system)
44
+def default_workspace_menu_entry(
45
+    workspace: Workspace,
46
+)-> typing.List[WorkspaceMenuEntry]:
47
+    """
48
+    Get default menu entry for a workspace
49
+    """
50
+    menu_entries = [
51
+        copy(dashboard_menu_entry),
52
+        copy(all_content_menu_entry),
53
+    ]
54
+    for app in applications:
55
+        if app.main_route:
56
+            new_entry = WorkspaceMenuEntry(
57
+                slug=app.slug,
58
+                label=app.label,
59
+                hexcolor=app.hexcolor,
60
+                fa_icon=app.fa_icon,
61
+                route=app.main_route
62
+            )
63
+            menu_entries.append(new_entry)
64
+
65
+    for entry in menu_entries:
66
+        entry.route = entry.route.replace(
67
+            '{workspace_id}',
68
+            str(workspace.workspace_id)
69
+        )
70
+
71
+    return menu_entries

+ 1 - 0
tracim/tests/__init__.py View File

@@ -162,6 +162,7 @@ class DefaultTest(StandardTest):
162 162
         WorkspaceApi(
163 163
             current_user=user,
164 164
             session=self.session,
165
+            config=self.app_config,
165 166
         ).create_workspace(name, save_now=True)
166 167
 
167 168
         eq_(

+ 2 - 0
tracim/tests/functional/test_mail_notification.py View File

@@ -127,6 +127,7 @@ class TestNotificationsSync(MailHogTest):
127 127
         wapi = WorkspaceApi(
128 128
             current_user=current_user,
129 129
             session=self.session,
130
+            config=self.app_config,
130 131
         )
131 132
         workspace = wapi.get_one_by_label('w1')
132 133
         user = uapi.get_one_by_email('bob@fsf.local')
@@ -218,6 +219,7 @@ class TestNotificationsAsync(MailHogTest):
218 219
         wapi = WorkspaceApi(
219 220
             current_user=current_user,
220 221
             session=self.session,
222
+            config=self.app_config,
221 223
         )
222 224
         workspace = wapi.get_one_by_label('w1')
223 225
         user = uapi.get_one_by_email('bob@fsf.local')

+ 77 - 0
tracim/tests/functional/test_system.py View File

@@ -0,0 +1,77 @@
1
+# coding=utf-8
2
+from tracim.tests import FunctionalTest
3
+
4
+"""
5
+Tests for /api/v2/system subpath endpoints.
6
+"""
7
+
8
+class TestApplicationEndpoint(FunctionalTest):
9
+    """
10
+    Tests for /api/v2/system/applications
11
+    """
12
+
13
+    def test_api__get_applications__ok_200__nominal_case(self):
14
+        """
15
+        Get applications list with a registered user.
16
+        """
17
+        self.testapp.authorization = (
18
+            'Basic',
19
+            (
20
+                'admin@admin.admin',
21
+                'admin@admin.admin'
22
+            )
23
+        )
24
+        res = self.testapp.get('/api/v2/system/applications', status=200)
25
+        res = res.json_body
26
+        application = res[0]
27
+        assert application['label'] == "Text Documents"
28
+        assert application['slug'] == 'contents/htmlpage'
29
+        assert application['fa_icon'] == 'file-text-o'
30
+        assert application['hexcolor'] == '#3f52e3'
31
+        assert application['is_active'] is True
32
+        assert 'config' in application
33
+        application = res[1]
34
+        assert application['label'] == "Markdown Plus Documents"
35
+        assert application['slug'] == 'contents/markdownpluspage'
36
+        assert application['fa_icon'] == 'file-code'
37
+        assert application['hexcolor'] == '#f12d2d'
38
+        assert application['is_active'] is True
39
+        assert 'config' in application
40
+        application = res[2]
41
+        assert application['label'] == "Files"
42
+        assert application['slug'] == 'contents/files'
43
+        assert application['fa_icon'] == 'paperclip'
44
+        assert application['hexcolor'] == '#FF9900'
45
+        assert application['is_active'] is True
46
+        assert 'config' in application
47
+        application = res[3]
48
+        assert application['label'] == "Threads"
49
+        assert application['slug'] == 'contents/threads'
50
+        assert application['fa_icon'] == 'comments-o'
51
+        assert application['hexcolor'] == '#ad4cf9'
52
+        assert application['is_active'] is True
53
+        assert 'config' in application
54
+        application = res[4]
55
+        assert application['label'] == "Calendar"
56
+        assert application['slug'] == 'calendar'
57
+        assert application['fa_icon'] == 'calendar-alt'
58
+        assert application['hexcolor'] == '#757575'
59
+        assert application['is_active'] is True
60
+        assert 'config' in application
61
+
62
+    def test_api__get_workspace__err_401__unregistered_user(self):
63
+        """
64
+        Get applications list with an unregistered user (bad auth)
65
+        """
66
+        self.testapp.authorization = (
67
+            'Basic',
68
+            (
69
+                'john@doe.doe',
70
+                'lapin'
71
+            )
72
+        )
73
+        res = self.testapp.get('/api/v2/system/applications', status=401)
74
+        assert isinstance(res.json, dict)
75
+        assert 'code' in res.json.keys()
76
+        assert 'message' in res.json.keys()
77
+        assert 'details' in res.json.keys()

+ 137 - 0
tracim/tests/functional/test_user.py View File

@@ -0,0 +1,137 @@
1
+# -*- coding: utf-8 -*-
2
+"""
3
+Tests for /api/v2/users subpath endpoints.
4
+"""
5
+from tracim.tests import FunctionalTest
6
+from tracim.fixtures.content import Content as ContentFixtures
7
+from tracim.fixtures.users_and_groups import Base as BaseFixture
8
+
9
+
10
+class TestUserWorkspaceEndpoint(FunctionalTest):
11
+    # -*- coding: utf-8 -*-
12
+    """
13
+    Tests for /api/v2/users/{user_id}/workspaces
14
+    """
15
+    fixtures = [BaseFixture, ContentFixtures]
16
+
17
+    def test_api__get_user_workspaces__ok_200__nominal_case(self):
18
+        """
19
+        Check obtain all workspaces reachables for user with user auth.
20
+        """
21
+        self.testapp.authorization = (
22
+            'Basic',
23
+            (
24
+                'admin@admin.admin',
25
+                'admin@admin.admin'
26
+            )
27
+        )
28
+        res = self.testapp.get('/api/v2/users/1/workspaces', status=200)
29
+        res = res.json_body
30
+        workspace = res[0]
31
+        assert workspace['id'] == 1
32
+        assert workspace['label'] == 'w1'
33
+        assert len(workspace['sidebar_entries']) == 7
34
+
35
+        sidebar_entry = workspace['sidebar_entries'][0]
36
+        assert sidebar_entry['slug'] == 'dashboard'
37
+        assert sidebar_entry['label'] == 'Dashboard'
38
+        assert sidebar_entry['route'] == '/#/workspaces/1/dashboard'  # nopep8
39
+        assert sidebar_entry['hexcolor'] == "#252525"
40
+        assert sidebar_entry['fa_icon'] == ""
41
+
42
+        sidebar_entry = workspace['sidebar_entries'][1]
43
+        assert sidebar_entry['slug'] == 'contents/all'
44
+        assert sidebar_entry['label'] == 'All Contents'
45
+        assert sidebar_entry['route'] == "/#/workspaces/1/contents"  # nopep8
46
+        assert sidebar_entry['hexcolor'] == "#fdfdfd"
47
+        assert sidebar_entry['fa_icon'] == ""
48
+
49
+        sidebar_entry = workspace['sidebar_entries'][2]
50
+        assert sidebar_entry['slug'] == 'contents/htmlpage'
51
+        assert sidebar_entry['label'] == 'Text Documents'
52
+        assert sidebar_entry['route'] == '/#/workspaces/1/contents?type=htmlpage'  # nopep8
53
+        assert sidebar_entry['hexcolor'] == "#3f52e3"
54
+        assert sidebar_entry['fa_icon'] == "file-text-o"
55
+
56
+        sidebar_entry = workspace['sidebar_entries'][3]
57
+        assert sidebar_entry['slug'] == 'contents/markdownpluspage'
58
+        assert sidebar_entry['label'] == 'Markdown Plus Documents'
59
+        assert sidebar_entry['route'] == "/#/workspaces/1/contents?type=markdownpluspage"    # nopep8
60
+        assert sidebar_entry['hexcolor'] == "#f12d2d"
61
+        assert sidebar_entry['fa_icon'] == "file-code"
62
+
63
+        sidebar_entry = workspace['sidebar_entries'][4]
64
+        assert sidebar_entry['slug'] == 'contents/files'
65
+        assert sidebar_entry['label'] == 'Files'
66
+        assert sidebar_entry['route'] == "/#/workspaces/1/contents?type=file"  # nopep8
67
+        assert sidebar_entry['hexcolor'] == "#FF9900"
68
+        assert sidebar_entry['fa_icon'] == "paperclip"
69
+
70
+        sidebar_entry = workspace['sidebar_entries'][5]
71
+        assert sidebar_entry['slug'] == 'contents/threads'
72
+        assert sidebar_entry['label'] == 'Threads'
73
+        assert sidebar_entry['route'] == "/#/workspaces/1/contents?type=thread"  # nopep8
74
+        assert sidebar_entry['hexcolor'] == "#ad4cf9"
75
+        assert sidebar_entry['fa_icon'] == "comments-o"
76
+
77
+        sidebar_entry = workspace['sidebar_entries'][6]
78
+        assert sidebar_entry['slug'] == 'calendar'
79
+        assert sidebar_entry['label'] == 'Calendar'
80
+        assert sidebar_entry['route'] == "/#/workspaces/1/calendar"  # nopep8
81
+        assert sidebar_entry['hexcolor'] == "#757575"
82
+        assert sidebar_entry['fa_icon'] == "calendar-alt"
83
+
84
+    def test_api__get_user_workspaces__err_403__unallowed_user(self):
85
+        """
86
+        Check obtain all workspaces reachables for one user
87
+        with another non-admin user auth.
88
+        """
89
+        self.testapp.authorization = (
90
+            'Basic',
91
+            (
92
+                'lawrence-not-real-email@fsf.local',
93
+                'foobarbaz'
94
+            )
95
+        )
96
+        res = self.testapp.get('/api/v2/users/1/workspaces', status=403)
97
+        assert isinstance(res.json, dict)
98
+        assert 'code' in res.json.keys()
99
+        assert 'message' in res.json.keys()
100
+        assert 'details' in res.json.keys()
101
+
102
+    def test_api__get_user_workspaces__err_401__unregistered_user(self):
103
+        """
104
+        Check obtain all workspaces reachables for one user
105
+        without correct user auth (user unregistered).
106
+        """
107
+        self.testapp.authorization = (
108
+            'Basic',
109
+            (
110
+                'john@doe.doe',
111
+                'lapin'
112
+            )
113
+        )
114
+        res = self.testapp.get('/api/v2/users/1/workspaces', status=401)
115
+        assert isinstance(res.json, dict)
116
+        assert 'code' in res.json.keys()
117
+        assert 'message' in res.json.keys()
118
+        assert 'details' in res.json.keys()
119
+
120
+    def test_api__get_user_workspaces__err_404__user_does_not_exist(self):
121
+        """
122
+        Check obtain all workspaces reachables for one user who does
123
+        not exist
124
+        with a correct user auth.
125
+        """
126
+        self.testapp.authorization = (
127
+            'Basic',
128
+            (
129
+                'admin@admin.admin',
130
+                'admin@admin.admin'
131
+            )
132
+        )
133
+        res = self.testapp.get('/api/v2/users/5/workspaces', status=404)
134
+        assert isinstance(res.json, dict)
135
+        assert 'code' in res.json.keys()
136
+        assert 'message' in res.json.keys()
137
+        assert 'details' in res.json.keys()

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

@@ -0,0 +1,218 @@
1
+# -*- coding: utf-8 -*-
2
+"""
3
+Tests for /api/v2/workspaces subpath endpoints.
4
+"""
5
+from tracim.tests import FunctionalTest
6
+from tracim.fixtures.content import Content as ContentFixtures
7
+from tracim.fixtures.users_and_groups import Base as BaseFixture
8
+
9
+
10
+class TestWorkspaceEndpoint(FunctionalTest):
11
+    """
12
+    Tests for /api/v2/workspaces/{workspace_id} endpoint
13
+    """
14
+
15
+    fixtures = [BaseFixture, ContentFixtures]
16
+
17
+    def test_api__get_workspace__ok_200__nominal_case(self) -> None:
18
+        """
19
+        Check obtain workspace reachable for user.
20
+        """
21
+        self.testapp.authorization = (
22
+            'Basic',
23
+            (
24
+                'admin@admin.admin',
25
+                'admin@admin.admin'
26
+            )
27
+        )
28
+        res = self.testapp.get('/api/v2/workspaces/1', status=200)
29
+        workspace = res.json_body
30
+        assert workspace['id'] == 1
31
+        assert workspace['slug'] == 'w1'
32
+        assert workspace['label'] == 'w1'
33
+        assert workspace['description'] == 'This is a workspace'
34
+        assert len(workspace['sidebar_entries']) == 7
35
+
36
+        sidebar_entry = workspace['sidebar_entries'][0]
37
+        assert sidebar_entry['slug'] == 'dashboard'
38
+        assert sidebar_entry['label'] == 'Dashboard'
39
+        assert sidebar_entry['route'] == '/#/workspaces/1/dashboard'  # nopep8
40
+        assert sidebar_entry['hexcolor'] == "#252525"
41
+        assert sidebar_entry['fa_icon'] == ""
42
+
43
+        sidebar_entry = workspace['sidebar_entries'][1]
44
+        assert sidebar_entry['slug'] == 'contents/all'
45
+        assert sidebar_entry['label'] == 'All Contents'
46
+        assert sidebar_entry['route'] == "/#/workspaces/1/contents"  # nopep8
47
+        assert sidebar_entry['hexcolor'] == "#fdfdfd"
48
+        assert sidebar_entry['fa_icon'] == ""
49
+
50
+        sidebar_entry = workspace['sidebar_entries'][2]
51
+        assert sidebar_entry['slug'] == 'contents/htmlpage'
52
+        assert sidebar_entry['label'] == 'Text Documents'
53
+        assert sidebar_entry['route'] == '/#/workspaces/1/contents?type=htmlpage'  # nopep8
54
+        assert sidebar_entry['hexcolor'] == "#3f52e3"
55
+        assert sidebar_entry['fa_icon'] == "file-text-o"
56
+
57
+        sidebar_entry = workspace['sidebar_entries'][3]
58
+        assert sidebar_entry['slug'] == 'contents/markdownpluspage'
59
+        assert sidebar_entry['label'] == 'Markdown Plus Documents'
60
+        assert sidebar_entry['route'] == "/#/workspaces/1/contents?type=markdownpluspage"    # nopep8
61
+        assert sidebar_entry['hexcolor'] == "#f12d2d"
62
+        assert sidebar_entry['fa_icon'] == "file-code"
63
+
64
+        sidebar_entry = workspace['sidebar_entries'][4]
65
+        assert sidebar_entry['slug'] == 'contents/files'
66
+        assert sidebar_entry['label'] == 'Files'
67
+        assert sidebar_entry['route'] == "/#/workspaces/1/contents?type=file"  # nopep8
68
+        assert sidebar_entry['hexcolor'] == "#FF9900"
69
+        assert sidebar_entry['fa_icon'] == "paperclip"
70
+
71
+        sidebar_entry = workspace['sidebar_entries'][5]
72
+        assert sidebar_entry['slug'] == 'contents/threads'
73
+        assert sidebar_entry['label'] == 'Threads'
74
+        assert sidebar_entry['route'] == "/#/workspaces/1/contents?type=thread"  # nopep8
75
+        assert sidebar_entry['hexcolor'] == "#ad4cf9"
76
+        assert sidebar_entry['fa_icon'] == "comments-o"
77
+
78
+        sidebar_entry = workspace['sidebar_entries'][6]
79
+        assert sidebar_entry['slug'] == 'calendar'
80
+        assert sidebar_entry['label'] == 'Calendar'
81
+        assert sidebar_entry['route'] == "/#/workspaces/1/calendar"  # nopep8
82
+        assert sidebar_entry['hexcolor'] == "#757575"
83
+        assert sidebar_entry['fa_icon'] == "calendar-alt"
84
+
85
+    def test_api__get_workspace__err_403__unallowed_user(self) -> None:
86
+        """
87
+        Check obtain workspace unreachable for user
88
+        """
89
+        self.testapp.authorization = (
90
+            'Basic',
91
+            (
92
+                'lawrence-not-real-email@fsf.local',
93
+                'foobarbaz'
94
+            )
95
+        )
96
+        res = self.testapp.get('/api/v2/workspaces/1', status=403)
97
+        assert isinstance(res.json, dict)
98
+        assert 'code' in res.json.keys()
99
+        assert 'message' in res.json.keys()
100
+        assert 'details' in res.json.keys()
101
+
102
+    def test_api__get_workspace__err_401__unregistered_user(self) -> None:
103
+        """
104
+        Check obtain workspace without registered user.
105
+        """
106
+        self.testapp.authorization = (
107
+            'Basic',
108
+            (
109
+                'john@doe.doe',
110
+                'lapin'
111
+            )
112
+        )
113
+        res = self.testapp.get('/api/v2/workspaces/1', status=401)
114
+        assert isinstance(res.json, dict)
115
+        assert 'code' in res.json.keys()
116
+        assert 'message' in res.json.keys()
117
+        assert 'details' in res.json.keys()
118
+
119
+    def test_api__get_workspace__err_403__workspace_does_not_exist(self) -> None:  # nopep8
120
+        """
121
+        Check obtain workspace who does not exist with an existing user.
122
+        """
123
+        self.testapp.authorization = (
124
+            'Basic',
125
+            (
126
+                'admin@admin.admin',
127
+                'admin@admin.admin'
128
+            )
129
+        )
130
+        res = self.testapp.get('/api/v2/workspaces/5', status=403)
131
+        assert isinstance(res.json, dict)
132
+        assert 'code' in res.json.keys()
133
+        assert 'message' in res.json.keys()
134
+        assert 'details' in res.json.keys()
135
+
136
+
137
+class TestWorkspaceMembersEndpoint(FunctionalTest):
138
+    """
139
+    Tests for /api/v2/workspaces/{workspace_id}/members endpoint
140
+    """
141
+
142
+    fixtures = [BaseFixture, ContentFixtures]
143
+
144
+    def test_api__get_workspace_members__ok_200__nominal_case(self):
145
+        """
146
+        Check obtain workspace members list with a reachable workspace for user
147
+        """
148
+        self.testapp.authorization = (
149
+            'Basic',
150
+            (
151
+                'admin@admin.admin',
152
+                'admin@admin.admin'
153
+            )
154
+        )
155
+        res = self.testapp.get('/api/v2/workspaces/1/members', status=200).json_body   # nopep8
156
+        assert len(res) == 2
157
+        user_role = res[0]
158
+        assert user_role['role_slug'] == 'workspace-manager'
159
+        assert user_role['role_id'] == 8
160
+        assert user_role['user_id'] == 1
161
+        assert user_role['workspace_id'] == 1
162
+        assert user_role['user']['display_name'] == 'Global manager'
163
+        # TODO - G.M - 24-05-2018 - [Avatar] Replace
164
+        # by correct value when avatar feature will be enabled
165
+        assert user_role['user']['avatar_url'] is None
166
+
167
+    def test_api__get_workspace_members__err_403__unallowed_user(self):
168
+        """
169
+        Check obtain workspace members list with an unreachable workspace for
170
+        user
171
+        """
172
+        self.testapp.authorization = (
173
+            'Basic',
174
+            (
175
+                'lawrence-not-real-email@fsf.local',
176
+                'foobarbaz'
177
+            )
178
+        )
179
+        res = self.testapp.get('/api/v2/workspaces/3/members', status=403)
180
+        assert isinstance(res.json, dict)
181
+        assert 'code' in res.json.keys()
182
+        assert 'message' in res.json.keys()
183
+        assert 'details' in res.json.keys()
184
+
185
+    def test_api__get_workspace_members__err_401__unregistered_user(self):
186
+        """
187
+        Check obtain workspace members list with an unregistered user
188
+        """
189
+        self.testapp.authorization = (
190
+            'Basic',
191
+            (
192
+                'john@doe.doe',
193
+                'lapin'
194
+            )
195
+        )
196
+        res = self.testapp.get('/api/v2/workspaces/1/members', status=401)
197
+        assert isinstance(res.json, dict)
198
+        assert 'code' in res.json.keys()
199
+        assert 'message' in res.json.keys()
200
+        assert 'details' in res.json.keys()
201
+
202
+    def test_api__get_workspace_members__err_403__workspace_does_not_exist(self):  # nopep8
203
+        """
204
+        Check obtain workspace members list with an existing user but
205
+        an unexisting workspace
206
+        """
207
+        self.testapp.authorization = (
208
+            'Basic',
209
+            (
210
+                'admin@admin.admin',
211
+                'admin@admin.admin'
212
+            )
213
+        )
214
+        res = self.testapp.get('/api/v2/workspaces/5/members', status=403)
215
+        assert isinstance(res.json, dict)
216
+        assert 'code' in res.json.keys()
217
+        assert 'message' in res.json.keys()
218
+        assert 'details' in res.json.keys()

+ 223 - 61
tracim/tests/library/test_content_api.py View File

@@ -107,7 +107,11 @@ class TestContentApi(DefaultTest):
107 107
             config=self.app_config,
108 108
             current_user=None,
109 109
         )
110
-        group_api = GroupApi(current_user=None,session=self.session)
110
+        group_api = GroupApi(
111
+            current_user=None,
112
+            session=self.session,
113
+            config=self.app_config,
114
+        )
111 115
         groups = [group_api.get_one(Group.TIM_USER),
112 116
                   group_api.get_one(Group.TIM_MANAGER),
113 117
                   group_api.get_one(Group.TIM_ADMIN)]
@@ -116,7 +120,8 @@ class TestContentApi(DefaultTest):
116 120
                                         groups=groups, save_now=True)
117 121
         workspace = WorkspaceApi(
118 122
             current_user=user,
119
-            session=self.session
123
+            session=self.session,
124
+            config=self.app_config,
120 125
         ).create_workspace('test workspace', save_now=True)
121 126
         api = ContentApi(
122 127
             current_user=user,
@@ -133,7 +138,11 @@ class TestContentApi(DefaultTest):
133 138
 
134 139
         # Refresh instances after commit
135 140
         user = uapi.get_one(uid)
136
-        workspace_api = WorkspaceApi(current_user=user, session=self.session)
141
+        workspace_api = WorkspaceApi(
142
+            current_user=user,
143
+            session=self.session,
144
+            config=self.app_config
145
+        )
137 146
         workspace = workspace_api.get_one(wid)
138 147
         api = ContentApi(
139 148
             current_user=user,
@@ -154,7 +163,11 @@ class TestContentApi(DefaultTest):
154 163
 
155 164
         # Refresh instances after commit
156 165
         user = uapi.get_one(uid)
157
-        workspace_api = WorkspaceApi(current_user=user, session=self.session)
166
+        workspace_api = WorkspaceApi(
167
+            current_user=user,
168
+            session=self.session,
169
+            config=self.app_config
170
+        )
158 171
         workspace = workspace_api.get_one(wid)
159 172
         api = ContentApi(
160 173
             current_user=user, 
@@ -168,7 +181,11 @@ class TestContentApi(DefaultTest):
168 181
         # Test that the item is still available if "show deleted" is activated
169 182
         # Refresh instances after commit
170 183
         user = uapi.get_one(uid)
171
-        workspace_api = WorkspaceApi(current_user=user, session=self.session)
184
+        workspace_api = WorkspaceApi(
185
+            current_user=user,
186
+            session=self.session,
187
+            config=self.app_config,
188
+        )
172 189
         api = ContentApi(
173 190
             current_user=user,
174 191
             session=self.session,
@@ -184,14 +201,26 @@ class TestContentApi(DefaultTest):
184 201
             config=self.app_config,
185 202
             current_user=None,
186 203
         )
187
-        group_api = GroupApi(current_user=None, session=self.session)
204
+        group_api = GroupApi(
205
+            current_user=None,
206
+            session=self.session,
207
+            config=self.app_config,
208
+        )
188 209
         groups = [group_api.get_one(Group.TIM_USER),
189 210
                   group_api.get_one(Group.TIM_MANAGER),
190 211
                   group_api.get_one(Group.TIM_ADMIN)]
191 212
 
192
-        user = uapi.create_minimal_user(email='this.is@user',
193
-                                        groups=groups, save_now=True)
194
-        workspace_api = WorkspaceApi(current_user=user, session=self.session)
213
+        user = uapi.create_minimal_user(
214
+            email='this.is@user',
215
+            groups=groups,
216
+            save_now=True
217
+        )
218
+        workspace_api = WorkspaceApi(
219
+            current_user=user,
220
+            session=self.session,
221
+            config=self.app_config
222
+        )
223
+
195 224
         workspace = workspace_api.create_workspace(
196 225
             'test workspace',
197 226
             save_now=True
@@ -210,7 +239,11 @@ class TestContentApi(DefaultTest):
210 239
         transaction.commit()
211 240
         # Refresh instances after commit
212 241
         user = uapi.get_one(uid)
213
-        workspace_api = WorkspaceApi(current_user=user, session=self.session)
242
+        workspace_api = WorkspaceApi(
243
+            current_user=user,
244
+            session=self.session,
245
+            config=self.app_config,
246
+        )
214 247
         api = ContentApi(
215 248
             session=self.session,
216 249
             current_user=user,
@@ -231,7 +264,11 @@ class TestContentApi(DefaultTest):
231 264
 
232 265
         # Refresh instances after commit
233 266
         user = uapi.get_one(uid)
234
-        workspace_api = WorkspaceApi(current_user=user, session=self.session)
267
+        workspace_api = WorkspaceApi(
268
+            current_user=user,
269
+            session=self.session,
270
+            config=self.app_config,
271
+        )
235 272
         workspace = workspace_api.get_one(wid)
236 273
         api = ContentApi(
237 274
             current_user=user, 
@@ -245,7 +282,11 @@ class TestContentApi(DefaultTest):
245 282
 
246 283
         # Refresh instances after commit
247 284
         user = uapi.get_one(uid)
248
-        workspace_api = WorkspaceApi(current_user=user, session=self.session)
285
+        workspace_api = WorkspaceApi(
286
+            current_user=user,
287
+            session=self.session,
288
+            config=self.app_config,
289
+        )
249 290
         workspace = workspace_api.get_one(wid)
250 291
         api = ContentApi(
251 292
             current_user=user,
@@ -269,7 +310,11 @@ class TestContentApi(DefaultTest):
269 310
             config=self.app_config,
270 311
             current_user=None,
271 312
         )
272
-        group_api = GroupApi(current_user=None, session=self.session)
313
+        group_api = GroupApi(
314
+            current_user=None,
315
+            session=self.session,
316
+            config=self.app_config,
317
+        )
273 318
         groups = [group_api.get_one(Group.TIM_USER),
274 319
                   group_api.get_one(Group.TIM_MANAGER),
275 320
                   group_api.get_one(Group.TIM_ADMIN)]
@@ -281,7 +326,8 @@ class TestContentApi(DefaultTest):
281 326
         )
282 327
         workspace = WorkspaceApi(
283 328
             current_user=user,
284
-            session=self.session
329
+            session=self.session,
330
+            config=self.app_config,
285 331
         ).create_workspace(
286 332
             'test workspace',
287 333
             save_now=True
@@ -301,7 +347,11 @@ class TestContentApi(DefaultTest):
301 347
 
302 348
         # Refresh instances after commit
303 349
         user = uapi.get_one(uid)
304
-        workspace_api = WorkspaceApi(current_user=user, session=self.session)
350
+        workspace_api = WorkspaceApi(
351
+            current_user=user,
352
+            session=self.session,
353
+            config=self.app_config,
354
+        )
305 355
         workspace = workspace_api.get_one(wid)
306 356
         api = ContentApi(
307 357
             current_user=user, 
@@ -326,7 +376,11 @@ class TestContentApi(DefaultTest):
326 376
             config=self.app_config,
327 377
             current_user=None,
328 378
         )
329
-        group_api = GroupApi(current_user=None, session=self.session)
379
+        group_api = GroupApi(
380
+            current_user=None,
381
+            session=self.session,
382
+            config=self.app_config
383
+        )
330 384
         groups = [group_api.get_one(Group.TIM_USER),
331 385
                   group_api.get_one(Group.TIM_MANAGER),
332 386
                   group_api.get_one(Group.TIM_ADMIN)]
@@ -335,7 +389,8 @@ class TestContentApi(DefaultTest):
335 389
                                         groups=groups, save_now=True)
336 390
         workspace = WorkspaceApi(
337 391
             current_user=user,
338
-            session=self.session
392
+            session=self.session,
393
+            config=self.app_config
339 394
         ).create_workspace('test workspace', save_now=True)
340 395
         api = ContentApi(
341 396
             current_user=user, 
@@ -371,7 +426,11 @@ class TestContentApi(DefaultTest):
371 426
 
372 427
         # Refresh instances after commit
373 428
         user = uapi.get_one(uid)
374
-        workspace_api = WorkspaceApi(current_user=user, session=self.session)
429
+        workspace_api = WorkspaceApi(
430
+            current_user=user,
431
+            session=self.session,
432
+            config=self.app_config,
433
+        )
375 434
         workspace = workspace_api.get_one(wid)
376 435
         api = ContentApi(
377 436
             current_user=user,
@@ -394,7 +453,8 @@ class TestContentApi(DefaultTest):
394 453
         )
395 454
         group_api = GroupApi(
396 455
             current_user=None,
397
-            session=self.session
456
+            session=self.session,
457
+            config=self.app_config,
398 458
         )
399 459
         groups = [group_api.get_one(Group.TIM_USER),
400 460
                   group_api.get_one(Group.TIM_MANAGER),
@@ -405,7 +465,8 @@ class TestContentApi(DefaultTest):
405 465
 
406 466
         workspace = WorkspaceApi(
407 467
             current_user=user,
408
-            session=self.session
468
+            session=self.session,
469
+            config=self.app_config,
409 470
         ).create_workspace(
410 471
             'test workspace',
411 472
             save_now=True
@@ -432,7 +493,8 @@ class TestContentApi(DefaultTest):
432 493
         )
433 494
         group_api = GroupApi(
434 495
             current_user=None,
435
-            session=self.session
496
+            session=self.session,
497
+            config=self.app_config,
436 498
         )
437 499
         groups = [group_api.get_one(Group.TIM_USER),
438 500
                   group_api.get_one(Group.TIM_MANAGER),
@@ -443,7 +505,8 @@ class TestContentApi(DefaultTest):
443 505
 
444 506
         workspace = WorkspaceApi(
445 507
             current_user=user,
446
-            session=self.session
508
+            session=self.session,
509
+            config=self.app_config,
447 510
         ).create_workspace(
448 511
             'test workspace',
449 512
             save_now=True
@@ -474,7 +537,8 @@ class TestContentApi(DefaultTest):
474 537
         )
475 538
         group_api = GroupApi(
476 539
             current_user=None,
477
-            session=self.session
540
+            session=self.session,
541
+            config=self.config,
478 542
         )
479 543
         groups = [group_api.get_one(Group.TIM_USER),
480 544
                   group_api.get_one(Group.TIM_MANAGER),
@@ -485,7 +549,8 @@ class TestContentApi(DefaultTest):
485 549
 
486 550
         workspace = WorkspaceApi(
487 551
             current_user=user,
488
-            session=self.session
552
+            session=self.session,
553
+            config=self.app_config,
489 554
         ).create_workspace(
490 555
             'test workspace',
491 556
             save_now=True
@@ -516,7 +581,8 @@ class TestContentApi(DefaultTest):
516 581
         )
517 582
         group_api = GroupApi(
518 583
             current_user=None,
519
-            session=self.session
584
+            session=self.session,
585
+            config=self.app_config
520 586
         )
521 587
         groups = [group_api.get_one(Group.TIM_USER),
522 588
                   group_api.get_one(Group.TIM_MANAGER),
@@ -534,12 +600,17 @@ class TestContentApi(DefaultTest):
534 600
         )
535 601
         workspace = WorkspaceApi(
536 602
             current_user=user,
537
-            session=self.session
603
+            session=self.session,
604
+            config=self.app_config,
538 605
         ).create_workspace(
539 606
             'test workspace',
540 607
             save_now=True
541 608
         )
542
-        RoleApi(current_user=user, session=self.session).create_one(
609
+        RoleApi(
610
+            current_user=user,
611
+            session=self.session,
612
+            config=self.app_config,
613
+        ).create_one(
543 614
             user2,
544 615
             workspace,
545 616
             UserRoleInWorkspace.WORKSPACE_MANAGER,
@@ -581,6 +652,7 @@ class TestContentApi(DefaultTest):
581 652
         workspace2 = WorkspaceApi(
582 653
             current_user=user2,
583 654
             session=self.session,
655
+            config=self.app_config,
584 656
         ).create_workspace(
585 657
             'test workspace2',
586 658
             save_now=True
@@ -628,7 +700,8 @@ class TestContentApi(DefaultTest):
628 700
         )
629 701
         group_api = GroupApi(
630 702
             current_user=None,
631
-            session=self.session
703
+            session=self.session,
704
+            config=self.app_config,
632 705
         )
633 706
         groups = [group_api.get_one(Group.TIM_USER),
634 707
                   group_api.get_one(Group.TIM_MANAGER),
@@ -646,12 +719,17 @@ class TestContentApi(DefaultTest):
646 719
         )
647 720
         workspace = WorkspaceApi(
648 721
             current_user=user,
649
-            session=self.session
722
+            session=self.session,
723
+            config=self.app_config,
650 724
         ).create_workspace(
651 725
             'test workspace',
652 726
             save_now=True
653 727
         )
654
-        RoleApi(current_user=user, session=self.session).create_one(
728
+        RoleApi(
729
+            current_user=user,
730
+            session=self.session,
731
+            config=self.app_config,
732
+        ).create_one(
655 733
             user2,
656 734
             workspace,
657 735
             UserRoleInWorkspace.WORKSPACE_MANAGER,
@@ -692,7 +770,8 @@ class TestContentApi(DefaultTest):
692 770
         )
693 771
         workspace2 = WorkspaceApi(
694 772
             current_user=user2,
695
-            session=self.session
773
+            session=self.session,
774
+            config=self.app_config,
696 775
         ).create_workspace(
697 776
             'test workspace2',
698 777
             save_now=True
@@ -738,7 +817,8 @@ class TestContentApi(DefaultTest):
738 817
         )
739 818
         group_api = GroupApi(
740 819
             current_user=None,
741
-            session=self.session
820
+            session=self.session,
821
+            config=self.app_config,
742 822
         )
743 823
         groups = [group_api.get_one(Group.TIM_USER),
744 824
                   group_api.get_one(Group.TIM_MANAGER),
@@ -756,12 +836,17 @@ class TestContentApi(DefaultTest):
756 836
         )
757 837
         workspace = WorkspaceApi(
758 838
             current_user=user,
759
-            session=self.session
839
+            session=self.session,
840
+            config=self.app_config,
760 841
         ).create_workspace(
761 842
             'test workspace',
762 843
             save_now=True
763 844
         )
764
-        RoleApi(current_user=user, session=self.session).create_one(
845
+        RoleApi(
846
+            current_user=user,
847
+            session=self.session,
848
+            config=self.app_config,
849
+        ).create_one(
765 850
             user2, workspace,
766 851
             UserRoleInWorkspace.WORKSPACE_MANAGER,
767 852
             with_notif=False
@@ -837,7 +922,8 @@ class TestContentApi(DefaultTest):
837 922
         )
838 923
         group_api = GroupApi(
839 924
             current_user=None,
840
-            session=self.session
925
+            session=self.session,
926
+            config=self.app_config,
841 927
         )
842 928
         groups = [group_api.get_one(Group.TIM_USER),
843 929
                   group_api.get_one(Group.TIM_MANAGER),
@@ -851,6 +937,7 @@ class TestContentApi(DefaultTest):
851 937
         wapi = WorkspaceApi(
852 938
             current_user=user_a,
853 939
             session=self.session,
940
+            config=self.app_config,
854 941
         )
855 942
         workspace1 = wapi.create_workspace(
856 943
             'test workspace n°1',
@@ -862,6 +949,7 @@ class TestContentApi(DefaultTest):
862 949
         role_api1 = RoleApi(
863 950
             current_user=user_a,
864 951
             session=self.session,
952
+            config=self.app_config,
865 953
         )
866 954
         role_api1.create_one(
867 955
             user_b,
@@ -873,6 +961,7 @@ class TestContentApi(DefaultTest):
873 961
         role_api2 = RoleApi(
874 962
             current_user=user_b,
875 963
             session=self.session,
964
+            config=self.app_config,
876 965
         )
877 966
         role_api2.create_one(user_b, workspace2, UserRoleInWorkspace.READER,
878 967
                              False)
@@ -940,7 +1029,8 @@ class TestContentApi(DefaultTest):
940 1029
         )
941 1030
         group_api = GroupApi(
942 1031
             current_user=None,
943
-            session=self.session
1032
+            session=self.session,
1033
+            config = self.app_config,
944 1034
         )
945 1035
         groups = [group_api.get_one(Group.TIM_USER),
946 1036
                   group_api.get_one(Group.TIM_MANAGER),
@@ -957,10 +1047,15 @@ class TestContentApi(DefaultTest):
957 1047
             save_now=True
958 1048
         )
959 1049
 
960
-        wapi = WorkspaceApi(current_user=user_a, session=self.session)
1050
+        wapi = WorkspaceApi(
1051
+            current_user=user_a,
1052
+            session=self.session,
1053
+            config=self.app_config,
1054
+        )
961 1055
         workspace_api = WorkspaceApi(
962 1056
             current_user=user_a,
963
-            session=self.session
1057
+            session=self.session,
1058
+            config=self.app_config,
964 1059
         )
965 1060
         workspace = wapi.create_workspace(
966 1061
             'test workspace',
@@ -969,6 +1064,7 @@ class TestContentApi(DefaultTest):
969 1064
         role_api = RoleApi(
970 1065
             current_user=user_a,
971 1066
             session=self.session,
1067
+            config=self.app_config,
972 1068
         )
973 1069
         role_api.create_one(
974 1070
             user_b,
@@ -1004,7 +1100,11 @@ class TestContentApi(DefaultTest):
1004 1100
             config=self.app_config,
1005 1101
             current_user=None,
1006 1102
         )
1007
-        group_api = GroupApi(current_user=None, session=self.session)
1103
+        group_api = GroupApi(
1104
+            current_user=None,
1105
+            session=self.session,
1106
+            config=self.app_config,
1107
+        )
1008 1108
         groups = [group_api.get_one(Group.TIM_USER),
1009 1109
                   group_api.get_one(Group.TIM_MANAGER),
1010 1110
                   group_api.get_one(Group.TIM_ADMIN)]
@@ -1023,6 +1123,7 @@ class TestContentApi(DefaultTest):
1023 1123
         wapi = WorkspaceApi(
1024 1124
             current_user=user_a,
1025 1125
             session=self.session,
1126
+            config=self.app_config,
1026 1127
         )
1027 1128
         workspace = wapi.create_workspace(
1028 1129
             'test workspace',
@@ -1031,6 +1132,7 @@ class TestContentApi(DefaultTest):
1031 1132
         role_api = RoleApi(
1032 1133
             current_user=user_a,
1033 1134
             session=self.session,
1135
+            config=self.app_config,
1034 1136
         )
1035 1137
         role_api.create_one(
1036 1138
             user_b,
@@ -1097,7 +1199,11 @@ class TestContentApi(DefaultTest):
1097 1199
             config=self.app_config,
1098 1200
             current_user=None,
1099 1201
         )
1100
-        group_api = GroupApi(current_user=None, session=self.session)
1202
+        group_api = GroupApi(
1203
+            current_user=None,
1204
+            session=self.session,
1205
+            config=self.app_config,
1206
+        )
1101 1207
         groups = [group_api.get_one(Group.TIM_USER),
1102 1208
                   group_api.get_one(Group.TIM_MANAGER),
1103 1209
                   group_api.get_one(Group.TIM_ADMIN)]
@@ -1108,7 +1214,11 @@ class TestContentApi(DefaultTest):
1108 1214
             save_now=True
1109 1215
         )
1110 1216
 
1111
-        workspace_api = WorkspaceApi(current_user=user1, session=self.session)
1217
+        workspace_api = WorkspaceApi(
1218
+            current_user=user1,
1219
+            session=self.session,
1220
+            config=self.app_config,
1221
+        )
1112 1222
         workspace = workspace_api.create_workspace(
1113 1223
             'test workspace',
1114 1224
             save_now=True
@@ -1121,7 +1231,8 @@ class TestContentApi(DefaultTest):
1121 1231
 
1122 1232
         RoleApi(
1123 1233
             current_user=user1,
1124
-            session=self.session
1234
+            session=self.session,
1235
+            config=self.app_config,
1125 1236
         ).create_one(
1126 1237
             user2,
1127 1238
             workspace,
@@ -1152,7 +1263,8 @@ class TestContentApi(DefaultTest):
1152 1263
         user1 = uapi.get_one(u1id)
1153 1264
         workspace = WorkspaceApi(
1154 1265
             current_user=user1,
1155
-            session=self.session
1266
+            session=self.session,
1267
+            config=self.app_config,
1156 1268
         ).get_one(wid)
1157 1269
         api = ContentApi(
1158 1270
             current_user=user1,
@@ -1193,6 +1305,7 @@ class TestContentApi(DefaultTest):
1193 1305
         workspace = WorkspaceApi(
1194 1306
             current_user=user1,
1195 1307
             session=self.session,
1308
+            config=self.app_config,
1196 1309
         ).get_one(wid)
1197 1310
         api = ContentApi(
1198 1311
             current_user=user1,
@@ -1216,7 +1329,8 @@ class TestContentApi(DefaultTest):
1216 1329
         )
1217 1330
         group_api = GroupApi(
1218 1331
             current_user=None,
1219
-            session=self.session
1332
+            session=self.session,
1333
+            config = self.app_config,
1220 1334
         )
1221 1335
         groups = [group_api.get_one(Group.TIM_USER),
1222 1336
                   group_api.get_one(Group.TIM_MANAGER),
@@ -1231,6 +1345,7 @@ class TestContentApi(DefaultTest):
1231 1345
         workspace = WorkspaceApi(
1232 1346
             current_user=user1,
1233 1347
             session=self.session,
1348
+            config=self.app_config,
1234 1349
         ).create_workspace(
1235 1350
             'test workspace',
1236 1351
             save_now=True
@@ -1241,7 +1356,8 @@ class TestContentApi(DefaultTest):
1241 1356
 
1242 1357
         RoleApi(
1243 1358
             current_user=user1,
1244
-            session=self.session
1359
+            session=self.session,
1360
+            config=self.app_config,
1245 1361
         ).create_one(
1246 1362
             user2,
1247 1363
             workspace,
@@ -1293,7 +1409,8 @@ class TestContentApi(DefaultTest):
1293 1409
         )
1294 1410
         group_api = GroupApi(
1295 1411
             current_user=None,
1296
-            session=self.session
1412
+            session=self.session,
1413
+            config=self.app_config,
1297 1414
         )
1298 1415
         groups = [group_api.get_one(Group.TIM_USER),
1299 1416
                   group_api.get_one(Group.TIM_MANAGER),
@@ -1305,7 +1422,11 @@ class TestContentApi(DefaultTest):
1305 1422
             save_now=True
1306 1423
         )
1307 1424
 
1308
-        workspace_api = WorkspaceApi(current_user=user1, session=self.session)
1425
+        workspace_api = WorkspaceApi(
1426
+            current_user=user1,
1427
+            session=self.session,
1428
+            config=self.app_config,
1429
+        )
1309 1430
         workspace = workspace_api.create_workspace(
1310 1431
             'test workspace',
1311 1432
             save_now=True
@@ -1318,6 +1439,7 @@ class TestContentApi(DefaultTest):
1318 1439
         RoleApi(
1319 1440
             current_user=user1,
1320 1441
             session=self.session,
1442
+            config=self.app_config,
1321 1443
         ).create_one(
1322 1444
             user2,
1323 1445
             workspace,
@@ -1345,7 +1467,11 @@ class TestContentApi(DefaultTest):
1345 1467
 
1346 1468
         # Refresh instances after commit
1347 1469
         user1 = uapi.get_one(u1id)
1348
-        workspace_api2 = WorkspaceApi(current_user=user1, session=self.session)
1470
+        workspace_api2 = WorkspaceApi(
1471
+            current_user=user1,
1472
+            session=self.session,
1473
+            config=self.app_config,
1474
+        )
1349 1475
         workspace = workspace_api2.get_one(wid)
1350 1476
         api = ContentApi(
1351 1477
             current_user=user1,
@@ -1387,6 +1513,7 @@ class TestContentApi(DefaultTest):
1387 1513
         workspace = WorkspaceApi(
1388 1514
             current_user=user1,
1389 1515
             session=self.session,
1516
+            config=self.app_config,
1390 1517
         ).get_one(wid)
1391 1518
 
1392 1519
         updated = api.get_one(pcid, ContentType.Any, workspace)
@@ -1407,6 +1534,7 @@ class TestContentApi(DefaultTest):
1407 1534
         group_api = GroupApi(
1408 1535
             current_user=None,
1409 1536
             session=self.session,
1537
+            config=self.app_config,
1410 1538
         )
1411 1539
         groups = [group_api.get_one(Group.TIM_USER),
1412 1540
                   group_api.get_one(Group.TIM_MANAGER),
@@ -1418,7 +1546,11 @@ class TestContentApi(DefaultTest):
1418 1546
             save_now=True,
1419 1547
         )
1420 1548
 
1421
-        workspace_api = WorkspaceApi(current_user=user1, session=self.session)
1549
+        workspace_api = WorkspaceApi(
1550
+            current_user=user1,
1551
+            session=self.session,
1552
+            config=self.app_config,
1553
+        )
1422 1554
         workspace = workspace_api.create_workspace(
1423 1555
             'test workspace',
1424 1556
             save_now=True
@@ -1430,6 +1562,7 @@ class TestContentApi(DefaultTest):
1430 1562
         RoleApi(
1431 1563
             current_user=user1,
1432 1564
             session=self.session,
1565
+            config=self.app_config,
1433 1566
         ).create_one(
1434 1567
             user2,
1435 1568
             workspace,
@@ -1485,7 +1618,11 @@ class TestContentApi(DefaultTest):
1485 1618
             config=self.app_config,
1486 1619
             current_user=None,
1487 1620
         )
1488
-        group_api = GroupApi(current_user=None, session=self.session)
1621
+        group_api = GroupApi(
1622
+            current_user=None,
1623
+            session=self.session,
1624
+            config=self.app_config,
1625
+        )
1489 1626
         groups = [group_api.get_one(Group.TIM_USER),
1490 1627
                   group_api.get_one(Group.TIM_MANAGER),
1491 1628
                   group_api.get_one(Group.TIM_ADMIN)]
@@ -1497,7 +1634,11 @@ class TestContentApi(DefaultTest):
1497 1634
         )
1498 1635
         u1id = user1.user_id
1499 1636
 
1500
-        workspace_api = WorkspaceApi(current_user=user1, session=self.session)
1637
+        workspace_api = WorkspaceApi(
1638
+            current_user=user1,
1639
+            session=self.session,
1640
+            config=self.app_config,
1641
+        )
1501 1642
         workspace = workspace_api.create_workspace(
1502 1643
             'test workspace',
1503 1644
             save_now=True
@@ -1509,7 +1650,8 @@ class TestContentApi(DefaultTest):
1509 1650
 
1510 1651
         RoleApi(
1511 1652
             current_user=user1,
1512
-            session=self.session
1653
+            session=self.session,
1654
+            config=self.app_config,
1513 1655
         ).create_one(
1514 1656
             user2,
1515 1657
             workspace,
@@ -1545,7 +1687,8 @@ class TestContentApi(DefaultTest):
1545 1687
         ).get_one(u1id)
1546 1688
         workspace = WorkspaceApi(
1547 1689
             current_user=user1,
1548
-            session=self.session
1690
+            session=self.session,
1691
+            config=self.app_config,
1549 1692
         ).get_one(wid)
1550 1693
 
1551 1694
         content = api.get_one(pcid, ContentType.Any, workspace)
@@ -1583,6 +1726,7 @@ class TestContentApi(DefaultTest):
1583 1726
         workspace = WorkspaceApi(
1584 1727
             current_user=user1,
1585 1728
             session=self.session,
1729
+            config=self.app_config,
1586 1730
         ).get_one(wid)
1587 1731
         u2 = UserApi(
1588 1732
             current_user=None,
@@ -1632,7 +1776,8 @@ class TestContentApi(DefaultTest):
1632 1776
         )
1633 1777
         group_api = GroupApi(
1634 1778
             current_user=None,
1635
-            session=self.session
1779
+            session=self.session,
1780
+            config=self.app_config,
1636 1781
         )
1637 1782
         groups = [group_api.get_one(Group.TIM_USER),
1638 1783
                   group_api.get_one(Group.TIM_MANAGER),
@@ -1645,7 +1790,11 @@ class TestContentApi(DefaultTest):
1645 1790
         )
1646 1791
         u1id = user1.user_id
1647 1792
 
1648
-        workspace_api = WorkspaceApi(current_user=user1, session=self.session)
1793
+        workspace_api = WorkspaceApi(
1794
+            current_user=user1,
1795
+            session=self.session,
1796
+            config=self.app_config,
1797
+        )
1649 1798
         workspace = workspace_api.create_workspace(
1650 1799
             'test workspace',
1651 1800
             save_now=True
@@ -1657,7 +1806,8 @@ class TestContentApi(DefaultTest):
1657 1806
 
1658 1807
         RoleApi(
1659 1808
             current_user=user1,
1660
-            session=self.session
1809
+            session=self.session,
1810
+            config=self.app_config,
1661 1811
         ).create_one(
1662 1812
             user2,
1663 1813
             workspace,
@@ -1692,6 +1842,7 @@ class TestContentApi(DefaultTest):
1692 1842
         workspace = WorkspaceApi(
1693 1843
             current_user=user1,
1694 1844
             session=self.session,
1845
+            config=self.app_config,
1695 1846
         ).get_one(wid)
1696 1847
 
1697 1848
         content = api.get_one(pcid, ContentType.Any, workspace)
@@ -1729,6 +1880,7 @@ class TestContentApi(DefaultTest):
1729 1880
         workspace = WorkspaceApi(
1730 1881
             current_user=user1,
1731 1882
             session=self.session,
1883
+            config=self.app_config,
1732 1884
         ).get_one(wid)
1733 1885
         # show archived is used at the top end of the test
1734 1886
         api = ContentApi(
@@ -1782,6 +1934,7 @@ class TestContentApi(DefaultTest):
1782 1934
         group_api = GroupApi(
1783 1935
             current_user=None,
1784 1936
             session=self.session,
1937
+            config=self.app_config,
1785 1938
         )
1786 1939
         groups = [group_api.get_one(Group.TIM_USER),
1787 1940
                   group_api.get_one(Group.TIM_MANAGER),
@@ -1792,7 +1945,8 @@ class TestContentApi(DefaultTest):
1792 1945
 
1793 1946
         workspace = WorkspaceApi(
1794 1947
             current_user=user,
1795
-            session=self.session
1948
+            session=self.session,
1949
+            config=self.app_config,
1796 1950
         ).create_workspace(
1797 1951
             'test workspace',
1798 1952
             save_now=True
@@ -1802,7 +1956,6 @@ class TestContentApi(DefaultTest):
1802 1956
             current_user=user, 
1803 1957
             session=self.session,
1804 1958
             config=self.app_config,
1805
-
1806 1959
         )
1807 1960
         a = api.create(ContentType.Folder, workspace, None,
1808 1961
                        'this is randomized folder', True)
@@ -1837,6 +1990,7 @@ class TestContentApi(DefaultTest):
1837 1990
         group_api = GroupApi(
1838 1991
             current_user=None,
1839 1992
             session=self.session,
1993
+            config=self.app_config,
1840 1994
         )
1841 1995
         groups = [group_api.get_one(Group.TIM_USER),
1842 1996
                   group_api.get_one(Group.TIM_MANAGER),
@@ -1847,7 +2001,8 @@ class TestContentApi(DefaultTest):
1847 2001
 
1848 2002
         workspace = WorkspaceApi(
1849 2003
             current_user=user,
1850
-            session=self.session
2004
+            session=self.session,
2005
+            config=self.app_config,
1851 2006
         ).create_workspace(
1852 2007
             'test workspace',
1853 2008
             save_now=True,
@@ -1888,7 +2043,11 @@ class TestContentApi(DefaultTest):
1888 2043
             config=self.app_config,
1889 2044
             current_user=None,
1890 2045
         )
1891
-        group_api = GroupApi(current_user=None, session=self.session)
2046
+        group_api = GroupApi(
2047
+            current_user=None,
2048
+            session=self.session,
2049
+            config=self.app_config,
2050
+        )
1892 2051
         groups = [group_api.get_one(Group.TIM_USER),
1893 2052
                   group_api.get_one(Group.TIM_MANAGER),
1894 2053
                   group_api.get_one(Group.TIM_ADMIN)]
@@ -1898,7 +2057,8 @@ class TestContentApi(DefaultTest):
1898 2057
 
1899 2058
         workspace = WorkspaceApi(
1900 2059
             current_user=user,
1901
-            session=self.session
2060
+            session=self.session,
2061
+            config=self.app_config,
1902 2062
         ).create_workspace('test workspace', save_now=True)
1903 2063
 
1904 2064
         api = ContentApi(
@@ -2032,6 +2192,7 @@ class TestContentApiSecurity(DefaultTest):
2032 2192
         bob_workspace = WorkspaceApi(
2033 2193
             current_user=bob,
2034 2194
             session=self.session,
2195
+            config=self.app_config,
2035 2196
         ).create_workspace(
2036 2197
             'bob_workspace',
2037 2198
             save_now=True,
@@ -2039,6 +2200,7 @@ class TestContentApiSecurity(DefaultTest):
2039 2200
         admin_workspace = WorkspaceApi(
2040 2201
             current_user=admin,
2041 2202
             session=self.session,
2203
+            config=self.app_config,
2042 2204
         ).create_workspace(
2043 2205
             'admin_workspace',
2044 2206
             save_now=True,

+ 4 - 4
tracim/tests/library/test_user_api.py View File

@@ -4,7 +4,7 @@ from sqlalchemy.orm.exc import NoResultFound
4 4
 
5 5
 import transaction
6 6
 
7
-from tracim.exceptions import UserNotExist, AuthenticationFailed
7
+from tracim.exceptions import UserDoesNotExist, AuthenticationFailed
8 8
 from tracim.lib.core.user import UserApi
9 9
 from tracim.models import User
10 10
 from tracim.models.context_models import UserInContext
@@ -91,7 +91,7 @@ class TestUserApi(DefaultTest):
91 91
             session=self.session,
92 92
             config=self.config,
93 93
         )
94
-        with pytest.raises(NoResultFound):
94
+        with pytest.raises(UserDoesNotExist):
95 95
             api.get_one_by_email('unknown')
96 96
 
97 97
     def test_unit__get_all__ok__nominal_case(self):
@@ -158,7 +158,7 @@ class TestUserApi(DefaultTest):
158 158
             session=self.session,
159 159
             config=self.config,
160 160
         )
161
-        with pytest.raises(UserNotExist):
161
+        with pytest.raises(UserDoesNotExist):
162 162
             api.get_current_user()
163 163
 
164 164
     def test_unit__authenticate_user___ok__nominal_case(self):
@@ -187,4 +187,4 @@ class TestUserApi(DefaultTest):
187 187
             config=self.config,
188 188
         )
189 189
         with pytest.raises(AuthenticationFailed):
190
-            api.authenticate_user('unknown_user', 'wrong_password')
190
+            api.authenticate_user('admin@admin.admin', 'wrong_password')

+ 8 - 2
tracim/tests/library/test_workspace.py View File

@@ -44,6 +44,7 @@ class TestThread(DefaultTest):
44 44
             .filter(User.email == 'admin@admin.admin').one()
45 45
         wapi = WorkspaceApi(
46 46
             session=self.session,
47
+            config=self.app_config,
47 48
             current_user=admin,
48 49
         )
49 50
         w = wapi.create_workspace(label='workspace w', save_now=True)
@@ -57,6 +58,7 @@ class TestThread(DefaultTest):
57 58
         rapi = RoleApi(
58 59
             session=self.session,
59 60
             current_user=admin,
61
+            config=self.app_config,
60 62
         )
61 63
         r = rapi.create_one(u, w, UserRoleInWorkspace.READER, with_notif=True)
62 64
         eq_([r, ], wapi.get_notifiable_roles(workspace=w))
@@ -75,6 +77,7 @@ class TestThread(DefaultTest):
75 77
         wapi = WorkspaceApi(
76 78
             session=self.session,
77 79
             current_user=admin,
80
+            config=self.app_config,
78 81
         )
79 82
         eq_([], wapi.get_all_manageable())
80 83
         # Checks an admin gets all workspaces.
@@ -87,15 +90,18 @@ class TestThread(DefaultTest):
87 90
         gapi = GroupApi(
88 91
             session=self.session,
89 92
             current_user=None,
93
+            config=self.app_config,
90 94
         )
91 95
         u = uapi.create_minimal_user('u.s@e.r', [gapi.get_one(Group.TIM_USER)], True)
92 96
         wapi = WorkspaceApi(
93 97
             session=self.session,
94
-            current_user=u
98
+            current_user=u,
99
+            config=self.app_config,
95 100
         )
96 101
         rapi = RoleApi(
97 102
             session=self.session,
98
-            current_user=u
103
+            current_user=u,
104
+            config=self.app_config,
99 105
         )
100 106
         rapi.create_one(u, w4, UserRoleInWorkspace.READER, False)
101 107
         rapi.create_one(u, w3, UserRoleInWorkspace.CONTRIBUTOR, False)

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


+ 62 - 3
tracim/views/core_api/schemas.py View File

@@ -1,13 +1,16 @@
1 1
 # coding=utf-8
2 2
 import marshmallow
3 3
 from marshmallow import post_load
4
+from marshmallow.validate import OneOf
4 5
 
5
-from tracim.models.context_models import LoginCredentials, UserInContext
6
+from tracim.models.auth import Profile
7
+from tracim.models.context_models import LoginCredentials
8
+from tracim.models.data import UserRoleInWorkspace
6 9
 
7 10
 
8 11
 class ProfileSchema(marshmallow.Schema):
9
-    id = marshmallow.fields.Int(dump_only=True)
10
-    slug = marshmallow.fields.String(attribute='name')
12
+    id = marshmallow.fields.Int(dump_only=True, validate=OneOf(Profile._IDS))
13
+    slug = marshmallow.fields.String(attribute='name', validate=OneOf(Profile._NAME))
11 14
 
12 15
 
13 16
 class UserSchema(marshmallow.Schema):
@@ -32,6 +35,14 @@ class UserSchema(marshmallow.Schema):
32 35
     )
33 36
 
34 37
 
38
+class UserIdPathSchema(marshmallow.Schema):
39
+    user_id = marshmallow.fields.Int()
40
+
41
+
42
+class WorkspaceIdPathSchema(marshmallow.Schema):
43
+    workspace_id = marshmallow.fields.Int()
44
+
45
+
35 46
 class BasicAuthSchema(marshmallow.Schema):
36 47
 
37 48
     email = marshmallow.fields.Email(required=True)
@@ -48,3 +59,51 @@ class LoginOutputHeaders(marshmallow.Schema):
48 59
 
49 60
 class NoContentSchema(marshmallow.Schema):
50 61
     pass
62
+
63
+
64
+class WorkspaceMenuEntrySchema(marshmallow.Schema):
65
+    slug = marshmallow.fields.String()
66
+    label = marshmallow.fields.String()
67
+    route = marshmallow.fields.String()
68
+    hexcolor = marshmallow.fields.String()
69
+    fa_icon = marshmallow.fields.String()
70
+
71
+
72
+class WorkspaceDigestSchema(marshmallow.Schema):
73
+    id = marshmallow.fields.Int()
74
+    label = marshmallow.fields.String()
75
+    sidebar_entries = marshmallow.fields.Nested(
76
+        WorkspaceMenuEntrySchema,
77
+        many=True,
78
+    )
79
+
80
+
81
+class WorkspaceSchema(WorkspaceDigestSchema):
82
+    slug = marshmallow.fields.String()
83
+    description = marshmallow.fields.String()
84
+
85
+
86
+class WorkspaceMemberSchema(marshmallow.Schema):
87
+    role_id = marshmallow.fields.Int(validate=OneOf(UserRoleInWorkspace.get_all_role_values()))  # nopep8
88
+    role_slug = marshmallow.fields.String(validate=OneOf(UserRoleInWorkspace.get_all_role_slug()))  # nopep8
89
+    user_id = marshmallow.fields.Int()
90
+    workspace_id = marshmallow.fields.Int()
91
+    user = marshmallow.fields.Nested(
92
+        UserSchema(only=('display_name', 'avatar_url'))
93
+    )
94
+
95
+
96
+class ApplicationConfigSchema(marshmallow.Schema):
97
+    pass
98
+    #  TODO - G.M - 24-05-2018 - Set this
99
+
100
+
101
+class ApplicationSchema(marshmallow.Schema):
102
+    label = marshmallow.fields.String()
103
+    slug = marshmallow.fields.String()
104
+    fa_icon = marshmallow.fields.String()
105
+    hexcolor = marshmallow.fields.String()
106
+    is_active = marshmallow.fields.Boolean()
107
+    config = marshmallow.fields.Nested(
108
+        ApplicationConfigSchema,
109
+    )

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

@@ -13,7 +13,7 @@ from tracim.views.core_api.schemas import UserSchema
13 13
 from tracim.views.core_api.schemas import NoContentSchema
14 14
 from tracim.views.core_api.schemas import LoginOutputHeaders
15 15
 from tracim.views.core_api.schemas import BasicAuthSchema
16
-from tracim.exceptions import NotAuthentificated
16
+from tracim.exceptions import NotAuthenticated
17 17
 from tracim.exceptions import AuthenticationFailed
18 18
 
19 19
 
@@ -52,7 +52,7 @@ class SessionController(Controller):
52 52
         return
53 53
 
54 54
     @hapic.with_api_doc()
55
-    @hapic.handle_exception(NotAuthentificated, HTTPStatus.UNAUTHORIZED)
55
+    @hapic.handle_exception(NotAuthenticated, HTTPStatus.UNAUTHORIZED)
56 56
     @hapic.output_body(UserSchema(),)
57 57
     def whoami(self, context, request: TracimRequest, hapic_data=None):
58 58
         """

+ 42 - 0
tracim/views/core_api/system_controller.py View File

@@ -0,0 +1,42 @@
1
+# coding=utf-8
2
+from pyramid.config import Configurator
3
+
4
+from tracim.exceptions import NotAuthenticated, InsufficientUserProfile
5
+from tracim.lib.utils.authorization import require_profile
6
+from tracim.models import Group
7
+from tracim.models.applications import applications
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.views.controllers import Controller
17
+from tracim.views.core_api.schemas import ApplicationSchema
18
+
19
+
20
+class SystemController(Controller):
21
+
22
+    @hapic.with_api_doc()
23
+    @hapic.handle_exception(NotAuthenticated, HTTPStatus.UNAUTHORIZED)
24
+    @hapic.handle_exception(InsufficientUserProfile, HTTPStatus.FORBIDDEN)
25
+    @require_profile(Group.TIM_USER)
26
+    @hapic.output_body(ApplicationSchema(many=True),)
27
+    def applications(self, context, request: TracimRequest, hapic_data=None):
28
+        """
29
+        Get list of alls applications installed in this tracim instance.
30
+        """
31
+        return applications
32
+
33
+    def bind(self, configurator: Configurator) -> None:
34
+        """
35
+        Create all routes and views using pyramid configurator
36
+        for this controller
37
+        """
38
+
39
+        # Applications
40
+        configurator.add_route('applications', '/system/applications', request_method='GET')  # nopep8
41
+        configurator.add_view(self.applications, route_name='applications')
42
+

+ 58 - 0
tracim/views/core_api/user_controller.py View File

@@ -0,0 +1,58 @@
1
+from pyramid.config import Configurator
2
+from sqlalchemy.orm.exc import NoResultFound
3
+
4
+from tracim.lib.utils.authorization import require_same_user_or_profile
5
+from tracim.models import Group
6
+from tracim.models.context_models import WorkspaceInContext
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 hapic, TracimRequest
14
+
15
+from tracim.exceptions import NotAuthenticated
16
+from tracim.exceptions import InsufficientUserProfile
17
+from tracim.exceptions import UserDoesNotExist
18
+from tracim.lib.core.workspace import WorkspaceApi
19
+from tracim.views.controllers import Controller
20
+from tracim.views.core_api.schemas import UserIdPathSchema
21
+from tracim.views.core_api.schemas import WorkspaceDigestSchema
22
+
23
+
24
+class UserController(Controller):
25
+
26
+    @hapic.with_api_doc()
27
+    @hapic.handle_exception(NotAuthenticated, HTTPStatus.UNAUTHORIZED)
28
+    @hapic.handle_exception(InsufficientUserProfile, HTTPStatus.FORBIDDEN)
29
+    @hapic.handle_exception(UserDoesNotExist, HTTPStatus.NOT_FOUND)
30
+    @require_same_user_or_profile(Group.TIM_ADMIN)
31
+    @hapic.input_path(UserIdPathSchema())
32
+    @hapic.output_body(WorkspaceDigestSchema(many=True),)
33
+    def user_workspace(self, context, request: TracimRequest, hapic_data=None):
34
+        """
35
+        Get list of user workspaces
36
+        """
37
+        app_config = request.registry.settings['CFG']
38
+        wapi = WorkspaceApi(
39
+            current_user=request.current_user,  # User
40
+            session=request.dbsession,
41
+            config=app_config,
42
+        )
43
+        
44
+        workspaces = wapi.get_all_for_user(request.candidate_user)
45
+        return [
46
+            WorkspaceInContext(workspace, request.dbsession, app_config)
47
+            for workspace in workspaces
48
+        ]
49
+
50
+    def bind(self, configurator: Configurator) -> None:
51
+        """
52
+        Create all routes and views using pyramid configurator
53
+        for this controller
54
+        """
55
+
56
+        # Applications
57
+        configurator.add_route('user_workspace', '/users/{user_id}/workspaces', request_method='GET')  # nopep8
58
+        configurator.add_view(self.user_workspace, route_name='user_workspace')

+ 90 - 0
tracim/views/core_api/workspace_controller.py View File

@@ -0,0 +1,90 @@
1
+import typing
2
+
3
+from pyramid.config import Configurator
4
+from sqlalchemy.orm.exc import NoResultFound
5
+
6
+from tracim.lib.core.userworkspace import RoleApi
7
+from tracim.lib.utils.authorization import require_workspace_role
8
+from tracim.models.context_models import WorkspaceInContext
9
+from tracim.models.context_models import UserRoleWorkspaceInContext
10
+from tracim.models.data import UserRoleInWorkspace
11
+
12
+try:  # Python 3.5+
13
+    from http import HTTPStatus
14
+except ImportError:
15
+    from http import client as HTTPStatus
16
+
17
+from tracim import hapic, TracimRequest
18
+from tracim.exceptions import NotAuthenticated
19
+from tracim.exceptions import InsufficientUserProfile
20
+from tracim.exceptions import WorkspaceNotFound
21
+from tracim.lib.core.user import UserApi
22
+from tracim.lib.core.workspace import WorkspaceApi
23
+from tracim.views.controllers import Controller
24
+from tracim.views.core_api.schemas import WorkspaceSchema
25
+from tracim.views.core_api.schemas import UserSchema
26
+from tracim.views.core_api.schemas import WorkspaceIdPathSchema
27
+from tracim.views.core_api.schemas import WorkspaceMemberSchema
28
+
29
+class WorkspaceController(Controller):
30
+
31
+    @hapic.with_api_doc()
32
+    @hapic.handle_exception(NotAuthenticated, HTTPStatus.UNAUTHORIZED)
33
+    @hapic.handle_exception(InsufficientUserProfile, HTTPStatus.FORBIDDEN)
34
+    @hapic.handle_exception(WorkspaceNotFound, HTTPStatus.FORBIDDEN)
35
+    @require_workspace_role(UserRoleInWorkspace.READER)
36
+    @hapic.input_path(WorkspaceIdPathSchema())
37
+    @hapic.output_body(WorkspaceSchema())
38
+    def workspace(self, context, request: TracimRequest, hapic_data=None):
39
+        """
40
+        Get workspace informations
41
+        """
42
+        wid = hapic_data.path['workspace_id']
43
+        app_config = request.registry.settings['CFG']
44
+        wapi = WorkspaceApi(
45
+            current_user=request.current_user,  # User
46
+            session=request.dbsession,
47
+            config=app_config,
48
+        )
49
+        return wapi.get_workspace_with_context(request.current_workspace)
50
+
51
+    @hapic.with_api_doc()
52
+    @hapic.handle_exception(NotAuthenticated, HTTPStatus.UNAUTHORIZED)
53
+    @hapic.handle_exception(InsufficientUserProfile, HTTPStatus.FORBIDDEN)
54
+    @hapic.handle_exception(WorkspaceNotFound, HTTPStatus.FORBIDDEN)
55
+    @require_workspace_role(UserRoleInWorkspace.READER)
56
+    @hapic.input_path(WorkspaceIdPathSchema())
57
+    @hapic.output_body(WorkspaceMemberSchema(many=True))
58
+    def workspaces_members(
59
+            self,
60
+            context,
61
+            request: TracimRequest,
62
+            hapic_data=None
63
+    ) -> typing.List[UserRoleWorkspaceInContext]:
64
+        """
65
+        Get Members of this workspace
66
+        """
67
+        app_config = request.registry.settings['CFG']
68
+        rapi = RoleApi(
69
+            current_user=request.current_user,
70
+            session=request.dbsession,
71
+            config=app_config,
72
+        )
73
+        
74
+        roles = rapi.get_all_for_workspace(request.current_workspace)
75
+        return [
76
+            rapi.get_user_role_workspace_with_context(user_role)
77
+            for user_role in roles
78
+        ]
79
+
80
+    def bind(self, configurator: Configurator) -> None:
81
+        """
82
+        Create all routes and views using
83
+        pyramid configurator for this controller
84
+        """
85
+
86
+        # Applications
87
+        configurator.add_route('workspace', '/workspaces/{workspace_id}', request_method='GET')  # nopep8
88
+        configurator.add_view(self.workspace, route_name='workspace')
89
+        configurator.add_route('workspace_members', '/workspaces/{workspace_id}/members', request_method='GET')  # nopep8
90
+        configurator.add_view(self.workspaces_members, route_name='workspace_members')  # nopep8