Browse Source

add ContentAPI, UserApi, GroupApi and others core libApi + converted test to pyramid

Guénaël Muller 7 years ago
parent
commit
25823d3093

File diff suppressed because it is too large
+ 1242 - 0
tracim/lib/content.py


+ 23 - 0
tracim/lib/group.py View File

@@ -0,0 +1,23 @@
1
+# -*- coding: utf-8 -*-
2
+
3
+__author__ = 'damien'
4
+
5
+from tracim.models.auth import Group, User
6
+from sqlalchemy.orm import Query
7
+from sqlalchemy.orm import Session
8
+
9
+
10
+class GroupApi(object):
11
+
12
+    def __init__(self, session: Session, current_user: User):
13
+        self._user = current_user
14
+        self._session = session
15
+
16
+    def _base_query(self) -> Query:
17
+        return self._session.query(Group)
18
+
19
+    def get_one(self, group_id) -> Group:
20
+        return self._base_query().filter(Group.group_id == group_id).one()
21
+
22
+    def get_one_with_name(self, group_name) -> Group:
23
+        return self._base_query().filter(Group.group_name == group_name).one()

+ 131 - 0
tracim/lib/userworkspace.py View File

@@ -0,0 +1,131 @@
1
+# -*- coding: utf-8 -*-
2
+
3
+__author__ = 'damien'
4
+
5
+from sqlalchemy.orm import Session
6
+from tracim.models.auth import User
7
+from tracim.models.data import Workspace
8
+from tracim.models.data import UserRoleInWorkspace
9
+from tracim.models.data import RoleType
10
+
11
+
12
+class RoleApi(object):
13
+
14
+    ALL_ROLE_VALUES = UserRoleInWorkspace.get_all_role_values()
15
+    # Dict containing readable members roles for given role
16
+    members_read_rights = {
17
+        UserRoleInWorkspace.NOT_APPLICABLE: [],
18
+        UserRoleInWorkspace.READER: [
19
+            UserRoleInWorkspace.WORKSPACE_MANAGER,
20
+        ],
21
+        UserRoleInWorkspace.CONTRIBUTOR: [
22
+            UserRoleInWorkspace.WORKSPACE_MANAGER,
23
+            UserRoleInWorkspace.CONTENT_MANAGER,
24
+            UserRoleInWorkspace.CONTRIBUTOR,
25
+        ],
26
+        UserRoleInWorkspace.CONTENT_MANAGER: [
27
+            UserRoleInWorkspace.WORKSPACE_MANAGER,
28
+            UserRoleInWorkspace.CONTENT_MANAGER,
29
+            UserRoleInWorkspace.CONTRIBUTOR,
30
+            UserRoleInWorkspace.READER,
31
+        ],
32
+        UserRoleInWorkspace.WORKSPACE_MANAGER: [
33
+            UserRoleInWorkspace.WORKSPACE_MANAGER,
34
+            UserRoleInWorkspace.CONTENT_MANAGER,
35
+            UserRoleInWorkspace.CONTRIBUTOR,
36
+            UserRoleInWorkspace.READER,
37
+        ],
38
+    }
39
+
40
+    @classmethod
41
+    def role_can_read_member_role(cls, reader_role: int, tested_role: int) \
42
+            -> bool:
43
+        """
44
+        :param reader_role: role as viewer
45
+        :param tested_role: role as viwed
46
+        :return: True if given role can view member role in workspace.
47
+        """
48
+        if reader_role in cls.members_read_rights:
49
+            return tested_role in cls.members_read_rights[reader_role]
50
+        return False
51
+
52
+    @classmethod
53
+    def create_role(cls) -> UserRoleInWorkspace:
54
+        role = UserRoleInWorkspace()
55
+
56
+        return role
57
+
58
+    def __init__(self, session: Session, current_user: User):
59
+        self._session = session
60
+        self._user = current_user
61
+
62
+    def _get_one_rsc(self, user_id, workspace_id):
63
+        """
64
+        :param user_id:
65
+        :param workspace_id:
66
+        :return: a Query object, filtered query but without fetching the object.
67
+        """
68
+        return self._session.query(UserRoleInWorkspace).\
69
+            filter(UserRoleInWorkspace.workspace_id == workspace_id).\
70
+            filter(UserRoleInWorkspace.user_id == user_id)
71
+
72
+    def get_one(self, user_id, workspace_id):
73
+        return self._get_one_rsc(user_id, workspace_id).one()
74
+
75
+    def create_one(
76
+            self,
77
+            user: User,
78
+            workspace: Workspace,
79
+            role_level: int,
80
+            with_notif: bool,
81
+            flush: bool=True
82
+    ) -> UserRoleInWorkspace:
83
+        role = self.create_role()
84
+        role.user_id = user.user_id
85
+        role.workspace = workspace
86
+        role.role = role_level
87
+        role.do_notify = with_notif
88
+        if flush:
89
+            self._session.flush()
90
+        return role
91
+
92
+    def delete_one(self, user_id, workspace_id, flush=True):
93
+        self._get_one_rsc(user_id, workspace_id).delete()
94
+        if flush:
95
+            self._session.flush()
96
+
97
+    def _get_all_for_user(self, user_id):
98
+        return self._session.query(UserRoleInWorkspace)\
99
+            .filter(UserRoleInWorkspace.user_id == user_id)
100
+
101
+    def get_all_for_user(self, user_id):
102
+        return self._get_all_for_user(user_id).all()
103
+
104
+    def get_all_for_user_order_by_workspace(
105
+            self,
106
+            user_id: int
107
+    ) -> UserRoleInWorkspace:
108
+        return self._get_all_for_user(user_id)\
109
+            .join(UserRoleInWorkspace.workspace).order_by(Workspace.label).all()
110
+
111
+    def get_all_for_workspace(self, workspace_id):
112
+        return self._session.query(UserRoleInWorkspace)\
113
+            .filter(UserRoleInWorkspace.workspace_id == workspace_id).all()
114
+
115
+    def save(self, role: UserRoleInWorkspace):
116
+        self._session.flush()
117
+
118
+    @classmethod
119
+    def get_roles_for_select_field(cls):
120
+        """
121
+
122
+        :return: list of DictLikeClass instances representing available Roles
123
+        (to be used in select fields)
124
+        """
125
+        result = list()
126
+
127
+        for role_id in UserRoleInWorkspace.get_all_role_values():
128
+            role = RoleType(role_id)
129
+            result.append(role)
130
+
131
+        return result

