Przeglądaj źródła

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

Bastien Sevajol (Algoo) 8 lat temu
rodzic
commit
8acbd62463

+ 15 - 3
tracim/tracim/command/user.py Wyświetl plik

3
 from sqlalchemy.exc import IntegrityError
3
 from sqlalchemy.exc import IntegrityError
4
 from tg import config
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
 from tracim.lib.auth.ldap import LDAPAuth
8
 from tracim.lib.auth.ldap import LDAPAuth
9
+from tracim.lib.daemons import DaemonsManager
10
+from tracim.lib.daemons import RadicaleDaemon
8
 from tracim.lib.email import get_email_manager
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
 from tracim.lib.group import GroupApi
14
 from tracim.lib.group import GroupApi
11
 from tracim.lib.user import UserApi
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
 class UserCommand(AppContextCommand):
20
 class UserCommand(AppContextCommand):
108
             user = User(email=login, password=password, **kwargs)
113
             user = User(email=login, password=password, **kwargs)
109
             self._session.add(user)
114
             self._session.add(user)
110
             self._session.flush()
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
         except IntegrityError:
123
         except IntegrityError:
112
             self._session.rollback()
124
             self._session.rollback()
113
             raise AlreadyExistError()
125
             raise AlreadyExistError()

+ 1 - 2
tracim/tracim/controllers/admin/user.py Wyświetl plik

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

+ 13 - 14
tracim/tracim/controllers/admin/workspace.py Wyświetl plik

5
 from tg.i18n import ugettext as _
5
 from tg.i18n import ugettext as _
6
 
6
 
7
 from tracim.controllers import TIMRestController
7
 from tracim.controllers import TIMRestController
8
-from tracim.controllers import TIMRestPathContextSetup
9
 
8
 
10
 
9
 
11
 from tracim.lib import CST
10
 from tracim.lib import CST
13
 from tracim.lib.helpers import on_off_to_boolean
12
 from tracim.lib.helpers import on_off_to_boolean
14
 from tracim.lib.user import UserApi
13
 from tracim.lib.user import UserApi
15
 from tracim.lib.userworkspace import RoleApi
14
 from tracim.lib.userworkspace import RoleApi
16
-from tracim.lib.content import ContentApi
17
 from tracim.lib.workspace import WorkspaceApi
15
 from tracim.lib.workspace import WorkspaceApi
18
-from tracim.model import DBSession
19
 
16
 
20
 from tracim.model.auth import Group
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
 from tracim.model.data import UserRoleInWorkspace
18
 from tracim.model.data import UserRoleInWorkspace
26
 
19
 
27
 from tracim.model.serializers import Context, CTX, DictLikeClass
20
 from tracim.model.serializers import Context, CTX, DictLikeClass
28
 
21
 
29
-from tracim.controllers.content import UserWorkspaceFolderRestController
30
-
31
-
32
-
33
 
22
 
34
 class RoleInWorkspaceRestController(TIMRestController, BaseController):
23
 class RoleInWorkspaceRestController(TIMRestController, BaseController):
35
 
24
 
198
         workspace_api_controller = WorkspaceApi(user)
187
         workspace_api_controller = WorkspaceApi(user)
199
         calendar_enabled = on_off_to_boolean(calendar_enabled)
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
         tg.flash(_('{} workspace created.').format(workspace.label), CST.STATUS_OK)
197
         tg.flash(_('{} workspace created.').format(workspace.label), CST.STATUS_OK)
206
         tg.redirect(self.url())
198
         tg.redirect(self.url())
228
         workspace.calendar_enabled = calendar_enabled
220
         workspace.calendar_enabled = calendar_enabled
229
         workspace_api_controller.save(workspace)
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
         tg.flash(_('{} workspace updated.').format(workspace.label), CST.STATUS_OK)
230
         tg.flash(_('{} workspace updated.').format(workspace.label), CST.STATUS_OK)
