Parcourir la source

Issue #101: API for list of workspace and calendars

Bastien Sevajol (Algoo) il y a 7 ans
Parent
révision
58ce06f56e

+ 63 - 0
API.md Voir le fichier

@@ -0,0 +1,63 @@
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 Voir le fichier

@@ -1,13 +1,27 @@
1 1
 # -*- coding: utf-8 -*-
2 2
 from tg import AppConfig
3
+from tg.appwrappers.errorpage import ErrorPageApplicationWrapper \
4
+    as BaseErrorPageApplicationWrapper
3 5
 
4 6
 from tracim.lib.auth.wrapper import AuthConfigWrapper
7
+from tracim.lib.utils import ErrorPageApplicationWrapper
5 8
 
6 9
 
7 10
 class TracimAppConfig(AppConfig):
8 11
     """
9 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 26
     def after_init_config(self, conf):
13 27
         AuthConfigWrapper.wrap(conf)

+ 75 - 0
tracim/tracim/controllers/api.py Voir le fichier

@@ -0,0 +1,75 @@
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 Voir le fichier

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

+ 20 - 5
tracim/tracim/lib/calendar.py Voir le fichier

@@ -5,6 +5,7 @@ import transaction
5 5
 
6 6
 from icalendar import Event as iCalendarEvent
7 7
 from sqlalchemy.orm.exc import NoResultFound
8
+from tg.i18n import ugettext as _
8 9
 
9 10
 from tracim.lib.content import ContentApi
10 11
 from tracim.lib.exceptions import UnknownCalendarType
@@ -37,6 +38,10 @@ CALENDAR_WORKSPACE_BASE_URL = '/workspace/'
37 38
 
38 39
 class CalendarManager(object):
39 40
     @classmethod
41
+    def get_personal_calendar_description(cls) -> str:
42
+        return _('My personal calendar')
43
+
44
+    @classmethod
40 45
     def get_base_url(cls):
41 46
         from tracim.config.app_cfg import CFG
42 47
         cfg = CFG.get_instance()
@@ -284,14 +289,24 @@ class CalendarManager(object):
284 289
     def get_workspace_readable_calendars_urls_for_user(cls, user: User)\
285 290
             -> [str]:
286 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 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 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 311
     def is_discovery_path(self, path: str) -> bool:
297 312
         """

+ 62 - 0
tracim/tracim/lib/utils.py Voir le fichier

@@ -3,7 +3,17 @@ import os
3 3
 import time
4 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 13
 from tracim.lib.base import logger
14
+from webob import Response
15
+from webob.exc import WSGIHTTPException
16
+
7 17
 
8 18
 def exec_time_monitor():
9 19
     def decorator_func(func):
@@ -61,3 +71,55 @@ def add_signal_handler(signal_id, handler) -> None:
61 71
         os.kill(os.getpid(), signal_id)  # Rethrow signal
62 72
 
63 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 Voir le fichier

@@ -86,6 +86,9 @@ class CTX(object):
86 86
     USER = 'USER'
87 87
     USERS = 'USERS'
88 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 94
 class DictLikeClass(dict):
@@ -1023,3 +1026,34 @@ def serialize_node_tree_item_for_menu_api_tree(item: NodeTreeItem, context: Cont
1023 1026
             type='workspace',
1024 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
+    )