Bläddra i källkod

Add endpoints to create role/update role

Guénaël Muller 6 år sedan
förälder
incheckning
64b5a92d33

+ 25 - 1
tracim/lib/core/user.py Visa fil

@@ -13,7 +13,8 @@ from tracim import CFG
13 13
 from tracim.models.auth import User
14 14
 from tracim.models.auth import Group
15 15
 from sqlalchemy.orm.exc import NoResultFound
16
-from tracim.exceptions import WrongUserPassword, UserDoesNotExist
16
+from tracim.exceptions import UserDoesNotExist
17
+from tracim.exceptions import WrongUserPassword
17 18
 from tracim.exceptions import AuthenticationFailed
18 19
 from tracim.models.context_models import UserInContext
19 20
 
@@ -68,7 +69,17 @@ class UserApi(object):
68 69
             raise UserDoesNotExist('User "{}" not found in database'.format(email)) from exc  # nopep8
69 70
         return user
70 71
 
72
+    def get_one_by_public_name(self, public_name: str) -> User:
73
+        """
74
+        Get one user by public_name
75
+        """
76
+        try:
77
+            user = self._base_query().filter(User.display_name == public_name).one()
78
+        except NoResultFound as exc:
79
+            raise UserDoesNotExist('User "{}" not found in database'.format(public_name)) from exc  # nopep8
80
+        return user
71 81
     # FIXME - G.M - 24-04-2018 - Duplicate method with get_one.
82
+
72 83
     def get_one_by_id(self, id: int) -> User:
73 84
         return self.get_one(user_id=id)
74 85
 
@@ -83,6 +94,19 @@ class UserApi(object):
83 94
     def get_all(self) -> typing.Iterable[User]:
84 95
         return self._session.query(User).order_by(User.display_name).all()
85 96
 
97
+    def find_one(self, user_email_or_public_name: str) -> User:
98
+        user = None
99
+        try:
100
+            user = self.get_one_by_email(email=user_email_or_public_name)
101
+        except UserDoesNotExist:
102
+            # INFO - G.M - 2018-07-183 - we discard this exception in order
103
+            # allow secondary check (public_name)
104
+            pass
105
+
106
+        if not user:
107
+            user = self.get_one_by_public_name(user_email_or_public_name)
108
+
109
+        return user
86 110
     # Check methods
87 111
 
88 112
     def user_with_email_exists(self, email: str) -> bool:

+ 24 - 0
tracim/lib/core/userworkspace.py Visa fil

@@ -3,6 +3,7 @@ import typing
3 3
 
4 4
 from tracim import CFG
5 5
 from tracim.models.context_models import UserRoleWorkspaceInContext
6
+from tracim.models.roles import WorkspaceRoles
6 7
 
7 8
 __author__ = 'damien'
8 9
 
@@ -93,6 +94,29 @@ class RoleApi(object):
93 94
     def get_one(self, user_id: int, workspace_id: int) -> UserRoleInWorkspace:
94 95
         return self._get_one_rsc(user_id, workspace_id).one()
95 96
 
