Browse Source

Merge branch 'develop' of github.com:tracim/tracim_backend into feature/671_read_unread_endpoints

Guénaël Muller 5 years ago
parent
commit
405989f80e

+ 13 - 0
tracim/exceptions.py View File

@@ -163,5 +163,18 @@ class EmptyLabelNotAllowed(EmptyValueNotAllowed):
163 163
 class EmptyCommentContentNotAllowed(EmptyValueNotAllowed):
164 164
     pass
165 165
 
166
+
167
+class RoleDoesNotExist(TracimException):
168
+    pass
169
+
170
+
171
+class EmailValidationFailed(TracimException):
172
+    pass
173
+
174
+
175
+class UserCreationFailed(TracimException):
176
+    pass
177
+
178
+
166 179
 class ParentNotFound(NotFound):
167 180
     pass

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

@@ -6,6 +6,7 @@ import transaction
6 6
 import typing as typing
7 7
 
8 8
 from tracim.exceptions import NotificationNotSend
9
+from tracim.exceptions import EmailValidationFailed
9 10
 from tracim.lib.mail_notifier.notifier import get_email_manager
10 11
 from sqlalchemy.orm import Session
11 12
 
@@ -13,9 +14,11 @@ from tracim import CFG
13 14
 from tracim.models.auth import User
14 15
 from tracim.models.auth import Group
15 16
 from sqlalchemy.orm.exc import NoResultFound
16
-from tracim.exceptions import WrongUserPassword, UserDoesNotExist
17
+from tracim.exceptions import UserDoesNotExist
18
+from tracim.exceptions import WrongUserPassword
17 19
 from tracim.exceptions import AuthenticationFailed
18 20
 from tracim.models.context_models import UserInContext
21
+from tracim.models.context_models import TypeUser
19 22
 
20 23
 
21 24
 class UserApi(object):
@@ -68,7 +71,17 @@ class UserApi(object):
68 71
             raise UserDoesNotExist('User "{}" not found in database'.format(email)) from exc  # nopep8
69 72
         return user
70 73
 
74
+    def get_one_by_public_name(self, public_name: str) -> User:
75
+        """
76
+        Get one user by public_name
77
+        """
78
+        try:
79
+            user = self._base_query().filter(User.display_name == public_name).one()
80
+        except NoResultFound as exc:
81
+            raise UserDoesNotExist('User "{}" not found in database'.format(public_name)) from exc  # nopep8
82
+        return user
71 83
     # FIXME - G.M - 24-04-2018 - Duplicate method with get_one.
84
+
72 85
     def get_one_by_id(self, id: int) -> User:
73 86
         return self.get_one(user_id=id)
74 87
 
@@ -83,6 +96,40 @@ class UserApi(object):
83 96
     def get_all(self) -> typing.Iterable[User]:
84 97
         return self._session.query(User).order_by(User.display_name).all()
85 98
 
99
+    def find(
100
+            self,
101
+            user_id: int=None,
102
+            email: str=None,
103
+            public_name: str=None
104
+    ) -> typing.Tuple[TypeUser, User]:
105
+        """
106
+        Find existing user from all theses params.
107
+        Check is made in this order: user_id, email, public_name
108
+        If no user found raise UserDoesNotExist exception
109
+        """
110
+        user = None
111
+
112
+        if user_id:
113
+            try:
114
+                user = self.get_one(user_id)
115
+                return TypeUser.USER_ID, user
116
+            except UserDoesNotExist:
117
+                pass
118
+        if email:
119
+            try:
120
+                user = self.get_one_by_email(email)
121
+                return TypeUser.EMAIL, user
122
+            except UserDoesNotExist:
123
+                pass
124
+        if public_name:
125
+            try:
126
+                user = self.get_one_by_public_name(public_name)
127
+                return TypeUser.PUBLIC_NAME, user
128
+            except UserDoesNotExist:
129
+                pass
130
+
131
+        raise UserDoesNotExist('User not found with any of given params.')
132
+
86 133
     # Check methods
87 134
 
88 135
     def user_with_email_exists(self, email: str) -> bool:
@@ -112,6 +159,15 @@ class UserApi(object):
112 159
 
113 160
     # Actions
114 161
 