+ 35 - 0
tracim/lib/utils.py View File

@@ -0,0 +1,35 @@
1
+import datetime
2
+
3
+def cmp_to_key(mycmp):
4
+    """
5
+    List sort related function
6
+
7
+    Convert a cmp= function into a key= function
8
+    """
9
+    class K(object):
10
+        def __init__(self, obj, *args):
11
+            self.obj = obj
12
+        def __lt__(self, other):
13
+            return mycmp(self.obj, other.obj) < 0
14
+        def __gt__(self, other):
15
+            return mycmp(self.obj, other.obj) > 0
16
+        def __eq__(self, other):
17
+            return mycmp(self.obj, other.obj) == 0
18
+        def __le__(self, other):
19
+            return mycmp(self.obj, other.obj) <= 0
20
+        def __ge__(self, other):
21
+            return mycmp(self.obj, other.obj) >= 0
22
+        def __ne__(self, other):
23
+            return mycmp(self.obj, other.obj) != 0
24
+    return K
25
+
26
+def current_date_for_filename() -> str:
27
+    """
28
+    ISO8601 current date, adapted to be used in filename (for
29
+    webdav feature for example), with trouble-free characters.
30
+    :return: current date as string like "2018-03-19T15.49.27.246592"
31
+    """
32
+    # INFO - G.M - 19-03-2018 - As ':' is in transform_to_bdd method in
33
+    # webdav utils, it may cause trouble. So, it should be replaced to
34
+    # a character which will not change in bdd.
35
+    return datetime.datetime.now().isoformat().replace(':', '.')

