瀏覽代碼

Radicale: server, auth and rights integration

Bastien Sevajol (Algoo) 9 年之前
父節點
當前提交
108d3622f6

+ 1 - 0
install/requirements.txt 查看文件

49
 lxml
49
 lxml
50
 python-ldap-test==0.2.0
50
 python-ldap-test==0.2.0
51
 who-ldap==3.1.0
51
 who-ldap==3.1.0
52
+unicode-slugify==0.1.3

+ 1 - 0
tracim/setup.py 查看文件

41
     "repoze.who",
41
     "repoze.who",
42
     "who-ldap==3.1.0",
42
     "who-ldap==3.1.0",
43
     "python-ldap-test==0.2.0",
43
     "python-ldap-test==0.2.0",
44
+    "unicode-slugify==0.1.3",
44
     ]
45
     ]
45
 
46
 
46
 setup(
47
 setup(

+ 8 - 0
tracim/tracim/config/app_cfg.py 查看文件

202
             # ContentType.Folder -- Folder is skipped
202
             # ContentType.Folder -- Folder is skipped
203
         ]
203
         ]
204
 
204
 
205
+        self.RADICALE_SERVER_HOST = '0.0.0.0'
206
+        self.RADICALE_SERVER_PORT = 5232
207
+        self.RADICALE_SERVER_SSL = False
208
+
209
+        self.RADICALE_CLIENT_HOST = None  # If None, current host will be used
210
+        self.RADICALE_CLIENT_PORT = 5232
211
+        self.RADICALE_CLIENT_SSL = False
212
+
205
 
213
 
206
     def get_tracker_js_content(self, js_tracker_file_path = None):
214
     def get_tracker_js_content(self, js_tracker_file_path = None):
207
         js_tracker_file_path = tg.config.get('js_tracker_path', None)
215
         js_tracker_file_path = tg.config.get('js_tracker_path', None)

+ 5 - 2
tracim/tracim/config/middleware.py 查看文件

3
 
3
 
4
 from tracim.config.app_cfg import base_config
4
 from tracim.config.app_cfg import base_config
5
 from tracim.config.environment import load_environment
5
 from tracim.config.environment import load_environment
6
-
6
+from tracim.lib.daemons import DaemonsManager
7
+from tracim.lib.daemons import RadicaleDaemon
7
 
8
 
8
 __all__ = ['make_app']
9
 __all__ = ['make_app']
9
 
10
 
35
     app = make_base_app(global_conf, full_stack=True, **app_conf)
36
     app = make_base_app(global_conf, full_stack=True, **app_conf)
36
     
37
     
37
     # Wrap your base TurboGears 2 application with custom middleware here
38
     # Wrap your base TurboGears 2 application with custom middleware here
38
-    
39
+    daemons = DaemonsManager(app)
40
+    daemons.run(RadicaleDaemon.name, RadicaleDaemon)
41
+
39
     return app
42
     return app

+ 2 - 1
tracim/tracim/controllers/admin/workspace.py 查看文件

212
         return DictLikeClass(result = dictified_workspace)
212
         return DictLikeClass(result = dictified_workspace)
213
 
213
 
214
     @tg.expose('tracim.templates.workspace.edit')
214
     @tg.expose('tracim.templates.workspace.edit')
215
-    def put(self, id, name, description):
215
+    def put(self, id, name, description, calendar_enabled):
216
         user = tmpl_context.current_user
216
         user = tmpl_context.current_user
217
         workspace_api_controller = WorkspaceApi(user)
217
         workspace_api_controller = WorkspaceApi(user)
218
 
218
 
219
         workspace = workspace_api_controller.get_one(id)
219
         workspace = workspace_api_controller.get_one(id)
220
         workspace.label = name
220
         workspace.label = name
221
         workspace.description = description
221
         workspace.description = description
222
+        workspace.calendar_enabled = calendar_enabled
222
         workspace_api_controller.save(workspace)
223
         workspace_api_controller.save(workspace)
223
 
224
 
224
         tg.flash(_('{} workspace updated.').format(workspace.label), CST.STATUS_OK)
225
         tg.flash(_('{} workspace updated.').format(workspace.label), CST.STATUS_OK)

+ 90 - 0
tracim/tracim/lib/calendar.py 查看文件

