Browse Source

add user workspace view + many changes to make it work

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

+ 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
 ]
38 39
 
39 40
 tests_require = [

+ 3 - 0
tracim/__init__.py View File

@@ -16,6 +16,7 @@ from tracim.lib.utils.authorization import TRACIM_DEFAULT_PERM
16 16
 from tracim.views import BASE_API_V2
17 17
 from tracim.views.core_api.session_controller import SessionController
18 18
 from tracim.views.core_api.system_controller import SystemController
19
+from tracim.views.core_api.user_controller import UserController
19 20
 from tracim.views.errors import ErrorSchema
20 21
 from tracim.lib.utils.cors import add_cors_support
21 22
 
@@ -59,8 +60,10 @@ def main(global_config, **settings):
59 60
     # Add controllers
60 61
     session_api = SessionController()
61 62
     system_api = SystemController()
63
+    user_api = UserController()
62 64
     configurator.include(session_api.bind, route_prefix=BASE_API_V2)
63 65
     configurator.include(system_api.bind, route_prefix=BASE_API_V2)
66
+    configurator.include(user_api.bind, route_prefix=BASE_API_V2)
64 67
     hapic.add_documentation_view(
65 68
         '/api/v2/doc',
66 69
         'Tracim v2 API',

+ 1 - 0
tracim/exceptions.py View File

@@ -92,5 +92,6 @@ class AuthenticationFailed(TracimException):
92 92
 class WrongUserPassword(TracimException):
93 93
     pass
94 94
 
95
+
95 96
 class UserNotExist(TracimException):
96 97
     pass

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

@@ -40,9 +40,21 @@ class Content(Fixture):
40 40
         )
41 41
 
42 42
         # 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)
43
+        w1 = admin_workspace_api.create_workspace(
44
+            'w1',
45
+            description='This is a workspace',
46
+            save_now=True
47
+        )
48
+        w2 = bob_workspace_api.create_workspace(
49
+            'w2',
50
+            description='A great workspace',
51
+            save_now=True
52
+        )
53
+        w3 = admin_workspace_api.create_workspace(
54
+            'w3',
55
+            description='Just another workspace',
56
+            save_now=True
57
+        )
46 58
 
47 59
         # Workspaces roles
48 60
         role_api.create_one(

+ 11 - 1
tracim/lib/core/user.py View File

@@ -7,7 +7,7 @@ import typing as typing
7 7
 from sqlalchemy.orm import Session
8 8
 
9 9
 from tracim import CFG
10
-from tracim.models.auth import User
10
+from tracim.models.auth import User, Group
11 11
 from sqlalchemy.orm.exc import NoResultFound
12 12
 from tracim.exceptions import WrongUserPassword, UserNotExist
13 13
 from tracim.exceptions import AuthenticationFailed
@@ -98,6 +98,16 @@ class UserApi(object):
98 98
         except (WrongUserPassword, NoResultFound):
99 99
             raise AuthenticationFailed()
100 100
 
101
+    def can_see_private_info_of_user(self, user: User):
102
+        """
103
+        Return boolean wheter current api user has right
104
+        to see private information of a user.
105
+        :param user:
106
+        :return:
107
+        """
108
+        return self._user and (
109
+                self._user.user_id == user.user_id or
110
+                self._user.profile.id >= Group.TIM_ADMIN)
101 111
     # Actions
102 112
 
103 113
     def update(

+ 8 - 17
tracim/models/applications.py View File

@@ -14,7 +14,7 @@ class Application(object):
14 14
             hexcolor: str,
15 15
             is_active: bool,
16 16
             config: typing.Dict[str, str],
17
-            routes: typing.List[str],
17
+            main_route: str,
18 18
     ) -> None:
19 19
         self.label = label
20 20
         self.slug = slug
@@ -22,7 +22,7 @@ class Application(object):
22 22
         self.hexcolor = hexcolor
23 23
         self.is_active = is_active
24 24
         self.config = config
25
-        self.routes = routes
25
+        self.main_route = main_route
26 26
 
27 27
 
28 28
 # TODO - G.M - 21-05-2018 Do not rely on hardcoded app list
@@ -34,9 +34,7 @@ calendar = Application(
34 34
     hexcolor='#757575',
35 35
     is_active=True,
36 36
     config={},
37
-    routes=[
38
-        '/#/workspaces/{workspace_id}/calendar',
39
-    ],
37
+    main_route='/#/workspaces/{workspace_id}/calendar',
40 38
 )
41 39
 
42 40
 thread = Application(
@@ -46,9 +44,8 @@ thread = Application(
46 44
     hexcolor='#ad4cf9',
47 45
     is_active=True,
48 46
     config={},
49
-    routes=[
50
-        '/#/workspaces/{workspace_id}/contents?type=thread',
51
-    ],
47
+    main_route='/#/workspaces/{workspace_id}/contents?type=thread',
48
+
52 49
 )
53 50
 
54 51
 file = Application(
@@ -58,9 +55,7 @@ file = Application(
58 55
     hexcolor='#FF9900',
59 56
     is_active=True,
60 57
     config={},
61
-    routes=[
62
-        '/#/workspaces/{workspace_id}/contents?type=file',
63
-    ],
58
+    main_route='/#/workspaces/{workspace_id}/contents?type=file',
64 59
 )
65 60
 
66 61
 pagemarkdownplus = Application(
@@ -70,9 +65,7 @@ pagemarkdownplus = Application(
70 65
     hexcolor='#f12d2d',
71 66
     is_active=True,
72 67
     config={},
73
-    routes=[
74
-        '/#/workspaces/{workspace_id}/contents?type=file',
75
-    ],
68
+    main_route='/#/workspaces/{workspace_id}/contents?type=pagemarkdownplus',
76 69
 )
77 70
 
78 71
 pagehtml = Application(
@@ -82,9 +75,7 @@ pagehtml = Application(
82 75
     hexcolor='#3f52e3',
83 76
     is_active=True,
84 77
     config={},
85
-    routes=[
86
-        '/#/workspaces/{workspace_id}/contents?type=file',
87
-    ],
78
+    main_route='/#/workspaces/{workspace_id}/contents?type=pagehtml',
88 79
 )
89 80
 # List of applications
90 81
 applications = [

+ 61 - 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
11
+from tracim.models.workspace_menu_entries import default_workspace_menu_entry, \
12
+    WorkspaceMenuEntry
9 13
 
10 14
 
11 15
 class LoginCredentials(object):
@@ -74,3 +78,60 @@ 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)

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

@@ -173,6 +173,7 @@ class UserRoleInWorkspace(DeclarativeBase):
173 173
             UserRoleInWorkspace.WORKSPACE_MANAGER
174 174
         ]
175 175
 
176
+
176 177
 class RoleType(object):
177 178
     def __init__(self, role_id):
178 179
         self.role_type_id = role_id

+ 34 - 1
tracim/models/workspace_menu_entries.py View File

@@ -1,4 +1,9 @@
1 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
2 7
 
3 8
 
4 9
 class WorkspaceMenuEntry(object):
@@ -29,10 +34,38 @@ dashboard_menu_entry = WorkspaceMenuEntry(
29 34
 )
30 35
 all_content_menu_entry = WorkspaceMenuEntry(
31 36
   slug="contents/all",
32
-  label="Tous les contenus",
37
+  label="All Contents",
33 38
   route="/#/workspaces/{workspace_id}/contents",
34 39
   hexcolor="#fdfdfd",
35 40
   icon="",
36 41
 )
37 42
 
38 43
 
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
+                icon=app.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

+ 58 - 11
tracim/tests/functional/test_user.py View File

@@ -1,8 +1,13 @@
1 1
 # coding=utf-8
2 2
 from tracim.tests import FunctionalTest
3
+from tracim.fixtures.content import Content as ContentFixtures
4
+from tracim.fixtures.users_and_groups import Base as BaseFixture
3 5
 
4 6
 
5 7
 class TestUserWorkspaceEndpoint(FunctionalTest):
8
+
9
+    fixtures = [BaseFixture, ContentFixtures]
10
+
6 11
     def test_api__get_user_workspaces__ok_200__nominal_case(self):
7 12
         self.testapp.authorization = (
8 13
             'Basic',
@@ -11,21 +16,63 @@ class TestUserWorkspaceEndpoint(FunctionalTest):
11 16
                 'admin@admin.admin'
12 17
             )
13 18
         )
14
-        res = self.testapp.post_json('/api/v2/users/1/workspaces', status=200)
19
+        res = self.testapp.get('/api/v2/users/1/workspaces', status=200)
20
+        res = res.json_body
15 21
         workspace = res[0]
16 22
         assert workspace['id'] == 1
17 23
         assert workspace['slug'] == 'w1'
18 24
         assert workspace['label'] == 'w1'
19
-        assert workspace['description'] == 'Just another description'
20
-        assert len(workspace['sidebar_entries']) == 3  # TODO change this
25
+        assert workspace['description'] == 'This is a workspace'
26
+        assert len(workspace['sidebar_entries']) == 7  # TODO change this
21 27
 
22 28
         sidebar_entry = workspace['sidebar_entries'][0]
23
-        assert sidebar_entry['slug'] == 'markdown-pages'
24
-        assert sidebar_entry['label'] == 'Document Markdown'
25
-        assert sidebar_entry['route'] == "/#/workspace/{workspace_id}/contents/?type=mardown-page"  # nopep8
26
-        assert sidebar_entry['hexcolor'] == "#F0F9DC"
29
+        assert sidebar_entry['slug'] == 'dashboard'
30
+        assert sidebar_entry['label'] == 'Dashboard'
31
+        assert sidebar_entry['route'] == '/#/workspaces/1/dashboard'  # nopep8
32
+        assert sidebar_entry['hexcolor'] == "#252525"
33
+        assert sidebar_entry['icon'] == ""
34
+
35
+        sidebar_entry = workspace['sidebar_entries'][1]
36
+        assert sidebar_entry['slug'] == 'contents/all'
37
+        assert sidebar_entry['label'] == 'All Contents'
38
+        assert sidebar_entry['route'] == "/#/workspaces/1/contents"  # nopep8
39
+        assert sidebar_entry['hexcolor'] == "#fdfdfd"
40
+        assert sidebar_entry['icon'] == ""
41
+
42
+        sidebar_entry = workspace['sidebar_entries'][2]
43
+        assert sidebar_entry['slug'] == 'contents/pagehtml'
44
+        assert sidebar_entry['label'] == 'Text Documents'
45
+        assert sidebar_entry['route'] == '/#/workspaces/1/contents?type=pagehtml'  # nopep8
46
+        assert sidebar_entry['hexcolor'] == "#3f52e3"
27 47
         assert sidebar_entry['icon'] == "file-text-o"
28
-        # TODO To this for the other
48
+
49
+        sidebar_entry = workspace['sidebar_entries'][3]
50
+        assert sidebar_entry['slug'] == 'contents/pagemarkdownplus'
51
+        assert sidebar_entry['label'] == 'Rich Markdown Files'
52
+        assert sidebar_entry['route'] == "/#/workspaces/1/contents?type=pagemarkdownplus"    # nopep8
53
+        assert sidebar_entry['hexcolor'] == "#f12d2d"
54
+        assert sidebar_entry['icon'] == "file-code"
55
+
56
+        sidebar_entry = workspace['sidebar_entries'][4]
57
+        assert sidebar_entry['slug'] == 'contents/files'
58
+        assert sidebar_entry['label'] == 'Files'
59
+        assert sidebar_entry['route'] == "/#/workspaces/1/contents?type=file"  # nopep8
60
+        assert sidebar_entry['hexcolor'] == "#FF9900"
61
+        assert sidebar_entry['icon'] == "paperclip"
62
+
63
+        sidebar_entry = workspace['sidebar_entries'][5]
64
+        assert sidebar_entry['slug'] == 'contents/threads'
65
+        assert sidebar_entry['label'] == 'Threads'
66
+        assert sidebar_entry['route'] == "/#/workspaces/1/contents?type=thread"  # nopep8
67
+        assert sidebar_entry['hexcolor'] == "#ad4cf9"
68
+        assert sidebar_entry['icon'] == "comments-o"
69
+
70
+        sidebar_entry = workspace['sidebar_entries'][6]
71
+        assert sidebar_entry['slug'] == 'calendar'
72
+        assert sidebar_entry['label'] == 'Calendar'
73
+        assert sidebar_entry['route'] == "/#/workspaces/1/calendar"  # nopep8
74
+        assert sidebar_entry['hexcolor'] == "#757575"
75
+        assert sidebar_entry['icon'] == "calendar-alt"
29 76
 
30 77
     def test_api__get_user_workspaces__err_403__unallowed_user(self):
31 78
         self.testapp.authorization = (
@@ -35,7 +82,7 @@ class TestUserWorkspaceEndpoint(FunctionalTest):
35 82
                 'foobarbaz'
36 83
             )
37 84
         )
38
-        res = self.testapp.post_json('/api/v2/users/1/workspaces', status=403)
85
+        res = self.testapp.get('/api/v2/users/1/workspaces', status=403)
39 86
         assert isinstance(res.json, dict)
40 87
         assert 'code' in res.json.keys()
41 88
         assert 'message' in res.json.keys()
@@ -49,7 +96,7 @@ class TestUserWorkspaceEndpoint(FunctionalTest):
49 96
                 'lapin'
50 97
             )
51 98
         )
52
-        res = self.testapp.post_json('/api/v2/users/1/workspaces', status=401)
99
+        res = self.testapp.get('/api/v2/users/1/workspaces', status=401)
53 100
         assert isinstance(res.json, dict)
54 101
         assert 'code' in res.json.keys()
55 102
         assert 'message' in res.json.keys()
@@ -63,7 +110,7 @@ class TestUserWorkspaceEndpoint(FunctionalTest):
63 110
                 'admin@admin.admin'
64 111
             )
