Browse Source

Issue #101: API for list of workspace and calendars

Bastien Sevajol (Algoo) 8 years ago
parent
commit
58ce06f56e

+ 63 - 0
API.md View File

1
+# API documentation
2
+
3
+## Authentication
4
+
5
+APi not actually implement authentication method. You must use cookies set by
6
+frontend login.
7
+
8
+## Workspaces
9
+
10
+### List
11
+
12
+    GET /api/workspaces/
13
+
14
+Return list of workspaces acessible by current connected user.
15
+
16
+#### Response
17
+
18
+    {
19
+       "value_list":[
20
+          {
21
+             "id":30,
22
+             "label":"my calendar",
23
+             "description":"blablabla",
24
+             "has_calendar":"true"
25
+          },
26
+          {
27
+             "id":230,
28
+             "label":"my calendar other",
29
+             "description":"blablabla 230",
30
+             "has_calendar":"true"
31
+          }
32
+       ]
33
+    }
34
+
35
+## Calendars
36
+
37
+### List
38
+
39
+    GET /api/calendars/
40
+
41
+Return list of calendars accessible by current connected user.
42
+
43
+#### Response
44
+
45
+    {
46
+       "value_list":[
47
+          {
48
+             "id":30,
49
+             "label":"my calendar",
50
+             "description":"blablabla 230"
51
+          },
52
+          {
53
+             "id":230,
54
+             "label":"my other calendar",
55
+             "description":"blablabla 230"
56
+          },
57
+          {
58
+             "id":20,
59
+             "label":"Name of the user",
60
+             "description":"my personnal calendar"
61
+          }
62
+       ]
63
+    }

+ 14 - 0
tracim/tracim/config/__init__.py View File

1
 # -*- coding: utf-8 -*-
1
 # -*- coding: utf-8 -*-
2
 from tg import AppConfig
2
 from tg import AppConfig
3
+from tg.appwrappers.errorpage import ErrorPageApplicationWrapper \
4
+    as BaseErrorPageApplicationWrapper
3
 
5
 
4
 from tracim.lib.auth.wrapper import AuthConfigWrapper
6
 from tracim.lib.auth.wrapper import AuthConfigWrapper
7
+from tracim.lib.utils import ErrorPageApplicationWrapper
5
 
8
 
6
 
9
 
7
 class TracimAppConfig(AppConfig):
10
 class TracimAppConfig(AppConfig):
8
     """
11
     """
9
     Tracim specific config processes.
12
     Tracim specific config processes.
10
     """
13
     """
14
+    def __init__(self, minimal=False, root_controller=None):
15
+        super().__init__(minimal, root_controller)
16
+        self._replace_errors_wrapper()
17
+
18
+    def _replace_errors_wrapper(self) -> None:
19
+        """
20
+        Replace tg ErrorPageApplicationWrapper by ourself
21
+        """
22
+        for index, wrapper_class in enumerate(self.application_wrappers):
23
+            if issubclass(wrapper_class, BaseErrorPageApplicationWrapper):
24
+                self.application_wrappers[index] = ErrorPageApplicationWrapper
11
 
25
 
12
     def after_init_config(self, conf):
26
     def after_init_config(self, conf):
13
         AuthConfigWrapper.wrap(conf)
27
         AuthConfigWrapper.wrap(conf)

+ 75 - 0
tracim/tracim/controllers/api.py View File

1
+# -*- coding: utf-8 -*-
2
+from tg import abort
3
+from tg import expose
4
+from tg import predicates
5
+from tg import request
6
+from tg import tmpl_context
7
+
8
+from tracim.lib.base import BaseController
9
+from tracim.lib.calendar import CalendarManager
10
+from tracim.lib.utils import api_require
11
+from tracim.lib.workspace import WorkspaceApi
12
+from tracim.model.serializers import Context, CTX
13
+
14
+"""
15
+To raise an error, use:
16
+
17
+```
18
+abort(
19
+    400,
20
+    detail={
21
+        'name': 'Parameter required'
22
+    },
23
+    comment='Missing data',
24
+)
25
+```
26
+
27
+"""
28
+
29
+
30
+class APIBaseController(BaseController):
31
+    def _before(self, *args, **kw):
32
+        if request.content_type != 'application/json':
33
+            abort(406, 'Only JSON requests are supported')
34
+
35
+        super()._before(*args, **kw)
36
+
37
+
38
+class WorkspaceController(APIBaseController):
39
+    @expose('json')
40
+    @api_require(predicates.not_anonymous())
41
+    def index(self):
42
+        # NOTE BS 20161025: I can't use tmpl_context.current_user,
43
+        # I d'ont know why
44
+        workspace_api = WorkspaceApi(tmpl_context.identity.get('user'))
45
+        workspaces = workspace_api.get_all()
46
+        serialized_workspaces = Context(CTX.API_WORKSPACE).toDict(workspaces)
47
+
48
+        return {
49
+            'value_list': serialized_workspaces
50
+        }
51
+
52
+
53
+class CalendarsController(APIBaseController):
54
+    @expose('json')
55
+    @api_require(predicates.not_anonymous())
56
+    def index(self):
57
+        # NOTE BS 20161025: I can't use tmpl_context.current_user,
58
+        # I d'ont know why
59
+        user = tmpl_context.identity.get('user')
60
+        calendar_workspaces = CalendarManager\
61
+            .get_workspace_readable_calendars_for_user(user)
62
+        calendars = Context(CTX.API_CALENDAR_WORKSPACE)\
63
+            .toDict(calendar_workspaces)
64
+
65
+        # Manually add information about user calendar
66
+        calendars.append(Context(CTX.API_CALENDAR_USER).toDict(user))
67
+
68
+        return {
69
+            'value_list': calendars
70
+        }
71
+
72
+
73
+class APIController(BaseController):
74
+    workspaces = WorkspaceController()
75
+    calendars = CalendarsController()