1
+import re
2
+
3
+from tracim.lib.exceptions import UnknownCalendarType
4
+from tracim.lib.exceptions import NotFound
5
+from tracim.lib.user import UserApi
6
+from tracim.lib.workspace import WorkspaceApi
7
+from tracim.model import User
8
+from tracim.model.organizational import Calendar
9
+from tracim.model.organizational import UserCalendar
10
+from tracim.model.organizational import WorkspaceCalendar
11
+
12
+CALENDAR_USER_PATH_RE = 'user\/([0-9]+)--([a-z-]*).ics'
13
+CALENDAR_WORKSPACE_PATH_RE = 'workspace\/([0-9]+)--([a-z0-9-]*).ics'
14
+
15
+CALENDAR_TYPE_USER = 'USER'
16
+CALENDAR_TYPE_WORKSPACE = 'WORKSPACE'
17
+
18
+
19
+class CalendarManager(object):
20
+    def __init__(self, user: User):
21
+        self._user = user
22
+
23
+    def get_type_for_path(self, path: str) -> str:
24
+        """
25
+        Return calendar type for given path. Raise
26
+        tracim.lib.exceptions.UnknownCalendarType if unknown type.
27
+        :param path: path representation like user/42--foo.ics
28
+        :return: Type of calendar, can be one of CALENDAR_TYPE_USER,
29
+        CALENDAR_TYPE_WORKSPACE
30
+        """
31
+        if re.match(CALENDAR_USER_PATH_RE, path):
32
+            return CALENDAR_TYPE_USER
33
+
34
+        if re.match(CALENDAR_WORKSPACE_PATH_RE, path):
35
+            return CALENDAR_TYPE_WORKSPACE
36
+
37
+        raise UnknownCalendarType(
38
+            'No match for calendar path "{0}"'.format(path)
39
+        )
40
+
41
+    def get_id_for_path(self, path: str, type: str) -> int:
42
+        """
43
+        Return related calendar id for given path. Raise
44
+        tracim.lib.exceptions.UnknownCalendarType if unknown type.
45
+        :param path: path representation like user/42--foo.ics
46
+        :param type: Type of calendar, can be one of CALENDAR_TYPE_USER,
47
+        CALENDAR_TYPE_WORKSPACE
48
+        :return: ID of related calendar object. For UserCalendar it will be
49
+        user id, for WorkspaceCalendar it will be Workspace id.
50
+        """
51
+        if type == CALENDAR_TYPE_USER:
52
+            return re.search(CALENDAR_USER_PATH_RE, path).group(1)
53
+        elif type == CALENDAR_TYPE_WORKSPACE:
54
+            return re.search(CALENDAR_WORKSPACE_PATH_RE, path).group(1)
55
+        raise UnknownCalendarType('Type "{0}" is not implemented'.format(type))
56
+
57
+    def find_calendar_with_path(self, path: str) -> Calendar:
58
+        """
59
+        Return calendar for given path. Raise tracim.lib.exceptions.NotFound if
60
+        calendar cannot be found.
61
+        :param path: path representation like user/42--foo.ics
62
+        :return: Calendar corresponding to path
63
+        """
64
+        try:
65
+            type = self.get_type_for_path(path)
66
+            id = self.get_id_for_path(path, type)
67
+        except UnknownCalendarType as exc:
68
+            raise NotFound(str(exc))
69
+
70
+        return self.get_calendar(type, id, path)
71
+
72
+    def get_calendar(self, type: str, id: str, path: str) -> Calendar:
73
+        """
74
+        Return tracim.model.organizational.Calendar instance for given
75
+        parameters.
76
+        :param type: Type of calendar, can be one of CALENDAR_TYPE_USER,
77
+        CALENDAR_TYPE_WORKSPACE
78
+        :param id: related calendar object id
79
+        :param path: path representation like user/42--foo.ics
80
+        :return: a calendar.
81
+        """
82
+        if type == CALENDAR_TYPE_USER:
83
+            user = UserApi(self._user).get_one_by_id(id)
84
+            return UserCalendar(user, path=path)
85
+
86
+        if type == CALENDAR_TYPE_WORKSPACE:
87
+            workspace = WorkspaceApi(self._user).get_one(id)
88
+            return WorkspaceCalendar(workspace, path=path)
89
+
90
+        raise UnknownCalendarType('Type "{0}" is not implemented'.format(type))