162
+    def _check_email(self, email: str) -> bool:
163
+        # TODO - G.M - 2018-07-05 - find a better way to check email
164
+        if not email:
165
+            return False
166
+        email = email.split('@')
167
+        if len(email) != 2:
168
+            return False
169
+        return True
170
+
115 171
     def update(
116 172
             self,
117 173
             user: User,
@@ -125,6 +181,9 @@ class UserApi(object):
125 181
             user.display_name = name
126 182
 
127 183
         if email is not None:
184
+            email_exist = self._check_email(email)
185
+            if not email_exist:
186
+                raise EmailValidationFailed('Email given form {} is uncorrect'.format(email))  # nopep8
128 187
             user.email = email
129 188
 
130 189
         if password is not None:
@@ -176,7 +235,11 @@ class UserApi(object):
176 235
         """Previous create_user method"""
177 236
         user = User()
178 237
 
238
+        email_exist = self._check_email(email)
239
+        if not email_exist:
240
+            raise EmailValidationFailed('Email given form {} is uncorrect'.format(email))  # nopep8
179 241
         user.email = email
242
+        user.display_name = email.split('@')[0]
180 243
 
181 244
         for group in groups:
182 245
             user.groups.append(group)

+ 108 - 72
tracim/lib/core/userworkspace.py View File

@@ -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
 
@@ -11,40 +12,55 @@ from sqlalchemy.orm import Query
11 12
 from tracim.models.auth import User
12 13
 from tracim.models.data import Workspace
13 14
 from tracim.models.data import UserRoleInWorkspace
14
-from tracim.models.data import RoleType
15 15
 
16 16
 
17 17
 class RoleApi(object):
18 18
 
19
-    ALL_ROLE_VALUES = UserRoleInWorkspace.get_all_role_values()
19
+    # TODO - G.M - 29-06-2018 - [Cleanup] Drop this
20
+    # ALL_ROLE_VALUES = UserRoleInWorkspace.get_all_role_values()
20 21
     # Dict containing readable members roles for given role
21
-    members_read_rights = {
22
-        UserRoleInWorkspace.NOT_APPLICABLE: [],
23
-        UserRoleInWorkspace.READER: [
24
-            UserRoleInWorkspace.WORKSPACE_MANAGER,
25
-        ],
26
-        UserRoleInWorkspace.CONTRIBUTOR: [
27
-            UserRoleInWorkspace.WORKSPACE_MANAGER,
28
-            UserRoleInWorkspace.CONTENT_MANAGER,
29
-            UserRoleInWorkspace.CONTRIBUTOR,
30
-        ],
31
-        UserRoleInWorkspace.CONTENT_MANAGER: [
32
-            UserRoleInWorkspace.WORKSPACE_MANAGER,
33
-            UserRoleInWorkspace.CONTENT_MANAGER,
34
-            UserRoleInWorkspace.CONTRIBUTOR,
35
-            UserRoleInWorkspace.READER,
36
-        ],
37
-        UserRoleInWorkspace.WORKSPACE_MANAGER: [
38
-            UserRoleInWorkspace.WORKSPACE_MANAGER,
39
-            UserRoleInWorkspace.CONTENT_MANAGER,
40
-            UserRoleInWorkspace.CONTRIBUTOR,
41
-            UserRoleInWorkspace.READER,
42
-        ],
43
-    }
22
+    # members_read_rights = {
23
+    #     UserRoleInWorkspace.NOT_APPLICABLE: [],
24
+    #     UserRoleInWorkspace.READER: [
25
+    #         UserRoleInWorkspace.WORKSPACE_MANAGER,
26
+    #     ],
27
+    #     UserRoleInWorkspace.CONTRIBUTOR: [
28
+    #         UserRoleInWorkspace.WORKSPACE_MANAGER,
29
+    #         UserRoleInWorkspace.CONTENT_MANAGER,
30
+    #         UserRoleInWorkspace.CONTRIBUTOR,
31
+    #     ],
32
+    #     UserRoleInWorkspace.CONTENT_MANAGER: [
33
+    #         UserRoleInWorkspace.WORKSPACE_MANAGER,
34
+    #         UserRoleInWorkspace.CONTENT_MANAGER,
35
+    #         UserRoleInWorkspace.CONTRIBUTOR,
36
+    #         UserRoleInWorkspace.READER,
37
+    #     ],
38
+    #     UserRoleInWorkspace.WORKSPACE_MANAGER: [
39
+    #         UserRoleInWorkspace.WORKSPACE_MANAGER,
40
+    #         UserRoleInWorkspace.CONTENT_MANAGER,
41
+    #         UserRoleInWorkspace.CONTRIBUTOR,
42
+    #         UserRoleInWorkspace.READER,
43
+    #     ],
44
+    # }
45
+
46
+    # TODO - G.M - 29-06-2018 - [Cleanup] Drop this
47
+    # @classmethod
48
+    # def role_can_read_member_role(cls, reader_role: int, tested_role: int) \
49
+    #         -> bool:
50
+    #     """
51
+    #     :param reader_role: role as viewer
52
+    #     :param tested_role: role as viwed
53
+    #     :return: True if given role can view member role in workspace.
54
+    #     """
55
+    #     if reader_role in cls.members_read_rights:
56
+    #         return tested_role in cls.members_read_rights[reader_role]
57
+    #     return False
44 58
 
45 59
     def get_user_role_workspace_with_context(
46 60
             self,
47
-            user_role: UserRoleInWorkspace
61
+            user_role: UserRoleInWorkspace,
62
+            newly_created:bool = None,
63
+            email_sent: bool = None,
48 64
     ) -> UserRoleWorkspaceInContext:
49 65
         """
50 66
         Return WorkspaceInContext object from Workspace
@@ -54,27 +70,11 @@ class RoleApi(object):
54 70
             user_role=user_role,
55 71
             dbsession=self._session,
56 72
             config=self._config,
73
+            newly_created=newly_created,
74
+            email_sent=email_sent,
57 75
         )
58 76
         return workspace
59 77
 
60
-    @classmethod
61
-    def role_can_read_member_role(cls, reader_role: int, tested_role: int) \
62
-            -> bool:
63
-        """
64
-        :param reader_role: role as viewer
65
-        :param tested_role: role as viwed
66
-        :return: True if given role can view member role in workspace.
67
-        """
68
-        if reader_role in cls.members_read_rights:
69
-            return tested_role in cls.members_read_rights[reader_role]
70
-        return False
71
-
72
-    @classmethod
73
-    def create_role(cls) -> UserRoleInWorkspace:
74
-        role = UserRoleInWorkspace()
75
-
76
-        return role
77
-
78 78
     def __init__(
79 79
         self,
80 80
         session: Session,
@@ -98,6 +98,29 @@ class RoleApi(object):
98 98
     def get_one(self, user_id: int, workspace_id: int) -> UserRoleInWorkspace:
99 99
         return self._get_one_rsc(user_id, workspace_id).one()
100 100
 
101
+    def update_role(
102
+        self,
103
+        role: UserRoleInWorkspace,
104
+        role_level: int,
105
+        with_notif: typing.Optional[bool] = None,
106
+        save_now: bool=False,
107
+    ):
108
+        """
109
+        Update role of user in this workspace
110
+        :param role: UserRoleInWorkspace object
111
+        :param role_level: level of new role wanted
112
+        :param with_notif: is user notification enabled in this workspace ?
113
+        :param save_now: database flush
114
+        :return: updated role
115
+        """
116
+        role.role = role_level
117
+        if with_notif is not None:
118
+            role.do_notify == with_notif
119
+        if save_now:
120
+            self.save(role)
121
+
122
+        return role
123
+
101 124
     def create_one(
102 125
         self,
103 126
         user: User,
@@ -106,7 +129,7 @@ class RoleApi(object):
106 129
         with_notif: bool,
107 130
         flush: bool=True
108 131
     ) -> UserRoleInWorkspace:
109
-        role = self.create_role()
132
+        role = UserRoleInWorkspace()
110 133
         role.user_id = user.user_id
111 134
         role.workspace = workspace
112 135
         role.role = role_level
@@ -120,20 +143,6 @@ class RoleApi(object):
120 143
         if flush:
121 144
             self._session.flush()
122 145
 
123
-    def _get_all_for_user(self, user_id) -> typing.List[UserRoleInWorkspace]:
124
-        return self._session.query(UserRoleInWorkspace)\
125
-            .filter(UserRoleInWorkspace.user_id == user_id)
126
-
127
-    def get_all_for_user(self, user: User) -> typing.List[UserRoleInWorkspace]:
128
-        return self._get_all_for_user(user.user_id).all()
129
-
130
-    def get_all_for_user_order_by_workspace(
131
-        self,
132
-        user_id: int
133
-    ) -> typing.List[UserRoleInWorkspace]:
134
-        return self._get_all_for_user(user_id)\
135
-            .join(UserRoleInWorkspace.workspace).order_by(Workspace.label).all()
136
-
137 146
     def get_all_for_workspace(
138 147
         self,
139 148
         workspace:Workspace
@@ -145,18 +154,45 @@ class RoleApi(object):
145 154
     def save(self, role: UserRoleInWorkspace) -> None:
146 155
         self._session.flush()
147 156
 
148
-    # TODO - G.M - 07-06-2018 - [Cleanup] Check if this method is already needed
149
-    @classmethod
150
-    def get_roles_for_select_field(cls) -> typing.List[RoleType]:
151
-        """
152
-
153
-        :return: list of DictLikeClass instances representing available Roles
154
-        (to be used in select fields)
155
-        """
156
-        result = list()
157 157
 
158
-        for role_id in UserRoleInWorkspace.get_all_role_values():
159
-            role = RoleType(role_id)
160
-            result.append(role)
158
+    # TODO - G.M - 29-06-2018 - [Cleanup] Drop this
159
+    # @classmethod
160
+    # def role_can_read_member_role(cls, reader_role: int, tested_role: int) \
161
+    #         -> bool:
162
+    #     """
163
+    #     :param reader_role: role as viewer
164
+    #     :param tested_role: role as viwed
165
+    #     :return: True if given role can view member role in workspace.
166
+    #     """
167
+    #     if reader_role in cls.members_read_rights:
168
+    #         return tested_role in cls.members_read_rights[reader_role]
169
+    #     return False
170
+    # def _get_all_for_user(self, user_id) -> typing.List[UserRoleInWorkspace]:
171
+    #     return self._session.query(UserRoleInWorkspace)\
172
+    #         .filter(UserRoleInWorkspace.user_id == user_id)
173
+    #
174
+    # def get_all_for_user(self, user: User) -> typing.List[UserRoleInWorkspace]:
175
+    #     return self._get_all_for_user(user.user_id).all()
176
+    #
177
+    # def get_all_for_user_order_by_workspace(
178
+    #     self,
179
+    #     user_id: int
180
+    # ) -> typing.List[UserRoleInWorkspace]:
181
+    #     return self._get_all_for_user(user_id)\
182
+    #         .join(UserRoleInWorkspace.workspace).order_by(Workspace.label).all()
161 183
 
162
-        return result
184
+    # TODO - G.M - 07-06-2018 - [Cleanup] Check if this method is already needed
185
+    # @classmethod
186
+    # def get_roles_for_select_field(cls) -> typing.List[RoleType]:
187
+    #     """
188
+    #
189
+    #     :return: list of DictLikeClass instances representing available Roles
190
+    #     (to be used in select fields)
191
+    #     """
192
+    #     result = list()
193
+    #
194
+    #     for role_id in UserRoleInWorkspace.get_all_role_values():
195
+    #         role = RoleType(role_id)
196
+    #         result.append(role)
197
+    #
198
+    #     return result

+ 27 - 1
tracim/lib/core/workspace.py View File

@@ -5,6 +5,7 @@ from sqlalchemy.orm import Query
5 5
 from sqlalchemy.orm import Session
6 6
 
7 7
 from tracim import CFG
8
+from tracim.exceptions import EmptyLabelNotAllowed
8 9
 from tracim.lib.utils.translation import fake_translator as _
9 10
 
10 11
 from tracim.lib.core.userworkspace import RoleApi
@@ -69,7 +70,7 @@ class WorkspaceApi(object):
69 70
             save_now: bool=False,
70 71
     ) -> Workspace:
71 72
         if not label:
72
-            label = self.generate_label()
73
+            raise EmptyLabelNotAllowed('Workspace label cannot be empty')
73 74
 
74 75
         workspace = Workspace()
75 76
         workspace.label = label
@@ -105,6 +106,31 @@ class WorkspaceApi(object):
105 106
 
106 107
         return workspace
107 108
 
109
+    def update_workspace(
110
+            self,
111
+            workspace: Workspace,
112
+            label: str,
113
+            description: str,
114
+            save_now: bool=False,
115
+    ) -> Workspace:
116
+        """
117
+        Update workspace
118
+        :param workspace: workspace to update
119
+        :param label: new label of workspace
120
+        :param description: new description
121
+        :param save_now: database flush
122
+        :return: updated workspace
123
+        """
124
+        if not label:
125
+            raise EmptyLabelNotAllowed('Workspace label cannot be empty')
126
+        workspace.label = label
127
+        workspace.description = description
128
+
129
+        if save_now:
130
+            self.save(workspace)
131
+
132
+        return workspace
133
+
108 134
     def get_one(self, id):
109 135
         return self._base_query().filter(Workspace.workspace_id == id).one()
110 136
 

+ 64 - 5
tracim/models/context_models.py View File

@@ -1,6 +1,7 @@
1 1
 # coding=utf-8
2 2
 import typing
3 3
 from datetime import datetime
4
+from enum import Enum
4 5
 
5 6
 from slugify import slugify
