Browse Source

Closes #87: Ensure Radicale create calendars for new user/workspaces

Bastien Sevajol (Algoo) 7 years ago
parent
commit
8acbd62463

+ 15 - 3
tracim/tracim/command/user.py View File

@@ -3,13 +3,18 @@ import transaction
3 3
 from sqlalchemy.exc import IntegrityError
4 4
 from tg import config
5 5
 
6
-from tracim.command import AppContextCommand, Extender
6
+from tracim.command import AppContextCommand
7
+from tracim.command import Extender
7 8
 from tracim.lib.auth.ldap import LDAPAuth
9
+from tracim.lib.daemons import DaemonsManager
10
+from tracim.lib.daemons import RadicaleDaemon
8 11
 from tracim.lib.email import get_email_manager
9
-from tracim.lib.exception import AlreadyExistError, CommandAbortedError
12
+from tracim.lib.exception import AlreadyExistError
13
+from tracim.lib.exception import CommandAbortedError
10 14
 from tracim.lib.group import GroupApi
11 15
 from tracim.lib.user import UserApi
12
-from tracim.model import DBSession, User
16
+from tracim.model import DBSession
17
+from tracim.model import User
13 18
 
14 19
 
15 20
 class UserCommand(AppContextCommand):
@@ -108,6 +113,13 @@ class UserCommand(AppContextCommand):
108 113
             user = User(email=login, password=password, **kwargs)
109 114
             self._session.add(user)
110 115
             self._session.flush()
116
+
117
+            # We need to enable radicale if it not already done
118
+            daemons = DaemonsManager()
119
+            daemons.run('radicale', RadicaleDaemon)
120
+
121
+            user_api = UserApi(user)
122
+            user_api.execute_created_user_actions(user)
111 123
         except IntegrityError:
112 124
             self._session.rollback()
113 125
             raise AlreadyExistError()

+ 1 - 2
tracim/tracim/controllers/admin/user.py View File

@@ -312,7 +312,6 @@ class UserRestController(TIMRestController):
312 312
             is_tracim_manager = False
313 313
             is_tracim_admin = False
314 314
 
315
-
316 315
         api = UserApi(current_user)
317 316
 
318 317
         if api.user_with_email_exists(email):
@@ -347,10 +346,10 @@ class UserRestController(TIMRestController):
347 346
             email_manager = get_email_manager()
348 347
             email_manager.notify_created_account(user, password=password)
349 348
 
349
+        api.execute_created_user_actions(user)
350 350
         tg.flash(_('User {} created.').format(user.get_display_name()), CST.STATUS_OK)
351 351
         tg.redirect(self.url())
352 352
 
353
-
354 353
     @tg.expose('tracim.templates.admin.user_getone')
355 354
     def get_one(self, user_id):
356 355
         current_user = tmpl_context.current_user

+ 13 - 14
tracim/tracim/controllers/admin/workspace.py View File

@@ -5,7 +5,6 @@ from tg import tmpl_context
5 5
 from tg.i18n import ugettext as _
6 6
 
7 7
 from tracim.controllers import TIMRestController
8
-from tracim.controllers import TIMRestPathContextSetup
9 8
 
10 9
 
11 10
 from tracim.lib import CST
@@ -13,23 +12,13 @@ from tracim.lib.base import BaseController
13 12
 from tracim.lib.helpers import on_off_to_boolean
14 13
 from tracim.lib.user import UserApi
15 14
 from tracim.lib.userworkspace import RoleApi
16
-from tracim.lib.content import ContentApi
17 15
 from tracim.lib.workspace import WorkspaceApi
18
-from tracim.model import DBSession
19 16
 
20 17
 from tracim.model.auth import Group
21
-from tracim.model.data import NodeTreeItem
22
-from tracim.model.data import Content
23
-from tracim.model.data import ContentType
24
-from tracim.model.data import Workspace
25 18
 from tracim.model.data import UserRoleInWorkspace
26 19
 
27 20
 from tracim.model.serializers import Context, CTX, DictLikeClass
28 21
 
29
-from tracim.controllers.content import UserWorkspaceFolderRestController
30
-
31
-
32
-
33 22
 
34 23
 class RoleInWorkspaceRestController(TIMRestController, BaseController):
35 24
 
@@ -198,9 +187,12 @@ class WorkspaceRestController(TIMRestController, BaseController):
198 187
         workspace_api_controller = WorkspaceApi(user)
199 188
         calendar_enabled = on_off_to_boolean(calendar_enabled)
200 189
 
201
-        workspace = workspace_api_controller.create_workspace(name, description)
202
-        workspace.calendar_enabled = calendar_enabled
203
-        DBSession.flush()
190
+        workspace = workspace_api_controller.create_workspace(
191
+            name,
192
+            description,
193
+            calendar_enabled=calendar_enabled,
194
+            save_now=True,
195
+        )
204 196
 
205 197
         tg.flash(_('{} workspace created.').format(workspace.label), CST.STATUS_OK)
206 198
         tg.redirect(self.url())
@@ -228,6 +220,13 @@ class WorkspaceRestController(TIMRestController, BaseController):
228 220
         workspace.calendar_enabled = calendar_enabled
229 221
         workspace_api_controller.save(workspace)
230 222
 
223
+        if calendar_enabled:
224
+            workspace_id = workspace.workspace_id
225
+            workspace_api_controller.ensure_calendar_exist(workspace)
226
+            # We must reload user because user_api.created_user_actions
227
+            # commit transaction
228
+            workspace = workspace_api_controller.get_one_by_id(workspace_id)
229
+
231 230
         tg.flash(_('{} workspace updated.').format(workspace.label), CST.STATUS_OK)
232 231
         tg.redirect(self.url(workspace.workspace_id))
233 232
         return

+ 5 - 22
tracim/tracim/controllers/user.py View File

@@ -1,33 +1,16 @@
1 1
 # -*- coding: utf-8 -*-
2 2
 from webob.exc import HTTPForbidden
3
-
4
-from tracim import model  as pm
5
-
6
-from sprox.tablebase import TableBase
7
-from sprox.formbase import EditableForm, AddRecordForm
8
-from sprox.fillerbase import TableFiller, EditFormFiller
9
-from tw2 import forms as tw2f
10 3
 import tg
11 4
 from tg import tmpl_context
12
-from tg.i18n import ugettext as _, lazy_ugettext as l_
13
-
14
-from sprox.widgets import PropertyMultipleSelectField
15
-from sprox._compat import unicode_text
16
-
17
-from formencode import Schema
18
-from formencode.validators import FieldsMatch
5
+from tg.i18n import ugettext as _
19 6
 
20 7
 from tracim.controllers import TIMRestController
21
-from tracim.lib import helpers as h
22 8
 from tracim.lib.user import UserApi
23
-from tracim.lib.group import GroupApi
24
-from tracim.lib.user import UserStaticApi
25
-from tracim.lib.userworkspace import RoleApi
26 9
 from tracim.lib.workspace import WorkspaceApi
27
-
28
-from tracim.model import DBSession
29
-from tracim.model.auth import Group, User
30
-from tracim.model.serializers import Context, CTX, DictLikeClass
10
+from tracim.model.serializers import Context
11
+from tracim.model.serializers import CTX
12
+from tracim.model.serializers import DictLikeClass
13
+from tracim import model as pm
31 14
 
32 15
 
33 16
 class UserWorkspaceRestController(TIMRestController):

+ 43 - 0
tracim/tracim/lib/calendar.py View File

@@ -1,3 +1,4 @@
1
+import caldav
1 2
 import os
2 3
 
3 4
 import re
@@ -5,6 +6,7 @@ import transaction
5 6
 
6 7
 from icalendar import Event as iCalendarEvent
7 8
 from sqlalchemy.orm.exc import NoResultFound
9
+from tg import tmpl_context
8 10
 from tg.i18n import ugettext as _
9 11
 
10 12
 from tracim.lib.content import ContentApi
@@ -316,3 +318,44 @@ class CalendarManager(object):
316 318
         :return: True if given collection path is an discover path
317 319
         """
318 320
         return path in ('user', 'workspace')
321
+
322
+    def create_then_remove_fake_event(
323
+            self,
324
+            calendar_class,
325
+            related_object_id,
326
+    ) -> None:
327
+        radicale_base_url = self.get_base_url()
328
+        client = caldav.DAVClient(
329
+            radicale_base_url,
330
+            username=self._user.email,
331
+            password=self._user.auth_token,
332
+        )
333
+        if calendar_class == WorkspaceCalendar:
334
+            calendar_url = self.get_workspace_calendar_url(related_object_id)
335
+        elif calendar_class == UserCalendar:
336
+            calendar_url = self.get_user_calendar_url(related_object_id)
337
+        else:
338
+            raise Exception('Unknown calendar type {0}'.format(calendar_class))
339
+
340
+        user_calendar = caldav.Calendar(
341
+            parent=client,
342
+            client=client,
343
+            url=calendar_url
344
+        )
345
+
346
+        event_ics = """BEGIN:VCALENDAR
347
+VERSION:2.0
348
+PRODID:-//Example Corp.//CalDAV Client//EN
349
+BEGIN:VEVENT
350
+UID:{uid}
351
+DTSTAMP:20100510T182145Z
352
+DTSTART:20100512T170000Z
353
+DTEND:20100512T180000Z
354
+SUMMARY:This is an event
355
+LOCATION:Here
356
+END:VEVENT
357
+END:VCALENDAR
358
+""".format(uid='{0}FAKEEVENT'.format(related_object_id))
359
+        event = user_calendar.add_event(event_ics)
360
+        event.delete()
361
+

+ 1 - 1
tracim/tracim/lib/content.py View File

@@ -184,7 +184,7 @@ class ContentApi(object):
184 184
         if workspace:
185 185
             result = result.filter(Content.workspace_id==workspace.workspace_id)
186 186
 
187
-        if self._user and not self._disable_user_workspaces_filter:
187
+        if self._user and workspace and not self._disable_user_workspaces_filter:
188 188
             user = DBSession.query(User).get(self._user_id)
189 189
             # Filter according to user workspaces
190 190
             workspace_ids = [r.workspace_id for r in user.roles \

+ 2 - 0
tracim/tracim/lib/radicale/auth.py View File

@@ -1,6 +1,7 @@
1 1
 from tg import config
2 2
 
3 3
 from tracim.lib.user import UserApi
4
+from tracim.model import DBSession
4 5
 
5 6
 
6 7
 class Auth(object):
@@ -33,4 +34,5 @@ def is_authenticated(user: str, password: str) -> bool:
33 34
     """
34 35
     see tracim.lib.radicale.auth.Auth#is_authenticated
35 36
     """
37
+    DBSession.expire_all()
36 38
     return Auth.is_authenticated(user, password)

+ 25 - 3
tracim/tracim/lib/user.py View File

@@ -1,6 +1,5 @@
1 1
 # -*- coding: utf-8 -*-
2
-
3
-__author__ = 'damien'
2
+import transaction
4 3
 
5 4
 import tg
6 5
 
@@ -8,7 +7,9 @@ from tracim.model.auth import User
8 7
 
9 8
 from tracim.model import auth as pbma
10 9
 from tracim.model import DBSession
11
-import tracim.model.data as pmd
10
+
11
+__author__ = 'damien'
12
+
12 13
 
13 14
 class UserApi(object):
14 15
 
@@ -70,6 +71,27 @@ class UserApi(object):
70 71
     def save(self, user: User):
71 72
         DBSession.flush()
72 73
 
74
+    def execute_created_user_actions(self, created_user: User) -> None:
75
+        """
76
+        Execute actions when user just been created
77
+        :return:
78
+        """
79
+        # NOTE: Cyclic import
80
+        from tracim.lib.calendar import CalendarManager
81
+        from tracim.model.organisational import UserCalendar
82
+
83
+        created_user.ensure_auth_token()
84
+
85
+        # Ensure database is up-to-date
86
+        DBSession.flush()
87
+        transaction.commit()
88
+
89
+        calendar_manager = CalendarManager(created_user)
90
+        calendar_manager.create_then_remove_fake_event(
91
+            calendar_class=UserCalendar,
92
+            related_object_id=created_user.user_id,
93
+        )
94
+
73 95
 
74 96
 class UserStaticApi(object):
75 97
 

+ 35 - 18
tracim/tracim/lib/workspace.py View File

@@ -1,29 +1,15 @@
1 1
 # -*- coding: utf-8 -*-
2
-
3
-__author__ = 'damien'
4
-
5
-import os
6
-from datetime import datetime
7
-from hashlib import sha256
8
-
9
-from sqlalchemy import Table, ForeignKey, Column
10
-from sqlalchemy.types import Unicode, Integer, DateTime, Text
11
-from sqlalchemy.orm import relation, synonym, contains_eager
12
-from sqlalchemy.orm import joinedload_all
13
-import sqlalchemy.orm as sqlao
14
-import sqlalchemy as sqla
15
-
16
-import tg
2
+import transaction
17 3
 
18 4
 from tracim.lib.userworkspace import RoleApi
19 5
 from tracim.model.auth import Group
20 6
 from tracim.model.auth import User
21 7
 from tracim.model.data import Workspace
22 8
 from tracim.model.data import UserRoleInWorkspace
23
-
24
-from tracim.model import auth as pbma
25 9
 from tracim.model import DBSession
26 10
 
11
+__author__ = 'damien'
12
+
27 13
 
28 14
 class WorkspaceApi(object):
29 15
 
@@ -39,10 +25,17 @@ class WorkspaceApi(object):
39 25
             filter(UserRoleInWorkspace.user_id==self._user.user_id).\
40 26
             filter(Workspace.is_deleted==False)
41 27
 
42
-    def create_workspace(self, label: str, description: str='', save_now:bool=False) -> Workspace:
28
+    def create_workspace(
29
+            self,
30
+            label: str,
31
+            description: str='',
32
+            calendar_enabled: bool=False,
33
+            save_now: bool=False,
34
+    ) -> Workspace:
43 35
         workspace = Workspace()
44 36
         workspace.label = label
45 37
         workspace.description = description
38
+        workspace.calendar_enabled = calendar_enabled
46 39
 
47 40
         # By default, we force the current user to be the workspace manager
48 41
         # And to receive email notifications
@@ -56,6 +49,9 @@ class WorkspaceApi(object):
56 49
         if save_now:
57 50
             DBSession.flush()
58 51
 
52
+        if calendar_enabled:
53
+            self.execute_created_workspace_actions(workspace)
54
+
59 55
         return workspace
60 56
 
61 57
     def get_one(self, id):
@@ -125,6 +121,27 @@ class WorkspaceApi(object):
125 121
 
126 122
         return workspace
127 123
 
124
+    def execute_created_workspace_actions(self, workspace: Workspace) -> None:
125
+        self.ensure_calendar_exist(workspace)
126
+
127
+    def ensure_calendar_exist(self, workspace: Workspace) -> None:
128
+        # Note: Cyclic imports
129
+        from tracim.lib.calendar import CalendarManager
130
+        from tracim.model.organisational import WorkspaceCalendar
131
+
132
+        if workspace.calendar_enabled:
133
+            self._user.ensure_auth_token()
134
+
135
+            # Ensure database is up-to-date
136
+            DBSession.flush()
137
+            transaction.commit()
138
+
139
+            calendar_manager = CalendarManager(self._user)
140
+            calendar_manager.create_then_remove_fake_event(
141
+                calendar_class=WorkspaceCalendar,
142
+                related_object_id=workspace.workspace_id,
143
+            )
144
+
128 145
 
129 146
 class UnsafeWorkspaceApi(WorkspaceApi):
130 147
     def _base_query(self):

+ 6 - 2
tracim/tracim/model/__init__.py View File

@@ -39,8 +39,12 @@ class RevisionsIntegrity(object):
39 39
 
40 40
 # Global session manager: DBSession() returns the Thread-local
41 41
 # session object appropriate for the current web request.
42
-maker = sessionmaker(autoflush=True, autocommit=False,
43
-                     extension=ZopeTransactionExtension())
42
+maker = sessionmaker(
43
+    autoflush=True,
44
+    autocommit=False,
45
+    extension=ZopeTransactionExtension(),
46
+    expire_on_commit=False,
47
+)
44 48
 DBSession = scoped_session(maker)
45 49
 
46 50
 # Base class for all of our model classes: By default, the data model is

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

@@ -354,7 +354,7 @@ class BaseTestThread(BaseTest):
354 354
         return thread
355 355
 
356 356
 
357
-class TestCalendar(TestController):
357
+class TestCalendar(TracimTestController):
358 358
     fixtures = [BaseFixture, TestFixture]
359 359
     application_under_test = 'radicale'
360 360
 

+ 109 - 0
tracim/tracim/tests/functional/test_calendar.py View File

@@ -1,14 +1,17 @@
1
+import os
1 2
 import time
2 3
 
3 4
 import caldav
4 5
 import transaction
5 6
 from caldav.lib.error import AuthorizationError
7
+from collections import OrderedDict
6 8
 from nose.tools import eq_
7 9
 from nose.tools import ok_
8 10
 from nose.tools import raises
9 11
 import requests
10 12
 from requests.exceptions import ConnectionError
11 13
 from sqlalchemy.orm.exc import NoResultFound
14
+from tg import config
12 15
 
13 16
 from tracim.config.app_cfg import daemons
14 17
 from tracim.lib.calendar import CalendarManager
@@ -18,6 +21,7 @@ from tracim.tests import TestCalendar as BaseTestCalendar
18 21
 from tracim.tests import not_raises
19 22
 from tracim.model.auth import User
20 23
 from tracim.model.data import Content
24
+from tracim.model.data import Workspace
21 25
 
22 26
 
23 27
 class TestCalendar(BaseTestCalendar):
@@ -203,3 +207,108 @@ END:VCALENDAR
203 207
         eq_(event.properties['location'], 'Here')
204 208
         eq_(event.properties['start'], '2010-05-12 18:00:00+0000')
205 209
         eq_(event.properties['end'], '2010-05-12 17:00:00+0000')
210
+
211
+    def test_created_user_radicale_calendar(self):
212
+        self._connect_user(
213
+            'admin@admin.admin',
214
+            'admin@admin.admin',
215
+        )
216
+
217
+        user_count = DBSession.query(User)\
218
+            .filter(User.email == 'an-other-email@test.local').count()
219
+        eq_(0, user_count, 'User should not exist yet')
220
+
221
+        radicale_users_folder = '{0}/user'\
222
+            .format(config.get('radicale.server.filesystem.folder'))
223
+        eq_(
224
+            False,
225
+            os.path.isdir(radicale_users_folder),
226
+            'Radicale users folder should not exist yet',
227
+        )
228
+
229
+        # Create a new user, his calendar should be created to
230
+        try_post_user = self.app.post(
231
+            '/admin/users',
232
+            OrderedDict([
233
+                ('name', 'TEST'),
234
+                ('email', 'an-other-email@test.local'),
235
+                ('password', 'an-other-email@test.local'),
236
+                ('is_tracim_manager', 'off'),
237
+                ('is_tracim_admin', 'off'),
238
+                ('send_email', 'off'),
239
+            ])
240
+        )
241
+
242
+        eq_(try_post_user.status_code, 302,
243
+            "Code should be 302, but is %d" % try_post_user.status_code)
244
+
245
+        users_calendars = len([
246
+            name for name in os.listdir(radicale_users_folder)
247
+            if name.endswith('.ics')
248
+        ])
249
+
250
+        user = DBSession.query(User) \
251
+            .filter(User.email == 'an-other-email@test.local').one()
252
+
253
+        eq_(1, users_calendars, 'Radicale user path should list 1 calendar')
254
+        user_calendar = '{0}/{1}.ics'.format(
255
+            radicale_users_folder,
256
+            user.user_id,
257
+        )
258
+        user_calendar_exist = os.path.isfile(user_calendar)
259
+        eq_(True, user_calendar_exist, 'User calendar should be created')
260
+
261
+    def test_created_workspace_radicale_calendar(self):
262
+        self._connect_user(
263
+            'admin@admin.admin',
264
+            'admin@admin.admin',
265
+        )
266
+
267
+        workspaces_count = DBSession.query(Workspace)\
268
+            .filter(Workspace.label == 'WTESTCAL').count()
269
+        eq_(0, workspaces_count, 'Workspace should not exist yet !')
270
+
271
+        radicale_workspaces_folder = '{0}/workspace'\
272
+            .format(config.get('radicale.server.filesystem.folder'))
273
+        eq_(
274
+            False,
275
+            os.path.isdir(radicale_workspaces_folder),
276
+            'Radicale workskpaces folder should not exist yet',
277
+        )
278
+
279
+        # Create a new workspace, his calendar should be created to
280
+        try_post_workspace = self.app.post(
281
+            '/admin/workspaces',
282
+            OrderedDict([
283
+                ('name', 'WTESTCAL'),
284
+                ('description', 'WTESTCALDESCR'),
285
+                ('calendar_enabled', 'on'),
286
+            ])
287
+        )
288
+
289
+        eq_(try_post_workspace.status_code, 302,
290
+            "Code should be 302, but is %d" % try_post_workspace.status_code)
291
+
292
+        workspaces_calendars = len([
293
+            name for name in os.listdir(radicale_workspaces_folder)
294
+            if name.endswith('.ics')
295
+        ])
296
+
297
+        workspace = DBSession.query(Workspace) \
298
+            .filter(Workspace.label == 'WTESTCAL').one()
299
+
300
+        eq_(
301
+            1,
302
+            workspaces_calendars,
303
+            'Radicale workspace path should list 1 calendar',
304
+        )
305
+        workspace_calendar = '{0}/{1}.ics'.format(
306
+            radicale_workspaces_folder,
307
+            workspace.workspace_id,
308
+        )
309
+        workspace_calendar_exist = os.path.isfile(workspace_calendar)
310
+        eq_(
311
+            True,
312
+            workspace_calendar_exist,
313
+            'Workspace calendar should be created',
314
+        )