+ 104 - 0
tracim/tracim/lib/daemons.py 查看文件

1
+import threading
2
+from wsgiref.simple_server import make_server
3
+
4
+import signal
5
+from radicale import Application as RadicaleApplication
6
+from radicale import HTTPServer as RadicaleHTTPServer
7
+from radicale import HTTPSServer as RadicaleHTTPSServer
8
+from radicale import RequestHandler as RadicaleRequestHandler
9
+from radicale import config as radicale_config
10
+from tg import TGApp
11
+
12
+from tracim.lib.exceptions import AlreadyRunningDaemon
13
+
14
+
15
+class DaemonsManager(object):
16
+    def __init__(self, app: TGApp):
17
+        self._app = app
18
+        self._daemons = {}
19
+
20
+    def run(self, name: str, daemon_class: object, **kwargs) -> None:
21
+        """
22
+        Start a daemon with given daemon class.
23
+        :param name: Name of runned daemon. It's not possible to start two
24
+        daemon with same name. In the opposite case, raise
25
+        tracim.lib.exceptions.AlreadyRunningDaemon
26
+        :param daemon_class: Daemon class to use for daemon instance.
27
+        :param kwargs: Other kwargs will be given to daemon class
28
+        instantiation.
29
+        """
30
+        if name in self._daemons:
31
+            raise AlreadyRunningDaemon(
32
+                'Daemon with name "{0}" already running'.format(name)
33
+            )
34
+
35
+        kwargs['app'] = self._app
36
+        shutdown_program = threading.Event()
37
+        # SIGTERM and SIGINT (aka KeyboardInterrupt) should just mark this for
38
+        # shutdown
39
+        signal.signal(signal.SIGTERM, lambda *_: shutdown_program.set())
40
+        signal.signal(signal.SIGINT, lambda *_: shutdown_program.set())
41
+
42
+        try:
43
+            threading.Thread(target=daemon_class.start, kwargs=kwargs).start()
44
+            self._daemons[name] = daemon_class
45
+        finally:
46
+            shutdown_program.set()
47
+
48
+
49
+class Daemon(object):
50
+    _name = NotImplemented
51
+
52
+    def __init__(self, app: TGApp):
53
+        self._app = app
54
+
55
+    @classmethod
56
+    def start(cls, **kwargs):
57
+        return cls(**kwargs)
58
+
59
+    @classmethod
60
+    def kill(cls):
61
+        raise NotImplementedError()
62
+
63
+    @property
64
+    def name(self):
65
+        return self._name
66
+
67
+
68
+class RadicaleDaemon(Daemon):
69
+    _name = 'tracim-radicale-server'
70
+
71
+    @classmethod
72
+    def kill(cls):
73
+        pass  # TODO
74
+
75
+    def __init__(self, app: TGApp):
76
+        """
77
+        To see origin radical server start method, refer to
78
+        radicale.__main__.run
79
+        """
80
+        super().__init__(app)
81
+        self._prepare_config()
82
+        server = self._get_server()
83
+        server.serve_forever()
84
+
85
+    def _prepare_config(self):
86
+        tracim_auth = 'tracim.lib.radicale.auth'
87
+        tracim_rights = 'tracim.lib.radicale.rights'
88
+
89
+        radicale_config.set('auth', 'type', 'custom')
90
+        radicale_config.set('auth', 'custom_handler', tracim_auth)
91
+
92
+        radicale_config.set('rights', 'type', 'custom')
93
+        radicale_config.set('rights', 'custom_handler', tracim_rights)
94
+
95
+    def _get_server(self):
96
+        from tracim.config.app_cfg import CFG
97
+        cfg = CFG.get_instance()
98
+        return make_server(
99
+            cfg.RADICALE_SERVER_HOST,
100
+            cfg.RADICALE_SERVER_PORT,
101
+            RadicaleApplication(),
102
+            RadicaleHTTPServer if cfg.RADICALE_SERVER_SSL else RadicaleHTTPSServer,
103
+            RadicaleRequestHandler
104
+        )

+ 22 - 0
tracim/tracim/lib/exceptions.py 查看文件