6 7
 from sqlalchemy.orm import Session
@@ -9,7 +10,9 @@ from tracim.models import User
9 10
 from tracim.models.auth import Profile
10 11
 from tracim.models.data import Content
11 12
 from tracim.models.data import ContentRevisionRO
12
-from tracim.models.data import Workspace, UserRoleInWorkspace
13
+from tracim.models.data import Workspace
14
+from tracim.models.data import UserRoleInWorkspace
15
+from tracim.models.roles import WorkspaceRoles
13 16
 from tracim.models.workspace_menu_entries import default_workspace_menu_entry
14 17
 from tracim.models.workspace_menu_entries import WorkspaceMenuEntry
15 18
 from tracim.models.contents import ContentTypeLegacy as ContentType
@@ -43,11 +46,11 @@ class WorkspaceAndContentPath(object):
43 46
         self.workspace_id = workspace_id
44 47
 
45 48
 
46
-class UserWorkspacePath(object):
49
+class WorkspaceAndUserPath(object):
47 50
     """
48
-    Paths params with user_id and workspace id model
51
+    Paths params with workspace id and user_id
49 52
     """
50
-    def __init__(self, user_id: int, workspace_id: int) -> None:
53
+    def __init__(self, workspace_id: int, user_id: int):
51 54
         self.workspace_id = workspace_id
52 55
         self.user_id = workspace_id
53 56
 
@@ -120,6 +123,45 @@ class ContentIdsQuery(object):
120 123
         self.contents_ids = contents_ids
121 124
 
122 125
 
126
+class RoleUpdate(object):
127
+    """
128
+    Update role
129
+    """
130
+    def __init__(
131
+        self,
132
+        role: str,
133
+    ):
134
+        self.role = role
135
+
136
+
137
+class WorkspaceMemberInvitation(object):
138
+    """
139
+    Workspace Member Invitation
140
+    """
141
+    def __init__(
142
+        self,
143
+        user_id: int,
144
+        user_email_or_public_name: str,
145
+        role: str,
146
+    ):
147
+        self.role = role
148
+        self.user_email_or_public_name = user_email_or_public_name
149
+        self.user_id = user_id
150
+
151
+
152
+class WorkspaceUpdate(object):
153
+    """
154
+    Update workspace
155
+    """
156
+    def __init__(
157
+        self,
158
+        label: str,
159
+        description: str,
160
+    ):
161
+        self.label = label
162
+        self.description = description
163
+
164
+
123 165
 class ContentCreation(object):
124 166
     """
125 167
     Content creation model
@@ -170,6 +212,13 @@ class TextBasedContentUpdate(object):
170 212
         self.raw_content = raw_content
171 213
 
172 214
 
215
+class TypeUser(Enum):
216
+    """Params used to find user"""
217
+    USER_ID = 'found_id'
218
+    EMAIL = 'found_email'
219
+    PUBLIC_NAME = 'found_public_name'
220
+
221
+
173 222
 class UserInContext(object):
174 223
     """
175 224
     Interface to get User data and User data related to context.
@@ -299,10 +348,16 @@ class UserRoleWorkspaceInContext(object):
299 348
             user_role: UserRoleInWorkspace,
300 349
             dbsession: Session,
301 350
             config: CFG,
351
+            # Extended params
352
+            newly_created: bool = None,
353
+            email_sent: bool = None
302 354
     )-> None:
303 355
         self.user_role = user_role
304 356
         self.dbsession = dbsession
305 357
         self.config = config
358
+        # Extended params
359
+        self.newly_created = newly_created
360
+        self.email_sent = email_sent
306 361
 
307 362
     @property
308 363
     def user_id(self) -> int:
@@ -342,7 +397,11 @@ class UserRoleWorkspaceInContext(object):
342 397
         'contributor', 'content-manager', 'workspace-manager'
343 398
         :return: user workspace role as slug.
344 399
         """
345
-        return UserRoleInWorkspace.SLUG[self.user_role.role]
400
+        return WorkspaceRoles.get_role_from_level(self.user_role.role).slug
401
+
402
+    @property
403
+    def is_active(self) -> bool:
404
+        return self.user.is_active
346 405
 
347 406
     @property
348 407
     def user(self) -> UserInContext:

+ 31 - 32
tracim/models/data.py View File

@@ -30,6 +30,7 @@ from tracim.lib.utils.translation import get_locale
30 30
 from tracim.exceptions import ContentRevisionUpdateError
31 31
 from tracim.models.meta import DeclarativeBase
32 32
 from tracim.models.auth import User
33
+from tracim.models.roles import WorkspaceRoles
33 34
 
34 35
 DEFAULT_PROPERTIES = dict(
35 36
     allowed_content=dict(
@@ -124,26 +125,27 @@ class UserRoleInWorkspace(DeclarativeBase):
124 125
     workspace = relationship('Workspace', remote_side=[Workspace.workspace_id], backref='roles', lazy='joined')
125 126
     user = relationship('User', remote_side=[User.user_id], backref='roles')
126 127
 
127
-    NOT_APPLICABLE = 0
128
-    READER = 1
129
-    CONTRIBUTOR = 2
130
-    CONTENT_MANAGER = 4
131
-    WORKSPACE_MANAGER = 8
132
-
133
-    SLUG = {
134
-        NOT_APPLICABLE: 'not-applicable',
135
-        READER: 'reader',
136
-        CONTRIBUTOR: 'contributor',
137
-        CONTENT_MANAGER: 'content-manager',
138
-        WORKSPACE_MANAGER: 'workspace-manager',
139
-    }
128
+    NOT_APPLICABLE = WorkspaceRoles.NOT_APPLICABLE.level
129
+    READER = WorkspaceRoles.READER.level
130
+    CONTRIBUTOR = WorkspaceRoles.CONTRIBUTOR.level
131
+    CONTENT_MANAGER = WorkspaceRoles.CONTENT_MANAGER.level
132
+    WORKSPACE_MANAGER = WorkspaceRoles.WORKSPACE_MANAGER.level
133
+
134
+    # TODO - G.M - 10-04-2018 - [Cleanup] Drop this
135
+    # SLUG = {
136
+    #     NOT_APPLICABLE: 'not-applicable',
137
+    #     READER: 'reader',
138
+    #     CONTRIBUTOR: 'contributor',
139
+    #     CONTENT_MANAGER: 'content-manager',
140
+    #     WORKSPACE_MANAGER: 'workspace-manager',
141
+    # }
140 142
 
141
-    LABEL = dict()
142
-    LABEL[0] = l_('N/A')
143
-    LABEL[1] = l_('Reader')
144
-    LABEL[2] = l_('Contributor')
145
-    LABEL[4] = l_('Content Manager')
146
-    LABEL[8] = l_('Workspace Manager')
143
+    # LABEL = dict()
144
+    # LABEL[0] = l_('N/A')
145
+    # LABEL[1] = l_('Reader')
146
+    # LABEL[2] = l_('Contributor')
147
+    # LABEL[4] = l_('Content Manager')
148
+    # LABEL[8] = l_('Workspace Manager')
147 149
     # TODO - G.M - 10-04-2018 - [Cleanup] Drop this
148 150
     #
149 151
     # STYLE = dict()
@@ -170,20 +172,18 @@ class UserRoleInWorkspace(DeclarativeBase):
170 172
     #     return UserRoleInWorkspace.STYLE[self.role]
171 173
     #
172 174
 
175
+    def role_object(self):
176
+        return WorkspaceRoles.get_role_from_level(level=self.role)
177
+
173 178
     def role_as_label(self):
174
-        return UserRoleInWorkspace.LABEL[self.role]
179
+        return self.role_object().label
175 180
 
176 181
     @classmethod
177 182
     def get_all_role_values(cls) -> typing.List[int]:
178 183
         """
179 184
         Return all valid role value