97
+    def update_role(
98
+        self,
99
+        role: UserRoleInWorkspace,
100
+        role_level: int,
101
+        with_notif: typing.Optional[bool] = None,
102
+        save_now: bool=False,
103
+    ):
104
+        """
105
+        Update role of user in this workspace
106
+        :param role: UserRoleInWorkspace object
107
+        :param role_level: level of new role wanted
108
+        :param with_notif: is user notification enabled in this workspace ?
109
+        :param save_now: database flush
110
+        :return: updated role
111
+        """
112
+        role.role = role_level
113
+        if with_notif is not None:
114
+            role.do_notify == with_notif
115
+        if save_now:
116
+            self.save(role)
117
+
118
+        return role
119
+
96 120
     def create_one(
97 121
         self,
98 122
         user: User,

+ 37 - 1
tracim/models/context_models.py Visa fil

@@ -9,7 +9,8 @@ from tracim.models import User
9 9
 from tracim.models.auth import Profile
10 10
 from tracim.models.data import Content
11 11
 from tracim.models.data import ContentRevisionRO
12
-from tracim.models.data import Workspace, UserRoleInWorkspace
12
+from tracim.models.data import Workspace
13
+from tracim.models.data import UserRoleInWorkspace
13 14
 from tracim.models.roles import WorkspaceRoles
14 15
 from tracim.models.workspace_menu_entries import default_workspace_menu_entry
15 16
 from tracim.models.workspace_menu_entries import WorkspaceMenuEntry
@@ -44,6 +45,15 @@ class WorkspaceAndContentPath(object):
44 45
         self.workspace_id = workspace_id
45 46
 
46 47
 
48
+class WorkspaceAndUserPath(object):
49
+    """
50
+    Paths params with workspace id and user_id
51
+    """
52
+    def __init__(self, workspace_id: int, user_id: int):
53
+        self.workspace_id = workspace_id
54
+        self.user_id = workspace_id
55
+
56
+
47 57
 class CommentPath(object):
48 58
     """
49 59
     Paths params with workspace id and content_id and comment_id model
@@ -76,6 +86,32 @@ class ContentFilter(object):
76 86
         self.show_active = bool(show_active)
77 87
 
78 88
 
89
+class RoleUpdate(object):
90
+    """
91
+    Update role
92
+    """
93
+    def __init__(
94
+        self,
95
+        role: str,
96
+    ):
97
+        self.role = role
98
+
99
+
100
+class WorkspaceMemberInvitation(object):
101
+    """
102
+    Workspace Member Invitation
103
+    """
104
+    def __init__(
105
+        self,
106
+        user_id: int,
107
+        user_email_or_public_name: str,
108
+        role: str,
109
+    ):
110
+        self.role = role
111
+        self.user_email_or_public_name = user_email_or_public_name
112
+        self.user_id = user_id
113
+
114
+
79 115
 class WorkspaceUpdate(object):
80 116
     """
81 117
     Update workspace

+ 1 - 1
tracim/models/roles.py Visa fil

@@ -28,7 +28,7 @@ class WorkspaceRoles(Enum):
28 28
         """ Return valid label associated to role"""
29 29
         # TODO - G.M - 2018-06-180 - Make this work correctly
30 30
         return self.slug
31
-    
31
+
32 32
     @classmethod
33 33
     def get_all_valid_role(cls) -> typing.List['WorkspaceRoles']:
34 34
         """

+ 223 - 1
tracim/tests/functional/test_workspaces.py Visa fil

@@ -307,6 +307,228 @@ class TestWorkspaceMembersEndpoint(FunctionalTest):
307 307
         assert 'message' in res.json.keys()
308 308
         assert 'details' in res.json.keys()
309 309
 
310
+    def test_api__create_workspace_member_role__ok_200__user_id(self):
311
+        """
312
+        Create workspace member role
313
+        :return:
314
+        """
315
+        self.testapp.authorization = (
316
+            'Basic',
317
+            (
318
+                'admin@admin.admin',
319
+                'admin@admin.admin'
320
+            )
321
+        )
322
+        # create workspace role
323
+        params = {
324
+            'user_id': 2,
325
+            'user_email_or_public_name': None,
326
+            'role': 'content-manager',
327
+        }
328
+        res = self.testapp.post_json(
329
+            '/api/v2/workspaces/1/members',
330
+            status=200,
331
+            params=params,
332
+        )
333
+        user_role_found = res.json_body
334
+        assert user_role_found['role'] == 'content-manager'
335
+        assert user_role_found['user_id'] == 2
336
+        assert user_role_found['workspace_id'] == 1
337
+
338
+        res = self.testapp.get('/api/v2/workspaces/1/members', status=200).json_body   # nopep8
339
+        assert len(res) == 2
340
+        user_role = res[0]
341
+        assert user_role['role'] == 'workspace-manager'
342
+        assert user_role['user_id'] == 1
343
+        assert user_role['workspace_id'] == 1
344
+        user_role = res[1]
345
+        assert user_role_found == user_role
346
+
347
+    def test_api__create_workspace_member_role__ok_200__user_email(self):
348
+        """
349
+        Create workspace member role
350
+        :return:
351
+        """
352
+        self.testapp.authorization = (
353
+            'Basic',
354
+            (
355
+                'admin@admin.admin',
356
+                'admin@admin.admin'
357
+            )
358
+        )
359
+        # create workspace role
360
+        params = {
361
+            'user_id': None,
362
+            'user_email_or_public_name': 'lawrence-not-real-email@fsf.local',
363
+            'role': 'content-manager',
364
+        }
365
+        res = self.testapp.post_json(
366
+            '/api/v2/workspaces/1/members',
367
+            status=200,
368
+            params=params,
369
+        )
370
+        user_role_found = res.json_body
371
+        assert user_role_found['role'] == 'content-manager'
372
+        assert user_role_found['user_id'] == 2
373
+        assert user_role_found['workspace_id'] == 1
374
+
375
+        res = self.testapp.get('/api/v2/workspaces/1/members', status=200).json_body   # nopep8
376
+        assert len(res) == 2
377
+        user_role = res[0]
378
+        assert user_role['role'] == 'workspace-manager'
379
+        assert user_role['user_id'] == 1
380
+        assert user_role['workspace_id'] == 1
381
+        user_role = res[1]
382
+        assert user_role_found == user_role
383
+
384
+    def test_api__create_workspace_member_role__ok_200__user_public_name(self):
385
+        """
386
+        Create workspace member role
387
+        :return:
388
+        """
389
+        self.testapp.authorization = (
390
+            'Basic',
391
+            (
392
+                'admin@admin.admin',
393
+                'admin@admin.admin'
394
+            )
395
+        )
396
+        # create workspace role
397
+        params = {
398
+            'user_id': None,
399
+            'user_email_or_public_name': 'Lawrence L.',
400
+            'role': 'content-manager',
401
+        }
402
+        res = self.testapp.post_json(
403
+            '/api/v2/workspaces/1/members',
404
+            status=200,
405
+            params=params,
406
+        )
407
+        user_role_found = res.json_body
408
+        assert user_role_found['role'] == 'content-manager'
409
+        assert user_role_found['user_id'] == 2
410
+        assert user_role_found['workspace_id'] == 1
411
+
412
+        res = self.testapp.get('/api/v2/workspaces/1/members', status=200).json_body   # nopep8
413
+        assert len(res) == 2
414
+        user_role = res[0]
415
+        assert user_role['role'] == 'workspace-manager'
416
+        assert user_role['user_id'] == 1
417
+        assert user_role['workspace_id'] == 1
418
+        user_role = res[1]
419
+        assert user_role_found == user_role
420
+
421
+    def test_api__create_workspace_member_role__err_400__nothing(self):
422
+        """
423
+        Create workspace member role
424
+        :return:
425
+        """
426
+        self.testapp.authorization = (
427
+            'Basic',
428
+            (
429
+                'admin@admin.admin',
430
+                'admin@admin.admin'
431
+            )
432
+        )
433
+        # create workspace role
434
+        params = {
435
+            'user_id': None,
436
+            'user_email_or_public_name': None,
437
+            'role': 'content-manager',
438
+        }
439
+        res = self.testapp.post_json(
440
+            '/api/v2/workspaces/1/members',
441
+            status=400,
442
+            params=params,
443
+        )
444
+
445
+    def test_api__create_workspace_member_role__err_400__wrong_user_id(self):
446
+        """
447
+        Create workspace member role
448
+        :return:
449
+        """
450
+        self.testapp.authorization = (
451
+            'Basic',
452
+            (
453
+                'admin@admin.admin',
454
+                'admin@admin.admin'
455
+            )
456
+        )
457
+        # create workspace role
458
+        params = {
459
+            'user_id': 47,
460
+            'user_email_or_public_name': None,
461
+            'role': 'content-manager',
462
+        }
463
+        res = self.testapp.post_json(
464
+            '/api/v2/workspaces/1/members',
465
+            status=400,
466
+            params=params,
467
+        )
468
+
469
+    def test_api__create_workspace_member_role__err_400__wrong_user_email_or_public_name(self):  # nopep8
470
+        """
471
+        Create workspace member role
472
+        :return:
473
+        """
474
+        self.testapp.authorization = (
475
+            'Basic',
476
+            (
477
+                'admin@admin.admin',
478
+                'admin@admin.admin'
479
+            )
480
+        )
481
+        # create workspace role
482
+        params = {
483
+            'user_id': None,
484
+            'user_email_or_public_name': 'nothing@nothing.nothing',
485
+            'role': 'content-manager',
486
+        }
487
+        res = self.testapp.post_json(
488
+            '/api/v2/workspaces/1/members',
489
+            status=400,
490
+            params=params,
491
+        )
492
+
493
+    def test_api__update_workspace_member_role__ok_200__nominal_case(self):
494
+        """
495
+        Update worskpace member role
496
+        """
497
+        # before
498
+        self.testapp.authorization = (
499
+            'Basic',
500
+            (
501
+                'admin@admin.admin',
502
+                'admin@admin.admin'
503
+            )
504
+        )
505
+        res = self.testapp.get('/api/v2/workspaces/1/members', status=200).json_body   # nopep8
506
+        assert len(res) == 1
507
+        user_role = res[0]
508
+        assert user_role['role'] == 'workspace-manager'
509
+        assert user_role['user_id'] == 1
510
+        assert user_role['workspace_id'] == 1
511
+        # update workspace role
512
+        params = {
513
+            'role': 'content-manager',
514
+        }
515
+        res = self.testapp.put_json(
516
+            '/api/v2/workspaces/1/members/1',
517
+            status=200,
518
+            params=params,
519
+        )
520
+        user_role = res.json_body
521
+        assert user_role['role'] == 'content-manager'
522
+        assert user_role['user_id'] == 1
523
+        assert user_role['workspace_id'] == 1
524
+        # after
525
+        res = self.testapp.get('/api/v2/workspaces/1/members', status=200).json_body   # nopep8
526
+        assert len(res) == 1
527
+        user_role = res[0]
528
+        assert user_role['role'] == 'content-manager'
529
+        assert user_role['user_id'] == 1
530
+        assert user_role['workspace_id'] == 1
531
+
310 532
 
311 533
 class TestWorkspaceContents(FunctionalTest):
312 534
     """
@@ -364,7 +586,7 @@ class TestWorkspaceContents(FunctionalTest):
364 586
         assert content['workspace_id'] == 1
365 587
 
366 588
     # Root related
367
-    def test_api__get_workspace_content__ok_200__get_all_root_content__legacy_html_slug(self):
589
+    def test_api__get_workspace_content__ok_200__get_all_root_content__legacy_html_slug(self):  # nopep8
368 590
         """
369 591
         Check obtain workspace all root contents
370 592
         """

+ 41 - 0
tracim/views/core_api/schemas.py Visa fil

@@ -10,13 +10,16 @@ from tracim.models.contents import open_status
10 10
 from tracim.models.contents import ContentTypeLegacy as ContentType
11 11
 from tracim.models.contents import ContentStatusLegacy as ContentStatus
12 12
 from tracim.models.context_models import ContentCreation
13
+from tracim.models.context_models import WorkspaceMemberInvitation
13 14
 from tracim.models.context_models import WorkspaceUpdate
15
+from tracim.models.context_models import RoleUpdate
14 16
 from tracim.models.context_models import CommentCreation
15 17
 from tracim.models.context_models import TextBasedContentUpdate
16 18
 from tracim.models.context_models import SetContentStatus
17 19
 from tracim.models.context_models import CommentPath
18 20
 from tracim.models.context_models import MoveParams
19 21
 from tracim.models.context_models import WorkspaceAndContentPath
22
+from tracim.models.context_models import WorkspaceAndUserPath
20 23
 from tracim.models.context_models import ContentFilter
21 24
 from tracim.models.context_models import LoginCredentials
22 25
 from tracim.models.data import UserRoleInWorkspace
@@ -91,6 +94,15 @@ class ContentIdPathSchema(marshmallow.Schema):
91 94
     content_id = marshmallow.fields.Int(example=6, required=True)
92 95
 
93 96
 
97
+class WorkspaceAndUserIdPathSchema(
98
+    UserIdPathSchema,
99
+    WorkspaceIdPathSchema
100
+):
101
+    @post_load
102
+    def make_path_object(self, data):
103
+        return WorkspaceAndUserPath(**data)
104
+
105
+
94 106
 class WorkspaceAndContentIdPathSchema(
95 107
     WorkspaceIdPathSchema,
96 108
     ContentIdPathSchema
@@ -106,6 +118,7 @@ class CommentsPathSchema(WorkspaceAndContentIdPathSchema):
106 118
         description='id of a comment related to content content_id',
107 119
         required=True
108 120
     )
121
+
109 122
     @post_load
110 123
     def make_path_object(self, data):
111 124
         return CommentPath(**data)
@@ -150,6 +163,34 @@ class FilterContentQuerySchema(marshmallow.Schema):
150 163
 ###
151 164
 
152 165
 
166
+class RoleUpdateSchema(marshmallow.Schema):
167
+    role = marshmallow.fields.String(
168
+        example='contributor',
169
+        validate=OneOf(UserRoleInWorkspace.get_all_role_slug())
170
+    )
171
+
172
+    @post_load
173
+    def make_role(self, data):
174
+        return RoleUpdate(**data)
175
+
176
+
177
+class WorkspaceMemberInviteSchema(RoleUpdateSchema):
178
+    user_id = marshmallow.fields.Int(
179
+        example=5,
180
+        default=None,
181
+        allow_none=True,
182
+    )
183
+    user_email_or_public_name = marshmallow.fields.String(
184
+        example='suri@cate.fr',
185
+        default=None,
186
+        allow_none=True,
187
+    )
188
+
189
+    @post_load
190
+    def make_role(self, data):
191
+        return WorkspaceMemberInvitation(**data)
192
+
193
+
153 194
 class BasicAuthSchema(marshmallow.Schema):
154 195
 
155 196
     email = marshmallow.fields.Email(

+ 91 - 0
tracim/views/core_api/workspace_controller.py Visa fil

@@ -1,6 +1,10 @@
1 1
 import typing
2 2
 import transaction
3 3
 from pyramid.config import Configurator
4
+
5
+from tracim.lib.core.user import UserApi
6
+from tracim.models.roles import WorkspaceRoles
7
+
4 8
 try:  # Python 3.5+
5 9
     from http import HTTPStatus
6 10
 except ImportError:
@@ -20,11 +24,15 @@ from tracim.models.data import ActionDescription
20 24
 from tracim.models.context_models import UserRoleWorkspaceInContext
21 25
 from tracim.models.context_models import ContentInContext
22 26
 from tracim.exceptions import EmptyLabelNotAllowed
27
+from tracim.exceptions import UserDoesNotExist
23 28
 from tracim.exceptions import WorkspacesDoNotMatch
24 29
 from tracim.views.controllers import Controller
25 30
 from tracim.views.core_api.schemas import FilterContentQuerySchema
31
+from tracim.views.core_api.schemas import WorkspaceMemberInviteSchema
32
+from tracim.views.core_api.schemas import RoleUpdateSchema
26 33
 from tracim.views.core_api.schemas import WorkspaceCreationSchema
27 34
 from tracim.views.core_api.schemas import WorkspaceModifySchema
35
+from tracim.views.core_api.schemas import WorkspaceAndUserIdPathSchema
28 36
 from tracim.views.core_api.schemas import ContentMoveSchema
29 37
 from tracim.views.core_api.schemas import NoContentSchema
30 38
 from tracim.views.core_api.schemas import ContentCreationSchema
@@ -128,6 +136,83 @@ class WorkspaceController(Controller):
128 136
         ]
129 137
 
130 138
     @hapic.with_api_doc(tags=[WORKSPACE_ENDPOINTS_TAG])
139
+    @require_workspace_role(UserRoleInWorkspace.WORKSPACE_MANAGER)
140
+    @hapic.input_path(WorkspaceAndUserIdPathSchema())
141
+    @hapic.input_body(RoleUpdateSchema())
142
+    @hapic.output_body(WorkspaceMemberSchema())
143
+    def update_workspaces_members_role(
144
+            self,
145
+            context,
146
+            request: TracimRequest,
147
+            hapic_data=None
148
+    ) -> UserRoleWorkspaceInContext:
149
+        """
150
+        Update Members to this workspace
151
+        """
152
+        app_config = request.registry.settings['CFG']
153
+        rapi = RoleApi(
154
+            current_user=request.current_user,
155
+            session=request.dbsession,
156
+            config=app_config,
157
+        )
158
+
159
+        role = rapi.get_one(
160
+            user_id=hapic_data.path.user_id,
161
+            workspace_id=hapic_data.path.workspace_id,
162
+        )
163
+        workspace_role = WorkspaceRoles.get_role_from_slug(hapic_data.body.role)
164
+        role = rapi.update_role(
165
+            role,
166
+            role_level=workspace_role.level
167
+        )
168
+        return rapi.get_user_role_workspace_with_context(role)
169
+
170
+    @hapic.with_api_doc(tags=[WORKSPACE_ENDPOINTS_TAG])
171
+    @require_workspace_role(UserRoleInWorkspace.WORKSPACE_MANAGER)
172
+    @hapic.input_path(WorkspaceIdPathSchema())
173
+    @hapic.input_body(WorkspaceMemberInviteSchema())
174
+    @hapic.output_body(WorkspaceMemberSchema())
175
+    def create_workspaces_members_role(
176
+            self,
177
+            context,
178
+            request: TracimRequest,
179
+            hapic_data=None
180
+    ) -> UserRoleWorkspaceInContext:
181
+        """
182
+        Add Members to this workspace
183
+        """
184
+        app_config = request.registry.settings['CFG']
185
+        rapi = RoleApi(
186
+            current_user=request.current_user,
187
+            session=request.dbsession,
188
+            config=app_config,
189
+        )
190
+        uapi = UserApi(
191
+            current_user=request.current_user,
192
+            session=request.dbsession,
193
+            config=app_config,
194
+        )
195
+        if hapic_data.body.user_id is not None:
196
+            user = uapi.get_one(user_id=hapic_data.body.user_id)
197
+        elif hapic_data.body.user_email_or_public_name:
198
+            user = uapi.find_one(
199
+                user_email_or_public_name=hapic_data.body.user_email_or_public_name  # nopep8
200
+            )
201
+        else:
202
+            raise UserDoesNotExist(
203
+                'user_id or user_email_or_public_name should have '
204
+                'valid value.'
205
+            )
206
+        role = rapi.create_one(
207
+            user=user,
208
+            workspace=request.current_workspace,
209
+            role_level=WorkspaceRoles.get_role_from_slug(hapic_data.body.role).level,  # nopep8
210
+            with_notif=False,
211
+            flush=True,
212
+        )
213
+        return rapi.get_user_role_workspace_with_context(role)
214
+
215
+    @hapic.with_api_doc(tags=[WORKSPACE_ENDPOINTS_TAG])
131 216
     @require_workspace_role(UserRoleInWorkspace.READER)
132 217
     @hapic.input_path(WorkspaceIdPathSchema())
133 218
     @hapic.input_query(FilterContentQuerySchema())
@@ -388,6 +473,12 @@ class WorkspaceController(Controller):
388 473
         # Workspace Members (Roles)
389 474
         configurator.add_route('workspace_members', '/workspaces/{workspace_id}/members', request_method='GET')  # nopep8
390 475
         configurator.add_view(self.workspaces_members, route_name='workspace_members')  # nopep8
476
+        # Update Workspace Members roles
477
+        configurator.add_route('update_workspace_member', '/workspaces/{workspace_id}/members/{user_id}', request_method='PUT')  # nopep8
478
+        configurator.add_view(self.update_workspaces_members_role, route_name='update_workspace_member')  # nopep8
479
+        # Create Workspace Members roles
480
+        configurator.add_route('create_workspace_member', '/workspaces/{workspace_id}/members', request_method='POST')  # nopep8
481
+        configurator.add_view(self.create_workspaces_members_role, route_name='create_workspace_member')  # nopep8
391 482
         # Workspace Content
392 483
         configurator.add_route('workspace_content', '/workspaces/{workspace_id}/contents', request_method='GET')  # nopep8
393 484
         configurator.add_view(self.workspace_content, route_name='workspace_content')  # nopep8