1
+class TracimException(Exception):
2
+    pass
3
+
4
+
5
+class DaemonException(TracimException):
6
+    pass
7
+
8
+
9
+class AlreadyRunningDaemon(DaemonException):
10
+    pass
11
+
12
+
13
+class CalendarException(TracimException):
14
+    pass
15
+
16
+
17
+class UnknownCalendarType(CalendarException):
18
+    pass
19
+
20
+
21
+class NotFound(TracimException):
22
+    pass

+ 5 - 0
tracim/tracim/lib/helpers.py 查看文件

6
 
6
 
7
 import datetime
7
 import datetime
8
 
8
 
9
+import slugify
9
 from babel.dates import format_date, format_time
10
 from babel.dates import format_date, format_time
10
 from markupsafe import Markup
11
 from markupsafe import Markup
11
 
12
 
216
     if not tg.config.get('auth_instance').is_internal:
217
     if not tg.config.get('auth_instance').is_internal:
217
         return field_name in tg.config.get('auth_instance').managed_fields
218
         return field_name in tg.config.get('auth_instance').managed_fields
218
     return False
219
     return False
220
+
221
+
222
+def slug(string):
223
+    return slugify.slugify(string, only_ascii=True)

+ 13 - 0
tracim/tracim/lib/radicale/auth.py 查看文件

1
+from tg import config
2
+
3
+
4
+def is_authenticated(user, password):
5
+    """
6
+    :param user: user email
7
+    :param password: user password
8
+    :return: True if auth success, False if not
9
+    """
10
+    return bool(config.get('sa_auth').authmetadata.authenticate({}, {
11
+        'login': user,
12
+        'password': password
13
+    }))

+ 25 - 0
tracim/tracim/lib/radicale/rights.py 查看文件

1
+from tracim.lib.calendar import CalendarManager
2
+from tracim.lib.exceptions import NotFound
3
+from tracim.lib.user import UserApi
4
+from tracim.model.organizational import CALENDAR_PERMISSION_READ
5
+
6
+
7
+def authorized(user, collection, permission):
8
+    """
9
+    :param user: radicale given user id, should be email
10
+    :param collection: Calendar representation
11
+    :param permission: 'r' or 'w'
12
+    :return: True if user can access calendar with asked permission
13
+    """
14
+    if not user:
15
+        return False
16
+    current_user = UserApi(None).get_one_by_email(user)
17
+    manager = CalendarManager(current_user)
18
+    try:
19
+        calendar = manager.find_calendar_with_path(collection.path)
20
+    except NotFound:
21
+        return False
22
+
23
+    if permission == CALENDAR_PERMISSION_READ:
24
+        return calendar.user_can_read(current_user)
25
+    return calendar.user_can_write(current_user)

+ 6 - 0
tracim/tracim/lib/radicale/storage.py 查看文件

1
+from radicale.storage.filesystem import Collection as BaseCollection
2
+
3
+
4
+class Collection(BaseCollection):
5
+    def __init__(self, path, principal=False):
6
+        super().__init__(path, principal)

+ 3 - 0
tracim/tracim/lib/user.py 查看文件

27
     def get_one_by_email(self, email: str):
27
     def get_one_by_email(self, email: str):
28
         return self._base_query().filter(User.email==email).one()
28
         return self._base_query().filter(User.email==email).one()
29
 
29
 
30
+    def get_one_by_id(self, id: int) -> User:
31
+        return self._base_query().filter(User.user_id==id).one()
32
+
30
     def update(self, user: User, name: str=None, email: str=None, do_save=True):
33
     def update(self, user: User, name: str=None, email: str=None, do_save=True):
31
         if name is not None:
34
         if name is not None:
32
             user.display_name = name
35
             user.display_name = name

+ 5 - 0
tracim/tracim/lib/utils.py 查看文件

41
         pass
41
         pass
42
     except KeyError:
42
     except KeyError:
43
         pass
43
         pass
44
+
45
+
46
+@property
47
+def NotImplemented():
48
+    raise NotImplementedError()

+ 27 - 1
tracim/tracim/model/auth.py 查看文件

11
 import os
11
 import os
12
 from datetime import datetime
12
 from datetime import datetime
13
 from hashlib import sha256
13
 from hashlib import sha256
14
+from slugify import slugify
14
 from sqlalchemy.ext.hybrid import hybrid_property