180 185
         """
181
-        return [
182
-            UserRoleInWorkspace.READER,
183
-            UserRoleInWorkspace.CONTRIBUTOR,
184
-            UserRoleInWorkspace.CONTENT_MANAGER,
185
-            UserRoleInWorkspace.WORKSPACE_MANAGER
186
-        ]
186
+        return [role.level for role in WorkspaceRoles.get_all_valid_role()]
187 187
 
188 188
     @classmethod
189 189
     def get_all_role_slug(cls) -> typing.List[str]:
@@ -193,13 +193,12 @@ class UserRoleInWorkspace(DeclarativeBase):
193 193
         # INFO - G.M - 25-05-2018 - Be carefull, as long as this method
194 194
         # and get_all_role_values are both used for API, this method should
195 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
-
196
+        return [role.slug for role in WorkspaceRoles.get_all_valid_role()]
198 197
 
199
-class RoleType(object):
200
-    def __init__(self, role_id):
201
-        self.role_type_id = role_id
202
-        # TODO - G.M - 10-04-2018 - [Cleanup] Drop this
198
+# TODO - G.M - 10-04-2018 - [Cleanup] Drop this
199
+# class RoleType(object):
200
+#     def __init__(self, role_id):
201
+#         self.role_type_id = role_id
203 202
         # self.fa_icon = UserRoleInWorkspace.ICON[role_id]
204 203
         # self.role_label = UserRoleInWorkspace.LABEL[role_id]
205 204
         # self.css_style = UserRoleInWorkspace.STYLE[role_id]

+ 61 - 0
tracim/models/roles.py View File

@@ -0,0 +1,61 @@
1
+import typing
2
+from enum import Enum
3
+
4
+from tracim.exceptions import RoleDoesNotExist
5
+
6
+
7
+class WorkspaceRoles(Enum):
8
+    """
9
+    Available role for workspace.
10
+    All roles should have a unique level and unique slug.
11
+    level is role value store in database and is also use for
12
+    permission check.
13
+    slug is for http endpoints and other place where readability is
14
+    needed.
15
+    """
16
+    NOT_APPLICABLE = (0, 'not-applicable')
17
+    READER = (1, 'reader')
18
+    CONTRIBUTOR = (2, 'contributor')
19
+    CONTENT_MANAGER = (4, 'content-manager')
20
+    WORKSPACE_MANAGER = (8, 'workspace-manager')
21
+
22
+    def __init__(self, level, slug):
23
+        self.level = level
24
+        self.slug = slug
25
+    
26
+    @property
27
+    def label(self):
28
+        """ Return valid label associated to role"""
29
+        # TODO - G.M - 2018-06-180 - Make this work correctly
30
+        return self.slug
31
+
32
+    @classmethod
33
+    def get_all_valid_role(cls) -> typing.List['WorkspaceRoles']:
34
+        """
35
+        Return all valid role value
36
+        """
37
+        return [item for item in list(WorkspaceRoles) if item.level > 0]
38
+
39
+    @classmethod
40
+    def get_role_from_level(cls, level: int) -> 'WorkspaceRoles':
41
+        """
42
+        Obtain Workspace role from a level value
43
+        :param level: level value as int
44
+        :return: correct workspace role related
45
+        """
46
+        roles = [item for item in list(WorkspaceRoles) if item.level == level]
47
+        if len(roles) != 1:
48
+            raise RoleDoesNotExist()
49
+        return roles[0]
50
+
51
+    @classmethod
52
+    def get_role_from_slug(cls, slug: str) -> 'WorkspaceRoles':
53
+        """
54
+        Obtain Workspace role from a slug value
55
+        :param slug: slug value as str
56
+        :return: correct workspace role related
57
+        """
58
+        roles = [item for item in list(WorkspaceRoles) if item.slug == slug]
59
+        if len(roles) != 1:
60
+            raise RoleDoesNotExist()
61
+        return roles[0]

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

@@ -68,6 +68,7 @@ class FunctionalTest(unittest.TestCase):
68 68
             'depot_storage_dir': '/tmp/test/depot',
69 69
             'depot_storage_name': 'test',
70 70
             'preview_cache_dir': '/tmp/test/preview_cache',
71
+            'email.notification.activated': 'false',
71 72
 
72 73
         }
73 74
         hapic.reset_context()

+ 386 - 1
tracim/tests/functional/test_workspaces.py View File

@@ -92,6 +92,133 @@ class TestWorkspaceEndpoint(FunctionalTest):
92 92
         assert sidebar_entry['hexcolor'] == "#757575"
93 93
         assert sidebar_entry['fa_icon'] == "calendar"
94 94
 
95
+    def test_api__update_workspace__ok_200__nominal_case(self) -> None:
96
+        """
97
+        Test update workspace
98
+        """
99
+        self.testapp.authorization = (
100
+            'Basic',
101
+            (
102
+                'admin@admin.admin',
103
+                'admin@admin.admin'
104
+            )
105
+        )
106
+        params = {
107
+            'label': 'superworkspace',
108
+            'description': 'mysuperdescription'
109
+        }
110
+        # Before
111
+        res = self.testapp.get(
112
+            '/api/v2/workspaces/1',
113
+            status=200
114
+        )
115
+        assert res.json_body
116
+        workspace = res.json_body
117
+        assert workspace['workspace_id'] == 1
118
+        assert workspace['slug'] == 'business'
119
+        assert workspace['label'] == 'Business'
120
+        assert workspace['description'] == 'All importants documents'
121
+        assert len(workspace['sidebar_entries']) == 7
122
+
123
+        # modify workspace
124
+        res = self.testapp.put_json(
125
+            '/api/v2/workspaces/1',
126
+            status=200,
127
+            params=params,
128
+        )
129
+        assert res.json_body
130
+        workspace = res.json_body
131
+        assert workspace['workspace_id'] == 1
132
+        assert workspace['slug'] == 'superworkspace'
133
+        assert workspace['label'] == 'superworkspace'
134
+        assert workspace['description'] == 'mysuperdescription'
135
+        assert len(workspace['sidebar_entries']) == 7
136
+
137
+        # after
138
+        res = self.testapp.get(
139
+            '/api/v2/workspaces/1',
140
+            status=200
141
+        )
142
+        assert res.json_body
143
+        workspace = res.json_body
144
+        assert workspace['workspace_id'] == 1
145
+        assert workspace['slug'] == 'superworkspace'
146
+        assert workspace['label'] == 'superworkspace'
147
+        assert workspace['description'] == 'mysuperdescription'
148
+        assert len(workspace['sidebar_entries']) == 7
149
+
150
+    def test_api__update_workspace__err_400__empty_label(self) -> None:
151
+        """
152
+        Test update workspace with empty label
153
+        """
154
+        self.testapp.authorization = (
155
+            'Basic',
156
+            (
157
+                'admin@admin.admin',
158
+                'admin@admin.admin'
159
+            )
160
+        )
161
+        params = {
162
+            'label': '',
163
+            'description': 'mysuperdescription'
164
+        }
165
+        res = self.testapp.put_json(
166
+            '/api/v2/workspaces/1',
167
+            status=400,
168
+            params=params,
169
+        )
170
+
171
+    def test_api__create_workspace__ok_200__nominal_case(self) -> None:
172
+        """
173
+        Test create workspace
174
+        """
175
+        self.testapp.authorization = (
176
+            'Basic',
177
+            (
178
+                'admin@admin.admin',
179
+                'admin@admin.admin'
180
+            )
181
+        )
182
+        params = {
183
+            'label': 'superworkspace',
184
+            'description': 'mysuperdescription'
185
+        }
186
+        res = self.testapp.post_json(
187
+            '/api/v2/workspaces',
188
+            status=200,
189
+            params=params,
190
+        )
191
+        assert res.json_body
192
+        workspace = res.json_body
193
+        workspace_id = res.json_body['workspace_id']
194
+        res = self.testapp.get(
195
+            '/api/v2/workspaces/{}'.format(workspace_id),
196
+            status=200
197
+        )
198
+        workspace_2 = res.json_body
199
+        assert workspace == workspace_2
200
+
201
+    def test_api__create_workspace__err_400__empty_label(self) -> None:
202
+        """
203
+        Test create workspace with empty label
204
+        """
205
+        self.testapp.authorization = (
206
+            'Basic',
207
+            (
208
+                'admin@admin.admin',
209
+                'admin@admin.admin'
210
+            )
211
+        )
212
+        params = {
213
+            'label': '',
214
+            'description': 'mysuperdescription'
215
+        }
216
+        res = self.testapp.post_json(
217
+            '/api/v2/workspaces',
218
+            status=400,
219
+            params=params,
220
+        )
221
+
95 222
     def test_api__get_workspace__err_400__unallowed_user(self) -> None:
96 223
         """
97 224
         Check obtain workspace unreachable for user
@@ -168,7 +295,12 @@ class TestWorkspaceMembersEndpoint(FunctionalTest):
168 295
         assert user_role['role'] == 'workspace-manager'
169 296
         assert user_role['user_id'] == 1
170 297
         assert user_role['workspace_id'] == 1
298
+        assert user_role['workspace']['workspace_id'] == 1
299
+        assert user_role['workspace']['label'] == 'Business'
300
+        assert user_role['workspace']['slug'] == 'business'
171 301
         assert user_role['user']['public_name'] == 'Global manager'
302
+        assert user_role['user']['user_id'] == 1
303
+        assert user_role['is_active'] is True
172 304
         # TODO - G.M - 24-05-2018 - [Avatar] Replace
173 305
         # by correct value when avatar feature will be enabled
174 306
         assert user_role['user']['avatar_url'] is None
@@ -226,6 +358,259 @@ class TestWorkspaceMembersEndpoint(FunctionalTest):
226 358
         assert 'message' in res.json.keys()
227 359
         assert 'details' in res.json.keys()
228 360
 
