From 1e1256227259d9ae0991e2ba2d9949b3abeea3d0 Mon Sep 17 00:00:00 2001 From: Jeremy Goecks Date: Tue, 3 Aug 2010 11:45:25 -0400 Subject: [PATCH] Add a ratings framework so that users can rate shared items. All sharable items--histories, workflows, datasets, visualizations, and pages--can be rated. Main framework components are (1) database tables for storing ratings; (2) UsesItemRatings mixin that supports rating items and obtaining an item's average rating; (3) controller methods to handle rating asynchronously; and (4) jQuery star rating plugin and supporting code that enables interactive item rating. (2) and (4) can be reused in the community space. --- lib/galaxy/item_attrs/__init__.py | 3 + lib/galaxy/item_attrs/ratings.py | 74 ++++ lib/galaxy/model/__init__.py | 35 ++ lib/galaxy/model/mapping.py | 78 +++- .../migrate/versions/0053_item_ratings.py | 133 ++++++ lib/galaxy/web/controllers/dataset.py | 29 +- lib/galaxy/web/controllers/history.py | 34 +- lib/galaxy/web/controllers/page.py | 34 +- lib/galaxy/web/controllers/visualization.py | 34 +- lib/galaxy/web/controllers/workflow.py | 31 +- static/images/delete.gif | Bin 0 -> 752 bytes static/images/star.gif | Bin 0 -> 815 bytes static/june_2007_style/blue/jquery.rating.css | 12 + static/scripts/jquery.rating.js | 383 ++++++++++++++++++ static/scripts/packed/jquery.rating.js | 1 + templates/display_base.mako | 130 +++++- 16 files changed, 987 insertions(+), 24 deletions(-) create mode 100644 lib/galaxy/item_attrs/__init__.py create mode 100644 lib/galaxy/item_attrs/ratings.py create mode 100644 lib/galaxy/model/migrate/versions/0053_item_ratings.py create mode 100755 static/images/delete.gif create mode 100755 static/images/star.gif create mode 100755 static/june_2007_style/blue/jquery.rating.css create mode 100755 static/scripts/jquery.rating.js create mode 100644 static/scripts/packed/jquery.rating.js diff --git a/lib/galaxy/item_attrs/__init__.py b/lib/galaxy/item_attrs/__init__.py new file mode 100644 index 00000000000..fedda204ea2 --- /dev/null +++ b/lib/galaxy/item_attrs/__init__.py @@ -0,0 +1,3 @@ +""" +Galaxy item attributes, such as tags, ratings, and annotations. +""" diff --git a/lib/galaxy/item_attrs/ratings.py b/lib/galaxy/item_attrs/ratings.py new file mode 100644 index 00000000000..f43a119b7a1 --- /dev/null +++ b/lib/galaxy/item_attrs/ratings.py @@ -0,0 +1,74 @@ +from sqlalchemy.sql.expression import func + +class UsesItemRatings: + """ + Mixin for getting and setting item ratings. + + Class makes two assumptions: + (1) item-rating association table is named RatingAssocation + and is in trans.model; + (2) item-rating association table has a column with a foreign key referencing + item table that contains the item's id. + """ + + def get_ave_item_rating_data( self, trans, item): + """ Returns the average rating for an item.""" + item_rating_assoc_class = self._get_item_rating_assoc_class( trans, item ) + if not item_rating_assoc_class: + raise RuntimeException( "Item does not have ratings: %s" % item.__class__.__name__ ) + item_id_filter = self._get_item_id_filter_str( trans, item, item_rating_assoc_class ) + ave_rating = trans.sa_session.query( func.avg( item_rating_assoc_class.rating ) ).filter( item_id_filter ).scalar() + # Convert ave_rating to float; note: if there are no item ratings, ave rating is None. + if ave_rating: + ave_rating = float( ave_rating ) + else: + ave_rating = 0 + num_ratings = int( trans.sa_session.query( func.count( item_rating_assoc_class.rating ) ).filter( item_id_filter ).scalar() ) + return ( ave_rating, num_ratings ) + + def rate_item( self, trans, user, item, rating ): + """ Rate an item. Return type is RatingAssociation. """ + item_rating = self.get_user_item_rating( trans, user, item ) + if not item_rating: + # User has not yet rated item; create rating. + item_rating_assoc_class = self._get_item_rating_assoc_class( trans, item ) + item_rating = item_rating_assoc_class() + item_rating.user = trans.get_user() + item_rating.set_item( item ) + item_rating.rating = rating + trans.sa_session.add( item_rating ) + trans.sa_session.flush() + elif item_rating.rating != rating: + # User has rated item; update rating. + item_rating.rating = rating + trans.sa_session.flush() + return item_rating + + def get_user_item_rating( self, trans, user, item ): + """ Returns user's rating for an item. Return type is RatingAssociation. """ + item_rating_assoc_class = self._get_item_rating_assoc_class( trans, item ) + if not item_rating_assoc_class: + raise RuntimeException( "Item does not have ratings: %s" % item.__class__.__name__ ) + + # Query rating table by user and item id. + item_id_filter = self._get_item_id_filter_str( trans, item, item_rating_assoc_class ) + return trans.sa_session.query( item_rating_assoc_class ).filter_by( user=user ).filter( item_id_filter ).first() + + def _get_item_rating_assoc_class( self, trans, item ): + """ Returns an item's item-rating association class. """ + item_rating_assoc_class = '%sRatingAssociation' % item.__class__.__name__ + return trans.model.get( item_rating_assoc_class, None ) + + def _get_item_id_filter_str( self, trans, item, item_rating_assoc_class ): + # Get foreign key in item-rating association table that references item table. + item_fk = None + for fk in item_rating_assoc_class.table.foreign_keys: + if fk.references( item.table ): + item_fk = fk + break + + if not item_fk: + raise RuntimeException( "Cannot find item id column in item-rating association table: %s, %s" % item_rating_assoc_class.__name__, item_rating_assoc_class.table.name ) + + # TODO: can we provide a better filter than a raw string? + return "%s=%i" % ( item_fk.parent.name, item.id ) \ No newline at end of file diff --git a/lib/galaxy/model/__init__.py b/lib/galaxy/model/__init__.py index ee865196006..4f0048992d7 100644 --- a/lib/galaxy/model/__init__.py +++ b/lib/galaxy/model/__init__.py @@ -1766,6 +1766,8 @@ class StoredWorkflowTagAssociation ( ItemTagAssociation ): class VisualizationTagAssociation ( ItemTagAssociation ): pass +# Item annotation classes. + class HistoryAnnotationAssociation( object ): pass @@ -1784,6 +1786,39 @@ class PageAnnotationAssociation( object ): class VisualizationAnnotationAssociation( object ): pass +# Item rating classes. + +class ItemRatingAssociation( object ): + def __init__( self, id=None, user=None, item=None, rating=0 ): + self.id = id + self.user = user + self.item = item + self.rating = rating + + def set_item( self, item ): + """ Set association's item. """ + pass + +class HistoryRatingAssociation( ItemRatingAssociation ): + def set_item( self, history ): + self.history = history + +class HistoryDatasetAssociationRatingAssociation( ItemRatingAssociation ): + def set_item( self, history_dataset_association ): + self.history_dataset_association = history_dataset_association + +class StoredWorkflowRatingAssociation( ItemRatingAssociation ): + def set_item( self, stored_workflow ): + self.stored_workflow = stored_workflow + +class PageRatingAssociation( ItemRatingAssociation ): + def set_item( self, page ): + self.page = page + +class VisualizationRatingAssociation( ItemRatingAssociation ): + def set_item( self, visualization ): + self.visualization = visualization + class UserPreference ( object ): def __init__( self, name=None, value=None ): self.name = name diff --git a/lib/galaxy/model/mapping.py b/lib/galaxy/model/mapping.py index 0600749fac7..97c564ed6ec 100644 --- a/lib/galaxy/model/mapping.py +++ b/lib/galaxy/model/mapping.py @@ -771,7 +771,38 @@ VisualizationAnnotationAssociation.table = Table( "visualization_annotation_asso Column( "visualization_id", Integer, ForeignKey( "visualization.id" ), index=True ), Column( "user_id", Integer, ForeignKey( "galaxy_user.id" ), index=True ), Column( "annotation", TEXT, index=True) ) - + +# Ratings tables. +HistoryRatingAssociation.table = Table( "history_rating_association", metadata, + Column( "id", Integer, primary_key=True ), + Column( "history_id", Integer, ForeignKey( "history.id" ), index=True ), + Column( "user_id", Integer, ForeignKey( "galaxy_user.id" ), index=True ), + Column( "rating", Integer, index=True) ) + +HistoryDatasetAssociationRatingAssociation.table = Table( "history_dataset_association_rating_association", metadata, + Column( "id", Integer, primary_key=True ), + Column( "history_dataset_association_id", Integer, ForeignKey( "history_dataset_association.id" ), index=True ), + Column( "user_id", Integer, ForeignKey( "galaxy_user.id" ), index=True ), + Column( "rating", Integer, index=True) ) + +StoredWorkflowRatingAssociation.table = Table( "stored_workflow_rating_association", metadata, + Column( "id", Integer, primary_key=True ), + Column( "stored_workflow_id", Integer, ForeignKey( "stored_workflow.id" ), index=True ), + Column( "user_id", Integer, ForeignKey( "galaxy_user.id" ), index=True ), + Column( "rating", Integer, index=True) ) + +PageRatingAssociation.table = Table( "page_rating_association", metadata, + Column( "id", Integer, primary_key=True ), + Column( "page_id", Integer, ForeignKey( "page.id" ), index=True ), + Column( "user_id", Integer, ForeignKey( "galaxy_user.id" ), index=True ), + Column( "rating", Integer, index=True) ) + +VisualizationRatingAssociation.table = Table( "visualization_rating_association", metadata, + Column( "id", Integer, primary_key=True ), + Column( "visualization_id", Integer, ForeignKey( "visualization.id" ), index=True ), + Column( "user_id", Integer, ForeignKey( "galaxy_user.id" ), index=True ), + Column( "rating", Integer, index=True) ) + # User tables. UserPreference.table = Table( "user_preference", metadata, @@ -916,7 +947,8 @@ assign_mapper( context, HistoryDatasetAssociation, HistoryDatasetAssociation.tab HistoryDatasetAssociation, primaryjoin=( ( HistoryDatasetAssociation.table.c.parent_id == HistoryDatasetAssociation.table.c.id ) & ( HistoryDatasetAssociation.table.c.visible == True ) ) ), tags=relation( HistoryDatasetAssociationTagAssociation, order_by=HistoryDatasetAssociationTagAssociation.table.c.id, backref='history_tag_associations' ), - annotations=relation( HistoryDatasetAssociationAnnotationAssociation, order_by=HistoryDatasetAssociationAnnotationAssociation.table.c.id, backref="hdas" ) ) + annotations=relation( HistoryDatasetAssociationAnnotationAssociation, order_by=HistoryDatasetAssociationAnnotationAssociation.table.c.id, backref="hdas" ), + ratings=relation( HistoryDatasetAssociationRatingAssociation, order_by=HistoryDatasetAssociationRatingAssociation.table.c.id, backref="hdas" ) ) ) assign_mapper( context, Dataset, Dataset.table, @@ -954,7 +986,8 @@ assign_mapper( context, History, History.table, datasets=relation( HistoryDatasetAssociation, backref="history", order_by=asc(HistoryDatasetAssociation.table.c.hid) ), active_datasets=relation( HistoryDatasetAssociation, primaryjoin=( ( HistoryDatasetAssociation.table.c.history_id == History.table.c.id ) & ( not_( HistoryDatasetAssociation.table.c.deleted ) ) ), order_by=asc( HistoryDatasetAssociation.table.c.hid ), viewonly=True ), tags=relation( HistoryTagAssociation, order_by=HistoryTagAssociation.table.c.id, backref="histories" ), - annotations=relation( HistoryAnnotationAssociation, order_by=HistoryAnnotationAssociation.table.c.id, backref="histories" ) ) + annotations=relation( HistoryAnnotationAssociation, order_by=HistoryAnnotationAssociation.table.c.id, backref="histories" ), + ratings=relation( HistoryRatingAssociation, order_by=HistoryRatingAssociation.table.c.id, backref="histories" ) ) ) # Set up proxy so that @@ -1220,7 +1253,8 @@ assign_mapper( context, StoredWorkflow, StoredWorkflow.table, primaryjoin=( StoredWorkflow.table.c.latest_workflow_id == Workflow.table.c.id ), lazy=False ), tags=relation( StoredWorkflowTagAssociation, order_by=StoredWorkflowTagAssociation.table.c.id, backref="stored_workflows" ), - annotations=relation( StoredWorkflowAnnotationAssociation, order_by=StoredWorkflowAnnotationAssociation.table.c.id, backref="stored_workflows" ) ) + annotations=relation( StoredWorkflowAnnotationAssociation, order_by=StoredWorkflowAnnotationAssociation.table.c.id, backref="stored_workflows" ), + ratings=relation( StoredWorkflowRatingAssociation, order_by=StoredWorkflowRatingAssociation.table.c.id, backref="stored_workflows" ) ) ) # Set up proxy so that @@ -1260,7 +1294,8 @@ assign_mapper( context, Page, Page.table, primaryjoin=( Page.table.c.latest_revision_id == PageRevision.table.c.id ), lazy=False ), tags=relation(PageTagAssociation, order_by=PageTagAssociation.table.c.id, backref="pages"), - annotations=relation( PageAnnotationAssociation, order_by=PageAnnotationAssociation.table.c.id, backref="pages" ) + annotations=relation( PageAnnotationAssociation, order_by=PageAnnotationAssociation.table.c.id, backref="pages" ), + ratings=relation( PageRatingAssociation, order_by=PageRatingAssociation.table.c.id, backref="pages" ) ) ) # Set up proxy so that @@ -1284,7 +1319,8 @@ assign_mapper( context, Visualization, Visualization.table, primaryjoin=( Visualization.table.c.latest_revision_id == VisualizationRevision.table.c.id ), lazy=False ), tags=relation( VisualizationTagAssociation, order_by=VisualizationTagAssociation.table.c.id, backref="visualizations" ), - annotations=relation( VisualizationAnnotationAssociation, order_by=VisualizationAnnotationAssociation.table.c.id, backref="visualizations" ) + annotations=relation( VisualizationAnnotationAssociation, order_by=VisualizationAnnotationAssociation.table.c.id, backref="visualizations" ), + ratings=relation( VisualizationRatingAssociation, order_by=VisualizationRatingAssociation.table.c.id, backref="visualizations" ) ) ) # Set up proxy so that @@ -1296,6 +1332,8 @@ assign_mapper( context, VisualizationUserShareAssociation, VisualizationUserShar properties=dict( user=relation( User, backref='visualizations_shared_by_others' ), visualization=relation( Visualization, backref='users_shared_with' ) ) ) + +# Tag tables. assign_mapper( context, Tag, Tag.table, properties=dict( children=relation(Tag, backref=backref( 'parent', remote_side=[Tag.table.c.id] ) ) @@ -1329,6 +1367,8 @@ assign_mapper( context, VisualizationTagAssociation, VisualizationTagAssociation properties=dict( tag=relation(Tag, backref="tagged_visualizations"), user=relation( User ) ) ) +# Annotation tables. + assign_mapper( context, HistoryAnnotationAssociation, HistoryAnnotationAssociation.table, properties=dict( history=relation( History ), user=relation( User ) ) ) @@ -1353,6 +1393,30 @@ assign_mapper( context, VisualizationAnnotationAssociation, VisualizationAnnotat properties=dict( visualization=relation( Visualization ), user=relation( User ) ) ) +# Rating tables. + +assign_mapper( context, HistoryRatingAssociation, HistoryRatingAssociation.table, + properties=dict( history=relation( History ), user=relation( User ) ) + ) + +assign_mapper( context, HistoryDatasetAssociationRatingAssociation, HistoryDatasetAssociationRatingAssociation.table, + properties=dict( hda=relation( HistoryDatasetAssociation ), user=relation( User ) ) + ) + +assign_mapper( context, StoredWorkflowRatingAssociation, StoredWorkflowRatingAssociation.table, + properties=dict( stored_workflow=relation( StoredWorkflow ), user=relation( User ) ) + ) + +assign_mapper( context, PageRatingAssociation, PageRatingAssociation.table, + properties=dict( page=relation( Page ), user=relation( User ) ) + ) + +assign_mapper( context, VisualizationRatingAssociation, VisualizationRatingAssociation.table, + properties=dict( visualization=relation( Visualization ), user=relation( User ) ) + ) + +# User tables. + assign_mapper( context, UserPreference, UserPreference.table, properties = {} ) @@ -1363,6 +1427,8 @@ assign_mapper( context, UserAction, UserAction.table, assign_mapper( context, APIKeys, APIKeys.table, properties = {} ) + +# Helper methods. def db_next_hid( self ): """ diff --git a/lib/galaxy/model/migrate/versions/0053_item_ratings.py b/lib/galaxy/model/migrate/versions/0053_item_ratings.py new file mode 100644 index 00000000000..8aa07c4f8d4 --- /dev/null +++ b/lib/galaxy/model/migrate/versions/0053_item_ratings.py @@ -0,0 +1,133 @@ +""" +Migration script to create tables for rating histories, datasets, workflows, pages, and visualizations. +""" + +from sqlalchemy import * +from sqlalchemy.orm import * +from migrate import * +from migrate.changeset import * + +import logging +log = logging.getLogger( __name__ ) + +metadata = MetaData( migrate_engine ) +db_session = scoped_session( sessionmaker( bind=migrate_engine, autoflush=False, autocommit=True ) ) + +# Rating tables. + +HistoryRatingAssociation_table = Table( "history_rating_association", metadata, + Column( "id", Integer, primary_key=True ), + Column( "history_id", Integer, ForeignKey( "history.id" ), index=True ), + Column( "user_id", Integer, ForeignKey( "galaxy_user.id" ), index=True ), + Column( "rating", Integer, index=True) ) + +HistoryDatasetAssociationRatingAssociation_table = Table( "history_dataset_association_rating_association", metadata, + Column( "id", Integer, primary_key=True ), + Column( "history_dataset_association_id", Integer, ForeignKey( "history_dataset_association.id" ), index=True ), + Column( "user_id", Integer, ForeignKey( "galaxy_user.id" ), index=True ), + Column( "rating", Integer, index=True) ) + +StoredWorkflowRatingAssociation_table = Table( "stored_workflow_rating_association", metadata, + Column( "id", Integer, primary_key=True ), + Column( "stored_workflow_id", Integer, ForeignKey( "stored_workflow.id" ), index=True ), + Column( "user_id", Integer, ForeignKey( "galaxy_user.id" ), index=True ), + Column( "rating", Integer, index=True) ) + +PageRatingAssociation_table = Table( "page_rating_association", metadata, + Column( "id", Integer, primary_key=True ), + Column( "page_id", Integer, ForeignKey( "page.id" ), index=True ), + Column( "user_id", Integer, ForeignKey( "galaxy_user.id" ), index=True ), + Column( "rating", Integer, index=True) ) + +VisualizationRatingAssociation_table = Table( "visualization_rating_association", metadata, + Column( "id", Integer, primary_key=True ), + Column( "visualization_id", Integer, ForeignKey( "visualization.id" ), index=True ), + Column( "user_id", Integer, ForeignKey( "galaxy_user.id" ), index=True ), + Column( "rating", Integer, index=True) ) + +def upgrade(): + print __doc__ + metadata.reflect() + + # Create history_rating_association table. + try: + HistoryRatingAssociation_table.create() + except Exception, e: + print str(e) + log.debug( "Creating history_rating_association table failed: %s" % str( e ) ) + + # Create history_dataset_association_rating_association table. + try: + HistoryDatasetAssociationRatingAssociation_table.create() + except Exception, e: + # MySQL cannot handle long index names; when we see this error, create the index name manually. + if migrate_engine.name == 'mysql' and \ + str(e).lower().find("identifier name 'ix_history_dataset_association_rating_association_history_dataset_association_id' is too long"): + i = Index( "ix_hda_rating_association_hda_id", HistoryDatasetAssociationRatingAssociation_table.c.history_dataset_association_id ) + try: + i.create() + except Exception, e: + print str(e) + log.debug( "Adding index 'ix_hda_rating_association_hda_id' to table 'history_dataset_association_rating_association' table failed: %s" % str( e ) ) + else: + print str(e) + log.debug( "Creating history_dataset_association_rating_association table failed: %s" % str( e ) ) + + # Create stored_workflow_rating_association table. + try: + StoredWorkflowRatingAssociation_table.create() + except Exception, e: + print str(e) + log.debug( "Creating stored_workflow_rating_association table failed: %s" % str( e ) ) + + # Create page_rating_association table. + try: + PageRatingAssociation_table.create() + except Exception, e: + print str(e) + log.debug( "Creating page_rating_association table failed: %s" % str( e ) ) + + # Create visualization_rating_association table. + try: + VisualizationRatingAssociation_table.create() + except Exception, e: + print str(e) + log.debug( "Creating visualization_rating_association table failed: %s" % str( e ) ) + +def downgrade(): + metadata.reflect() + + # Drop history_rating_association table. + try: + HistoryRatingAssociation_table.drop() + except Exception, e: + print str(e) + log.debug( "Dropping history_rating_association table failed: %s" % str( e ) ) + + # Drop history_dataset_association_rating_association table. + try: + HistoryDatasetAssociationRatingAssociation_table.drop() + except Exception, e: + print str(e) + log.debug( "Dropping history_dataset_association_rating_association table failed: %s" % str( e ) ) + + # Drop stored_workflow_rating_association table. + try: + StoredWorkflowRatingAssociation_table.drop() + except Exception, e: + print str(e) + log.debug( "Dropping stored_workflow_rating_association table failed: %s" % str( e ) ) + + # Drop page_rating_association table. + try: + PageRatingAssociation_table.drop() + except Exception, e: + print str(e) + log.debug( "Dropping page_rating_association table failed: %s" % str( e ) ) + + # Drop visualization_rating_association table. + try: + VisualizationRatingAssociation_table.drop() + except Exception, e: + print str(e) + log.debug( "Dropping visualization_rating_association table failed: %s" % str( e ) ) \ No newline at end of file diff --git a/lib/galaxy/web/controllers/dataset.py b/lib/galaxy/web/controllers/dataset.py index 9ba4ecc684f..d28b23783ae 100644 --- a/lib/galaxy/web/controllers/dataset.py +++ b/lib/galaxy/web/controllers/dataset.py @@ -6,6 +6,7 @@ from galaxy import util, datatypes, jobs, web, model from cgi import escape, FieldStorage from galaxy.datatypes.display_applications.util import encode_dataset_user, decode_dataset_user from galaxy.util.sanitize_html import sanitize_html +from galaxy.item_attrs.ratings import UsesItemRatings from email.MIMEText import MIMEText import pkg_resources; @@ -466,6 +467,21 @@ class DatasetInterface( BaseController, UsesAnnotations, UsesHistoryDatasetAssoc """ Does nothing because datasets do not have an importable/accessible attribute. This method could potentially set another attribute. """ return + @web.expose + @web.require_login( "rate items" ) + @web.json + def rate_async( self, trans, id, rating ): + """ Rate a dataset asynchronously and return updated community data. """ + + dataset = self.get_dataset( trans, id, check_ownership=False, check_accessible=True ) + if not dataset: + return trans.show_error_message( "The specified dataset does not exist." ) + + # Rate dataset. + dataset_rating = self.rate_item( trans, trans.get_user(), dataset, rating ) + + return self.get_ave_item_rating_data( trans, dataset ) + @web.expose def display_by_username_and_slug( self, trans, username, slug, preview=True ): """ Display dataset by username and slug; because datasets do not yet have slugs, the slug is the dataset's id. """ @@ -481,7 +497,18 @@ class DatasetInterface( BaseController, UsesAnnotations, UsesHistoryDatasetAssoc trans.response.set_content_type( mime ) return open( dataset.file_name ) else: - return trans.fill_template_mako( "/dataset/display.mako", item=dataset, item_data=dataset_data, truncated=truncated ) + # Get rating data. + user_item_rating = 0 + if trans.get_user(): + user_item_rating = self.get_user_item_rating( trans, trans.get_user(), dataset ) + if user_item_rating: + user_item_rating = user_item_rating.rating + else: + user_item_rating = 0 + ave_item_rating, num_ratings = self.get_ave_item_rating_data( trans, dataset ) + + return trans.fill_template_mako( "/dataset/display.mako", item=dataset, item_data=dataset_data, truncated=truncated, + user_item_rating = user_item_rating, ave_item_rating=ave_item_rating, num_ratings=num_ratings ) else: raise web.httpexceptions.HTTPNotFound() diff --git a/lib/galaxy/web/controllers/history.py b/lib/galaxy/web/controllers/history.py index f3f7eda1582..519557ba76d 100644 --- a/lib/galaxy/web/controllers/history.py +++ b/lib/galaxy/web/controllers/history.py @@ -9,6 +9,7 @@ from galaxy.util.sanitize_html import sanitize_html from galaxy.tools.parameters.basic import UnvalidatedValue from galaxy.tools.actions import upload_common from galaxy.tags.tag_handler import GalaxyTagHandler +from galaxy.item_attrs.ratings import UsesItemRatings from sqlalchemy.sql.expression import ClauseElement import webhelpers, logging, operator, os, tempfile, subprocess, shutil, tarfile from datetime import datetime @@ -147,7 +148,7 @@ class HistoryAllPublishedGrid( grids.Grid ): # A public history is published, has a slug, and is not deleted. return query.filter( self.model_class.published == True ).filter( self.model_class.slug != None ).filter( self.model_class.deleted == False ) -class HistoryController( BaseController, Sharable, UsesAnnotations, UsesHistory ): +class HistoryController( BaseController, Sharable, UsesAnnotations, UsesItemRatings, UsesHistory ): @web.expose def index( self, trans ): return "" @@ -415,7 +416,22 @@ class HistoryController( BaseController, Sharable, UsesAnnotations, UsesHistory trans.log_event( "History id %d marked as deleted" % history.id ) # Regardless of whether it was previously deleted, we make a new history active trans.new_history() - return trans.show_ok_message( "History deleted, a new history is active", refresh_frames=['history'] ) + return trans.show_ok_message( "History deleted, a new history is active", refresh_frames=['history'] ) + + @web.expose + @web.require_login( "rate items" ) + @web.json + def rate_async( self, trans, id, rating ): + """ Rate a history asynchronously and return updated community data. """ + + history = self.get_history( trans, id, check_ownership=False, check_accessible=True ) + if not history: + return trans.show_error_message( "The specified history does not exist." ) + + # Rate history. + history_rating = self.rate_item( trans, trans.get_user(), history, rating ) + + return self.get_ave_item_rating_data( trans, history ) @web.expose def rename_async( self, trans, id=None, new_name=None ): @@ -1047,8 +1063,18 @@ class HistoryController( BaseController, Sharable, UsesAnnotations, UsesHistory history.annotation = self.get_item_annotation_str( trans, history.user, history ) for dataset in datasets: dataset.annotation = self.get_item_annotation_str( trans, history.user, dataset ) - return trans.stream_template_mako( "history/display.mako", - item = history, item_data = datasets ) + + # Get rating data. + user_item_rating = 0 + if trans.get_user(): + user_item_rating = self.get_user_item_rating( trans, trans.get_user(), history ) + if user_item_rating: + user_item_rating = user_item_rating.rating + else: + user_item_rating = 0 + ave_item_rating, num_ratings = self.get_ave_item_rating_data( trans, history ) + return trans.stream_template_mako( "history/display.mako", item = history, item_data = datasets, + user_item_rating = user_item_rating, ave_item_rating=ave_item_rating, num_ratings=num_ratings ) @web.expose @web.require_login( "share Galaxy histories" ) diff --git a/lib/galaxy/web/controllers/page.py b/lib/galaxy/web/controllers/page.py index b9eb095cf6b..a80e0c42502 100644 --- a/lib/galaxy/web/controllers/page.py +++ b/lib/galaxy/web/controllers/page.py @@ -4,6 +4,7 @@ from galaxy.web.framework.helpers import time_ago, grids from galaxy.util.sanitize_html import sanitize_html, _BaseHTMLProcessor from galaxy.util.odict import odict from galaxy.util.json import from_json_string +from galaxy.item_attrs.ratings import UsesItemRatings def format_bool( b ): if b: @@ -269,7 +270,8 @@ class _PageContentProcessor( _BaseHTMLProcessor ): # Default behavior: _BaseHTMLProcessor.unknown_endtag( self, tag ) -class PageController( BaseController, Sharable, UsesAnnotations, UsesHistory, UsesStoredWorkflow, UsesHistoryDatasetAssociation, UsesVisualization ): +class PageController( BaseController, Sharable, UsesAnnotations, UsesHistory, + UsesStoredWorkflow, UsesHistoryDatasetAssociation, UsesVisualization, UsesItemRatings ): _page_list = PageListGrid() _all_published_list = PageAllPublishedGrid() @@ -582,9 +584,22 @@ class PageController( BaseController, Sharable, UsesAnnotations, UsesHistory, Us # Process page content. processor = _PageContentProcessor( trans, 'utf-8', 'text/html', self._get_embed_html ) processor.feed( page.latest_revision.content ) + + # Get rating data. + user_item_rating = 0 + if trans.get_user(): + user_item_rating = self.get_user_item_rating( trans, trans.get_user(), page ) + if user_item_rating: + user_item_rating = user_item_rating.rating + else: + user_item_rating = 0 + ave_item_rating, num_ratings = self.get_ave_item_rating_data( trans, page ) + # Output is string, so convert to unicode for display. page_content = unicode( processor.output(), 'utf-8' ) - return trans.fill_template_mako( "page/display.mako", item=page, item_data=page_content, content_only=True ) + return trans.fill_template_mako( "page/display.mako", item=page, item_data=page_content, + user_item_rating = user_item_rating, ave_item_rating=ave_item_rating, num_ratings=num_ratings, + content_only=True ) @web.expose @web.require_login( "use Galaxy pages" ) @@ -611,6 +626,21 @@ class PageController( BaseController, Sharable, UsesAnnotations, UsesHistory, Us trans.sa_session.flush() return page.slug + @web.expose + @web.require_login( "rate items" ) + @web.json + def rate_async( self, trans, id, rating ): + """ Rate a page asynchronously and return updated community data. """ + + page = self.get_page( trans, id, check_ownership=False, check_accessible=True ) + if not page: + return trans.show_error_message( "The specified page does not exist." ) + + # Rate page. + page_rating = self.rate_item( trans, trans.get_user(), page, rating ) + + return self.get_ave_item_rating_data( trans, page ) + @web.expose def get_embed_html_async( self, trans, id ): """ Returns HTML for embedding a workflow in a page. """ diff --git a/lib/galaxy/web/controllers/visualization.py b/lib/galaxy/web/controllers/visualization.py index 2b59207c521..cdb8780d61c 100644 --- a/lib/galaxy/web/controllers/visualization.py +++ b/lib/galaxy/web/controllers/visualization.py @@ -1,5 +1,6 @@ from galaxy import model from galaxy.web.base.controller import * +from galaxy.item_attrs.ratings import UsesItemRatings from galaxy.web.framework.helpers import time_ago, grids, iff from galaxy.util.sanitize_html import sanitize_html @@ -65,7 +66,7 @@ class VisualizationAllPublishedGrid( grids.Grid ): return query.filter( self.model_class.deleted==False ).filter( self.model_class.published==True ) -class VisualizationController( BaseController, Sharable, UsesAnnotations, UsesVisualization ): +class VisualizationController( BaseController, Sharable, UsesAnnotations, UsesVisualization, UsesItemRatings ): _user_list_grid = VisualizationListGrid() _published_list_grid = VisualizationAllPublishedGrid() @@ -138,6 +139,21 @@ class VisualizationController( BaseController, Sharable, UsesAnnotations, UsesVi return + @web.expose + @web.require_login( "rate items" ) + @web.json + def rate_async( self, trans, id, rating ): + """ Rate a visualization asynchronously and return updated community data. """ + + visualization = self.get_visualization( trans, id, check_ownership=False, check_accessible=True ) + if not visualization: + return trans.show_error_message( "The specified visualization does not exist." ) + + # Rate visualization. + visualization_rating = self.rate_item( trans, trans.get_user(), visualization, rating ) + + return self.get_ave_item_rating_data( trans, visualization ) + @web.expose @web.require_login( "share Galaxy visualizations" ) def imp( self, trans, id ): @@ -263,9 +279,21 @@ class VisualizationController( BaseController, Sharable, UsesAnnotations, UsesVi # Security check raises error if user cannot access visualization. self.security_check( trans.get_user(), visualization, False, True) + # Get rating data. + user_item_rating = 0 + if trans.get_user(): + user_item_rating = self.get_user_item_rating( trans, trans.get_user(), visualization ) + if user_item_rating: + user_item_rating = user_item_rating.rating + else: + user_item_rating = 0 + ave_item_rating, num_ratings = self.get_ave_item_rating_data( trans, visualization ) + # Display. - visualization_config = self.get_visualization_config( trans, visualization ) - return trans.fill_template_mako( "visualization/display.mako", item=visualization, item_data=visualization_config, content_only=True ) + visualization_config = self.get_visualization_config( trans, visualization ) + return trans.stream_template_mako( "visualization/display.mako", item = visualization, item_data = visualization_config, + user_item_rating = user_item_rating, ave_item_rating=ave_item_rating, num_ratings=num_ratings, + content_only=True ) @web.expose @web.json diff --git a/lib/galaxy/web/controllers/workflow.py b/lib/galaxy/web/controllers/workflow.py index e4f3cd77c9f..4a3959e92ff 100644 --- a/lib/galaxy/web/controllers/workflow.py +++ b/lib/galaxy/web/controllers/workflow.py @@ -18,6 +18,7 @@ from galaxy import model from galaxy.model.mapping import desc from galaxy.model.orm import * from galaxy.jobs.actions.post import * +from galaxy.item_attrs.ratings import UsesItemRatings import urllib2 @@ -83,7 +84,7 @@ class StoredWorkflowAllPublishedGrid( grids.Grid ): # A public workflow is published, has a slug, and is not deleted. return query.filter( self.model_class.published==True ).filter( self.model_class.slug != None ).filter( self.model_class.deleted == False ) -class WorkflowController( BaseController, Sharable, UsesStoredWorkflow, UsesAnnotations ): +class WorkflowController( BaseController, Sharable, UsesStoredWorkflow, UsesAnnotations, UsesItemRatings ): stored_list_grid = StoredWorkflowListGrid() published_list_grid = StoredWorkflowAllPublishedGrid() @@ -185,7 +186,18 @@ class WorkflowController( BaseController, Sharable, UsesStoredWorkflow, UsesAnno stored_workflow.annotation = self.get_item_annotation_str( trans, stored_workflow.user, stored_workflow ) for step in stored_workflow.latest_workflow.steps: step.annotation = self.get_item_annotation_str( trans, stored_workflow.user, step ) - return trans.fill_template_mako( "workflow/display.mako", item=stored_workflow, item_data=stored_workflow.latest_workflow.steps ) + + # Get rating data. + user_item_rating = 0 + if trans.get_user(): + user_item_rating = self.get_user_item_rating( trans, trans.get_user(), stored_workflow ) + if user_item_rating: + user_item_rating = user_item_rating.rating + else: + user_item_rating = 0 + ave_item_rating, num_ratings = self.get_ave_item_rating_data( trans, stored_workflow ) + return trans.fill_template_mako( "workflow/display.mako", item=stored_workflow, item_data=stored_workflow.latest_workflow.steps, + user_item_rating = user_item_rating, ave_item_rating=ave_item_rating, num_ratings=num_ratings ) @web.expose def get_item_content_async( self, trans, id ): @@ -376,6 +388,21 @@ class WorkflowController( BaseController, Sharable, UsesStoredWorkflow, UsesAnno trans.sa_session.flush() return new_annotation + @web.expose + @web.require_login( "rate items" ) + @web.json + def rate_async( self, trans, id, rating ): + """ Rate a workflow asynchronously and return updated community data. """ + + stored = self.get_stored_workflow( trans, id, check_ownership=False, check_accessible=True ) + if not stored: + return trans.show_error_message( "The specified workflow does not exist." ) + + # Rate workflow. + stored_rating = self.rate_item( trans, trans.get_user(), stored, rating ) + + return self.get_ave_item_rating_data( trans, stored ) + @web.expose @web.require_login( "use Galaxy workflows" ) def set_accessible_async( self, trans, id=None, accessible=False ): diff --git a/static/images/delete.gif b/static/images/delete.gif new file mode 100755 index 0000000000000000000000000000000000000000..43c6ca8763d79bde87bcf437e497af00c8be562d GIT binary patch literal 752 zcmZ?wbhEHb6kt$bc*el6GthYN-n}zt&b)vB{*)i+-#|NQy$ zABXln&Q1FL`t{#CH*SY|{oJwc*Qz;h;)0))7aYq9ewP~fc2e)_P3vAY)$9wl{IzJp zy{ydlr;a^HiGHwZ{*SH8FZkJhE{gei`OM?;ir*_{?@ji478&@mD*tSH_`A&5@6Vqe z@G<&xeCOwiq-WvY-eS5f?;vgCP8#IHGBw=+^cY}xvy zx8c*8mDl2ff1W;kCeZrNm9vlQYJg5Caki?(^G9DBs4DA0KlA4-ZT3fl8 z8F_gb*}2-1v>0WWwY1oIg_&Emc-aM+Wn|bpRc1GF^$N-etzA1qr9X*XNNWw_j-BjG zGEKc(x)1gUF{(82E@ad^eMXp9hUxg0Q)f<}lVRDncBb&%dq*yAR+(|l-D@bZwvw%g)cL7VG z)An_f*+XqOetkIP*eg{wS7K(+qJ{-x0ue3?v&y~1d1To>MqD_2keNGNE=Pc|!BJ7b fV_(F^#N=Z_Jo9)pKRPly^YODPxtB5!M# z{q=^=j}*N*e*EQepZ7;K-(U6l^W*)qy-uI5Mx04YJ9FmD@2@w0e|`3BzvYV!svmDJ zy1UBtA9dUx3IVz=bW<9TmSpMHPB|LHc1x92?H@6~*NPV?1P?R#4d zPbMV1-s|#ytJ9}D;U7+0e!n;8#jahi&$Zv4GUd&Fk1akv*Jf&ey1!wEi_7gU#eYv#yB=$CE4*Ha3+f942mV(XO4k$!yUkYI4!^v z(7()>%8M?Gl%7kx>Gn`^RAQF96>H~ zqm`q-QcY~aQ7_g~!8sNO9r=2u3Mlqi#CWtc9T(=ypIRf>c$iT@(<;xV@nLFbqN;ve z&H|wp@2R5NS2`L#KJ=KR(rKEJaLDyUmqb%cL}by=)-=YHmH-A@k7GPXma%d?aC*qL zh)X7I#x;cnUQ^jS^;|kOFmg@Nv0U}W!=<5%L(6DYL1B*CL?*3JjSUV#i&`dVbSiW! SUO9NY!6jXVS5kz7!5RQ1){ + var args = arguments; + return this.each(function(){ + $.fn.rating.apply($(this), args); + }); + }; + // Invoke API method handler + $.fn.rating[arguments[0]].apply(this, $.makeArray(arguments).slice(1) || []); + // Quick exit... + return this; + }; + + // Initialize options for this call + var options = $.extend( + {}/* new object */, + $.fn.rating.options/* default options */, + options || {} /* just-in-time options */ + ); + + // Allow multiple controls with the same name by making each call unique + $.fn.rating.calls++; + + // loop through each matched element + this + .not('.star-rating-applied') + .addClass('star-rating-applied') + .each(function(){ + + // Load control parameters / find context / etc + var control, input = $(this); + var eid = (this.name || 'unnamed-rating').replace(/\[|\]/g, '_').replace(/^\_+|\_+$/g,''); + var context = $(this.form || document.body); + + // FIX: http://code.google.com/p/jquery-star-rating-plugin/issues/detail?id=23 + var raters = context.data('rating'); + if(!raters || raters.call!=$.fn.rating.calls) raters = { count:0, call:$.fn.rating.calls }; + var rater = raters[eid]; + + // if rater is available, verify that the control still exists + if(rater) control = rater.data('rating'); + + if(rater && control)//{// save a byte! + // add star to control if rater is available and the same control still exists + control.count++; + + //}// save a byte! + else{ + // create new control if first star or control element was removed/replaced + + // Initialize options for this raters + control = $.extend( + {}/* new object */, + options || {} /* current call options */, + ($.metadata? input.metadata(): ($.meta?input.data():null)) || {}, /* metadata options */ + { count:0, stars: [], inputs: [] } + ); + + // increment number of rating controls + control.serial = raters.count++; + + // create rating element + rater = $(''); + input.before(rater); + + // Mark element for initialization (once all stars are ready) + rater.addClass('rating-to-be-drawn'); + + // Accept readOnly setting from 'disabled' property + if(input.attr('disabled')) control.readOnly = true; + + // Create 'cancel' button + rater.append( + control.cancel = $('') + .mouseover(function(){ + $(this).rating('drain'); + $(this).addClass('star-rating-hover'); + //$(this).rating('focus'); + }) + .mouseout(function(){ + $(this).rating('draw'); + $(this).removeClass('star-rating-hover'); + //$(this).rating('blur'); + }) + .click(function(){ + $(this).rating('select'); + }) + .data('rating', control) + ); + + }; // first element of group + + // insert rating star + var star = $(''); + rater.append(star); + + // inherit attributes from input element + if(this.id) star.attr('id', this.id); + if(this.className) star.addClass(this.className); + + // Half-stars? + if(control.half) control.split = 2; + + // Prepare division control + if(typeof control.split=='number' && control.split>0){ + var stw = ($.fn.width ? star.width() : 0) || control.starWidth; + var spi = (control.count % control.split), spw = Math.floor(stw/control.split); + star + // restrict star's width and hide overflow (already in CSS) + .width(spw) + // move the star left by using a negative margin + // this is work-around to IE's stupid box model (position:relative doesn't work) + .find('a').css({ 'margin-left':'-'+ (spi*spw) +'px' }) + }; + + // readOnly? + if(control.readOnly)//{ //save a byte! + // Mark star as readOnly so user can customize display + star.addClass('star-rating-readonly'); + //} //save a byte! + else//{ //save a byte! + // Enable hover css effects + star.addClass('star-rating-live') + // Attach mouse events + .mouseover(function(){ + $(this).rating('fill'); + $(this).rating('focus'); + }) + .mouseout(function(){ + $(this).rating('draw'); + $(this).rating('blur'); + }) + .click(function(){ + $(this).rating('select'); + }) + ; + //}; //save a byte! + + // set current selection + if(this.checked) control.current = star; + + // hide input element + input.hide(); + + // backward compatibility, form element to plugin + input.change(function(){ + $(this).rating('select'); + }); + + // attach reference to star to input element and vice-versa + star.data('rating.input', input.data('rating.star', star)); + + // store control information in form (or body when form not available) + control.stars[control.stars.length] = star[0]; + control.inputs[control.inputs.length] = input[0]; + control.rater = raters[eid] = rater; + control.context = context; + + input.data('rating', control); + rater.data('rating', control); + star.data('rating', control); + context.data('rating', raters); + }); // each element + + // Initialize ratings (first draw) + $('.rating-to-be-drawn').rating('draw').removeClass('rating-to-be-drawn'); + + return this; // don't break the chain... + }; + + /*--------------------------------------------------------*/ + + /* + ### Core functionality and API ### + */ + $.extend($.fn.rating, { + // Used to append a unique serial number to internal control ID + // each time the plugin is invoked so same name controls can co-exist + calls: 0, + + focus: function(){ + var control = this.data('rating'); if(!control) return this; + if(!control.focus) return this; // quick fail if not required + // find data for event + var input = $(this).data('rating.input') || $( this.tagName=='INPUT' ? this : null ); + // focus handler, as requested by focusdigital.co.uk + if(control.focus) control.focus.apply(input[0], [input.val(), $('a', input.data('rating.star'))[0]]); + }, // $.fn.rating.focus + + blur: function(){ + var control = this.data('rating'); if(!control) return this; + if(!control.blur) return this; // quick fail if not required + // find data for event + var input = $(this).data('rating.input') || $( this.tagName=='INPUT' ? this : null ); + // blur handler, as requested by focusdigital.co.uk + if(control.blur) control.blur.apply(input[0], [input.val(), $('a', input.data('rating.star'))[0]]); + }, // $.fn.rating.blur + + fill: function(){ // fill to the current mouse position. + var control = this.data('rating'); if(!control) return this; + // do not execute when control is in read-only mode + if(control.readOnly) return; + // Reset all stars and highlight them up to this element + this.rating('drain'); + this.prevAll().andSelf().filter('.rater-'+ control.serial).addClass('star-rating-hover'); + },// $.fn.rating.fill + + drain: function() { // drain all the stars. + var control = this.data('rating'); if(!control) return this; + // do not execute when control is in read-only mode + if(control.readOnly) return; + // Reset all stars + control.rater.children().filter('.rater-'+ control.serial).removeClass('star-rating-on').removeClass('star-rating-hover'); + },// $.fn.rating.drain + + draw: function(){ // set value and stars to reflect current selection + var control = this.data('rating'); if(!control) return this; + // Clear all stars + this.rating('drain'); + // Set control value + if(control.current){ + control.current.data('rating.input').attr('checked','checked'); + control.current.prevAll().andSelf().filter('.rater-'+ control.serial).addClass('star-rating-on'); + } + else + $(control.inputs).removeAttr('checked'); + // Show/hide 'cancel' button + control.cancel[control.readOnly || control.required?'hide':'show'](); + // Add/remove read-only classes to remove hand pointer + this.siblings()[control.readOnly?'addClass':'removeClass']('star-rating-readonly'); + },// $.fn.rating.draw + + + + + + select: function(value,wantCallBack){ // select a value + + // ***** MODIFICATION ***** + // Thanks to faivre.thomas - http://code.google.com/p/jquery-star-rating-plugin/issues/detail?id=27 + // + // ***** LIST OF MODIFICATION ***** + // ***** added Parameter wantCallBack : false if you don't want a callback. true or undefined if you want postback to be performed at the end of this method' + // ***** recursive calls to this method were like : ... .rating('select') it's now like .rating('select',undefined,wantCallBack); (parameters are set.) + // ***** line which is calling callback + // ***** /LIST OF MODIFICATION ***** + + var control = this.data('rating'); if(!control) return this; + // do not execute when control is in read-only mode + if(control.readOnly) return; + // clear selection + control.current = null; + // programmatically (based on user input) + if(typeof value!='undefined'){ + // select by index (0 based) + if(typeof value=='number') + return $(control.stars[value]).rating('select',undefined,wantCallBack); + // select by literal value (must be passed as a string + if(typeof value=='string') + //return + $.each(control.stars, function(){ + if($(this).data('rating.input').val()==value) $(this).rating('select',undefined,wantCallBack); + }); + } + else + control.current = this[0].tagName=='INPUT' ? + this.data('rating.star') : + (this.is('.rater-'+ control.serial) ? this : null); + + // Update rating control state + this.data('rating', control); + // Update display + this.rating('draw'); + // find data for event + var input = $( control.current ? control.current.data('rating.input') : null ); + // click callback, as requested here: http://plugins.jquery.com/node/1655 + + // **** MODIFICATION ***** + // Thanks to faivre.thomas - http://code.google.com/p/jquery-star-rating-plugin/issues/detail?id=27 + // + //old line doing the callback : + //if(control.callback) control.callback.apply(input[0], [input.val(), $('a', control.current)[0]]);// callback event + // + //new line doing the callback (if i want :) + if((wantCallBack ||wantCallBack == undefined) && control.callback) control.callback.apply(input[0], [input.val(), $('a', control.current)[0]]);// callback event + //to ensure retro-compatibility, wantCallBack must be considered as true by default + // **** /MODIFICATION ***** + + },// $.fn.rating.select + + + + + + readOnly: function(toggle, disable){ // make the control read-only (still submits value) + var control = this.data('rating'); if(!control) return this; + // setread-only status + control.readOnly = toggle || toggle==undefined ? true : false; + // enable/disable control value submission + if(disable) $(control.inputs).attr("disabled", "disabled"); + else $(control.inputs).removeAttr("disabled"); + // Update rating control state + this.data('rating', control); + // Update display + this.rating('draw'); + },// $.fn.rating.readOnly + + disable: function(){ // make read-only and never submit value + this.rating('readOnly', true, true); + },// $.fn.rating.disable + + enable: function(){ // make read/write and submit value + this.rating('readOnly', false, false); + }// $.fn.rating.select + + }); + + /*--------------------------------------------------------*/ + + /* + ### Default Settings ### + eg.: You can override default control like this: + $.fn.rating.options.cancel = 'Clear'; + */ + $.fn.rating.options = { //$.extend($.fn.rating, { options: { + cancel: 'Cancel Rating', // advisory title for the 'cancel' link + cancelValue: '', // value to submit when user click the 'cancel' link + split: 0, // split the star into how many parts? + + // Width of star image in case the plugin can't work it out. This can happen if + // the jQuery.dimensions plugin is not available OR the image is hidden at installation + starWidth: 16//, + + //NB.: These don't need to be pre-defined (can be undefined/null) so let's save some code! + //half: false, // just a shortcut to control.split = 2 + //required: false, // disables the 'cancel' button so user can only select one of the specified values + //readOnly: false, // disable rating plugin interaction/ values cannot be changed + //focus: function(){}, // executed when stars are focused + //blur: function(){}, // executed when stars are focused + //callback: function(){}, // executed when a star is clicked + }; //} }); + + /*--------------------------------------------------------*/ + + /* + ### Default implementation ### + The plugin will attach itself to file inputs + with the class 'multi' when the page loads + */ + $(function(){ + $('input[type=radio].star').rating(); + }); + + + +/*# AVOID COLLISIONS #*/ +})(jQuery); +/*# AVOID COLLISIONS #*/ diff --git a/static/scripts/packed/jquery.rating.js b/static/scripts/packed/jquery.rating.js new file mode 100644 index 00000000000..3006eb595d8 --- /dev/null +++ b/static/scripts/packed/jquery.rating.js @@ -0,0 +1 @@ +if(window.jQuery){(function(a){if(a.browser.msie){try{document.execCommand("BackgroundImageCache",false,true)}catch(b){}}a.fn.rating=function(d){if(this.length==0){return this}if(typeof arguments[0]=="string"){if(this.length>1){var c=arguments;return this.each(function(){a.fn.rating.apply(a(this),c)})}a.fn.rating[arguments[0]].apply(this,a.makeArray(arguments).slice(1)||[]);return this}var d=a.extend({},a.fn.rating.options,d||{});a.fn.rating.calls++;this.not(".star-rating-applied").addClass("star-rating-applied").each(function(){var g,l=a(this);var e=(this.name||"unnamed-rating").replace(/\[|\]/g,"_").replace(/^\_+|\_+$/g,"");var f=a(this.form||document.body);var k=f.data("rating");if(!k||k.call!=a.fn.rating.calls){k={count:0,call:a.fn.rating.calls}}var n=k[e];if(n){g=n.data("rating")}if(n&&g){g.count++}else{g=a.extend({},d||{},(a.metadata?l.metadata():(a.meta?l.data():null))||{},{count:0,stars:[],inputs:[]});g.serial=k.count++;n=a('');l.before(n);n.addClass("rating-to-be-drawn");if(l.attr("disabled")){g.readOnly=true}n.append(g.cancel=a('").mouseover(function(){a(this).rating("drain");a(this).addClass("star-rating-hover")}).mouseout(function(){a(this).rating("draw");a(this).removeClass("star-rating-hover")}).click(function(){a(this).rating("select")}).data("rating",g))}var j=a('");n.append(j);if(this.id){j.attr("id",this.id)}if(this.className){j.addClass(this.className)}if(g.half){g.split=2}if(typeof g.split=="number"&&g.split>0){var i=(a.fn.width?j.width():0)||g.starWidth;var h=(g.count%g.split),m=Math.floor(i/g.split);j.width(m).find("a").css({"margin-left":"-"+(h*m)+"px"})}if(g.readOnly){j.addClass("star-rating-readonly")}else{j.addClass("star-rating-live").mouseover(function(){a(this).rating("fill");a(this).rating("focus")}).mouseout(function(){a(this).rating("draw");a(this).rating("blur")}).click(function(){a(this).rating("select")})}if(this.checked){g.current=j}l.hide();l.change(function(){a(this).rating("select")});j.data("rating.input",l.data("rating.star",j));g.stars[g.stars.length]=j[0];g.inputs[g.inputs.length]=l[0];g.rater=k[e]=n;g.context=f;l.data("rating",g);n.data("rating",g);j.data("rating",g);f.data("rating",k)});a(".rating-to-be-drawn").rating("draw").removeClass("rating-to-be-drawn");return this};a.extend(a.fn.rating,{calls:0,focus:function(){var d=this.data("rating");if(!d){return this}if(!d.focus){return this}var c=a(this).data("rating.input")||a(this.tagName=="INPUT"?this:null);if(d.focus){d.focus.apply(c[0],[c.val(),a("a",c.data("rating.star"))[0]])}},blur:function(){var d=this.data("rating");if(!d){return this}if(!d.blur){return this}var c=a(this).data("rating.input")||a(this.tagName=="INPUT"?this:null);if(d.blur){d.blur.apply(c[0],[c.val(),a("a",c.data("rating.star"))[0]])}},fill:function(){var c=this.data("rating");if(!c){return this}if(c.readOnly){return}this.rating("drain");this.prevAll().andSelf().filter(".rater-"+c.serial).addClass("star-rating-hover")},drain:function(){var c=this.data("rating");if(!c){return this}if(c.readOnly){return}c.rater.children().filter(".rater-"+c.serial).removeClass("star-rating-on").removeClass("star-rating-hover")},draw:function(){var c=this.data("rating");if(!c){return this}this.rating("drain");if(c.current){c.current.data("rating.input").attr("checked","checked");c.current.prevAll().andSelf().filter(".rater-"+c.serial).addClass("star-rating-on")}else{a(c.inputs).removeAttr("checked")}c.cancel[c.readOnly||c.required?"hide":"show"]();this.siblings()[c.readOnly?"addClass":"removeClass"]("star-rating-readonly")},select:function(d,f){var e=this.data("rating");if(!e){return this}if(e.readOnly){return}e.current=null;if(typeof d!="undefined"){if(typeof d=="number"){return a(e.stars[d]).rating("select",undefined,f)}if(typeof d=="string"){a.each(e.stars,function(){if(a(this).data("rating.input").val()==d){a(this).rating("select",undefined,f)}})}}else{e.current=this[0].tagName=="INPUT"?this.data("rating.star"):(this.is(".rater-"+e.serial)?this:null)}this.data("rating",e);this.rating("draw");var c=a(e.current?e.current.data("rating.input"):null);if((f||f==undefined)&&e.callback){e.callback.apply(c[0],[c.val(),a("a",e.current)[0]])}},readOnly:function(c,d){var e=this.data("rating");if(!e){return this}e.readOnly=c||c==undefined?true:false;if(d){a(e.inputs).attr("disabled","disabled")}else{a(e.inputs).removeAttr("disabled")}this.data("rating",e);this.rating("draw")},disable:function(){this.rating("readOnly",true,true)},enable:function(){this.rating("readOnly",false,false)}});a.fn.rating.options={cancel:"Cancel Rating",cancelValue:"",split:0,starWidth:16};a(function(){a("input[type=radio].star").rating()})})(jQuery)}; \ No newline at end of file diff --git a/templates/display_base.mako b/templates/display_base.mako index c7db3a1f93c..611af93a7db 100644 --- a/templates/display_base.mako +++ b/templates/display_base.mako @@ -32,7 +32,7 @@ <%def name="javascripts()"> ${parent.javascripts()} - ${h.js( "jquery", "jquery.tipsy", "galaxy.base", "json2", "class", "jquery.jstore", "jquery.autocomplete", "autocomplete_tagging", "trackster" )} + ${h.js( "jquery", "jquery.tipsy", "galaxy.base", "json2", "class", "jquery.jstore", "jquery.autocomplete", "jquery.rating", "autocomplete_tagging", "trackster" )} <%def name="stylesheets()"> ${parent.stylesheets()} - ${h.css( "autocomplete_tagging", "embed_item", "trackster" )} + ${h.css( "autocomplete_tagging", "embed_item", "trackster", "jquery.rating" )}