15
 from sqlalchemy.ext.hybrid import hybrid_property
15
 from tg.i18n import lazy_ugettext as l_
16
 from tg.i18n import lazy_ugettext as l_
16
 
17
 
26
 from sqlalchemy.types import DateTime
27
 from sqlalchemy.types import DateTime
27
 from sqlalchemy.types import Boolean
28
 from sqlalchemy.types import Boolean
28
 from sqlalchemy.orm import relation, relationship, synonym
29
 from sqlalchemy.orm import relation, relationship, synonym
30
+from tg import request
31
+from tg import config
29
 
32
 
30
 from tracim.model import DeclarativeBase, metadata, DBSession
33
 from tracim.model import DeclarativeBase, metadata, DBSession
31
 
34
 
147
             profile_id = max(group.group_id for group in self.groups)
150
             profile_id = max(group.group_id for group in self.groups)
148
         return Profile(profile_id)
151
         return Profile(profile_id)
149
 
152
 
153
+    @property
154
+    def calendar_url(self) -> str:
155
+        # TODO - 20160531 - Bastien: Cyclic import if import in top of file
156
+        from tracim.config.app_cfg import CFG
157
+        cfg = CFG.get_instance()
158
+        return '{proto}://{domain}:{port}/user/{id}--{slug_name}.ics'.format(
159
+            proto='https' if cfg.RADICALE_CLIENT_SSL else 'http',
160
+            domain=cfg.RADICALE_CLIENT_HOST or request.domain,
161
+            port=cfg.RADICALE_CLIENT_PORT,
162
+            id=self.user_id,
163
+            slug_name=slugify(self.get_display_name(
164
+                remove_email_part=True
165
+            ), only_ascii=True)
166
+        )
167
+
150
     @classmethod
168
     @classmethod
151
     def by_email_address(cls, email):
169
     def by_email_address(cls, email):
152
         """Return the user object whose email address is ``email``."""
170
         """Return the user object whose email address is ``email``."""
206
         hash.update((password + self.password[:64]).encode('utf-8'))
224
         hash.update((password + self.password[:64]).encode('utf-8'))
207
         return self.password[64:] == hash.hexdigest()
225
         return self.password[64:] == hash.hexdigest()
208
 
226
 
209
-    def get_display_name(self):
227
+    def get_display_name(self, remove_email_part=False):
228
+        """
229
+        :param remove_email_part: If True and display name based on email,
230
+         remove @xxx.xxx part of email in returned value
231
+        :return: display name based on user name or email.
232
+        """
210
         if self.display_name!=None and self.display_name!='':
233
         if self.display_name!=None and self.display_name!='':
211
             return self.display_name
234
             return self.display_name
212
         else:
235
         else:
236
+            if remove_email_part:
237
+                at_pos = self.email.index('@')
238
+                return self.email[0:at_pos]
213
             return self.email
239
             return self.email
214
 
240
 
215
     def get_role(self, workspace: 'Workspace') -> int:
241
     def get_role(self, workspace: 'Workspace') -> int:

+ 15 - 1
tracim/tracim/model/data.py 查看文件

7
 import tg
7
 import tg
8
 from babel.dates import format_timedelta
8
 from babel.dates import format_timedelta
9
 from bs4 import BeautifulSoup
9
 from bs4 import BeautifulSoup
10
+from slugify import slugify
10
 from sqlalchemy import Column, inspect, Index
11
 from sqlalchemy import Column, inspect, Index
11
 from sqlalchemy import ForeignKey
12
 from sqlalchemy import ForeignKey
12
 from sqlalchemy import Sequence
13
 from sqlalchemy import Sequence
13
-from sqlalchemy import func
14
 from sqlalchemy.ext.associationproxy import association_proxy
14
 from sqlalchemy.ext.associationproxy import association_proxy
15
 from sqlalchemy.ext.hybrid import hybrid_property
15
 from sqlalchemy.ext.hybrid import hybrid_property
16
 from sqlalchemy.orm import backref
16
 from sqlalchemy.orm import backref
51
 
51
 
52
     label   = Column(Unicode(1024), unique=False, nullable=False, default='')
52
     label   = Column(Unicode(1024), unique=False, nullable=False, default='')