361
+    def test_api__create_workspace_member_role__ok_200__user_id(self):
362
+        """
363
+        Create workspace member role
364
+        :return:
365
+        """
366
+        self.testapp.authorization = (
367
+            'Basic',
368
+            (
369
+                'admin@admin.admin',
370
+                'admin@admin.admin'
371
+            )
372
+        )
373
+        # create workspace role
374
+        params = {
375
+            'user_id': 2,
376
+            'user_email_or_public_name': None,
377
+            'role': 'content-manager',
378
+        }
379
+        res = self.testapp.post_json(
380
+            '/api/v2/workspaces/1/members',
381
+            status=200,
382
+            params=params,
383
+        )
384
+        user_role_found = res.json_body
385
+        assert user_role_found['role'] == 'content-manager'
386
+        assert user_role_found['user_id'] == 2
387
+        assert user_role_found['workspace_id'] == 1
388
+        assert user_role_found['newly_created'] is False
389
+        assert user_role_found['email_sent'] is False
390
+
391
+        res = self.testapp.get('/api/v2/workspaces/1/members', status=200).json_body   # nopep8
392
+        assert len(res) == 2
393
+        user_role = res[0]
394
+        assert user_role['role'] == 'workspace-manager'
395
+        assert user_role['user_id'] == 1
396
+        assert user_role['workspace_id'] == 1
397
+        user_role = res[1]
398
+        assert user_role_found['role'] == user_role['role']
399
+        assert user_role_found['user_id'] == user_role['user_id']
400
+        assert user_role_found['workspace_id'] == user_role['workspace_id']
401
+
402
+    def test_api__create_workspace_member_role__ok_200__user_email(self):
403
+        """
404
+        Create workspace member role
405
+        :return:
406
+        """
407
+        self.testapp.authorization = (
408
+            'Basic',
409
+            (
410
+                'admin@admin.admin',
411
+                'admin@admin.admin'
412
+            )
413
+        )
414
+        # create workspace role
415
+        params = {
416
+            'user_id': None,
417
+            'user_email_or_public_name': 'lawrence-not-real-email@fsf.local',
418
+            'role': 'content-manager',
419
+        }
420
+        res = self.testapp.post_json(
421
+            '/api/v2/workspaces/1/members',
422
+            status=200,
423
+            params=params,
424
+        )
425
+        user_role_found = res.json_body
426
+        assert user_role_found['role'] == 'content-manager'
427
+        assert user_role_found['user_id'] == 2
428
+        assert user_role_found['workspace_id'] == 1
429
+        assert user_role_found['newly_created'] is False
430
+        assert user_role_found['email_sent'] is False
431
+
432
+        res = self.testapp.get('/api/v2/workspaces/1/members', status=200).json_body   # nopep8
433
+        assert len(res) == 2
434
+        user_role = res[0]
435
+        assert user_role['role'] == 'workspace-manager'
436
+        assert user_role['user_id'] == 1
437
+        assert user_role['workspace_id'] == 1
438
+        user_role = res[1]
439
+        assert user_role_found['role'] == user_role['role']
440
+        assert user_role_found['user_id'] == user_role['user_id']
441
+        assert user_role_found['workspace_id'] == user_role['workspace_id']
442
+
443
+    def test_api__create_workspace_member_role__ok_200__user_public_name(self):
444
+        """
445
+        Create workspace member role
446
+        :return:
447
+        """
448
+        self.testapp.authorization = (
449
+            'Basic',
450
+            (
451
+                'admin@admin.admin',
452
+                'admin@admin.admin'
453
+            )
454
+        )
455
+        # create workspace role
456
+        params = {
457
+            'user_id': None,
458
+            'user_email_or_public_name': 'Lawrence L.',
459
+            'role': 'content-manager',
460
+        }
461
+        res = self.testapp.post_json(
462
+            '/api/v2/workspaces/1/members',
463
+            status=200,
464
+            params=params,
465
+        )
466
+        user_role_found = res.json_body
467
+        assert user_role_found['role'] == 'content-manager'
468
+        assert user_role_found['user_id'] == 2
469
+        assert user_role_found['workspace_id'] == 1
470
+        assert user_role_found['newly_created'] is False
471
+        assert user_role_found['email_sent'] is False
472
+
473
+        res = self.testapp.get('/api/v2/workspaces/1/members', status=200).json_body   # nopep8
474
+        assert len(res) == 2
475
+        user_role = res[0]
476
+        assert user_role['role'] == 'workspace-manager'
477
+        assert user_role['user_id'] == 1
478
+        assert user_role['workspace_id'] == 1
479
+        user_role = res[1]
480
+        assert user_role_found['role'] == user_role['role']
481
+        assert user_role_found['user_id'] == user_role['user_id']
482
+        assert user_role_found['workspace_id'] == user_role['workspace_id']
483
+
484
+    def test_api__create_workspace_member_role__err_400__nothing(self):
485
+        """
486
+        Create workspace member role
487
+        :return:
488
+        """
489
+        self.testapp.authorization = (
490
+            'Basic',
491
+            (
492
+                'admin@admin.admin',
493
+                'admin@admin.admin'
494
+            )
495
+        )
496
+        # create workspace role
497
+        params = {
498
+            'user_id': None,
499
+            'user_email_or_public_name': None,
500
+            'role': 'content-manager',
501
+        }
502
+        res = self.testapp.post_json(
503
+            '/api/v2/workspaces/1/members',
504
+            status=400,
505
+            params=params,
506
+        )
507
+
508
+    def test_api__create_workspace_member_role__err_400__wrong_user_id(self):
509
+        """
510
+        Create workspace member role
511
+        :return:
512
+        """
513
+        self.testapp.authorization = (
514
+            'Basic',
515
+            (
516
+                'admin@admin.admin',
517
+                'admin@admin.admin'
518
+            )
519
+        )
520
+        # create workspace role
521
+        params = {
522
+            'user_id': 47,
523
+            'user_email_or_public_name': None,
524
+            'role': 'content-manager',
525
+        }
526
+        res = self.testapp.post_json(
527
+            '/api/v2/workspaces/1/members',
528
+            status=400,
529
+            params=params,
530
+        )
531
+
532
+    def test_api__create_workspace_member_role__ok_200__new_user(self):  # nopep8
533
+        """
534
+        Create workspace member role
535
+        :return:
536
+        """
537
+        self.testapp.authorization = (
538
+            'Basic',
539
+            (
540
+                'admin@admin.admin',
541
+                'admin@admin.admin'
542
+            )
543
+        )
544
+        # create workspace role
545
+        params = {
546
+            'user_id': None,
547
+            'user_email_or_public_name': 'nothing@nothing.nothing',
548
+            'role': 'content-manager',
549
+        }
550
+        res = self.testapp.post_json(
551
+            '/api/v2/workspaces/1/members',
552
+            status=200,
553
+            params=params,
554
+        )
555
+        user_role_found = res.json_body
556
+        assert user_role_found['role'] == 'content-manager'
557
+        assert user_role_found['user_id']
558
+        user_id = user_role_found['user_id']
559
+        assert user_role_found['workspace_id'] == 1
560
+        assert user_role_found['newly_created'] is True
561
+        assert user_role_found['email_sent'] is False
562
+
563
+        res = self.testapp.get('/api/v2/workspaces/1/members',
564
+                               status=200).json_body  # nopep8
565
+        assert len(res) == 2
566
+        user_role = res[0]
567
+        assert user_role['role'] == 'workspace-manager'
568
+        assert user_role['user_id'] == 1
569
+        assert user_role['workspace_id'] == 1
570
+        user_role = res[1]
571
+        assert user_role_found['role'] == user_role['role']
572
+        assert user_role_found['user_id'] == user_role['user_id']
573
+        assert user_role_found['workspace_id'] == user_role['workspace_id']
574
+
575
+    def test_api__update_workspace_member_role__ok_200__nominal_case(self):
576
+        """
577
+        Update worskpace member role
578
+        """
579
+        # before
580
+        self.testapp.authorization = (
581
+            'Basic',
582
+            (
583
+                'admin@admin.admin',
584
+                'admin@admin.admin'
585
+            )
586
+        )
587
+        res = self.testapp.get('/api/v2/workspaces/1/members', status=200).json_body   # nopep8
588
+        assert len(res) == 1
589
+        user_role = res[0]
590
+        assert user_role['role'] == 'workspace-manager'
591
+        assert user_role['user_id'] == 1
592
+        assert user_role['workspace_id'] == 1
593
+        # update workspace role
594
+        params = {
595
+            'role': 'content-manager',
596
+        }
597
+        res = self.testapp.put_json(
598
+            '/api/v2/workspaces/1/members/1',
599
+            status=200,
600
+            params=params,
601
+        )
602
+        user_role = res.json_body
603
+        assert user_role['role'] == 'content-manager'
604
+        assert user_role['user_id'] == 1
605
+        assert user_role['workspace_id'] == 1
606
+        # after
607
+        res = self.testapp.get('/api/v2/workspaces/1/members', status=200).json_body   # nopep8
608
+        assert len(res) == 1
609
+        user_role = res[0]
610
+        assert user_role['role'] == 'content-manager'
611
+        assert user_role['user_id'] == 1
612
+        assert user_role['workspace_id'] == 1
613
+
229 614
 
230 615
 class TestWorkspaceContents(FunctionalTest):
231 616
     """
@@ -316,7 +701,7 @@ class TestWorkspaceContents(FunctionalTest):
316 701
         assert content['workspace_id'] == 1
317 702
 
318 703
     # Root related
319
-    def test_api__get_workspace_content__ok_200__get_all_root_content__legacy_html_slug(self):
704
+    def test_api__get_workspace_content__ok_200__get_all_root_content__legacy_html_slug(self):  # nopep8
320 705
         """
321 706
         Check obtain workspace all root contents
322 707
         """

+ 77 - 0
tracim/tests/library/test_role_api.py View File

@@ -0,0 +1,77 @@
1
+# coding=utf-8
2
+import pytest
3
+from sqlalchemy.orm.exc import NoResultFound
4
+
5
+from tracim.lib.core.userworkspace import RoleApi
6
+from tracim.models import User
7
+from tracim.models.roles import WorkspaceRoles
8
+from tracim.tests import DefaultTest
9
+from tracim.fixtures.users_and_groups import Base as BaseFixture
10
+from tracim.fixtures.content import Content as ContentFixture
11
+
12
+
13
+class TestRoleApi(DefaultTest):
14
+
15
+    fixtures = [BaseFixture, ContentFixture]
16
+
17
+    def test_unit__get_one__ok__nominal_case(self):
18
+        admin = self.session.query(User)\
19
+            .filter(User.email == 'admin@admin.admin').one()
20
+        rapi = RoleApi(
21
+            current_user=admin,
22
+            session=self.session,
23
+            config=self.config,
24
+        )
25
+        rapi.get_one(admin.user_id, 1)
26
+
27
+    def test_unit__get_one__err__role_does_not_exist(self):
28
+        admin = self.session.query(User)\
29
+            .filter(User.email == 'admin@admin.admin').one()
30
+        rapi = RoleApi(
31
+            current_user=admin,
32
+            session=self.session,
33
+            config=self.config,
34
+        )
35
+        with pytest.raises(NoResultFound):
36
+            rapi.get_one(admin.user_id, 100)  # workspace 100 does not exist
37
+
38
+    def test_unit__create_one__nominal_case(self):
39
+        admin = self.session.query(User)\
40
+            .filter(User.email == 'admin@admin.admin').one()
41
+        workspace = self._create_workspace_and_test(
42
+            'workspace_1',
43
+            admin
44
+        )
45
+        bob = self.session.query(User)\
46
+            .filter(User.email == 'bob@fsf.local').one()
47
+        rapi = RoleApi(
48
+            current_user=admin,
49
+            session=self.session,
50
+            config=self.config,
51
+        )
52
+        created_role = rapi.create_one(
53
+            user=bob,
54
+            workspace=workspace,
55
+            role_level=WorkspaceRoles.CONTENT_MANAGER.level,
56
+            with_notif=False,
57
+        )
58
+        obtain_role = rapi.get_one(bob.user_id, workspace.workspace_id)
59
+        assert created_role == obtain_role
60
+
61
+    def test_unit__get_all_for_usages(self):
62
+        admin = self.session.query(User)\
63
+            .filter(User.email == 'admin@admin.admin').one()
64
+        rapi = RoleApi(
65
+            current_user=admin,
66
+            session=self.session,
67
+            config=self.config,
68
+        )
69
+        workspace = self._create_workspace_and_test(
70
+            'workspace_1',
71
+            admin
72
+        )
73
+        roles = rapi.get_all_for_workspace(workspace)
74
+        len(roles) == 1
75
+        roles[0].user_id == admin.user_id
76
+        roles[0].role == WorkspaceRoles.WORKSPACE_MANAGER.level
77
+

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

@@ -22,7 +22,7 @@ class TestUserApi(DefaultTest):
22 22
         )
23 23
         u = api.create_minimal_user('bob@bob')
24 24
         assert u.email == 'bob@bob'
25
-        assert u.display_name is None
25
+        assert u.display_name == 'bob'
26 26
 
27 27
     def test_unit__create_minimal_user_and_update__ok__nominal_case(self):
28 28
         api = UserApi(

+ 80 - 0
tracim/tests/models/tests_roles.py View File

@@ -0,0 +1,80 @@
1
+# coding=utf-8
2
+import unittest
3
+import pytest
4
+from tracim.exceptions import RoleDoesNotExist
5
+from tracim.models.roles import WorkspaceRoles
6
+
7
+
8
+class TestWorkspacesRoles(unittest.TestCase):
9
+    """
10
+    Test for WorkspaceRoles Enum Object
11
+    """
12
+    def test_workspace_roles__ok__all_list(self):
13
+        roles = list(WorkspaceRoles)
14
+        assert len(roles) == 5
15
+        for role in roles:
16
+            assert role
17
+            assert role.slug
18
+            assert isinstance(role.slug, str)
19
+            assert role.level or role.level == 0
20
+            assert isinstance(role.level, int)
21
+            assert role.label
22
+            assert isinstance(role.slug, str)
23
+        assert WorkspaceRoles['READER']
24
+        assert WorkspaceRoles['NOT_APPLICABLE']
25
+        assert WorkspaceRoles['CONTRIBUTOR']
26
+        assert WorkspaceRoles['WORKSPACE_MANAGER']
27
+        assert WorkspaceRoles['CONTENT_MANAGER']
28
+
29
+    def test__workspace_roles__ok__check_model(self):
30
+        role = WorkspaceRoles.WORKSPACE_MANAGER
31
+        assert role
32
+        assert role.slug
33
+        assert isinstance(role.slug, str)
34
+        assert role.level
35
+        assert isinstance(role.level, int)
36
+        assert role.label
37
+        assert isinstance(role.slug, str)
38
+
39
+    def test_workspace_roles__ok__get_all_valid_roles(self):
40
+        roles = WorkspaceRoles.get_all_valid_role()
41
+        assert len(roles) == 4
42
+        for role in roles:
43
+            assert role
44
+            assert role.slug
45
+            assert isinstance(role.slug, str)
46
+            assert role.level or role.level == 0
47
+            assert isinstance(role.level, int)
48
+            assert role.level > 0
49
+            assert role.label
50
+            assert isinstance(role.slug, str)
51
+
52
+    def test_workspace_roles__ok__get_role__from_level__ok__nominal_case(self):
53
+        role = WorkspaceRoles.get_role_from_level(0)
54
+
55
+        assert role
56
+        assert role.slug
57
+        assert isinstance(role.slug, str)
58
+        assert role.level == 0
59
+        assert isinstance(role.level, int)
60
+        assert role.label
61
+        assert isinstance(role.slug, str)
62
+
63
+    def test_workspace_roles__ok__get_role__from_slug__ok__nominal_case(self):
64
+        role = WorkspaceRoles.get_role_from_slug('reader')
65
+
66
+        assert role
67
+        assert role.slug
68
+        assert isinstance(role.slug, str)
69
+        assert role.level > 0
70
+        assert isinstance(role.level, int)
71
+        assert role.label
72
+        assert isinstance(role.slug, str)
73
+
74
+    def test_workspace_roles__ok__get_role__from_level__err__role_does_not_exist(self):  # nopep8
75
+        with pytest.raises(RoleDoesNotExist):
76
+            WorkspaceRoles.get_role_from_level(-1000)
77
+
78
+    def test_workspace_roles__ok__get_role__from_slug__err__role_does_not_exist(self):  # nopep8
79
+        with pytest.raises(RoleDoesNotExist):
80
+            WorkspaceRoles.get_role_from_slug('this slug does not exist')

+ 82 - 5
tracim/views/core_api/schemas.py View File

@@ -10,16 +10,20 @@ from tracim.models.contents import GlobalStatus
10 10
 from tracim.models.contents import open_status
11 11
 from tracim.models.contents import ContentTypeLegacy as ContentType
12 12
 from tracim.models.contents import ContentStatusLegacy as ContentStatus
13
-from tracim.models.context_models import ContentCreation, ActiveContentFilter, \
14
-    ContentIdsQuery
15
-from tracim.models.context_models import UserWorkspacePath
13
+from tracim.models.context_models import ActiveContentFilter
14
+from tracim.models.context_models import ContentIdsQuery
16 15
 from tracim.models.context_models import UserWorkspaceAndContentPath
16
+from tracim.models.context_models import ContentCreation
17
+from tracim.models.context_models import WorkspaceMemberInvitation
18
+from tracim.models.context_models import WorkspaceUpdate
19
+from tracim.models.context_models import RoleUpdate
17 20
 from tracim.models.context_models import CommentCreation
18 21
 from tracim.models.context_models import TextBasedContentUpdate
19 22
 from tracim.models.context_models import SetContentStatus
20 23
 from tracim.models.context_models import CommentPath
21 24
 from tracim.models.context_models import MoveParams
22 25
 from tracim.models.context_models import WorkspaceAndContentPath
26
+from tracim.models.context_models import WorkspaceAndUserPath
23 27
 from tracim.models.context_models import ContentFilter
24 28
 from tracim.models.context_models import LoginCredentials
25 29
 from tracim.models.data import UserRoleInWorkspace
@@ -110,6 +114,15 @@ class ContentIdPathSchema(marshmallow.Schema):
110 114
     )
111 115
 
112 116
 
117
+class WorkspaceAndUserIdPathSchema(
118
+    UserIdPathSchema,
119
+    WorkspaceIdPathSchema
120
+):
121
+    @post_load
122
+    def make_path_object(self, data):
123
+        return WorkspaceAndUserPath(**data)
124
+
125
+
113 126
 class WorkspaceAndContentIdPathSchema(
114 127
     WorkspaceIdPathSchema,
115 128
     ContentIdPathSchema
@@ -135,7 +148,7 @@ class UserWorkspaceIdPathSchema(
135 148
 ):
136 149
     @post_load
137 150
     def make_path_object(self, data):
138
-        return UserWorkspacePath(**data)
151
+        return WorkspaceAndUserPath(**data)
139 152
 
140 153
 
141 154
 class CommentsPathSchema(WorkspaceAndContentIdPathSchema):
@@ -145,6 +158,7 @@ class CommentsPathSchema(WorkspaceAndContentIdPathSchema):
145 158
         required=True,
146 159
         validate=Range(min=1, error="Value must be greater than 0"),
147 160
     )
161
+
148 162
     @post_load
149 163
     def make_path_object(self, data):
150 164
         return CommentPath(**data)
@@ -228,6 +242,34 @@ class ContentIdsQuerySchema(marshmallow.Schema):
228 242
 ###
229 243
 
230 244
 
245
+class RoleUpdateSchema(marshmallow.Schema):
246
+    role = marshmallow.fields.String(
247
+        example='contributor',
248
+        validate=OneOf(UserRoleInWorkspace.get_all_role_slug())
249
+    )
250
+
251
+    @post_load
252
+    def make_role(self, data):
253
+        return RoleUpdate(**data)
254
+
255
+
256
+class WorkspaceMemberInviteSchema(RoleUpdateSchema):
257
+    user_id = marshmallow.fields.Int(
258
+        example=5,
259
+        default=None,
260
+        allow_none=True,
261
+    )
262
+    user_email_or_public_name = marshmallow.fields.String(
263
+        example='suri@cate.fr',
264
+        default=None,
265
+        allow_none=True,
266
+    )
267
+
268
+    @post_load
269
+    def make_role(self, data):
270
+        return WorkspaceMemberInvitation(**data)
271
+
272
+
231 273
 class BasicAuthSchema(marshmallow.Schema):
232 274
 
233 275
     email = marshmallow.fields.Email(
@@ -252,6 +294,23 @@ class LoginOutputHeaders(marshmallow.Schema):
252 294
     expire_after = marshmallow.fields.String()
253 295
 
254 296
 
297
+class WorkspaceModifySchema(marshmallow.Schema):
298
+    label = marshmallow.fields.String(
299
+        example='My Workspace',
300
+    )
301
+    description = marshmallow.fields.String(
302
+        example='A super description of my workspace.',
303
+    )
304
+
305
+    @post_load
306
+    def make_workspace_modifications(self, data):
307
+        return WorkspaceUpdate(**data)
308
+
309
+
310
+class WorkspaceCreationSchema(WorkspaceModifySchema):
311
+    pass
312
+
313
+
255 314
 class NoContentSchema(marshmallow.Schema):
256 315
 
257 316
     class Meta:
@@ -319,13 +378,31 @@ class WorkspaceMemberSchema(marshmallow.Schema):
319 378
         validate=Range(min=1, error="Value must be greater than 0"),
320 379
     )
321 380
     user = marshmallow.fields.Nested(
322
-        UserSchema(only=('public_name', 'avatar_url'))
381
+        UserDigestSchema()
323 382
     )
383
+    workspace = marshmallow.fields.Nested(
384
+        WorkspaceDigestSchema(exclude=('sidebar_entries',))
385
+    )
386
+    is_active = marshmallow.fields.Bool()
324 387
 
325 388
     class Meta:
326 389
         description = 'Workspace Member information'
327 390
 
328 391
 
392
+class WorkspaceMemberCreationSchema(WorkspaceMemberSchema):
393
+    newly_created = marshmallow.fields.Bool(
394
+        exemple=False,
395
+        description='Is the user completely new '
396
+                    '(and account was just created) or not ?',
397
+    )
398
+    email_sent = marshmallow.fields.Bool(
399
+        exemple=False,
400
+        description='Has an email been sent to user to inform him about '
401
+                    'this new workspace registration and eventually his account'
402
+                    'creation'
403
+    )
404
+
405
+
329 406
 class ApplicationConfigSchema(marshmallow.Schema):
330 407
     pass
331 408
     #  TODO - G.M - 24-05-2018 - Set this

+ 169 - 7
tracim/views/core_api/workspace_controller.py View File

@@ -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:
@@ -12,17 +16,28 @@ from tracim.lib.core.workspace import WorkspaceApi
12 16
 from tracim.lib.core.content import ContentApi
13 17
 from tracim.lib.core.userworkspace import RoleApi
14 18
 from tracim.lib.utils.authorization import require_workspace_role
19
+from tracim.lib.utils.authorization import require_profile
20
+from tracim.models import Group
15 21
 from tracim.lib.utils.authorization import require_candidate_workspace_role
16 22
 from tracim.models.data import UserRoleInWorkspace
17 23
 from tracim.models.data import ActionDescription
18 24
 from tracim.models.context_models import UserRoleWorkspaceInContext
19 25
 from tracim.models.context_models import ContentInContext
20 26
 from tracim.exceptions import EmptyLabelNotAllowed
27
+from tracim.exceptions import EmailValidationFailed
28
+from tracim.exceptions import UserCreationFailed
29
+from tracim.exceptions import UserDoesNotExist
21 30
 from tracim.exceptions import ContentNotFound
22 31
 from tracim.exceptions import WorkspacesDoNotMatch
23 32
 from tracim.exceptions import ParentNotFound
24 33
 from tracim.views.controllers import Controller
25 34
 from tracim.views.core_api.schemas import FilterContentQuerySchema
35
+from tracim.views.core_api.schemas import WorkspaceMemberCreationSchema
36
+from tracim.views.core_api.schemas import WorkspaceMemberInviteSchema
37
+from tracim.views.core_api.schemas import RoleUpdateSchema
38
+from tracim.views.core_api.schemas import WorkspaceCreationSchema
39
+from tracim.views.core_api.schemas import WorkspaceModifySchema
40
+from tracim.views.core_api.schemas import WorkspaceAndUserIdPathSchema
26 41
 from tracim.views.core_api.schemas import ContentMoveSchema
27 42
 from tracim.views.core_api.schemas import NoContentSchema
28 43
 from tracim.views.core_api.schemas import ContentCreationSchema
@@ -47,7 +62,6 @@ class WorkspaceController(Controller):
47 62
         """