+ 4 - 0
tracim/tracim/controllers/root.py View File

11
 from tg import url
11
 from tg import url
12
 
12
 
13
 from tg.i18n import ugettext as _
13
 from tg.i18n import ugettext as _
14
+from tracim.controllers.api import APIController
14
 
15
 
15
 from tracim.lib import CST
16
 from tracim.lib import CST
16
 from tracim.lib.base import logger
17
 from tracim.lib.base import logger
60
     workspaces = UserWorkspaceRestController()
61
     workspaces = UserWorkspaceRestController()
61
     user = UserRestController()
62
     user = UserRestController()
62
 
63
 
64
+    # api
65
+    api = APIController()
66
+
63
     def _render_response(self, tgl, controller, response):
67
     def _render_response(self, tgl, controller, response):
64
         replace_reset_password_templates(controller.decoration.engines)
68
         replace_reset_password_templates(controller.decoration.engines)
65
         return super()._render_response(tgl, controller, response)
69
         return super()._render_response(tgl, controller, response)

+ 20 - 5
tracim/tracim/lib/calendar.py View File

5
 
5
 
6
 from icalendar import Event as iCalendarEvent
6
 from icalendar import Event as iCalendarEvent
7
 from sqlalchemy.orm.exc import NoResultFound
7
 from sqlalchemy.orm.exc import NoResultFound
8
+from tg.i18n import ugettext as _
8
 
9
 
9
 from tracim.lib.content import ContentApi
10
 from tracim.lib.content import ContentApi
10
 from tracim.lib.exceptions import UnknownCalendarType
11
 from tracim.lib.exceptions import UnknownCalendarType
37
 
38
 
38
 class CalendarManager(object):
39
 class CalendarManager(object):
39
     @classmethod
40
     @classmethod
41
+    def get_personal_calendar_description(cls) -> str:
42
+        return _('My personal calendar')
43
+
44
+    @classmethod
40
     def get_base_url(cls):
45
     def get_base_url(cls):
41
         from tracim.config.app_cfg import CFG
46
         from tracim.config.app_cfg import CFG
42
         cfg = CFG.get_instance()
47
         cfg = CFG.get_instance()
284
     def get_workspace_readable_calendars_urls_for_user(cls, user: User)\
289
     def get_workspace_readable_calendars_urls_for_user(cls, user: User)\
285
             -> [str]:
290
             -> [str]:
286
         calendar_urls = []
291
         calendar_urls = []
292
+        for workspace in cls.get_workspace_readable_calendars_for_user(user):
293
+            calendar_urls.append(cls.get_workspace_calendar_url(
294
+                workspace_id=workspace.workspace_id,
295
+            ))
296
+
297
+        return calendar_urls
298
+
299
+    @classmethod
300
+    def get_workspace_readable_calendars_for_user(cls, user: User)\
301
+            -> ['Workspace']:
302
+        workspaces = []
287
         workspace_api = WorkspaceApi(user)
303
         workspace_api = WorkspaceApi(user)
288
-        for workspace in workspace_api.get_all_for_user(user):
304
+
305
+        for workspace in workspace_api.get_all():
289
             if workspace.calendar_enabled:
306
             if workspace.calendar_enabled:
290
-                calendar_urls.append(cls.get_workspace_calendar_url(
291
-                    workspace_id=workspace.workspace_id,
292
-                ))
307
+                workspaces.append(workspace)
293
 
308
 
294
-        return calendar_urls
309
+        return workspaces
295
 
310
 
296
     def is_discovery_path(self, path: str) -> bool:
311
     def is_discovery_path(self, path: str) -> bool:
297
         """
312
         """

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

3
 import time
3
 import time
4
 import signal
4
 import signal
5
 
5
 