53
     description = Column(Text(), unique=False, nullable=False, default='')
53
     description = Column(Text(), unique=False, nullable=False, default='')
54
+    calendar_enabled = Column(Boolean, unique=False, nullable=False, default=False)
54
 
55
 
55
     #  Default value datetime.utcnow, see: http://stackoverflow.com/a/13370382/801924 (or http://pastebin.com/VLyWktUn)
56
     #  Default value datetime.utcnow, see: http://stackoverflow.com/a/13370382/801924 (or http://pastebin.com/VLyWktUn)
56
     created = Column(DateTime, unique=False, nullable=False, default=datetime.utcnow)
57
     created = Column(DateTime, unique=False, nullable=False, default=datetime.utcnow)
66
         # Return a list of unique revisions parent content
67
         # Return a list of unique revisions parent content
67
         return list(set([revision.node for revision in self.revisions]))
68
         return list(set([revision.node for revision in self.revisions]))
68
 
69
 
70
+    @property
71
+    def calendar_url(self) -> str:
72
+        # TODO - 20160531 - Bastien: Cyclic import if import in top of file
73
+        from tracim.config.app_cfg import CFG
74
+        cfg = CFG.get_instance()
75
+        return '{proto}://{domain}:{port}/workspace/{id}--{slug}.ics'.format(
76
+            proto='https' if cfg.RADICALE_CLIENT_SSL else 'http',
77
+            domain=cfg.RADICALE_CLIENT_HOST or tg.request.domain,
78
+            port=cfg.RADICALE_CLIENT_PORT,
79
+            id=self.workspace_id,
80
+            slug=slugify(self.label)
81
+        )
82
+
69
     def get_user_role(self, user: User) -> int:
83
     def get_user_role(self, user: User) -> int:
70
         for role in user.roles:
84
         for role in user.roles:
71
             if role.workspace.workspace_id==self.workspace_id:
85
             if role.workspace.workspace_id==self.workspace_id:

+ 49 - 0
tracim/tracim/model/organizational.py 查看文件

1
+from tracim.model import User
2
+from tracim.model.data import UserRoleInWorkspace
3
+
4
+
5
+CALENDAR_PERMISSION_READ = 'r'
6
+CALENDAR_PERMISSION_WRITE = 'w'
7
+
8
+
9
+class Calendar(object):
10
+    def __init__(self, related_object, path):
11
+        self._related_object = related_object
12
+        self._path = path
13
+
14
+    def user_can_read(self, user: User) -> bool:
15
+        raise NotImplementedError()
16
+
17
+    def user_can_write(self, user: User) -> bool:
18
+        raise NotImplementedError()
19
+
20
+
21
+class UserCalendar(Calendar):
22
+    def user_can_write(self, user: User) -> bool:
23
+        return self._related_object.user_id == user.user_id
24
+
25
+    def user_can_read(self, user: User) -> bool:
26
+        return self._related_object.user_id == user.user_id
27
+
28
+
29
+class WorkspaceCalendar(Calendar):
30
+    _workspace_rights = {
31
+        UserRoleInWorkspace.NOT_APPLICABLE:
32
+            [],
33
+        UserRoleInWorkspace.READER:
34
+            [CALENDAR_PERMISSION_READ],
35
+        UserRoleInWorkspace.CONTRIBUTOR:
36
+            [CALENDAR_PERMISSION_READ, CALENDAR_PERMISSION_WRITE],
37
+        UserRoleInWorkspace.CONTENT_MANAGER:
38
+            [CALENDAR_PERMISSION_READ, CALENDAR_PERMISSION_WRITE],
39
+        UserRoleInWorkspace.WORKSPACE_MANAGER:
40
+            [CALENDAR_PERMISSION_READ, CALENDAR_PERMISSION_WRITE],
41
+    }
42
+
43
+    def user_can_write(self, user: User) -> bool:
44
+        role = user.get_role(self._related_object)
45
+        return CALENDAR_PERMISSION_WRITE in self._workspace_rights[role]
46
+
47
+    def user_can_read(self, user: User) -> bool:
48
+        role = user.get_role(self._related_object)
49
+        return CALENDAR_PERMISSION_READ in self._workspace_rights[role]

+ 3 - 0
tracim/tracim/model/serializers.py 查看文件

