diff --git a/lib/galaxy/web/framework/decorators.py b/lib/galaxy/web/framework/decorators.py index 315c65f9de1..94d639c7e16 100644 --- a/lib/galaxy/web/framework/decorators.py +++ b/lib/galaxy/web/framework/decorators.py @@ -15,6 +15,8 @@ from galaxy.web.framework import url_for log = logging.getLogger( __name__ ) JSON_CONTENT_TYPE = "application/json" +JSONP_CONTENT_TYPE = "application/javascript" +JSONP_CALLBACK_KEY = 'callback' def error( message ): @@ -36,15 +38,22 @@ def expose( func ): return func -def json( func, **json_kwargs ): +def json( func, pretty=False ): """ Format the response as JSON and set the response content type to JSON_CONTENT_TYPE. """ @wraps(func) def call_and_format( self, trans, *args, **kwargs ): - trans.response.set_content_type( JSON_CONTENT_TYPE ) - return safe_dumps( func( self, trans, *args, **kwargs ), **json_kwargs ) + # pull out any callback argument to the api endpoint and set the content type to json or javascript + jsonp_callback = kwargs.pop( JSONP_CALLBACK_KEY, None ) + if jsonp_callback: + trans.response.set_content_type( JSONP_CONTENT_TYPE ) + else: + trans.response.set_content_type( JSON_CONTENT_TYPE ) + rval = func( self, trans, *args, **kwargs ) + return _format_return_as_json( rval, jsonp_callback, pretty=( pretty or trans.debug ) ) + if not hasattr( func, '_orig' ): call_and_format._orig = func return expose( _save_orig_fn( call_and_format, func ) ) @@ -54,7 +63,7 @@ def json_pretty( func ): """ Indent and sort returned JSON. """ - return json( func, indent=4, sort_keys=True ) + return json( func, pretty=True ) def require_login( verb="perform this action", use_panels=False, webapp='galaxy' ): @@ -113,9 +122,17 @@ def expose_api( func, to_json=True, user_required=True ): error_status = '400 Bad Request' error_message = 'Your request did not appear to be valid JSON, please consult the API documentation' return error - trans.response.set_content_type( "application/json" ) + + # pull out any callback argument to the api endpoint and set the content type to json or javascript + jsonp_callback = kwargs.pop( JSONP_CALLBACK_KEY, None ) + if jsonp_callback: + trans.response.set_content_type( JSONP_CONTENT_TYPE ) + else: + trans.response.set_content_type( JSON_CONTENT_TYPE ) + # send 'do not cache' headers to handle IE's caching of ajax get responses trans.response.headers[ 'Cache-Control' ] = "max-age=0,no-cache,no-store" + # Perform api_run_as processing, possibly changing identity if 'payload' in kwargs and 'run_as' in kwargs['payload']: if not trans.user_can_do_run_as(): @@ -135,10 +152,8 @@ def expose_api( func, to_json=True, user_required=True ): return "That user does not exist." try: rval = func( self, trans, *args, **kwargs) - if to_json and trans.debug: - rval = safe_dumps( rval, indent=4, sort_keys=True ) - elif to_json: - rval = safe_dumps( rval ) + if to_json: + rval = _format_return_as_json( rval, jsonp_callback, pretty=trans.debug ) return rval except paste.httpexceptions.HTTPException: raise # handled @@ -206,7 +221,7 @@ def expose_api_anonymous( func, to_json=True ): # ----------------------------------------------------------------------------- (new) api decorators # TODO: rename as expose_api and make default. -def _future_expose_api( func, to_json=True, user_required=True, user_or_session_required=True ): +def _future_expose_api( func, to_json=True, user_required=True, user_or_session_required=True, handle_jsonp=True ): """ Expose this function via the API. """ @@ -233,7 +248,13 @@ def _future_expose_api( func, to_json=True, user_required=True, user_or_session_ error_code = error_codes.USER_INVALID_JSON return __api_error_response( trans, status_code=400, err_code=error_code ) - trans.response.set_content_type( JSON_CONTENT_TYPE ) + # pull out any callback argument to the api endpoint and set the content type to json or javascript + # TODO: use handle_jsonp to NOT overwrite existing tool_shed JSONP + jsonp_callback = kwargs.pop( JSONP_CALLBACK_KEY, None ) if handle_jsonp else None + if jsonp_callback: + trans.response.set_content_type( JSONP_CONTENT_TYPE ) + else: + trans.response.set_content_type( JSON_CONTENT_TYPE ) # send 'do not cache' headers to handle IE's caching of ajax get responses trans.response.headers[ 'Cache-Control' ] = "max-age=0,no-cache,no-store" @@ -258,11 +279,9 @@ def _future_expose_api( func, to_json=True, user_required=True, user_or_session_ error_code = error_codes.USER_INVALID_RUN_AS return __api_error_response( trans, err_code=error_code, status_code=400 ) try: - rval = func( self, trans, *args, **kwargs) - if to_json and trans.debug: - rval = safe_dumps( rval, indent=4, sort_keys=True ) - elif to_json: - rval = safe_dumps( rval ) + rval = func( self, trans, *args, **kwargs ) + if to_json: + rval = _format_return_as_json( rval, jsonp_callback, pretty=trans.debug ) return rval except MessageException as e: traceback_string = format_exc() @@ -288,6 +307,19 @@ def _future_expose_api( func, to_json=True, user_required=True, user_or_session_ return decorator +def _format_return_as_json( rval, jsonp_callback=None, pretty=False ): + """ + Formats a return value as JSON or JSONP if `jsonp_callback` is present. + + Use `pretty=True` to return pretty printed json. + """ + dumps_kwargs = dict( indent=4, sort_keys=True ) if pretty else {} + json = safe_dumps( rval, **dumps_kwargs ) + if jsonp_callback: + json = "{}({});".format( jsonp_callback, json ) + return json + + def __api_error_message( trans, **kwds ): exception = kwds.get( "exception", None ) if exception: @@ -362,4 +394,11 @@ def _future_expose_api_raw_anonymous( func ): def _future_expose_api_raw_anonymous_and_sessionless( func ): - return _future_expose_api( func, to_json=False, user_required=False, user_or_session_required=False ) + # TODO: tool_shed api implemented JSONP first on a method-by-method basis, don't overwrite that for now + return _future_expose_api( + func, + to_json=False, + user_required=False, + user_or_session_required=False, + handle_jsonp=False + )