浏览代码

add user workspace view + many changes to make it work

Guénaël Muller 7 年前
父节点
当前提交
ddfb06b572

+ 1 - 0
setup.py 查看文件

34
     # others
34
     # others
35
     'filedepot',
35
     'filedepot',
36
     'babel',
36
     'babel',
37
+    'python-slugify',
37
 ]
38
 ]
38
 
39
 
39
 tests_require = [
40
 tests_require = [

+ 3 - 0
tracim/__init__.py 查看文件

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

+ 1 - 0
tracim/exceptions.py 查看文件

92
 class WrongUserPassword(TracimException):
92
 class WrongUserPassword(TracimException):
93
     pass
93
     pass
94
 
94
 
95
+
95
 class UserNotExist(TracimException):
96
 class UserNotExist(TracimException):
96
     pass
97
     pass

+ 15 - 3
tracim/fixtures/content.py 查看文件

40
         )
40
         )
41
 
41
 
42
         # Workspaces
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
         # Workspaces roles
59
         # Workspaces roles
48
         role_api.create_one(
60
         role_api.create_one(

+ 11 - 1
tracim/lib/core/user.py 查看文件

7
 from sqlalchemy.orm import Session
7
 from sqlalchemy.orm import Session
8
 
8
 
9
 from tracim import CFG
9
 from tracim import CFG
10
-from tracim.models.auth import User
10
+from tracim.models.auth import User, Group
11
 from sqlalchemy.orm.exc import NoResultFound
11
 from sqlalchemy.orm.exc import NoResultFound
12
 from tracim.exceptions import WrongUserPassword, UserNotExist
12
 from tracim.exceptions import WrongUserPassword, UserNotExist
13
 from tracim.exceptions import AuthenticationFailed
13
 from tracim.exceptions import AuthenticationFailed
98
         except (WrongUserPassword, NoResultFound):
98
         except (WrongUserPassword, NoResultFound):
99
             raise AuthenticationFailed()
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
     # Actions
111
     # Actions
102
 
112
 
103
     def update(
113
     def update(

+ 8 - 17
tracim/models/applications.py 查看文件

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

+ 61 - 0
tracim/models/context_models.py 查看文件

2
 import typing
2
 import typing
3
 from datetime import datetime
3
 from datetime import datetime
4
 
4
 
5
+from slugify import slugify
5
 from sqlalchemy.orm import Session
6
 from sqlalchemy.orm import Session
6
 from tracim import CFG
7
 from tracim import CFG
7
 from tracim.models import User
8
 from tracim.models import User
8
 from tracim.models.auth import Profile
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
 class LoginCredentials(object):
15
 class LoginCredentials(object):
74
     def avatar_url(self) -> typing.Optional[str]:
78
     def avatar_url(self) -> typing.Optional[str]:
75
         # TODO - G-M - 20-04-2018 - [Avatar] Add user avatar feature
79
         # TODO - G-M - 20-04-2018 - [Avatar] Add user avatar feature
76
         return None
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 查看文件

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

+ 34 - 1
tracim/models/workspace_menu_entries.py 查看文件

1
 # coding=utf-8
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
 class WorkspaceMenuEntry(object):
9
 class WorkspaceMenuEntry(object):
29
 )
34
 )
30
 all_content_menu_entry = WorkspaceMenuEntry(
35
 all_content_menu_entry = WorkspaceMenuEntry(
31
   slug="contents/all",
36
   slug="contents/all",
32
-  label="Tous les contenus",
37
+  label="All Contents",
33
   route="/#/workspaces/{workspace_id}/contents",
38
   route="/#/workspaces/{workspace_id}/contents",
34
   hexcolor="#fdfdfd",
39
   hexcolor="#fdfdfd",
35
   icon="",
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 查看文件

1
 # coding=utf-8
1
 # coding=utf-8
2
 from tracim.tests import FunctionalTest
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
 class TestUserWorkspaceEndpoint(FunctionalTest):
7
 class TestUserWorkspaceEndpoint(FunctionalTest):
8
+
9
+    fixtures = [BaseFixture, ContentFixtures]
10
+
6
     def test_api__get_user_workspaces__ok_200__nominal_case(self):
11
     def test_api__get_user_workspaces__ok_200__nominal_case(self):
7
         self.testapp.authorization = (
12
         self.testapp.authorization = (
8
             'Basic',
13
             'Basic',
11
                 'admin@admin.admin'
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
         workspace = res[0]
21
         workspace = res[0]
16
         assert workspace['id'] == 1
22
         assert workspace['id'] == 1
17
         assert workspace['slug'] == 'w1'
23
         assert workspace['slug'] == 'w1'
18
         assert workspace['label'] == 'w1'
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
         sidebar_entry = workspace['sidebar_entries'][0]
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
         assert sidebar_entry['icon'] == "file-text-o"
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
     def test_api__get_user_workspaces__err_403__unallowed_user(self):
77
     def test_api__get_user_workspaces__err_403__unallowed_user(self):
31
         self.testapp.authorization = (
78
         self.testapp.authorization = (
35
                 'foobarbaz'
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
         assert isinstance(res.json, dict)
86
         assert isinstance(res.json, dict)
40
         assert 'code' in res.json.keys()
87
         assert 'code' in res.json.keys()
41
         assert 'message' in res.json.keys()
88
         assert 'message' in res.json.keys()
49
                 'lapin'
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
         assert isinstance(res.json, dict)
100
         assert isinstance(res.json, dict)
54
         assert 'code' in res.json.keys()
101
         assert 'code' in res.json.keys()
55
         assert 'message' in res.json.keys()
102
         assert 'message' in res.json.keys()
63
                 'admin@admin.admin'
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
         assert isinstance(res.json, dict)
114
         assert isinstance(res.json, dict)
68
         assert 'code' in res.json.keys()
115
         assert 'code' in res.json.keys()
69
         assert 'message' in res.json.keys()
116
         assert 'message' in res.json.keys()

+ 0 - 0
tracim/views/core_api/__init__.py 查看文件


+ 4 - 1
tracim/views/core_api/schemas.py 查看文件

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

+ 68 - 0
tracim/views/core_api/user_controller.py 查看文件

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')