48 63
         Get workspace informations
49 64
         """
50
-        wid = hapic_data.path['workspace_id']
51 65
         app_config = request.registry.settings['CFG']
52 66
         wapi = WorkspaceApi(
53 67
             current_user=request.current_user,  # User
@@ -57,6 +71,52 @@ class WorkspaceController(Controller):
57 71
         return wapi.get_workspace_with_context(request.current_workspace)
58 72
 
59 73
     @hapic.with_api_doc(tags=[WORKSPACE_ENDPOINTS_TAG])
74
+    @hapic.handle_exception(EmptyLabelNotAllowed, HTTPStatus.BAD_REQUEST)
75
+    @require_workspace_role(UserRoleInWorkspace.WORKSPACE_MANAGER)
76
+    @hapic.input_path(WorkspaceIdPathSchema())
77
+    @hapic.input_body(WorkspaceModifySchema())
78
+    @hapic.output_body(WorkspaceSchema())
79
+    def update_workspace(self, context, request: TracimRequest, hapic_data=None):  # nopep8
80
+        """
81
+        Update workspace informations
82
+        """
83
+        app_config = request.registry.settings['CFG']
84
+        wapi = WorkspaceApi(
85
+            current_user=request.current_user,  # User
86
+            session=request.dbsession,
87
+            config=app_config,
88
+        )
89
+        wapi.update_workspace(
90
+            request.current_workspace,
91
+            label=hapic_data.body.label,
92
+            description=hapic_data.body.description,
93
+            save_now=True,
94
+        )
95
+        return wapi.get_workspace_with_context(request.current_workspace)
96
+
97
+    @hapic.with_api_doc(tags=[WORKSPACE_ENDPOINTS_TAG])
98
+    @hapic.handle_exception(EmptyLabelNotAllowed, HTTPStatus.BAD_REQUEST)
99
+    @require_profile(Group.TIM_MANAGER)
100
+    @hapic.input_body(WorkspaceCreationSchema())
101
+    @hapic.output_body(WorkspaceSchema())
102
+    def create_workspace(self, context, request: TracimRequest, hapic_data=None):  # nopep8
103
+        """
104
+        create workspace
105
+        """
106
+        app_config = request.registry.settings['CFG']
107
+        wapi = WorkspaceApi(
108
+            current_user=request.current_user,  # User
109
+            session=request.dbsession,
110
+            config=app_config,
111
+        )
112
+        workspace = wapi.create_workspace(
113
+            label=hapic_data.body.label,
114
+            description=hapic_data.body.description,
115
+            save_now=True,
116
+        )
117
+        return wapi.get_workspace_with_context(workspace)
118
+
119
+    @hapic.with_api_doc(tags=[WORKSPACE_ENDPOINTS_TAG])
60 120
     @require_workspace_role(UserRoleInWorkspace.READER)
61 121
     @hapic.input_path(WorkspaceIdPathSchema())
62 122
     @hapic.output_body(WorkspaceMemberSchema(many=True))
@@ -83,6 +143,96 @@ class WorkspaceController(Controller):
83 143
         ]
84 144
 
85 145
     @hapic.with_api_doc(tags=[WORKSPACE_ENDPOINTS_TAG])
146
+    @require_workspace_role(UserRoleInWorkspace.WORKSPACE_MANAGER)
147
+    @hapic.input_path(WorkspaceAndUserIdPathSchema())
148
+    @hapic.input_body(RoleUpdateSchema())
149
+    @hapic.output_body(WorkspaceMemberSchema())
150
+    def update_workspaces_members_role(
151
+            self,
152
+            context,
153
+            request: TracimRequest,
154
+            hapic_data=None
155
+    ) -> UserRoleWorkspaceInContext:
156
+        """
157
+        Update Members to this workspace
158
+        """
159
+        app_config = request.registry.settings['CFG']
160
+        rapi = RoleApi(
161
+            current_user=request.current_user,
162
+            session=request.dbsession,
163
+            config=app_config,
164
+        )
165
+
166
+        role = rapi.get_one(
167
+            user_id=hapic_data.path.user_id,
168
+            workspace_id=hapic_data.path.workspace_id,
169
+        )
170
+        workspace_role = WorkspaceRoles.get_role_from_slug(hapic_data.body.role)
171
+        role = rapi.update_role(
172
+            role,
173
+            role_level=workspace_role.level
174
+        )
175
+        return rapi.get_user_role_workspace_with_context(role)
176
+
177
+    @hapic.with_api_doc(tags=[WORKSPACE_ENDPOINTS_TAG])
178
+    @hapic.handle_exception(UserCreationFailed, HTTPStatus.BAD_REQUEST)
179
+    @require_workspace_role(UserRoleInWorkspace.WORKSPACE_MANAGER)
180
+    @hapic.input_path(WorkspaceIdPathSchema())
181
+    @hapic.input_body(WorkspaceMemberInviteSchema())
182
+    @hapic.output_body(WorkspaceMemberCreationSchema())
183
+    def create_workspaces_members_role(
184
+            self,
185
+            context,
186
+            request: TracimRequest,
187
+            hapic_data=None
188
+    ) -> UserRoleWorkspaceInContext:
189
+        """
190
+        Add Members to this workspace
191
+        """
192
+        newly_created = False
193
+        email_sent = False
194
+        app_config = request.registry.settings['CFG']
195
+        rapi = RoleApi(
196
+            current_user=request.current_user,
197
+            session=request.dbsession,
198
+            config=app_config,
199
+        )
200
+        uapi = UserApi(
201
+            current_user=request.current_user,
202
+            session=request.dbsession,
203
+            config=app_config,
204
+        )
205
+        try:
206
+            _, user = uapi.find(
207
+                user_id=hapic_data.body.user_id,
208
+                email=hapic_data.body.user_email_or_public_name,
209
+                public_name=hapic_data.body.user_email_or_public_name
210
+            )
211
+        except UserDoesNotExist:
212
+            try:
213
+                # TODO - G.M - 2018-07-05 - [UserCreation] Reenable email
214
+                # notification for creation
215
+                user = uapi.create_user(
216
+                    hapic_data.body.user_email_or_public_name,
217
+                    do_notify=False
218
+                )  # nopep8
219
+                newly_created = True
220
+            except EmailValidationFailed:
221
+                raise UserCreationFailed('no valid mail given')
222
+        role = rapi.create_one(
223
+            user=user,
224
+            workspace=request.current_workspace,
225
+            role_level=WorkspaceRoles.get_role_from_slug(hapic_data.body.role).level,  # nopep8
226
+            with_notif=False,
227
+            flush=True,
228
+        )
229
+        return rapi.get_user_role_workspace_with_context(
230
+            role,
231
+            newly_created=newly_created,
232
+            email_sent=email_sent,
233
+        )
234
+
235
+    @hapic.with_api_doc(tags=[WORKSPACE_ENDPOINTS_TAG])
86 236
     @require_workspace_role(UserRoleInWorkspace.READER)
87 237
     @hapic.input_path(WorkspaceIdPathSchema())
88 238
     @hapic.input_query(FilterContentQuerySchema())
@@ -168,7 +318,7 @@ class WorkspaceController(Controller):
168 318
             context,
169 319
             request: TracimRequest,
170 320
             hapic_data=None,
171
-    ) -> typing.List[ContentInContext]:
321
+    ) -> ContentInContext:
172 322
         """