232
         tg.redirect(self.url(workspace.workspace_id))
231
         tg.redirect(self.url(workspace.workspace_id))
233
         return
232
         return

+ 5 - 22
tracim/tracim/controllers/user.py Wyświetl plik

1
 # -*- coding: utf-8 -*-
1
 # -*- coding: utf-8 -*-
2
 from webob.exc import HTTPForbidden
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
 import tg
3
 import tg
11
 from tg import tmpl_context
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
 from tracim.controllers import TIMRestController
7
 from tracim.controllers import TIMRestController
21
-from tracim.lib import helpers as h
22
 from tracim.lib.user import UserApi
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
 from tracim.lib.workspace import WorkspaceApi
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
 class UserWorkspaceRestController(TIMRestController):
16
 class UserWorkspaceRestController(TIMRestController):

+ 43 - 0
tracim/tracim/lib/calendar.py Wyświetl plik

1
+import caldav
1
 import os
2
 import os
2
 
3
 
3
 import re
4
 import re
5
 
6
 
6
 from icalendar import Event as iCalendarEvent
7
 from icalendar import Event as iCalendarEvent
7
 from sqlalchemy.orm.exc import NoResultFound
8
 from sqlalchemy.orm.exc import NoResultFound
9
+from tg import tmpl_context
8
 from tg.i18n import ugettext as _
10
 from tg.i18n import ugettext as _
9
 
11
 
10
 from tracim.lib.content import ContentApi
12
 from tracim.lib.content import ContentApi
316
         :return: True if given collection path is an discover path
318
         :return: True if given collection path is an discover path
317
         """
319
         """
318
         return path in ('user', 'workspace')
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 Wyświetl plik

184
         if workspace:
184
         if workspace:
185
             result = result.filter(Content.workspace_id==workspace.workspace_id)
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
             user = DBSession.query(User).get(self._user_id)
188
             user = DBSession.query(User).get(self._user_id)
189
             # Filter according to user workspaces
189
             # Filter according to user workspaces