+ 237 - 0
tracim/lib/workspace.py View File

@@ -0,0 +1,237 @@
1
+# -*- coding: utf-8 -*-
2
+import typing
3
+
4
+from sqlalchemy.orm import Query
5
+from sqlalchemy.orm import Session
6
+from tracim.translation import fake_translator as _
7
+
8
+from tracim.lib.userworkspace import RoleApi
9
+from tracim.models.auth import Group
10
+from tracim.models.auth import User
11
+from tracim.models.data import UserRoleInWorkspace
12
+from tracim.models.data import Workspace
13
+
14
+__author__ = 'damien'
15
+
16
+
17
+class WorkspaceApi(object):
18
+
19
+    def __init__(
20
+            self,
21
+            session: Session,
22
+            current_user: User,
23
+            force_role: bool=False
24
+    ):
25
+        """
26
+        :param current_user: Current user of context
27
+        :param force_role: If True, app role in queries even if admin
28
+        """
29
+        self._session = session
30
+        self._user = current_user
31
+        self._force_role = force_role
32
+
33
+    def _base_query_without_roles(self):
34
+        return self._session.query(Workspace).filter(Workspace.is_deleted == False)
35
+
36
+    def _base_query(self):
37
+        if not self._force_role and self._user.profile.id>=Group.TIM_ADMIN:
38
+            return self._base_query_without_roles()
39
+
40
+        return self._session.query(Workspace).\
41
+            join(Workspace.roles).\
42
+            filter(UserRoleInWorkspace.user_id == self._user.user_id).\
43
+            filter(Workspace.is_deleted == False)
44
+
45
+    def create_workspace(
46
+            self,
47
+            label: str='',
48
+            description: str='',
49
+            calendar_enabled: bool=False,
50
+            save_now: bool=False,
51
+    ) -> Workspace:
52
+        if not label:
53
+            label = self.generate_label()
54
+
55
+        workspace = Workspace()
56
+        workspace.label = label
57
+        workspace.description = description
58
+        workspace.calendar_enabled = calendar_enabled
59
+
60
+        # By default, we force the current user to be the workspace manager
61
+        # And to receive email notifications
62
+        role_api = RoleApi(
63
+            session=self._session,
64
+            current_user=self._user,
65
+        )
66
+
67
+        role = role_api.create_one(
68
+            self._user,
69
+            workspace,
70
+            UserRoleInWorkspace.WORKSPACE_MANAGER,
71
+            with_notif=True,
72
+        )
73
+
74
+        self._session.add(workspace)
75
+        self._session.add(role)
76
+
77
+        if save_now:
78
+            self._session.flush()
79
+
80
+        # TODO - G.M - 28-03-2018 - [Calendar] Reenable calendar stuff
81
+        # if calendar_enabled:
82
+        #     self._ensure_calendar_exist(workspace)
83
+        # else:
84
+        #     self._disable_calendar(workspace)
85
+
86
+        return workspace
87
+
88
+    def get_one(self, id):
89
+        return self._base_query().filter(Workspace.workspace_id == id).one()
90
+
91
+    def get_one_by_label(self, label: str) -> Workspace:
92
+        return self._base_query().filter(Workspace.label == label).one()
93
+
94
+    """
95
+    def get_one_for_current_user(self, id):
96
+        return self._base_query().filter(Workspace.workspace_id==id).\
97
+            session.query(ZKContact).filter(ZKContact.groups.any(ZKGroup.id.in_([1,2,3])))
98
+            filter(sqla.).one()
99
+    """
100
+
101
+    def get_all(self):
102
+        return self._base_query().all()
103
+
104
+    def get_all_for_user(self, user: User, ignored_ids=None):
105
+        workspaces = []
106
+
107
+        for role in user.roles:
108
+            if not role.workspace.is_deleted:
109
+                if not ignored_ids:
110
+                    workspaces.append(role.workspace)
111
+                elif role.workspace.workspace_id not in ignored_ids:
112
+                        workspaces.append(role.workspace)
113
+                else:
114
+                    pass  # do not return workspace
115
+
116
+        workspaces.sort(key=lambda workspace: workspace.label.lower())
117
+        return workspaces
118
+
119
+    def get_all_manageable(self) -> typing.List[Workspace]:
120
+        """Get all workspaces the current user has manager rights on."""
121
+        workspaces = []  # type: typing.List[Workspace]
122
+        if self._user.profile.id == Group.TIM_ADMIN:
123
+            workspaces = self._base_query().order_by(Workspace.label).all()
124
+        elif self._user.profile.id == Group.TIM_MANAGER:
125
+            workspaces = self._base_query() \
126
+                .filter(
127
+                    UserRoleInWorkspace.role ==
128
+                    UserRoleInWorkspace.WORKSPACE_MANAGER
129
+                ) \
130
+                .order_by(Workspace.label) \
131
+                .all()
132
+        return workspaces
133
+
134
+    def disable_notifications(self, user: User, workspace: Workspace):
135
+        for role in user.roles:
136
+            if role.workspace==workspace:
137
+                role.do_notify = False
138
+
139
+    def enable_notifications(self, user: User, workspace: Workspace):
140
+        for role in user.roles:
141
+            if role.workspace==workspace:
142
+                role.do_notify = True
143
+
144
+    def get_notifiable_roles(self, workspace: Workspace) -> [UserRoleInWorkspace]:
145
+        roles = []
146
+        for role in workspace.roles:
147
+            if role.do_notify==True \
148
+                    and role.user!=self._user \
149
+                    and role.user.is_active:
150
+                roles.append(role)
151
+        return roles
152
+
153
+    def save(self, workspace: Workspace):
154
+        self._session.flush()
155
+
156
+    def delete_one(self, workspace_id, flush=True):
157
+        workspace = self.get_one(workspace_id)
158
+        workspace.is_deleted = True
159
+
160
+        if flush:
161
+            self._session.flush()
162
+
163
+    def restore_one(self, workspace_id, flush=True):
164
+        workspace = self._session.query(Workspace)\
165
+            .filter(Workspace.is_deleted==True)\
166
+            .filter(Workspace.workspace_id==workspace_id).one()
167
+        workspace.is_deleted = False
168
+
169
+        if flush:
170
+            self._session.flush()
171
+
172
+        return workspace
173
+
174
+    def execute_created_workspace_actions(self, workspace: Workspace) -> None:
175
+        pass
176
+        # TODO - G.M - 28-03-2018 - [Calendar] Re-enable this calendar stuff
177
+        # self.ensure_calendar_exist(workspace)
178
+
179
+    # TODO - G.M - 28-03-2018 - [Calendar] Re-enable this calendar stuff
180
+    # def ensure_calendar_exist(self, workspace: Workspace) -> None:
181
+    #     # Note: Cyclic imports
182
+    #     from tracim.lib.calendar import CalendarManager
183
+    #     from tracim.model.organisational import WorkspaceCalendar
184
+    #
185
+    #     calendar_manager = CalendarManager(self._user)
186
+    #
187
+    #     try:
188
+    #         calendar_manager.enable_calendar_file(
189
+    #             calendar_class=WorkspaceCalendar,
190
+    #             related_object_id=workspace.workspace_id,
191
+    #             raise_=True,
192
+    #         )
193
+    #     # If previous calendar file no exist, calendar must be created
194
+    #     except FileNotFoundError:
195
+    #         self._user.ensure_auth_token()
196
+    #
197
+    #         # Ensure database is up-to-date
198
+    #         self.session.flush()
199
+    #         transaction.commit()
200
+    #
201
+    #         calendar_manager.create_then_remove_fake_event(
202
+    #             calendar_class=WorkspaceCalendar,
203
+    #             related_object_id=workspace.workspace_id,
204
+    #         )
205
+    #
206
+    # def disable_calendar(self, workspace: Workspace) -> None:
207
+    #     # Note: Cyclic imports
208
+    #     from tracim.lib.calendar import CalendarManager
209
+    #     from tracim.model.organisational import WorkspaceCalendar
210
+    #
211
+    #     calendar_manager = CalendarManager(self._user)
212
+    #     calendar_manager.disable_calendar_file(
213
+    #         calendar_class=WorkspaceCalendar,
214
+    #         related_object_id=workspace.workspace_id,
215
+    #         raise_=False,
216
+    #     )
217
+
218
+    def get_base_query(self) -> Query:
219
+        return self._base_query()
220
+
221
+    def generate_label(self) -> str:
222
+        """
223
+        :return: Generated workspace label
224
+        """
225
+        query = self._base_query_without_roles() \
226
+            .filter(Workspace.label.ilike('{0}%'.format(
227
+                _('Workspace'),
228
+            )))
229
+
230
+        return _('Workspace {}').format(
231
+            query.count() + 1,
232
+        )
233
+
234
+
235
+class UnsafeWorkspaceApi(WorkspaceApi):
236
+    def _base_query(self):
237
+        return self.session.query(Workspace).filter(Workspace.is_deleted==False)