6
+from tg import config
7
+from tg import require
8
+from tg import response
9
+from tg.controllers.util import abort
10
+from tg.appwrappers.errorpage import ErrorPageApplicationWrapper \
11
+    as BaseErrorPageApplicationWrapper
12
+
6
 from tracim.lib.base import logger
13
 from tracim.lib.base import logger
14
+from webob import Response
15
+from webob.exc import WSGIHTTPException
16
+
7
 
17
 
8
 def exec_time_monitor():
18
 def exec_time_monitor():
9
     def decorator_func(func):
19
     def decorator_func(func):
61
         os.kill(os.getpid(), signal_id)  # Rethrow signal
71
         os.kill(os.getpid(), signal_id)  # Rethrow signal
62
 
72
 
63
     signal.signal(signal_id, _handler)
73
     signal.signal(signal_id, _handler)
74
+
75
+
76
+class APIWSGIHTTPException(WSGIHTTPException):
77
+    def json_formatter(self, body, status, title, environ):
78
+        if self.comment:
79
+            msg = '{0}: {1}'.format(title, self.comment)
80
+        else:
81
+            msg = title
82
+        return {
83
+            'code': self.code,
84
+            'msg': msg,
85
+            'detail': self.detail,
86
+        }
87
+
88
+
89
+class api_require(require):
90
+    def default_denial_handler(self, reason):
91
+        # Add code here if we have to hide 401 errors (security reasons)
92
+
93
+        abort(response.status_int, reason, passthrough='json')
94
+
95
+
96
+class ErrorPageApplicationWrapper(BaseErrorPageApplicationWrapper):
97
+    # Define here response code to manage in APIWSGIHTTPException
98
+    api_managed_error_codes = [
99
+        400, 401, 403, 404,
100
+    ]
101
+
102
+    def __call__(self, controller, environ, context) -> Response:
103
+        # We only do ou work when it's /api request
104
+        # TODO BS 20161025: Look at PATH_INFO is not smart, find better way
105
+        if not environ['PATH_INFO'].startswith('/api'):
106
+            return super().__call__(controller, environ, context)
107
+
108
+        try:
109
+            resp = self.next_handler(controller, environ, context)
110
+        except:  # We catch all exception to display an 500 error json response
111
+            if config.get('debug', False):  # But in debug, we want to see it
112
+                raise
113
+            return APIWSGIHTTPException()
114
+
115
+        # We manage only specified errors codes
116
+        if resp.status_int not in self.api_managed_error_codes:
117
+            return resp
118
+
119
+        # Rewrite error in api format
120
+        return APIWSGIHTTPException(
121
+            code=resp.status_int,
122
+            detail=resp.detail,
123
+            title=resp.title,
124
+            comment=resp.comment,
125
+        )

+ 34 - 0
tracim/tracim/model/serializers.py View File

86
     USER = 'USER'
86
     USER = 'USER'
87
     USERS = 'USERS'
87
     USERS = 'USERS'
88
     WORKSPACE = 'WORKSPACE'
88
     WORKSPACE = 'WORKSPACE'
89
+    API_WORKSPACE = 'API_WORKSPACE'
90
+    API_CALENDAR_WORKSPACE = 'API_CALENDAR_WORKSPACE'
91
+    API_CALENDAR_USER = 'API_CALENDAR_USER'
89
 
92
 
90
 
93
 
91
 class DictLikeClass(dict):
94
 class DictLikeClass(dict):
1023
             type='workspace',
1026
             type='workspace',
1024
             state={'opened': True if len(item.children)>0 else False, 'selected': item.is_selected}
1027
             state={'opened': True if len(item.children)>0 else False, 'selected': item.is_selected}
1025
         )
1028
         )
1029
+
1030
+
1031
+@pod_serializer(Workspace, CTX.API_WORKSPACE)
1032
+def serialize_api_workspace(item: Workspace, context: Context):
1033
+    return DictLikeClass(
1034
+        id=item.workspace_id,
1035
+        label=item.label,
1036
+        description=item.description,
1037
+        has_calendar=item.calendar_enabled,
1038
+    )
1039
+
1040
+
1041
+@pod_serializer(Workspace, CTX.API_CALENDAR_WORKSPACE)
1042
+def serialize_api_calendar_workspace(item: Workspace, context: Context):
1043
+    return DictLikeClass(
1044
+        id=item.workspace_id,
1045
+        label=item.label,
1046
+        description=item.description,
1047
+        type='workspace',
1048
+    )
1049
+
1050
+
1051
+@pod_serializer(User, CTX.API_CALENDAR_USER)
1052
+def serialize_api_calendar_workspace(item: User, context: Context):
1053
+    from tracim.lib.calendar import CalendarManager  # Cyclic import
1054
+    return DictLikeClass(
1055
+        id=item.user_id,
1056
+        label=item.display_name,
1057
+        description=CalendarManager.get_personal_calendar_description(),
1058
+        type='user',
1059
+    )