173 323
         move a content
174 324
         """
@@ -217,7 +367,7 @@ class WorkspaceController(Controller):
217 367
             context,
218 368
             request: TracimRequest,
219 369
             hapic_data=None,
220
-    ) -> typing.List[ContentInContext]:
370
+    ) -> None:
221 371
         """
222 372
         delete a content
223 373
         """
@@ -249,7 +399,7 @@ class WorkspaceController(Controller):
249 399
             context,
250 400
             request: TracimRequest,
251 401
             hapic_data=None,
252
-    ) -> typing.List[ContentInContext]:
402
+    ) -> None:
253 403
         """
254 404
         undelete a content
255 405
         """
@@ -282,7 +432,7 @@ class WorkspaceController(Controller):
282 432
             context,
283 433
             request: TracimRequest,
284 434
             hapic_data=None,
285
-    ) -> typing.List[ContentInContext]:
435
+    ) -> None:
286 436
         """
287 437
         archive a content
288 438
         """
@@ -293,7 +443,7 @@ class WorkspaceController(Controller):
293 443
             session=request.dbsession,
294 444
             config=app_config,
295 445
         )
296
-        content = api.get_one(path_data.content_id, content_type=ContentType.Any)
446
+        content = api.get_one(path_data.content_id, content_type=ContentType.Any)  # nopep8
297 447
         with new_revision(
298 448
                 session=request.dbsession,
299 449
                 tm=transaction.manager,
@@ -311,7 +461,7 @@ class WorkspaceController(Controller):
311 461
             context,
312 462
             request: TracimRequest,
313 463
             hapic_data=None,
314
-    ) -> typing.List[ContentInContext]:
464
+    ) -> None:
315 465
         """
316 466
         unarchive a content
317 467
         """
@@ -344,9 +494,21 @@ class WorkspaceController(Controller):
344 494
         # Workspace
345 495
         configurator.add_route('workspace', '/workspaces/{workspace_id}', request_method='GET')  # nopep8
346 496
         configurator.add_view(self.workspace, route_name='workspace')
497
+        # Create workspace
498
+        configurator.add_route('create_workspace', '/workspaces', request_method='POST')  # nopep8
499
+        configurator.add_view(self.create_workspace, route_name='create_workspace')  # nopep8
500
+        # Update Workspace
501
+        configurator.add_route('update_workspace', '/workspaces/{workspace_id}', request_method='PUT')  # nopep8
502
+        configurator.add_view(self.update_workspace, route_name='update_workspace')  # nopep8
347 503
         # Workspace Members (Roles)
348 504
         configurator.add_route('workspace_members', '/workspaces/{workspace_id}/members', request_method='GET')  # nopep8
349 505
         configurator.add_view(self.workspaces_members, route_name='workspace_members')  # nopep8
506
+        # Update Workspace Members roles
507
+        configurator.add_route('update_workspace_member', '/workspaces/{workspace_id}/members/{user_id}', request_method='PUT')  # nopep8
508
+        configurator.add_view(self.update_workspaces_members_role, route_name='update_workspace_member')  # nopep8
509
+        # Create Workspace Members roles
510
+        configurator.add_route('create_workspace_member', '/workspaces/{workspace_id}/members', request_method='POST')  # nopep8
511
+        configurator.add_view(self.create_workspaces_members_role, route_name='create_workspace_member')  # nopep8
350 512
         # Workspace Content
351 513
         configurator.add_route('workspace_content', '/workspaces/{workspace_id}/contents', request_method='GET')  # nopep8
352 514
         configurator.add_view(self.workspace_content, route_name='workspace_content')  # nopep8