2 new commits in galaxy-central: https://bitbucket.org/galaxy/galaxy-central/commits/8536414e0358/ Changeset: 8536414e0358 User: jmchilton Date: 2014-07-23 22:43:13 Summary: Initial BibTeX/DOI citation support in tools and histories. Allow tool authors to specify citations using a DOI or BibTeX. BibTeX can be specified either by pointing at a BibTeX file in the tool directory or by embedding bibtex entries right in tool citation blocks. If referencing a file parallel to the tool, the file should contain only a single BibTeX entry, this restriction can be easily lifted by adding a BibTeX parser as a Python dependency for Galaxy - but I do not have permission to do this. These citations will appear at the bottom of the tool form in a formatted way but the user will have to option to select RAW BibTeX for copying and pasting. Likewise, the history menu now has an option allowing users to aggregatesuch citations across an analysis a comparable list of citations. UI interactions are implemented using a Backbone model and view and data is fetched from the Galaxy server as BibTeX using the API. Two API entry points have been added - one to fetch the BibTeX entries for a tool and another for a history. BibTeX entries for citations annotated with DOIs will be fetched from http://dx.doi.org/ and cached using Beaker. Additional Limitations: - I am not super happy with a few different GUI elements of this. It is ugly and I didn't write the BibTeX parser but I did write the code that takes parsed BibTeX and converts it to a formatted entry. If merged, I will outline a Trello card to follow up and improve the UI and find some more standard way to build a formatted HTML citation from a parsed BibTeX entry. - BibTeX Limitations: LaTeX embedded in the BibTeX entries doesn't render properly when producing a "pretty" citation in the GUI (should still be exported to citation managers properly though). Cross references aren't supported at this time. Alternative Implementations: BibTex/DOI vs. PROV: There was some discussion of PROV encoding citation information on the development mailing list. The citation tags on tools are typed so this could certainly be added - but it was discussed at the BOSC 2014 codefest and there was some conensus that tool authors are more likely to already have BibTeX or DOIs available for the tool's references and the major reference managers end users will likely plug these citations into while writing papers are more likely to be able to consume BibTeX than anything else. The bench biologist using Galaxy is the consumer of this work I most concerned with - if we need to convert citations into other formats such as EndNote or Word's bibliography support there is a suite of tools we could optionally plug into Galaxy (http://sourceforge.net/p/bibutils/home/Bibutils/) to enable this down the road. PROV seems to have such an ecosystem to leverage - I could not even find a tool to convert it BibTeX. Parse BibTeX Client vs Server: As mentioned above it would be nice in some ways to be able to parse and reason about BibTeX on the backend - but it would require adding a new dependency to Python. Since we have to ship BibTeX to the browser anyway to allow users to copy and paste it - I decided it was easier to start with parsing and formatting BibTeX on the client side. I therefore added the following library BSD JavaScript dependency https://github.com/mayanklahiri/bib2json to enable this. I would be happy to revisit this decision and produce formatted entries server side - there seem to be more Python options for doing this than JavaScript. Affected #: 16 files diff -r 6fbe4d95a8dc64bd222dbf1170bdadcc5321e855 -r 8536414e03580828831dbd68ba6fd8a888630cd9 .hgignore --- a/.hgignore +++ b/.hgignore @@ -16,6 +16,7 @@ # Database stuff database/beaker_sessions +database/citations database/community_files database/compiled_templates database/files diff -r 6fbe4d95a8dc64bd222dbf1170bdadcc5321e855 -r 8536414e03580828831dbd68ba6fd8a888630cd9 lib/galaxy/config.py --- a/lib/galaxy/config.py +++ b/lib/galaxy/config.py @@ -378,6 +378,10 @@ # Default chunk size for chunkable datatypes -- 64k self.display_chunk_size = int( kwargs.get( 'display_chunk_size', 65536) ) + self.citation_cache_type = kwargs.get( "citation_cache_type", "file" ) + self.citation_cache_data_dir = self.resolve_path( kwargs.get( "citation_cache_data_dir", "database/citations/data" ) ) + self.citation_cache_lock_dir = self.resolve_path( kwargs.get( "citation_cache_lock_dir", "database/citations/locks" ) ) + @property def sentry_dsn_public( self ): """ @@ -572,6 +576,10 @@ tool_configs = self.config.tool_configs if self.config.migrated_tools_config not in tool_configs: tool_configs.append( self.config.migrated_tools_config ) + + from galaxy.managers.citations import CitationsManager + self.citations_manager = CitationsManager( self ) + from galaxy import tools self.toolbox = tools.ToolBox( tool_configs, self.config.tool_path, self ) # Search support for tools diff -r 6fbe4d95a8dc64bd222dbf1170bdadcc5321e855 -r 8536414e03580828831dbd68ba6fd8a888630cd9 lib/galaxy/managers/citations.py --- /dev/null +++ b/lib/galaxy/managers/citations.py @@ -0,0 +1,170 @@ +import pkg_resources + +import functools +import os +import urllib2 + +pkg_resources.require('Beaker') +from beaker.cache import CacheManager +from beaker.util import parse_cache_config_options + +import logging +log = logging.getLogger( __name__ ) + + +class CitationsManager( object ): + + def __init__( self, app ): + self.app = app + self.doi_cache = DoiCache( app.config ) + + def citations_for_tool( self, tool ): + return tool.citations + + def citations_for_tool_ids( self, tool_ids ): + citation_collection = CitationCollection() + for tool_id in tool_ids: + tool = self._get_tool( tool_id ) + for citation in self.citations_for_tool( tool ): + citation_collection.add( citation ) + return citation_collection.citations + + def parse_citation( self, citation_elem, tool_directory ): + return parse_citation( citation_elem, tool_directory, self ) + + def _get_tool( self, tool_id ): + tool = self.app.toolbox.get_tool( tool_id ) + return tool + + +class DoiCache( object ): + + def __init__( self, config ): + cache_opts = { + 'cache.type': getattr( config, 'citation_cache_type', 'file'), + 'cache.data_dir': getattr( config, 'citation_cache_data_dir', None), + 'cache.lock_dir': getattr( config, 'citation_cache_lock_dir', None), + } + self._cache = CacheManager(**parse_cache_config_options(cache_opts)).get_cache('doi') + + def _raw_get_bibtex( self, doi ): + dx_url = "http://dx.doi.org/" + doi + headers = {'Accept': "text/bibliography; style=bibtex" } + req = urllib2.Request(dx_url, data="", headers=headers) + response = urllib2.urlopen(req) + bibtex = response.read() + return bibtex + + def get_bibtex( self, doi ): + createfunc = functools.partial(self._raw_get_bibtex, doi) + return self._cache.get(key=doi, createfunc=createfunc) + + +def parse_citation( elem, directory, citation_manager ): + """ Parse an abstract citation entry from the specified XML element. + The directory parameter should be used to find external files for this + citation. + """ + citation_type = elem.attrib.get( 'type', None ) + citation_class = CITATION_CLASSES.get( citation_type, None ) + if not citation_class: + log.warn("Unknown or unspecified citation type: %s" % citation_type) + return None + return citation_class( elem, directory, citation_manager ) + + +class CitationCollection( object ): + + def __init__( self ): + self.citations = [] + + def __iter__( self ): + return self.citations.__iter__() + + def __len__( self ): + return len( self.citations ) + + def add( self, new_citation ): + for citation in self.citations: + if citation.equals( new_citation ): + # TODO: We have two equivalent citations, pick the more + # informative/complete/correct. + return False + + self.citations.append( new_citation ) + return True + + +class BaseCitation( object ): + + def to_dict( self, citation_format ): + if citation_format == "bibtex": + return dict( + format="bibtex", + content=self.to_bibtex(), + ) + else: + raise Exception("Unknown citation format %s" % citation_format) + + def equals( self, other_citation ): + if self.has_doi() and other_citation.has_doi(): + return self.doi() == other_citation.doi() + else: + # TODO: Do a better job figuring out if this is the same citation. + return self.to_bibtex() == other_citation.to_bibtex() + + def has_doi( self ): + return False + + +class BibtexCitation( BaseCitation ): + + def __init__( self, elem, directory, citation_manager ): + bibtex_file = elem.attrib.get("file", None) + if bibtex_file: + raw_bibtex = open(os.path.join(directory, bibtex_file), "r").read() + else: + raw_bibtex = elem.text.strip() + self._set_raw_bibtex( raw_bibtex ) + + def _set_raw_bibtex( self, raw_bibtex ): + self.raw_bibtex = raw_bibtex + + def to_bibtex( self ): + return self.raw_bibtex + + +class DoiCitation( BaseCitation ): + BIBTEX_UNSET = object() + + def __init__( self, elem, directory, citation_manager ): + self.__doi = elem.text.strip() + self.doi_cache = citation_manager.doi_cache + self.raw_bibtex = DoiCitation.BIBTEX_UNSET + + def has_doi( self ): + return True + + def doi( self ): + return self.__doi + + def to_bibtex( self ): + if self.raw_bibtex is DoiCitation.BIBTEX_UNSET: + try: + self.raw_bibtex = self.doi_cache.get_bibtex(self.__doi) + except Exception: + log.exception("Failed to fetch bibtex for DOI %s" % self.__doi) + + if self.raw_bibtex is DoiCitation.BIBTEX_UNSET: + return """@MISC{%s, + DOI = '%s', + note = 'Failed to fetch BibTeX for DOI.' + }""" % (self.__doi, self.__doi) + else: + return self.raw_bibtex + + +CITATION_CLASSES = dict( + bibtex=BibtexCitation, + doi=DoiCitation, +) diff -r 6fbe4d95a8dc64bd222dbf1170bdadcc5321e855 -r 8536414e03580828831dbd68ba6fd8a888630cd9 lib/galaxy/tools/__init__.py --- a/lib/galaxy/tools/__init__.py +++ b/lib/galaxy/tools/__init__.py @@ -1360,6 +1360,9 @@ requirements, containers = parse_requirements_from_xml( root ) self.requirements = requirements self.containers = containers + + self.citations = self._parse_citations( root ) + # Determine if this tool can be used in workflows self.is_workflow_compatible = self.check_workflow_compatible(root) # Trackster configuration. @@ -1686,6 +1689,20 @@ trace_msg = repr( traceback.format_tb( trace ) ) log.error( "Traceback: %s" % trace_msg ) + def _parse_citations( self, root ): + citations = [] + citations_elem = root.find("citations") + if not citations_elem: + return citations + + for citation_elem in citations_elem: + if citation_elem.tag != "citation": + pass + citation = self.app.citations_manager.parse_citation( citation_elem, self.tool_dir ) + if citation: + citations.append( citation ) + return citations + # TODO: This method doesn't have to be part of the Tool class. def parse_error_level( self, err_level ): """ diff -r 6fbe4d95a8dc64bd222dbf1170bdadcc5321e855 -r 8536414e03580828831dbd68ba6fd8a888630cd9 lib/galaxy/webapps/galaxy/api/histories.py --- a/lib/galaxy/webapps/galaxy/api/histories.py +++ b/lib/galaxy/webapps/galaxy/api/histories.py @@ -18,7 +18,7 @@ from galaxy.web.base.controller import ExportsHistoryMixin from galaxy.web.base.controller import ImportsHistoryMixin -from galaxy.managers import histories +from galaxy.managers import histories, citations from galaxy import util from galaxy.util import string_as_bool @@ -34,6 +34,7 @@ def __init__( self, app ): super( HistoriesController, self ).__init__( app ) + self.citations_manager = citations.CitationsManager( app ) self.mgrs = util.bunch.Bunch( histories=histories.HistoryManager() ) @@ -117,6 +118,20 @@ history_data[ 'contents_url' ] = url_for( 'history_contents', history_id=history_id ) return history_data + @expose_api_anonymous + def citations( self, trans, history_id, **kwd ): + history = self.mgrs.histories.get( trans, self._decode_id( trans, history_id ), check_ownership=False, check_accessible=True ) + tool_ids = set([]) + for dataset in history.datasets: + job = dataset.creating_job + if not job: + continue + tool_id = job.tool_id + if not tool_id: + continue + tool_ids.add(tool_id) + return map( lambda citation: citation.to_dict( "bibtex" ), self.citations_manager.citations_for_tool_ids( tool_ids ) ) + @expose_api def set_as_current( self, trans, id, **kwd ): """ diff -r 6fbe4d95a8dc64bd222dbf1170bdadcc5321e855 -r 8536414e03580828831dbd68ba6fd8a888630cd9 lib/galaxy/webapps/galaxy/api/tools.py --- a/lib/galaxy/webapps/galaxy/api/tools.py +++ b/lib/galaxy/webapps/galaxy/api/tools.py @@ -1,6 +1,8 @@ import urllib +from galaxy import exceptions from galaxy import web, util +from galaxy.web import _future_expose_api_anonymous from galaxy.web.base.controller import BaseAPIController from galaxy.web.base.controller import UsesVisualizationMixin from galaxy.web.base.controller import UsesHistoryMixin @@ -44,7 +46,7 @@ trans.response.status = 500 return { 'error': str( exc ) } - @web.expose_api + @_future_expose_api_anonymous def show( self, trans, id, **kwd ): """ GET /api/tools/{tool_id} @@ -52,18 +54,16 @@ """ io_details = util.string_as_bool( kwd.get( 'io_details', False ) ) link_details = util.string_as_bool( kwd.get( 'link_details', False ) ) - try: - id = urllib.unquote_plus( id ) - tool = self.app.toolbox.get_tool( id ) - if not tool: - trans.response.status = 404 - return { 'error': 'tool not found', 'id': id } - return tool.to_dict( trans, io_details=io_details, link_details=link_details ) + tool = self._get_tool( id ) + return tool.to_dict( trans, io_details=io_details, link_details=link_details ) - except Exception, exc: - log.error( 'could not convert tool (%s) to dictionary: %s', id, str( exc ), exc_info=True ) - trans.response.status = 500 - return { 'error': str( exc ) } + @_future_expose_api_anonymous + def citations( self, trans, id, **kwds ): + tool = self._get_tool( id ) + rval = [] + for citation in tool.citations: + rval.append( citation.to_dict( 'bibtex' ) ) + return rval @web.expose_api_anonymous def create( self, trans, payload, **kwd ): @@ -170,6 +170,12 @@ # # -- Helper methods -- # + def _get_tool( self, id ): + id = urllib.unquote_plus( id ) + tool = self.app.toolbox.get_tool( id ) + if not tool: + raise exceptions.ObjectNotFound("Could not find tool with id '%s'" % id) + return tool def _rerun_tool( self, trans, payload, **kwargs ): """ diff -r 6fbe4d95a8dc64bd222dbf1170bdadcc5321e855 -r 8536414e03580828831dbd68ba6fd8a888630cd9 lib/galaxy/webapps/galaxy/buildapp.py --- a/lib/galaxy/webapps/galaxy/buildapp.py +++ b/lib/galaxy/webapps/galaxy/buildapp.py @@ -174,6 +174,7 @@ webapp.mapper.resource( 'ftp_file', 'ftp_files', path_prefix='/api' ) webapp.mapper.resource( 'group', 'groups', path_prefix='/api' ) webapp.mapper.resource_with_deleted( 'quota', 'quotas', path_prefix='/api' ) + webapp.mapper.connect( '/api/tools/{id:.+?}/citations', action='citations', controller="tools" ) webapp.mapper.connect( '/api/tools/{id:.+?}', action='show', controller="tools" ) webapp.mapper.resource( 'tool', 'tools', path_prefix='/api' ) webapp.mapper.resource_with_deleted( 'user', 'users', path_prefix='/api' ) @@ -181,6 +182,7 @@ webapp.mapper.resource( 'visualization', 'visualizations', path_prefix='/api' ) webapp.mapper.resource( 'workflow', 'workflows', path_prefix='/api' ) webapp.mapper.resource_with_deleted( 'history', 'histories', path_prefix='/api' ) + webapp.mapper.connect( '/api/histories/{history_id}/citations', action='citations', controller="histories" ) webapp.mapper.resource( 'configuration', 'configuration', path_prefix='/api' ) webapp.mapper.resource( 'datatype', 'datatypes', diff -r 6fbe4d95a8dc64bd222dbf1170bdadcc5321e855 -r 8536414e03580828831dbd68ba6fd8a888630cd9 lib/galaxy/webapps/galaxy/controllers/history.py --- a/lib/galaxy/webapps/galaxy/controllers/history.py +++ b/lib/galaxy/webapps/galaxy/controllers/history.py @@ -448,6 +448,13 @@ # ......................................................................... html @web.expose + def citations( self, trans ): + # Get history + history = trans.history + history_id = trans.security.encode_id( history.id ) + return trans.fill_template( "history/citations.mako", history=history, history_id=history_id ) + + @web.expose def display_structured( self, trans, id=None ): """ Display a history as a nested structure showing the jobs and workflow This diff is so big that we needed to truncate the remainder. https://bitbucket.org/galaxy/galaxy-central/commits/d5870d822b24/ Changeset: d5870d822b24 User: jmchilton Date: 2014-07-28 19:15:32 Summary: Merged in jmchilton/galaxy-central-fork-1 (pull request #440) Initial BibTeX/DOI citation support in tools and histories. Affected #: 16 files diff -r 6292ada3115b4d2b658d1e574d20b08d887e7bec -r d5870d822b2492d99cc46fc31ab21c5b9b237cae .hgignore --- a/.hgignore +++ b/.hgignore @@ -16,6 +16,7 @@ # Database stuff database/beaker_sessions +database/citations database/community_files database/compiled_templates database/files diff -r 6292ada3115b4d2b658d1e574d20b08d887e7bec -r d5870d822b2492d99cc46fc31ab21c5b9b237cae lib/galaxy/config.py --- a/lib/galaxy/config.py +++ b/lib/galaxy/config.py @@ -376,6 +376,10 @@ # Default chunk size for chunkable datatypes -- 64k self.display_chunk_size = int( kwargs.get( 'display_chunk_size', 65536) ) + self.citation_cache_type = kwargs.get( "citation_cache_type", "file" ) + self.citation_cache_data_dir = self.resolve_path( kwargs.get( "citation_cache_data_dir", "database/citations/data" ) ) + self.citation_cache_lock_dir = self.resolve_path( kwargs.get( "citation_cache_lock_dir", "database/citations/locks" ) ) + @property def sentry_dsn_public( self ): """ @@ -570,6 +574,10 @@ tool_configs = self.config.tool_configs if self.config.migrated_tools_config not in tool_configs: tool_configs.append( self.config.migrated_tools_config ) + + from galaxy.managers.citations import CitationsManager + self.citations_manager = CitationsManager( self ) + from galaxy import tools self.toolbox = tools.ToolBox( tool_configs, self.config.tool_path, self ) # Search support for tools diff -r 6292ada3115b4d2b658d1e574d20b08d887e7bec -r d5870d822b2492d99cc46fc31ab21c5b9b237cae lib/galaxy/managers/citations.py --- /dev/null +++ b/lib/galaxy/managers/citations.py @@ -0,0 +1,170 @@ +import pkg_resources + +import functools +import os +import urllib2 + +pkg_resources.require('Beaker') +from beaker.cache import CacheManager +from beaker.util import parse_cache_config_options + +import logging +log = logging.getLogger( __name__ ) + + +class CitationsManager( object ): + + def __init__( self, app ): + self.app = app + self.doi_cache = DoiCache( app.config ) + + def citations_for_tool( self, tool ): + return tool.citations + + def citations_for_tool_ids( self, tool_ids ): + citation_collection = CitationCollection() + for tool_id in tool_ids: + tool = self._get_tool( tool_id ) + for citation in self.citations_for_tool( tool ): + citation_collection.add( citation ) + return citation_collection.citations + + def parse_citation( self, citation_elem, tool_directory ): + return parse_citation( citation_elem, tool_directory, self ) + + def _get_tool( self, tool_id ): + tool = self.app.toolbox.get_tool( tool_id ) + return tool + + +class DoiCache( object ): + + def __init__( self, config ): + cache_opts = { + 'cache.type': getattr( config, 'citation_cache_type', 'file'), + 'cache.data_dir': getattr( config, 'citation_cache_data_dir', None), + 'cache.lock_dir': getattr( config, 'citation_cache_lock_dir', None), + } + self._cache = CacheManager(**parse_cache_config_options(cache_opts)).get_cache('doi') + + def _raw_get_bibtex( self, doi ): + dx_url = "http://dx.doi.org/" + doi + headers = {'Accept': "text/bibliography; style=bibtex" } + req = urllib2.Request(dx_url, data="", headers=headers) + response = urllib2.urlopen(req) + bibtex = response.read() + return bibtex + + def get_bibtex( self, doi ): + createfunc = functools.partial(self._raw_get_bibtex, doi) + return self._cache.get(key=doi, createfunc=createfunc) + + +def parse_citation( elem, directory, citation_manager ): + """ Parse an abstract citation entry from the specified XML element. + The directory parameter should be used to find external files for this + citation. + """ + citation_type = elem.attrib.get( 'type', None ) + citation_class = CITATION_CLASSES.get( citation_type, None ) + if not citation_class: + log.warn("Unknown or unspecified citation type: %s" % citation_type) + return None + return citation_class( elem, directory, citation_manager ) + + +class CitationCollection( object ): + + def __init__( self ): + self.citations = [] + + def __iter__( self ): + return self.citations.__iter__() + + def __len__( self ): + return len( self.citations ) + + def add( self, new_citation ): + for citation in self.citations: + if citation.equals( new_citation ): + # TODO: We have two equivalent citations, pick the more + # informative/complete/correct. + return False + + self.citations.append( new_citation ) + return True + + +class BaseCitation( object ): + + def to_dict( self, citation_format ): + if citation_format == "bibtex": + return dict( + format="bibtex", + content=self.to_bibtex(), + ) + else: + raise Exception("Unknown citation format %s" % citation_format) + + def equals( self, other_citation ): + if self.has_doi() and other_citation.has_doi(): + return self.doi() == other_citation.doi() + else: + # TODO: Do a better job figuring out if this is the same citation. + return self.to_bibtex() == other_citation.to_bibtex() + + def has_doi( self ): + return False + + +class BibtexCitation( BaseCitation ): + + def __init__( self, elem, directory, citation_manager ): + bibtex_file = elem.attrib.get("file", None) + if bibtex_file: + raw_bibtex = open(os.path.join(directory, bibtex_file), "r").read() + else: + raw_bibtex = elem.text.strip() + self._set_raw_bibtex( raw_bibtex ) + + def _set_raw_bibtex( self, raw_bibtex ): + self.raw_bibtex = raw_bibtex + + def to_bibtex( self ): + return self.raw_bibtex + + +class DoiCitation( BaseCitation ): + BIBTEX_UNSET = object() + + def __init__( self, elem, directory, citation_manager ): + self.__doi = elem.text.strip() + self.doi_cache = citation_manager.doi_cache + self.raw_bibtex = DoiCitation.BIBTEX_UNSET + + def has_doi( self ): + return True + + def doi( self ): + return self.__doi + + def to_bibtex( self ): + if self.raw_bibtex is DoiCitation.BIBTEX_UNSET: + try: + self.raw_bibtex = self.doi_cache.get_bibtex(self.__doi) + except Exception: + log.exception("Failed to fetch bibtex for DOI %s" % self.__doi) + + if self.raw_bibtex is DoiCitation.BIBTEX_UNSET: + return """@MISC{%s, + DOI = '%s', + note = 'Failed to fetch BibTeX for DOI.' + }""" % (self.__doi, self.__doi) + else: + return self.raw_bibtex + + +CITATION_CLASSES = dict( + bibtex=BibtexCitation, + doi=DoiCitation, +) diff -r 6292ada3115b4d2b658d1e574d20b08d887e7bec -r d5870d822b2492d99cc46fc31ab21c5b9b237cae lib/galaxy/tools/__init__.py --- a/lib/galaxy/tools/__init__.py +++ b/lib/galaxy/tools/__init__.py @@ -1360,6 +1360,9 @@ requirements, containers = parse_requirements_from_xml( root ) self.requirements = requirements self.containers = containers + + self.citations = self._parse_citations( root ) + # Determine if this tool can be used in workflows self.is_workflow_compatible = self.check_workflow_compatible(root) # Trackster configuration. @@ -1686,6 +1689,20 @@ trace_msg = repr( traceback.format_tb( trace ) ) log.error( "Traceback: %s" % trace_msg ) + def _parse_citations( self, root ): + citations = [] + citations_elem = root.find("citations") + if not citations_elem: + return citations + + for citation_elem in citations_elem: + if citation_elem.tag != "citation": + pass + citation = self.app.citations_manager.parse_citation( citation_elem, self.tool_dir ) + if citation: + citations.append( citation ) + return citations + # TODO: This method doesn't have to be part of the Tool class. def parse_error_level( self, err_level ): """ diff -r 6292ada3115b4d2b658d1e574d20b08d887e7bec -r d5870d822b2492d99cc46fc31ab21c5b9b237cae lib/galaxy/webapps/galaxy/api/histories.py --- a/lib/galaxy/webapps/galaxy/api/histories.py +++ b/lib/galaxy/webapps/galaxy/api/histories.py @@ -18,7 +18,7 @@ from galaxy.web.base.controller import ExportsHistoryMixin from galaxy.web.base.controller import ImportsHistoryMixin -from galaxy.managers import histories +from galaxy.managers import histories, citations from galaxy import util from galaxy.util import string_as_bool @@ -34,6 +34,7 @@ def __init__( self, app ): super( HistoriesController, self ).__init__( app ) + self.citations_manager = citations.CitationsManager( app ) self.mgrs = util.bunch.Bunch( histories=histories.HistoryManager() ) @@ -117,6 +118,20 @@ history_data[ 'contents_url' ] = url_for( 'history_contents', history_id=history_id ) return history_data + @expose_api_anonymous + def citations( self, trans, history_id, **kwd ): + history = self.mgrs.histories.get( trans, self._decode_id( trans, history_id ), check_ownership=False, check_accessible=True ) + tool_ids = set([]) + for dataset in history.datasets: + job = dataset.creating_job + if not job: + continue + tool_id = job.tool_id + if not tool_id: + continue + tool_ids.add(tool_id) + return map( lambda citation: citation.to_dict( "bibtex" ), self.citations_manager.citations_for_tool_ids( tool_ids ) ) + @expose_api def set_as_current( self, trans, id, **kwd ): """ diff -r 6292ada3115b4d2b658d1e574d20b08d887e7bec -r d5870d822b2492d99cc46fc31ab21c5b9b237cae lib/galaxy/webapps/galaxy/api/tools.py --- a/lib/galaxy/webapps/galaxy/api/tools.py +++ b/lib/galaxy/webapps/galaxy/api/tools.py @@ -1,6 +1,8 @@ import urllib +from galaxy import exceptions from galaxy import web, util +from galaxy.web import _future_expose_api_anonymous from galaxy.web.base.controller import BaseAPIController from galaxy.web.base.controller import UsesVisualizationMixin from galaxy.web.base.controller import UsesHistoryMixin @@ -44,7 +46,7 @@ trans.response.status = 500 return { 'error': str( exc ) } - @web.expose_api + @_future_expose_api_anonymous def show( self, trans, id, **kwd ): """ GET /api/tools/{tool_id} @@ -52,18 +54,16 @@ """ io_details = util.string_as_bool( kwd.get( 'io_details', False ) ) link_details = util.string_as_bool( kwd.get( 'link_details', False ) ) - try: - id = urllib.unquote_plus( id ) - tool = self.app.toolbox.get_tool( id ) - if not tool: - trans.response.status = 404 - return { 'error': 'tool not found', 'id': id } - return tool.to_dict( trans, io_details=io_details, link_details=link_details ) + tool = self._get_tool( id ) + return tool.to_dict( trans, io_details=io_details, link_details=link_details ) - except Exception, exc: - log.error( 'could not convert tool (%s) to dictionary: %s', id, str( exc ), exc_info=True ) - trans.response.status = 500 - return { 'error': str( exc ) } + @_future_expose_api_anonymous + def citations( self, trans, id, **kwds ): + tool = self._get_tool( id ) + rval = [] + for citation in tool.citations: + rval.append( citation.to_dict( 'bibtex' ) ) + return rval @web.expose_api_anonymous def create( self, trans, payload, **kwd ): @@ -170,6 +170,12 @@ # # -- Helper methods -- # + def _get_tool( self, id ): + id = urllib.unquote_plus( id ) + tool = self.app.toolbox.get_tool( id ) + if not tool: + raise exceptions.ObjectNotFound("Could not find tool with id '%s'" % id) + return tool def _rerun_tool( self, trans, payload, **kwargs ): """ diff -r 6292ada3115b4d2b658d1e574d20b08d887e7bec -r d5870d822b2492d99cc46fc31ab21c5b9b237cae lib/galaxy/webapps/galaxy/buildapp.py --- a/lib/galaxy/webapps/galaxy/buildapp.py +++ b/lib/galaxy/webapps/galaxy/buildapp.py @@ -174,6 +174,7 @@ webapp.mapper.resource( 'ftp_file', 'ftp_files', path_prefix='/api' ) webapp.mapper.resource( 'group', 'groups', path_prefix='/api' ) webapp.mapper.resource_with_deleted( 'quota', 'quotas', path_prefix='/api' ) + webapp.mapper.connect( '/api/tools/{id:.+?}/citations', action='citations', controller="tools" ) webapp.mapper.connect( '/api/tools/{id:.+?}', action='show', controller="tools" ) webapp.mapper.resource( 'tool', 'tools', path_prefix='/api' ) webapp.mapper.resource_with_deleted( 'user', 'users', path_prefix='/api' ) @@ -181,6 +182,7 @@ webapp.mapper.resource( 'visualization', 'visualizations', path_prefix='/api' ) webapp.mapper.resource( 'workflow', 'workflows', path_prefix='/api' ) webapp.mapper.resource_with_deleted( 'history', 'histories', path_prefix='/api' ) + webapp.mapper.connect( '/api/histories/{history_id}/citations', action='citations', controller="histories" ) webapp.mapper.resource( 'configuration', 'configuration', path_prefix='/api' ) webapp.mapper.resource( 'datatype', 'datatypes', diff -r 6292ada3115b4d2b658d1e574d20b08d887e7bec -r d5870d822b2492d99cc46fc31ab21c5b9b237cae lib/galaxy/webapps/galaxy/controllers/history.py --- a/lib/galaxy/webapps/galaxy/controllers/history.py +++ b/lib/galaxy/webapps/galaxy/controllers/history.py @@ -448,6 +448,13 @@ # ......................................................................... html @web.expose + def citations( self, trans ): + # Get history + history = trans.history + history_id = trans.security.encode_id( history.id ) + return trans.fill_template( "history/citations.mako", history=history, history_id=history_id ) + + @web.expose def display_structured( self, trans, id=None ): """ Display a history as a nested structure showing the jobs and workflow This diff is so big that we needed to truncate the remainder. Repository URL: https://bitbucket.org/galaxy/galaxy-central/ -- This is a commit notification from bitbucket.org. You are receiving this because you have the service enabled, addressing the recipient of this email.