876
     result['roles'] = context.toDict(user.roles)
876
     result['roles'] = context.toDict(user.roles)
877
     result['enabled'] = user.is_active
877
     result['enabled'] = user.is_active
878
     result['profile'] = user.profile
878
     result['profile'] = user.profile
879
+    result['calendar_url'] = user.calendar_url
879
 
880
 
880
     return result
881
     return result
881
 
882
 
979
     result['members'] = context.toDict(workspace.roles)
980
     result['members'] = context.toDict(workspace.roles)
980
     result['member_nb'] = len(workspace.roles)
981
     result['member_nb'] = len(workspace.roles)
981
     result['allowed_content_types'] = context.toDict(workspace.get_allowed_content_types())
982
     result['allowed_content_types'] = context.toDict(workspace.get_allowed_content_types())
983
+    result['calendar_enabled'] = workspace.calendar_enabled
984
+    result['calendar_url'] = workspace.calendar_url
982
 
985
 
983
     return result
986
     return result
984
 
987
 

+ 5 - 0
tracim/tracim/templates/user_workspace_forms.mak 查看文件

110
                 % endif
110
                 % endif
111
                 <input name="email" type="text" class="form-control" id="email" placeholder="${_('Name')}" value="${user.email}" ${'readonly="readonly"' if h.is_user_externalized_field('email') else '' | n}>
111
                 <input name="email" type="text" class="form-control" id="email" placeholder="${_('Name')}" value="${user.email}" ${'readonly="readonly"' if h.is_user_externalized_field('email') else '' | n}>
112
             </div>
112
             </div>
113
+            <div class="form-group">
114
+                <label for="calendar">${_('Personal calendar')}</label>
115
+                <span class="info readonly">${_('This calendar URL will work with CalDav compatibles clients')}</span>
116
+                <input id="calendar" type="text" class="form-control"  disabled="disabled" value="${user.calendar_url}" />
117
+            </div>
113
         </div>
118
         </div>
114
         <div class="modal-footer">
119
         <div class="modal-footer">
115
             <span class="pull-right t-modal-form-submit-button">
120
             <span class="pull-right t-modal-form-submit-button">

+ 8 - 0
tracim/tracim/templates/workspace/edit.mak 查看文件

17
             <label for="workspaceDescription">${_('Description')}</label>
17
             <label for="workspaceDescription">${_('Description')}</label>
18
             <textarea name="description" class="form-control" id="workspaceDescription" placeholder="${_('You may add a description of the workspace')}">${result.workspace.description}</textarea>
18
             <textarea name="description" class="form-control" id="workspaceDescription" placeholder="${_('You may add a description of the workspace')}">${result.workspace.description}</textarea>
19
         </div>
19
         </div>
20
+        <div class="form-group">
21
+            <label for="workspaceCalendarEnabled">${_('Calendar enabled')}</label>
22
+            <input id="workspaceCalendarEnabled" name="calendar_enabled" class="form-control" type="checkbox" ${'checked' if result.workspace.calendar_enabled else ''} />
23
+        </div>
24
+        <div class="form-group calendar-url">
25
+            <label for="workspaceCalendarUrl">${_('Calendar URL')}</label>
26
+            <input id="workspaceCalendarUrl" type="text" class="form-control"  disabled="disabled" value="${result.workspace.calendar_url}" />
27
+        </div>
20
     </div>
28
     </div>
21
     <div class="modal-footer">
29
     <div class="modal-footer">
22
         <span class="pull-right" style="margin-top: 0.5em;">
30
         <span class="pull-right" style="margin-top: 0.5em;">

+ 5 - 0
tracim/tracim/templates/workspace/getone.mak 查看文件

59
         % else:
59
         % else:
60
             <p class="t-less-visible">${_('No description available')}</p>
60
             <p class="t-less-visible">${_('No description available')}</p>
61
         % endif
61
         % endif
62
+        % if result.workspace.calendar_enabled:
63
+            <p>
64
+                ${_('Calendar URL')}: ${result.workspace.calendar_url}
65
+            </p>
66
+        % endif
62
 
67
 
63
         <% member_nb = len(result.workspace.members) %>
68
         <% member_nb = len(result.workspace.members) %>
64
         % if member_nb<=0:
69
         % if member_nb<=0: