[hg] galaxy 3130: Framework for recording user actions for repor...
details: http://www.bx.psu.edu/hg/galaxy/rev/f98643c26eb7 changeset: 3130:f98643c26eb7 user: jeremy goecks <jeremy.goecks@emory.edu> date: Mon Nov 30 17:53:25 2009 -0500 description: Framework for recording user actions for reports; framework is modelled after event recording. Currently two types of actions are recorded: (a) tagging/untagging and (b) filtering/searching in grids. diffstat: lib/galaxy/config.py | 1 + lib/galaxy/model/__init__.py | 10 +++++ lib/galaxy/model/mapping.py | 13 ++++++ lib/galaxy/model/migrate/versions/0029_user_actions.py | 47 +++++++++++++++++++++++ lib/galaxy/web/controllers/dataset.py | 2 +- lib/galaxy/web/controllers/history.py | 2 +- lib/galaxy/web/controllers/tag.py | 24 ++++++++--- lib/galaxy/web/framework/__init__.py | 17 ++++++++ lib/galaxy/web/framework/helpers/grids.py | 13 +++++- templates/dataset/edit_attributes.mako | 2 +- templates/history/view.mako | 2 +- templates/root/history.mako | 2 +- templates/tagging_common.mako | 8 ++-- universe_wsgi.ini.sample | 3 + 14 files changed, 128 insertions(+), 18 deletions(-) diffs (335 lines): diff -r 9d4945bbdcf5 -r f98643c26eb7 lib/galaxy/config.py --- a/lib/galaxy/config.py Mon Nov 30 15:33:45 2009 -0500 +++ b/lib/galaxy/config.py Mon Nov 30 17:53:25 2009 -0500 @@ -79,6 +79,7 @@ self.use_heartbeat = string_as_bool( kwargs.get( 'use_heartbeat', 'False' ) ) self.use_memdump = string_as_bool( kwargs.get( 'use_memdump', 'False' ) ) self.log_memory_usage = string_as_bool( kwargs.get( 'log_memory_usage', 'False' ) ) + self.log_actions = string_as_bool( kwargs.get( 'log_actions', 'False' ) ) self.log_events = string_as_bool( kwargs.get( 'log_events', 'False' ) ) self.ucsc_display_sites = kwargs.get( 'ucsc_display_sites', "main,test,archaea,ucla" ).lower().split(",") self.gbrowse_display_sites = kwargs.get( 'gbrowse_display_sites', "main,test,tair" ).lower().split(",") diff -r 9d4945bbdcf5 -r f98643c26eb7 lib/galaxy/model/__init__.py --- a/lib/galaxy/model/__init__.py Mon Nov 30 15:33:45 2009 -0500 +++ b/lib/galaxy/model/__init__.py Mon Nov 30 17:53:25 2009 -0500 @@ -1441,6 +1441,16 @@ def __init__( self, name=None, value=None ): self.name = name self.value = value + +class UserAction( object ): + def __init__( self, id=None, create_time=None, user_id=None, session_id=None, action=None, params=None, context=None): + self.id = id + self.create_time = create_time + self.user_id = user_id + self.session_id = session_id + self.action = action + self.params = params + self.context = context ## ---- Utility methods ------------------------------------------------------- diff -r 9d4945bbdcf5 -r f98643c26eb7 lib/galaxy/model/mapping.py --- a/lib/galaxy/model/mapping.py Mon Nov 30 15:33:45 2009 -0500 +++ b/lib/galaxy/model/mapping.py Mon Nov 30 17:53:25 2009 -0500 @@ -743,6 +743,15 @@ Column( "name", Unicode( 255 ), index=True), Column( "value", Unicode( 1024 ) ) ) +UserAction.table = Table( "user_action", metadata, + Column( "id", Integer, primary_key=True ), + Column( "create_time", DateTime, default=now ), + Column( "user_id", Integer, ForeignKey( "galaxy_user.id" ), index=True ), + Column( "session_id", Integer, ForeignKey( "galaxy_session.id" ), index=True ), + Column( "action", Unicode( 255 ) ), + Column( "context", Unicode( 512 ) ), + Column( "params", Unicode( 1024 ) ) ) + # With the tables defined we can define the mappers and setup the # relationships between the model objects. @@ -1237,6 +1246,10 @@ assign_mapper( context, UserPreference, UserPreference.table, properties = {} ) + +assign_mapper( context, UserAction, UserAction.table, + properties = dict( user=relation( User.mapper ) ) + ) def db_next_hid( self ): """ diff -r 9d4945bbdcf5 -r f98643c26eb7 lib/galaxy/model/migrate/versions/0029_user_actions.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/lib/galaxy/model/migrate/versions/0029_user_actions.py Mon Nov 30 17:53:25 2009 -0500 @@ -0,0 +1,47 @@ +""" +This migration script adds a user actions table to Galaxy. +""" + +from sqlalchemy import * +from migrate import * + +import datetime +now = datetime.datetime.utcnow + +import logging +log = logging.getLogger( __name__ ) + +metadata = MetaData( migrate_engine ) + +def display_migration_details(): + print "" + print "This migration script adds a user actions table to Galaxy." + print "" + + +# New table to store user actions. +UserAction_table = Table( "user_action", metadata, + Column( "id", Integer, primary_key=True ), + Column( "create_time", DateTime, default=now ), + Column( "user_id", Integer, ForeignKey( "galaxy_user.id" ), index=True ), + Column( "session_id", Integer, ForeignKey( "galaxy_session.id" ), index=True ), + Column( "action", Unicode( 255 ) ), + Column( "context", Unicode( 512 ) ), + Column( "params", Unicode( 1024 ) ) ) + +def upgrade(): + display_migration_details() + metadata.reflect() + try: + UserAction_table.create() + except Exception, e: + print str(e) + log.debug( "Creating user_action table failed: %s" % str( e ) ) + +def downgrade(): + metadata.reflect() + try: + UserAction_table.drop() + except Exception, e: + print str(e) + log.debug( "Dropping user_action table failed: %s" % str( e ) ) \ No newline at end of file diff -r 9d4945bbdcf5 -r f98643c26eb7 lib/galaxy/web/controllers/dataset.py --- a/lib/galaxy/web/controllers/dataset.py Mon Nov 30 15:33:45 2009 -0500 +++ b/lib/galaxy/web/controllers/dataset.py Mon Nov 30 17:53:25 2009 -0500 @@ -87,7 +87,7 @@ link=( lambda item: iff( item.history.deleted, None, dict( operation="switch", id=item.id ) ) ), filterable="advanced" ), HistoryColumn( "History", key="history", link=( lambda item: iff( item.history.deleted, None, dict( operation="switch_history", id=item.id ) ) ) ), - grids.TagsColumn( "Tags", "tags", model.HistoryDatasetAssociation, model.HistoryDatasetAssociationTagAssociation, filterable="advanced" ), + grids.TagsColumn( "Tags", "tags", model.HistoryDatasetAssociation, model.HistoryDatasetAssociationTagAssociation, filterable="advanced", grid_name="HistoryDatasetAssocationListGrid" ), StatusColumn( "Status", key="deleted", attach_popup=False ), grids.GridColumn( "Created", key="create_time", format=time_ago ), grids.GridColumn( "Last Updated", key="update_time", format=time_ago ), diff -r 9d4945bbdcf5 -r f98643c26eb7 lib/galaxy/web/controllers/history.py --- a/lib/galaxy/web/controllers/history.py Mon Nov 30 15:33:45 2009 -0500 +++ b/lib/galaxy/web/controllers/history.py Mon Nov 30 17:53:25 2009 -0500 @@ -94,7 +94,7 @@ link=( lambda history: iff( history.deleted, None, dict( operation="Switch", id=history.id ) ) ), attach_popup=True, filterable="advanced" ), DatasetsByStateColumn( "Datasets (by state)", ncells=4 ), - grids.TagsColumn( "Tags", "tags", model.History, model.HistoryTagAssociation, filterable="advanced"), + grids.TagsColumn( "Tags", "tags", model.History, model.HistoryTagAssociation, filterable="advanced", grid_name="HistoryListGrid" ), StatusColumn( "Status", attach_popup=False ), grids.GridColumn( "Created", key="create_time", format=time_ago ), grids.GridColumn( "Last Updated", key="update_time", format=time_ago ), diff -r 9d4945bbdcf5 -r f98643c26eb7 lib/galaxy/web/controllers/tag.py --- a/lib/galaxy/web/controllers/tag.py Mon Nov 30 15:33:45 2009 -0500 +++ b/lib/galaxy/web/controllers/tag.py Mon Nov 30 17:53:25 2009 -0500 @@ -28,28 +28,38 @@ @web.expose @web.require_login( "Add tag to an item." ) - def add_tag_async( self, trans, id=None, item_class=None, new_tag=None ): + def add_tag_async( self, trans, id=None, item_class=None, new_tag=None, context=None ): """ Add tag to an item. """ - item = self._get_item(trans, item_class, trans.security.decode_id(id)) - self._do_security_check(trans, item) + # Check that user owns item. + item = self._get_item(trans, item_class, trans.security.decode_id( id ) ) + self._do_security_check( trans, item ) + # Apply tag. self.tag_handler.apply_item_tags( trans.sa_session, item, new_tag.encode('utf-8') ) trans.sa_session.flush() + # Log. + params = dict( item_id=item.id, item_class=item_class, tag=new_tag) + trans.log_action( unicode( "tag"), context, params ) + @web.expose @web.require_login( "Remove tag from an item." ) - def remove_tag_async( self, trans, id=None, item_class=None, tag_name=None ): + def remove_tag_async( self, trans, id=None, item_class=None, tag_name=None, context=None ): """ Remove tag from an item. """ + + # Check that user owns item. item = self._get_item(trans, item_class, trans.security.decode_id(id)) - self._do_security_check(trans, item) + # Remove tag. self.tag_handler.remove_item_tag( trans, item, tag_name.encode('utf-8') ) - #print tag_name - #print unicode(tag_name) trans.sa_session.flush() + # Log. + params = dict( item_id=item.id, item_class=item_class, tag=tag_name) + trans.log_action( unicode( "untag"), context, params ) + # Retag an item. All previous tags are deleted and new tags are applied. @web.expose @web.require_login( "Apply a new set of tags to an item; previous tags are deleted." ) diff -r 9d4945bbdcf5 -r f98643c26eb7 lib/galaxy/web/framework/__init__.py --- a/lib/galaxy/web/framework/__init__.py Mon Nov 30 15:33:45 2009 -0500 +++ b/lib/galaxy/web/framework/__init__.py Mon Nov 30 17:53:25 2009 -0500 @@ -10,6 +10,7 @@ import base import pickle from galaxy import util +from galaxy.util.json import to_json_string pkg_resources.require( "simplejson" ) import simplejson @@ -170,6 +171,22 @@ to allow migration toward a more SQLAlchemy 0.4 style of use. """ return self.app.model.context.current + def log_action( self, action, context, params): + """ + Application-level logging of user actions. + """ + if self.app.config.log_actions: + action = self.app.model.UserAction(action=action, context=context, params=unicode( to_json_string( params ) ) ) + try: + action.user = self.user + except: + action.user = None + try: + action.session_id = self.galaxy_session.id + except: + action.session_id = None + self.sa_session.add( action ) + self.sa_session.flush() def log_event( self, message, tool_id=None, **kwargs ): """ Application level logging. Still needs fleshing out (log levels and such) diff -r 9d4945bbdcf5 -r f98643c26eb7 lib/galaxy/web/framework/helpers/grids.py --- a/lib/galaxy/web/framework/helpers/grids.py Mon Nov 30 15:33:45 2009 -0500 +++ b/lib/galaxy/web/framework/helpers/grids.py Mon Nov 30 17:53:25 2009 -0500 @@ -200,6 +200,13 @@ trans.get_user().preferences[pref_name] = unicode( to_json_string( sort_key ) ) trans.sa_session.flush() + # Log grid view. + context = unicode( self.__class__.__name__ ) + params = cur_filter_dict.copy() + params['sort'] = sort_key + params['async'] = ( 'async' in kwargs ) + trans.log_action( unicode( "grid.view"), context, params ) + # Render grid. def url( *args, **kwargs ): # Only include sort/filter arguments if not linking to another @@ -340,17 +347,19 @@ # Generic column that supports tagging. class TagsColumn( TextColumn ): - def __init__( self, col_name, key, model_class, model_tag_association_class, filterable ): + def __init__( self, col_name, key, model_class, model_tag_association_class, filterable, grid_name=None ): GridColumn.__init__(self, col_name, key=key, model_class=model_class, filterable=filterable) self.model_tag_association_class = model_tag_association_class # Tags cannot be sorted. self.sortable = False + # Column-specific attributes. self.tag_elt_id_gen = 0 + self.grid_name = grid_name def get_value( self, trans, grid, item ): self.tag_elt_id_gen += 1 elt_id="tagging-elt" + str( self.tag_elt_id_gen ) div_elt = "<div id=%s></div>" % elt_id - return div_elt + trans.fill_template( "/tagging_common.mako", trans=trans, tagged_item=item, + return div_elt + trans.fill_template( "/tagging_common.mako", trans=trans, tagged_item=item, elt_context=self.grid_name, elt_id = elt_id, in_form="true", input_size="20", tag_click_fn="add_tag_to_grid_filter" ) def filter( self, db_session, query, column_filter ): """ Modify query to filter model_class by tag. Multiple filters are ANDed. """ diff -r 9d4945bbdcf5 -r f98643c26eb7 templates/dataset/edit_attributes.mako --- a/templates/dataset/edit_attributes.mako Mon Nov 30 15:33:45 2009 -0500 +++ b/templates/dataset/edit_attributes.mako Mon Nov 30 17:53:25 2009 -0500 @@ -59,7 +59,7 @@ </div> <div style="clear: both"></div> </div> - ${render_tagging_element(data, "dataset-tag-area", use_toggle_link="false", in_form="true", input_size="30")} + ${render_tagging_element(data, "dataset-tag-area", "edit_attributes.mako", use_toggle_link="false", in_form="true", input_size="30")} %endif %for name, spec in data.metadata.spec.items(): %if spec.visible: diff -r 9d4945bbdcf5 -r f98643c26eb7 templates/history/view.mako --- a/templates/history/view.mako Mon Nov 30 15:33:45 2009 -0500 +++ b/templates/history/view.mako Mon Nov 30 17:53:25 2009 -0500 @@ -327,7 +327,7 @@ %if trans.get_user() is not None: <div id='history-tag-area' class="tag-element"></div> - ${render_tagging_element(history, "history-tag-area", use_toggle_link='false', get_toggle_link_text_fn='get_toggle_link_text', editable=user_owns_history)} + ${render_tagging_element(history, "history-tag-area", "history/view.mako", use_toggle_link='false', get_toggle_link_text_fn='get_toggle_link_text', editable=user_owns_history)} %endif %if not datasets: diff -r 9d4945bbdcf5 -r f98643c26eb7 templates/root/history.mako --- a/templates/root/history.mako Mon Nov 30 15:33:45 2009 -0500 +++ b/templates/root/history.mako Mon Nov 30 17:53:25 2009 -0500 @@ -325,7 +325,7 @@ %if trans.get_user() is not None: <div id='history-tag-area' class="tag-element"></div> - ${render_tagging_element(history, "history-tag-area", get_toggle_link_text_fn='get_toggle_link_text')} + ${render_tagging_element( tagged_item=history, elt_id="history-tag-area", elt_context="history.mako", get_toggle_link_text_fn='get_toggle_link_text' )} %endif %if not datasets: diff -r 9d4945bbdcf5 -r f98643c26eb7 templates/tagging_common.mako --- a/templates/tagging_common.mako Mon Nov 30 15:33:45 2009 -0500 +++ b/templates/tagging_common.mako Mon Nov 30 17:53:25 2009 -0500 @@ -1,10 +1,10 @@ ## Render a tagging element if there is a tagged_item. %if tagged_item is not None and elt_id is not None: - ${render_tagging_element(tagged_item, elt_id=elt_id, in_form=in_form, input_size=input_size, tag_click_fn=tag_click_fn)} + ${render_tagging_element(tagged_item=tagged_item, elt_id=elt_id, elt_context=elt_context, in_form=in_form, input_size=input_size, tag_click_fn=tag_click_fn)} %endif ## Render the tags 'tags' as an autocomplete element. -<%def name="render_tagging_element(tagged_item, elt_id, use_toggle_link='true', in_form='false', input_size='15', tag_click_fn='default_tag_click_fn', get_toggle_link_text_fn='default_get_toggle_link_text_fn', editable='true')"> +<%def name="render_tagging_element(tagged_item, elt_id, elt_context, use_toggle_link='true', in_form='false', input_size='15', tag_click_fn='default_tag_click_fn', get_toggle_link_text_fn='default_get_toggle_link_text_fn', editable='true')"> <script type="text/javascript"> // // Set up autocomplete tagger. @@ -75,8 +75,8 @@ tag_click_fn: ${tag_click_fn}, <% tagged_item_id = trans.security.encode_id(tagged_item.id) %> ajax_autocomplete_tag_url: "${h.url_for( controller='tag', action='tag_autocomplete_data', id=tagged_item_id, item_class=tagged_item.__class__.__name__ )}", - ajax_add_tag_url: "${h.url_for( controller='tag', action='add_tag_async', id=tagged_item_id, item_class=tagged_item.__class__.__name__ )}", - ajax_delete_tag_url: "${h.url_for( controller='tag', action='remove_tag_async', id=tagged_item_id, item_class=tagged_item.__class__.__name__ )}", + ajax_add_tag_url: "${h.url_for( controller='tag', action='add_tag_async', id=tagged_item_id, item_class=tagged_item.__class__.__name__, context=elt_context )}", + ajax_delete_tag_url: "${h.url_for( controller='tag', action='remove_tag_async', id=tagged_item_id, item_class=tagged_item.__class__.__name__, context=elt_context )}", delete_tag_img: "${h.url_for('/static/images/delete_tag_icon_gray.png')}", delete_tag_img_rollover: "${h.url_for('/static/images/delete_tag_icon_white.png')}", add_tag_img: "${h.url_for('/static/images/add_icon.png')}", diff -r 9d4945bbdcf5 -r f98643c26eb7 universe_wsgi.ini.sample --- a/universe_wsgi.ini.sample Mon Nov 30 15:33:45 2009 -0500 +++ b/universe_wsgi.ini.sample Mon Nov 30 17:53:25 2009 -0500 @@ -144,6 +144,9 @@ # Log events log_events = True +# Log user actions +log_actions = True + # Configuration for debugging middleware debug = True use_lint = False
participants (1)
-
Greg Von Kuster