190
             workspace_ids = [r.workspace_id for r in user.roles \
190
             workspace_ids = [r.workspace_id for r in user.roles \

+ 2 - 0
tracim/tracim/lib/radicale/auth.py Wyświetl plik

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

+ 25 - 3
tracim/tracim/lib/user.py Wyświetl plik

1
 # -*- coding: utf-8 -*-
1
 # -*- coding: utf-8 -*-
2
-
3
-__author__ = 'damien'
2
+import transaction
4
 
3
 
5
 import tg
4
 import tg
6
 
5
 
8
 
7
 
9
 from tracim.model import auth as pbma
8
 from tracim.model import auth as pbma
10
 from tracim.model import DBSession
9
 from tracim.model import DBSession
11
-import tracim.model.data as pmd
10
+
11
+__author__ = 'damien'
12
+
12
 
13
 
13
 class UserApi(object):
14
 class UserApi(object):
14
 
15
 
70
     def save(self, user: User):
71
     def save(self, user: User):
71
         DBSession.flush()
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
 class UserStaticApi(object):
96
 class UserStaticApi(object):
75
 
97
 

+ 35 - 18
tracim/tracim/lib/workspace.py Wyświetl plik

1
 # -*- coding: utf-8 -*-
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
 from tracim.lib.userworkspace import RoleApi
4
 from tracim.lib.userworkspace import RoleApi
19
 from tracim.model.auth import Group
5
 from tracim.model.auth import Group
20
 from tracim.model.auth import User
6
 from tracim.model.auth import User
21
 from tracim.model.data import Workspace
7
 from tracim.model.data import Workspace
22
 from tracim.model.data import UserRoleInWorkspace
8
 from tracim.model.data import UserRoleInWorkspace
23
-
24
-from tracim.model import auth as pbma
25
 from tracim.model import DBSession
9
 from tracim.model import DBSession
26
 
10
 
11
+__author__ = 'damien'
12
+
27
 
13
 
28
 class WorkspaceApi(object):
14
 class WorkspaceApi(object):
29
 
15
 
39
             filter(UserRoleInWorkspace.user_id==self._user.user_id).\
25
             filter(UserRoleInWorkspace.user_id==self._user.user_id).\
40
             filter(Workspace.is_deleted==False)
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
         workspace = Workspace()
35
         workspace = Workspace()
44
         workspace.label = label
36
         workspace.label = label
45
         workspace.description = description
37
         workspace.description = description
38
+        workspace.calendar_enabled = calendar_enabled
46
 
39
 
47
         # By default, we force the current user to be the workspace manager
40
         # By default, we force the current user to be the workspace manager
48
         # And to receive email notifications
41
         # And to receive email notifications
56
         if save_now:
49
         if save_now:
57
             DBSession.flush()
50
             DBSession.flush()
58
 
51
 
52
+        if calendar_enabled:
53
+            self.execute_created_workspace_actions(workspace)
54
+
59
         return workspace
55
         return workspace
60
 
56
 
61
     def get_one(self, id):
57
     def get_one(self, id):
125
 
121
 
126
         return workspace
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
 class UnsafeWorkspaceApi(WorkspaceApi):
146
 class UnsafeWorkspaceApi(WorkspaceApi):
130
     def _base_query(self):
147
     def _base_query(self):

+ 6 - 2
tracim/tracim/model/__init__.py Wyświetl plik

39
 
39
 
40
 # Global session manager: DBSession() returns the Thread-local
40
 # Global session manager: DBSession() returns the Thread-local
41
 # session object appropriate for the current web request.
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
 DBSession = scoped_session(maker)
48
 DBSession = scoped_session(maker)
45
 
49
 
46
 # Base class for all of our model classes: By default, the data model is
50
 # Base class for all of our model classes: By default, the data model is

+ 1 - 1
tracim/tracim/tests/__init__.py Wyświetl plik

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

+ 109 - 0
tracim/tracim/tests/functional/test_calendar.py Wyświetl plik

1
+import os
1
 import time
2
 import time
2
 
3
 
3
 import caldav
4
 import caldav
4
 import transaction
5
 import transaction
5
 from caldav.lib.error import AuthorizationError
6
 from caldav.lib.error import AuthorizationError
7
+from collections import OrderedDict
6
 from nose.tools import eq_
8
 from nose.tools import eq_
7
 from nose.tools import ok_
9
 from nose.tools import ok_
8
 from nose.tools import raises
10
 from nose.tools import raises
9
 import requests
11
 import requests
10
 from requests.exceptions import ConnectionError
12
 from requests.exceptions import ConnectionError
11
 from sqlalchemy.orm.exc import NoResultFound
13
 from sqlalchemy.orm.exc import NoResultFound
14
+from tg import config
12
 
15
 
13
 from tracim.config.app_cfg import daemons
16
 from tracim.config.app_cfg import daemons
14
 from tracim.lib.calendar import CalendarManager
17
 from tracim.lib.calendar import CalendarManager
18
 from tracim.tests import not_raises
21
 from tracim.tests import not_raises
19
 from tracim.model.auth import User
22
 from tracim.model.auth import User
20
 from tracim.model.data import Content
23
 from tracim.model.data import Content
24
+from tracim.model.data import Workspace
21
 
25
 
22
 
26
 
23
 class TestCalendar(BaseTestCalendar):
27
 class TestCalendar(BaseTestCalendar):
203
         eq_(event.properties['location'], 'Here')
207
         eq_(event.properties['location'], 'Here')
204
         eq_(event.properties['start'], '2010-05-12 18:00:00+0000')
208
         eq_(event.properties['start'], '2010-05-12 18:00:00+0000')
205
         eq_(event.properties['end'], '2010-05-12 17:00:00+0000')
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
+        )