+ 1 - 1
tracim/models/__init__.py View File

@@ -21,7 +21,7 @@ def get_engine(settings, prefix='sqlalchemy.'):
21 21
 
22 22
 
23 23
 def get_session_factory(engine):
24
-    factory = sessionmaker()
24
+    factory = sessionmaker(expire_on_commit=False)
25 25
     factory.configure(bind=engine)
26 26
     return factory
27 27
 

+ 3 - 3
tracim/models/revision_protection.py View File

@@ -63,7 +63,7 @@ class RevisionsIntegrity(object):
63 63
 
64 64
 @contextmanager
65 65
 def new_revision(
66
-        dbsession: Session,
66
+        session: Session,
67 67
         tm: TransactionManager,
68 68
         content: Content,
69 69
         force_create_new_revision: bool=False,
@@ -71,14 +71,14 @@ def new_revision(
71 71
     """
72 72
     Prepare context to update a Content. It will add a new updatable revision
73 73
     to the content.
74
-    :param dbsession: Database _session
74
+    :param session: Database _session
75 75
     :param tm: TransactionManager
76 76
     :param content: Content instance to update
77 77
     :param force_create_new_revision: Decide if new_rev should or should not
78 78
     be forced.
79 79
     :return:
80 80
     """
81
-    with dbsession.no_autoflush:
81
+    with session.no_autoflush:
82 82
         try:
83 83
             if force_create_new_revision \
84 84
                     or inspect(content.revision).has_identity:

File diff suppressed because it is too large
+ 1101 - 0
tracim/models/serializers.py


+ 37 - 27
tracim/tests/__init__.py View File

@@ -3,6 +3,9 @@ import transaction
3 3
 from depot.manager import DepotManager
4 4
 from pyramid import testing
5 5
 
6
+from nose.tools import eq_
7
+from tracim.lib.content import ContentApi
8
+from tracim.lib.workspace import WorkspaceApi
6 9
 from tracim.models.data import Workspace
7 10
 from tracim.models.data import Content
8 11
 from tracim.logger import logger
@@ -57,33 +60,6 @@ class BaseTest(unittest.TestCase):
57 60
         transaction.abort()
58 61
         DeclarativeBase.metadata.drop_all(self.engine)
59 62
 
60
-# class DefaultTest(object):
61
-#
62
-#     def _create_workspace_and_test(self, name, user) -> Workspace:
63
-#         """
64
-#         All extra parameters (*args, **kwargs) are for Workspace init
65
-#         :return: Created workspace instance
66
-#         """
67
-#         WorkspaceApi(user).create_workspace(name, save_now=True)
68
-#
69
-#         eq_(1, self._session.query(Workspace).filter(Workspace.label == name).count())
70
-#         return self._session.query(Workspace).filter(Workspace.label == name).one()
71
-#
72
-#     def _create_content_and_test(self, name, workspace, *args, **kwargs) -> Content:
73
-#         """
74
-#         All extra parameters (*args, **kwargs) are for Content init
75
-#         :return: Created Content instance
76
-#         """
77
-#         content = Content(*args, **kwargs)
78
-#         content.label = name
79
-#         content.workspace = workspace
80
-#         self._session.add(content)
81
-#         self._session.flush()
82
-#
83
-#         eq_(1, ContentApi.get_canonical_query().filter(Content.label == name).count())
84
-#         return ContentApi.get_canonical_query().filter(Content.label == name).one()
85
-
86
-
87 63
 class StandardTest(BaseTest):
88 64
     """
89 65
     BaseTest with default fixtures
@@ -96,3 +72,37 @@ class StandardTest(BaseTest):
96 72
             session=self.session,
97 73
             config=CFG(self.config.get_settings()))
98 74
         fixtures_loader.loads(self.fixtures)
75
+
76
+
77
+class DefaultTest(StandardTest):
78
+
79
+    def _create_workspace_and_test(self, name, user) -> Workspace:
80
+        """
81
+        All extra parameters (*args, **kwargs) are for Workspace init
82
+        :return: Created workspace instance
83
+        """
84
+        WorkspaceApi(
85
+            current_user=user,
86
+            session=self.session,
87
+        ).create_workspace(name, save_now=True)
88
+
89
+        eq_(1, self.session.query(Workspace).filter(Workspace.label == name).count())
90
+        return self.session.query(Workspace).filter(Workspace.label == name).one()
91
+
92
+    def _create_content_and_test(self, name, workspace, *args, **kwargs) -> Content:
93
+        """
94
+        All extra parameters (*args, **kwargs) are for Content init
95
+        :return: Created Content instance
96
+        """
97
+        content = Content(*args, **kwargs)
98
+        content.label = name
99
+        content.workspace = workspace
100
+        self.session.add(content)
101
+        self.session.flush()
102
+
103
+        content_api = ContentApi(
104
+            current_user=None,
105
+            session=self.session,
106
+        )
107
+        eq_(1, content_api.get_canonical_query().filter(Content.label == name).count())
108
+        return content_api.get_canonical_query().filter(Content.label == name).one()

+ 2 - 0
tracim/tests/library/__init__.py View File

@@ -0,0 +1,2 @@
1
+# -*- coding: utf-8 -*-
2
+"""Unit test suite for the library of the application."""

File diff suppressed because it is too large
+ 1815 - 0
tracim/tests/library/test_content_api.py


+ 2 - 2
tracim/tests/models/test_content.py View File

@@ -76,7 +76,7 @@ class TestContent(StandardTest):
76 76
         eq_(1, self.session.query(ContentRevisionRO).filter(ContentRevisionRO.label == 'TEST_CONTENT_1').count())
77 77
 
78 78
         with new_revision(
79
-                dbsession=self.session,
79
+                session=self.session,
80 80
                 tm=transaction.manager,
81 81
                 content=content
82 82
         ):
@@ -88,7 +88,7 @@ class TestContent(StandardTest):
88 88
         eq_(1, self.session.query(Content).filter(Content.id == created_content.id).count())
89 89
 
90 90
         with new_revision(
91
-                dbsession=self.session,
91
+                session=self.session,
92 92
                 tm=transaction.manager,
93 93
                 content=content
94 94
         ):