65 112
         )
66
-        res = self.testapp.post_json('/api/v2/users/5/workspaces', status=404)
113
+        res = self.testapp.get('/api/v2/users/5/workspaces', status=404)
67 114
         assert isinstance(res.json, dict)
68 115
         assert 'code' in res.json.keys()
69 116
         assert 'message' in res.json.keys()

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


+ 4 - 1
tracim/views/core_api/schemas.py View File

@@ -32,6 +32,10 @@ class UserSchema(marshmallow.Schema):
32 32
     )
33 33
 
34 34
 
35
+class UserIdPathSchema(marshmallow.Schema):
36
+    user_id = marshmallow.fields.Int()
37
+
38
+
35 39
 class BasicAuthSchema(marshmallow.Schema):
36 40
 
37 41
     email = marshmallow.fields.Email(required=True)
@@ -68,7 +72,6 @@ class WorkspaceSchema(marshmallow.Schema):
68 72
         many=True,
69 73
     )
70 74
 
71
-
72 75
 class WorkspaceDigestSchema(marshmallow.Schema):
73 76
     id = marshmallow.fields.Int()
74 77
     label = marshmallow.fields.String()

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

@@ -0,0 +1,68 @@
1
+from pyramid.config import Configurator
2
+from sqlalchemy.orm.exc import NoResultFound
3
+
4
+from tracim.models.context_models import WorkspaceInContext
5
+
6
+try:  # Python 3.5+
7
+    from http import HTTPStatus
8
+except ImportError:
9
+    from http import client as HTTPStatus
10
+
11
+from tracim import hapic, TracimRequest
12
+from tracim.exceptions import NotAuthentificated, InsufficientUserProfile, \
13
+    UserNotExist
14
+from tracim.lib.core.user import UserApi
15
+from tracim.lib.core.workspace import WorkspaceApi
16
+from tracim.views.controllers import Controller
17
+from tracim.views.core_api.schemas import WorkspaceSchema, UserSchema, \
18
+    UserIdPathSchema
19
+
20
+
21
+class UserController(Controller):
22
+
23
+    @hapic.with_api_doc()
24
+    @hapic.input_path(UserIdPathSchema())
25
+    @hapic.handle_exception(NotAuthentificated, HTTPStatus.UNAUTHORIZED)
26
+    @hapic.handle_exception(InsufficientUserProfile, HTTPStatus.FORBIDDEN)
27
+    @hapic.handle_exception(UserNotExist, HTTPStatus.NOT_FOUND)
28
+    @hapic.output_body(WorkspaceSchema(many=True),)
29
+    def user_workspace(self, context, request: TracimRequest, hapic_data=None):
30
+        """
31
+        Get list of user workspaces
32
+        """
33
+        uid = hapic_data.path['user_id']
34
+        app_config = request.registry.settings['CFG']
35
+        uapi = UserApi(
36
+            request.current_user,
37
+            session=request.dbsession,
38
+            config=app_config,
39
+        )
40
+        wapi = WorkspaceApi(
41
+            current_user=request.current_user,  # User
42
+            session=request.dbsession,
43
+        )
44
+        # TODO - G.M - 22-05-2018 - Refactor this in a more lib way( avoid
45
+        # try/catch and complex code here).
46
+        try:
47
+            user = uapi.get_one(uid)
48
+        except NoResultFound:
49
+            raise UserNotExist()
50
+        if not uapi.can_see_private_info_of_user(user):
51
+            raise InsufficientUserProfile()
52
+        workspaces = wapi.get_all_for_user(user)
53
+        workspaces_in_context = []
54
+        for workspace in workspaces:
55
+            workspaces_in_context.append(
56
+                WorkspaceInContext(workspace, request.dbsession, app_config)
57
+            )
58
+        return workspaces_in_context
59
+
60
+    def bind(self, configurator: Configurator) -> None:
61
+        """
62
+        Create all routes and views using pyramid configurator
63
+        for this controller
64
+        """
65
+
66
+        # Applications
67
+        configurator.add_route('user_workspace', '/users/{user_id}/workspaces', request_method='GET')  # nopep8
68
+        configurator.add_view(self.user_workspace, route_name='user_workspace')