mirror of
https://github.com/galaxyproject/galaxy.git
synced 2026-09-24 16:30:27 +08:00
Refactored grid code to (a) generalize numerous column types and (b) fully support multiple, arbitrary, multi-column categorical and free-text queries. Python code provides support for TextColumn and TagColumn objects, and grids_common.mako provides support for programmatically creating free text and categorical filtering UIs. History and HDA grids have been updated to use the refactored code.
This commit is contained in:
@@ -246,7 +246,7 @@ class History( object ):
|
||||
# This needs to be a list
|
||||
return [ hda for hda in self.datasets if not hda.dataset.deleted ]
|
||||
def get_display_name( self ):
|
||||
## History name can be either a string or a unicode object. If string, convert to unicode object assuming 'utf-8' format.
|
||||
""" History name can be either a string or a unicode object. If string, convert to unicode object assuming 'utf-8' format. """
|
||||
history_name = self.name
|
||||
if isinstance(history_name, str):
|
||||
history_name = unicode(history_name, 'utf-8')
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import logging, os, string, shutil, re, socket, mimetypes, smtplib, urllib
|
||||
|
||||
from galaxy.web.base.controller import *
|
||||
from galaxy.tags.tag_handler import TagHandler
|
||||
from galaxy.web.framework.helpers import time_ago, iff, grids
|
||||
from galaxy import util, datatypes, jobs, web, model
|
||||
from cgi import escape, FieldStorage
|
||||
@@ -49,55 +48,7 @@ class HistoryDatasetAssociationListGrid( grids.Grid ):
|
||||
class HistoryColumn( grids.GridColumn ):
|
||||
def get_value( self, trans, grid, hda):
|
||||
return hda.history.name
|
||||
|
||||
class StatusColumn( grids.GridColumn ):
|
||||
def get_value( self, trans, grid, hda ):
|
||||
if hda.deleted:
|
||||
return "deleted"
|
||||
return ""
|
||||
def get_link( self, trans, grid, hda ):
|
||||
return None
|
||||
|
||||
class TagsColumn( grids.GridColumn ):
|
||||
def __init__(self, col_name, key, filterable):
|
||||
grids.GridColumn.__init__(self, col_name, key=key, filterable=filterable)
|
||||
# Tags cannot be sorted.
|
||||
self.sortable = False
|
||||
self.tag_elt_id_gen = 0
|
||||
def get_value( self, trans, grid, hda ):
|
||||
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=hda,
|
||||
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 include only hdas with tags in column_filter. """
|
||||
if column_filter == "All":
|
||||
pass
|
||||
elif column_filter:
|
||||
# Parse filter to extract multiple tags.
|
||||
tag_handler = TagHandler()
|
||||
raw_tags = tag_handler.parse_tags( column_filter.encode("utf-8") )
|
||||
for name, value in raw_tags.items():
|
||||
tag = tag_handler.get_tag_by_name( db_session, name )
|
||||
if tag:
|
||||
query = query.filter( model.HistoryDatasetAssociation.tags.any( tag_id=tag.id ) )
|
||||
if value:
|
||||
query = query.filter( model.HistoryDatasetAssociation.tags.any( value=value.lower() ) )
|
||||
else:
|
||||
# Tag doesn't exist; unclear what to do here, but the literal thing to do is add the criterion, which
|
||||
# will then yield a query that returns no results.
|
||||
query = query.filter( model.HistoryDatasetAssociation.tags.any( user_tname=name ) )
|
||||
return query
|
||||
def get_accepted_filters( self ):
|
||||
""" Returns a list of accepted filters for this column. """
|
||||
accepted_filter_labels_and_vals = { "All": "All" }
|
||||
accepted_filters = []
|
||||
for label, val in accepted_filter_labels_and_vals.items():
|
||||
args = { self.key: val }
|
||||
accepted_filters.append( grids.GridColumnFilter( label, args) )
|
||||
return accepted_filters
|
||||
|
||||
class StatusColumn( grids.GridColumn ):
|
||||
def get_value( self, trans, grid, hda ):
|
||||
if hda.deleted:
|
||||
@@ -118,19 +69,25 @@ class HistoryDatasetAssociationListGrid( grids.Grid ):
|
||||
template='/dataset/grid.mako'
|
||||
default_sort_key = "-create_time"
|
||||
columns = [
|
||||
grids.GridColumn( "Name", key="name",
|
||||
grids.TextColumn( "Name", key="name", model_class=model.HistoryDatasetAssociation,
|
||||
# Link name to dataset's history.
|
||||
link=( lambda item: iff( item.history.deleted, None, dict( operation="switch", id=item.id ) ) ) ),
|
||||
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 ) ) ) ),
|
||||
TagsColumn( "Tags", key="tags", filterable=True ),
|
||||
grids.TagsColumn( "Tags", "tags", model.HistoryDatasetAssociation, model.HistoryDatasetAssociationTagAssociation, filterable="advanced" ),
|
||||
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 ),
|
||||
]
|
||||
columns.append(
|
||||
grids.MulticolFilterColumn(
|
||||
"Search",
|
||||
cols_to_filter=[ columns[0], columns[2] ],
|
||||
key="free-text-search", visible=False, filterable="default" )
|
||||
)
|
||||
operations = []
|
||||
standard_filters = []
|
||||
default_filter = dict( deleted="False", tags="All" )
|
||||
default_filter = dict( name="All", deleted="False", tags="All" )
|
||||
preserve_state = False
|
||||
use_paging = True
|
||||
num_rows_per_page = 50
|
||||
|
||||
@@ -2,7 +2,6 @@ from galaxy.web.base.controller import *
|
||||
from galaxy.web.framework.helpers import time_ago, iff, grids
|
||||
from galaxy import util
|
||||
from galaxy.model.mapping import desc
|
||||
from galaxy.model import History
|
||||
from galaxy.model.orm import *
|
||||
from galaxy.util.json import *
|
||||
from galaxy.util.odict import odict
|
||||
@@ -19,30 +18,10 @@ SUCCESS, INFO, WARNING, ERROR = "done", "info", "warning", "error"
|
||||
|
||||
class HistoryListGrid( grids.Grid ):
|
||||
# Custom column types
|
||||
class NameColumn( grids.GridColumn ):
|
||||
def __init( self, key, link, attach_popup, filterable ):
|
||||
grids.GridColumn.__init__(self, key, link, attach_popup)
|
||||
|
||||
def get_value( self, trans, grid, history ):
|
||||
class NameColumn( grids.TextColumn ):
|
||||
def get_value(self, trans, grid, history):
|
||||
return history.get_display_name()
|
||||
|
||||
def filter( self, db_session, query, column_filter ):
|
||||
""" Modify query to filter histories by name. """
|
||||
if column_filter == "All":
|
||||
pass
|
||||
elif column_filter:
|
||||
query = query.filter( func.lower( History.name ).like( "%" + column_filter.lower() + "%" ) )
|
||||
return query
|
||||
def get_accepted_filters( self ):
|
||||
""" Returns a list of accepted filters for this column. """
|
||||
accepted_filter_labels_and_vals = odict()
|
||||
accepted_filter_labels_and_vals["FREETEXT"] = "FREETEXT"
|
||||
accepted_filters = []
|
||||
for label, val in accepted_filter_labels_and_vals.iteritems():
|
||||
args = { self.key: val }
|
||||
accepted_filters.append( grids.GridColumnFilter( label, args) )
|
||||
return accepted_filters
|
||||
|
||||
class DatasetsByStateColumn( grids.GridColumn ):
|
||||
def get_value( self, trans, grid, history ):
|
||||
rval = []
|
||||
@@ -53,6 +32,7 @@ class HistoryListGrid( grids.Grid ):
|
||||
else:
|
||||
rval.append( '' )
|
||||
return rval
|
||||
|
||||
class StatusColumn( grids.GridColumn ):
|
||||
def get_value( self, trans, grid, history ):
|
||||
if history.deleted:
|
||||
@@ -66,44 +46,6 @@ class HistoryListGrid( grids.Grid ):
|
||||
if item.users_shared_with or item.importable:
|
||||
return dict( operation="sharing" )
|
||||
return None
|
||||
|
||||
class TagsColumn( grids.GridColumn ):
|
||||
def __init__( self, col_name, key, filterable ):
|
||||
grids.GridColumn.__init__(self, col_name, key=key, filterable=filterable)
|
||||
# Tags cannot be sorted.
|
||||
self.sortable = False
|
||||
self.tag_elt_id_gen = 0
|
||||
def get_value( self, trans, grid, history ):
|
||||
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=history,
|
||||
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 histories by tag. """
|
||||
if column_filter == "All":
|
||||
pass
|
||||
elif column_filter:
|
||||
# Parse filter to extract multiple tags.
|
||||
tag_handler = TagHandler()
|
||||
raw_tags = tag_handler.parse_tags( column_filter.encode("utf-8") )
|
||||
for name, value in raw_tags.items():
|
||||
if name:
|
||||
# Search for tag names.
|
||||
query = query.filter( History.tags.any( func.lower( model.HistoryTagAssociation.user_tname ).like( "%" + name.lower() + "%" ) ) )
|
||||
if value:
|
||||
# Search for tag values.
|
||||
query = query.filter( History.tags.any( func.lower( model.HistoryTagAssociation.user_value ).like( "%" + value.lower() + "%" ) ) )
|
||||
return query
|
||||
def get_accepted_filters( self ):
|
||||
""" Returns a list of accepted filters for this column. """
|
||||
accepted_filter_labels_and_vals = odict()
|
||||
accepted_filter_labels_and_vals["FREETEXT"] = "FREETEXT"
|
||||
accepted_filters = []
|
||||
for label, val in accepted_filter_labels_and_vals.iteritems():
|
||||
args = { self.key: val }
|
||||
accepted_filters.append( grids.GridColumnFilter( label, args) )
|
||||
return accepted_filters
|
||||
|
||||
class DeletedColumn( grids.GridColumn ):
|
||||
def get_accepted_filters( self ):
|
||||
@@ -122,12 +64,12 @@ class HistoryListGrid( grids.Grid ):
|
||||
pass
|
||||
elif column_filter:
|
||||
if column_filter == "private":
|
||||
query = query.filter( History.users_shared_with == None )
|
||||
query = query.filter( History.importable == False )
|
||||
query = query.filter( model.History.users_shared_with == None )
|
||||
query = query.filter( model.History.importable == False )
|
||||
elif column_filter == "shared":
|
||||
query = query.filter( History.users_shared_with != None )
|
||||
query = query.filter( model.History.users_shared_with != None )
|
||||
elif column_filter == "importable":
|
||||
query = query.filter( History.importable == True )
|
||||
query = query.filter( model.History.importable == True )
|
||||
return query
|
||||
def get_accepted_filters( self ):
|
||||
""" Returns a list of accepted filters for this column. """
|
||||
@@ -141,43 +83,6 @@ class HistoryListGrid( grids.Grid ):
|
||||
args = { self.key: val }
|
||||
accepted_filters.append( grids.GridColumnFilter( label, args) )
|
||||
return accepted_filters
|
||||
|
||||
class FreeTextSearchColumn( grids.GridColumn ):
|
||||
def filter( self, db_session, query, column_filter ):
|
||||
""" Modify query to search tags and history names. """
|
||||
if column_filter == "All":
|
||||
pass
|
||||
elif column_filter:
|
||||
# Build tags filter.
|
||||
tag_handler = TagHandler()
|
||||
raw_tags = tag_handler.parse_tags( column_filter.encode("utf-8") )
|
||||
tags_filter = None
|
||||
for name, value in raw_tags.items():
|
||||
if name:
|
||||
# Search for tag names.
|
||||
tags_filter = History.tags.any( func.lower( model.HistoryTagAssociation.user_tname ).like( "%" + name.lower() + "%" ) )
|
||||
if value:
|
||||
# Search for tag values.
|
||||
tags_filter = and_( tags_filter, func.lower( History.tags.any( model.HistoryTagAssociation.user_value ).like( "%" + value.lower() + "%" ) ) )
|
||||
|
||||
# Build history name filter.
|
||||
history_name_filter = func.lower( History.name ).like( "%" + column_filter.lower() + "%" )
|
||||
|
||||
# Apply filters to query.
|
||||
if tags_filter:
|
||||
query = query.filter( or_( tags_filter, history_name_filter ) )
|
||||
else:
|
||||
query = query.filter( history_name_filter )
|
||||
return query
|
||||
def get_accepted_filters( self ):
|
||||
""" Returns a list of accepted filters for this column. """
|
||||
accepted_filter_labels_and_vals = odict()
|
||||
accepted_filter_labels_and_vals["FREETEXT"] = "FREETEXT"
|
||||
accepted_filters = []
|
||||
for label, val in accepted_filter_labels_and_vals.iteritems():
|
||||
args = { self.key: val }
|
||||
accepted_filters.append( grids.GridColumnFilter( label, args) )
|
||||
return accepted_filters
|
||||
|
||||
# Grid definition
|
||||
title = "Saved Histories"
|
||||
@@ -185,19 +90,25 @@ class HistoryListGrid( grids.Grid ):
|
||||
template='/history/grid.mako'
|
||||
default_sort_key = "-create_time"
|
||||
columns = [
|
||||
NameColumn( "Name", key="name",
|
||||
NameColumn( "Name", key="name", model_class=model.History,
|
||||
link=( lambda history: iff( history.deleted, None, dict( operation="switch", id=history.id ) ) ),
|
||||
attach_popup=True, filterable=True ),
|
||||
attach_popup=True, filterable="advanced" ),
|
||||
DatasetsByStateColumn( "Datasets (by state)", ncells=4 ),
|
||||
TagsColumn( "Tags", key="tags", filterable=True),
|
||||
grids.TagsColumn( "Tags", "tags", model.History, model.HistoryTagAssociation, filterable="advanced"),
|
||||
StatusColumn( "Status", attach_popup=False ),
|
||||
grids.GridColumn( "Created", key="create_time", format=time_ago ),
|
||||
grids.GridColumn( "Last Updated", key="update_time", format=time_ago ),
|
||||
# Columns that are valid for filtering but are not visible.
|
||||
DeletedColumn( "Deleted", key="deleted", visible=False, filterable=True ),
|
||||
SharingColumn( "Shared", key="shared", visible=False, filterable=True ),
|
||||
FreeTextSearchColumn( "Search", key="free-text-search", visible=False ) # Not filterable because it's the default search.
|
||||
DeletedColumn( "Deleted", key="deleted", visible=False, filterable="advanced" ),
|
||||
SharingColumn( "Shared", key="shared", visible=False, filterable="advanced" ),
|
||||
]
|
||||
columns.append(
|
||||
grids.MulticolFilterColumn(
|
||||
"Search",
|
||||
cols_to_filter=[ columns[0], columns[2] ],
|
||||
key="free-text-search", visible=False, filterable="default" )
|
||||
)
|
||||
|
||||
operations = [
|
||||
grids.GridOperation( "Switch", allow_multiple=False, condition=( lambda item: not item.deleted ) ),
|
||||
grids.GridOperation( "Share", condition=( lambda item: not item.deleted ) ),
|
||||
@@ -464,7 +375,7 @@ class HistoryController( BaseController ):
|
||||
return
|
||||
|
||||
ac_data = ""
|
||||
for history in trans.sa_session.query( History ).filter_by( user=user ).filter( func.lower( History.name ) .like(q.lower() + "%") ):
|
||||
for history in trans.sa_session.query( model.History ).filter_by( user=user ).filter( func.lower( model.History.name ) .like(q.lower() + "%") ):
|
||||
ac_data = ac_data + history.name + "\n"
|
||||
return ac_data
|
||||
|
||||
|
||||
@@ -105,7 +105,7 @@ class TagsController ( BaseController ):
|
||||
|
||||
# Build select statement.
|
||||
cols_to_select = [ item_tag_assoc_class.table.c.tag_id, func.count('*') ]
|
||||
from_obj = item_tag_assoc_class.table.join(item_class.table).join(Tag)
|
||||
from_obj = item_tag_assoc_class.table.join(item_class.table).join(Tag.table)
|
||||
where_clause = and_(self._get_column_for_filtering_item_by_user_id(item_class)==trans.get_user().id,
|
||||
Tag.table.c.name.like(q + "%"))
|
||||
order_by = [ func.count("*").desc() ]
|
||||
@@ -154,7 +154,7 @@ class TagsController ( BaseController ):
|
||||
|
||||
# Build select statement.
|
||||
cols_to_select = [ item_tag_assoc_class.table.c.value, func.count('*') ]
|
||||
from_obj = item_tag_assoc_class.table.join(item_class.table).join(Tag)
|
||||
from_obj = item_tag_assoc_class.table.join(item_class.table).join(Tag.table)
|
||||
where_clause = and_(self._get_column_for_filtering_item_by_user_id(item_class)==trans.get_user().id,
|
||||
Tag.table.c.id==tag.id,
|
||||
item_tag_assoc_class.table.c.value.like(tag_value + "%"))
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from galaxy.model import *
|
||||
from galaxy.model.orm import *
|
||||
|
||||
from galaxy.tags.tag_handler import TagHandler
|
||||
from galaxy.web import url_for
|
||||
from galaxy.util.json import from_json_string, to_json_string
|
||||
|
||||
@@ -87,18 +88,51 @@ class Grid( object ):
|
||||
column_filter = kwargs.get( "f-" + column.key )
|
||||
elif column.key in base_filter:
|
||||
column_filter = base_filter.get( column.key )
|
||||
|
||||
|
||||
# Method (1) combines a mix of strings and lists of strings into a single string and (2) attempts to de-jsonify all strings.
|
||||
def from_json_string_recurse(item):
|
||||
decoded_list = []
|
||||
if isinstance( item, basestring):
|
||||
try:
|
||||
# Not clear what we're decoding, so recurse to ensure that we catch everything.
|
||||
decoded_item = from_json_string( item )
|
||||
if isinstance( decoded_item, list):
|
||||
decoded_list = from_json_string_recurse( decoded_item )
|
||||
else:
|
||||
decoded_list = [ str( decoded_item ) ]
|
||||
except ValueError:
|
||||
decoded_list = [ str( item ) ]
|
||||
elif isinstance( item, list):
|
||||
return_val = []
|
||||
for element in item:
|
||||
a_list = from_json_string_recurse( element )
|
||||
decoded_list = decoded_list + a_list
|
||||
return decoded_list
|
||||
|
||||
# If column filter found, apply it.
|
||||
if column_filter is not None:
|
||||
# TextColumns may have a mix of json and strings.
|
||||
if isinstance( column, TextColumn ):
|
||||
column_filter = from_json_string_recurse( column_filter )
|
||||
if len( column_filter ) == 1:
|
||||
column_filter = column_filter[0]
|
||||
# Update query.
|
||||
query = column.filter( trans.sa_session, query, column_filter )
|
||||
# Upate current filter dict.
|
||||
cur_filter_dict[ column.key ] = column_filter
|
||||
# Carry filter along to newly generated urls; make sure filter is a string so
|
||||
# Carry filter along to newly generated urls; make sure filter is a string so
|
||||
# that we can encode to UTF-8 and thus handle user input to filters.
|
||||
if not isinstance( column_filter, basestring ):
|
||||
column_filter = unicode(column_filter)
|
||||
extra_url_args[ "f-" + column.key ] = column_filter.encode("utf-8")
|
||||
if isinstance( column_filter, list ):
|
||||
# Filter is a list; process each item.
|
||||
for filter in column_filter:
|
||||
if not isinstance( filter, basestring ):
|
||||
filter = unicode( filter ).encode("utf-8")
|
||||
extra_url_args[ "f-" + column.key ] = to_json_string( column_filter )
|
||||
else:
|
||||
# Process singleton filter.
|
||||
if not isinstance( column_filter, basestring ):
|
||||
column_filter = unicode(column_filter)
|
||||
extra_url_args[ "f-" + column.key ] = column_filter.encode("utf-8")
|
||||
|
||||
# Process sort arguments.
|
||||
sort_key = sort_order = None
|
||||
@@ -218,9 +252,12 @@ class Grid( object ):
|
||||
return query
|
||||
|
||||
class GridColumn( object ):
|
||||
def __init__( self, label, key=None, method=None, format=None, link=None, attach_popup=False, visible=True, ncells=1, filterable=False ):
|
||||
def __init__( self, label, key=None, model_class=None, method=None, format=None, link=None, attach_popup=False, visible=True, ncells=1,
|
||||
# Valid values for filterable are ['default', 'advanced', None]
|
||||
filterable=None ):
|
||||
self.label = label
|
||||
self.key = key
|
||||
self.model_class = model_class
|
||||
self.method = method
|
||||
self.format = format
|
||||
self.link = link
|
||||
@@ -265,6 +302,88 @@ class GridColumn( object ):
|
||||
args = { self.key: val }
|
||||
accepted_filters.append( GridColumnFilter( val, args) )
|
||||
return accepted_filters
|
||||
|
||||
# Generic column that employs freetext and, hence, supports freetext, case-independent filtering.
|
||||
class TextColumn( GridColumn ):
|
||||
def filter( self, db_session, query, column_filter ):
|
||||
""" Modify query to filter using free text, case independence. """
|
||||
if column_filter == "All":
|
||||
pass
|
||||
elif column_filter:
|
||||
query = query.filter( self.get_filter( column_filter ) )
|
||||
return query
|
||||
def get_filter( self, column_filter ):
|
||||
""" Returns a SQLAlchemy criterion derived from column_filter. """
|
||||
# This is a pretty ugly way to get the key attribute of model_class. TODO: Can this be fixed?
|
||||
model_class_key_field = eval( "self.model_class." + self.key )
|
||||
|
||||
if isinstance( column_filter, basestring ):
|
||||
return func.lower( model_class_key_field ).like( "%" + column_filter.lower() + "%" )
|
||||
elif isinstance( column_filter, list ):
|
||||
composite_filter = True
|
||||
for filter in column_filter:
|
||||
composite_filter = and_( composite_filter, func.lower( model_class_key_field ).like( "%" + filter.lower() + "%" ) )
|
||||
return composite_filter
|
||||
|
||||
# Generic column that supports tagging.
|
||||
class TagsColumn( TextColumn ):
|
||||
def __init__( self, col_name, key, model_class, model_tag_association_class, filterable ):
|
||||
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
|
||||
self.tag_elt_id_gen = 0
|
||||
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,
|
||||
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. """
|
||||
if column_filter == "All":
|
||||
pass
|
||||
elif column_filter:
|
||||
query = query.filter( self.get_filter( column_filter ) )
|
||||
return query
|
||||
def get_filter( self, column_filter ):
|
||||
# Parse filter to extract multiple tags.
|
||||
tag_handler = TagHandler()
|
||||
if isinstance( column_filter, list ):
|
||||
# Collapse list of tags into a single string; this is redundant but effective. TODO: fix this by iterating over tags.
|
||||
column_filter = ",".join( column_filter )
|
||||
raw_tags = tag_handler.parse_tags( column_filter.encode("utf-8") )
|
||||
filter = True
|
||||
for name, value in raw_tags.items():
|
||||
if name:
|
||||
# Search for tag names.
|
||||
filter = and_( filter, self.model_class.tags.any( func.lower( self.model_tag_association_class.user_tname ).like( "%" + name.lower() + "%" ) ) )
|
||||
if value:
|
||||
# Search for tag values.
|
||||
filter = and_( filter, self.model_class.tags.any( func.lower( self.model_tag_association_class.user_value ).like( "%" + value.lower() + "%" ) ) )
|
||||
return filter
|
||||
|
||||
# Column that performs multicolumn filtering.
|
||||
class MulticolFilterColumn( TextColumn ):
|
||||
def __init__( self, col_name, cols_to_filter, key, visible, filterable="default" ):
|
||||
GridColumn.__init__( self, col_name, key=key, visible=visible, filterable=filterable)
|
||||
self.cols_to_filter = cols_to_filter
|
||||
def filter( self, db_session, query, column_filter ):
|
||||
""" Modify query to filter model_class by tag. Multiple filters are ANDed. """
|
||||
if column_filter == "All":
|
||||
return query
|
||||
if isinstance( column_filter, list):
|
||||
composite_filter = True
|
||||
for filter in column_filter:
|
||||
part_composite_filter = False
|
||||
for column in self.cols_to_filter:
|
||||
part_composite_filter = or_( part_composite_filter, column.get_filter( filter ) )
|
||||
composite_filter = and_( composite_filter, part_composite_filter )
|
||||
else:
|
||||
composite_filter = False
|
||||
for column in self.cols_to_filter:
|
||||
composite_filter = or_( composite_filter, column.get_filter( column_filter ) )
|
||||
return query.filter( composite_filter )
|
||||
|
||||
class GridOperation( object ):
|
||||
def __init__( self, label, key=None, condition=None, allow_multiple=True, allow_popup=True, target=None, url_args=None ):
|
||||
|
||||
@@ -295,6 +295,66 @@ jQuery.extend(WYMeditor, {
|
||||
|
||||
});
|
||||
|
||||
/*
|
||||
Galaxy code that integrates into the WYM Editor.
|
||||
*/
|
||||
var Galaxy =
|
||||
{
|
||||
/*
|
||||
Galaxy constants for WYM Editor:
|
||||
TOOLS - A string replaced by the galaxy toolbar's HTML.
|
||||
TOOLS_ITEMS - A string replaced by the galaxy toolbar items.
|
||||
INSERT_HISTORY - Command: open the insert history dialog.
|
||||
INSERT_DATASET - Command: open the insert dataset dialog.
|
||||
DIALOG_HISTORY - A dialog to insert a history.
|
||||
DIALOG_DATASET - A dialog to insert a dataset.
|
||||
*/
|
||||
TOOLS : "{Galaxy_Tools}",
|
||||
TOOLS_ITEMS : "{Galaxy_Tools_Items}",
|
||||
INSERT_HISTORY : "InsertHistory",
|
||||
INSERT_DATASET : "InsertDataset",
|
||||
DIALOG_HISTORY : "DialogHistory",
|
||||
DIALOG_DATASET : "DialogDataset",
|
||||
|
||||
// Tool items overview.
|
||||
toolsItems: [
|
||||
{'name': "InsertHistory", 'title': 'History', 'css': 'galaxy_tools_insert_history_link'},
|
||||
{'name': "InsertDataset", 'title': 'Dataset', 'css': 'galaxy_dataset'}
|
||||
],
|
||||
|
||||
// Tools HTML.
|
||||
toolsHtml: "<div class='wym_tools wym_section'>"
|
||||
+ "<h2>" + this.TOOLS + "</h2>"
|
||||
+ "<ul>"
|
||||
+ this.TOOLS_ITEMS
|
||||
+ "</ul>"
|
||||
+ "</div>",
|
||||
|
||||
// Insert history dialog.
|
||||
dialogHistoryHtml: "<body class='wym_dialog wym_dialog_history'"
|
||||
+ " onload='WYMeditor.INIT_DIALOG(" + WYMeditor.INDEX + ")'"
|
||||
+ ">"
|
||||
+ "<form>"
|
||||
+ "<fieldset>"
|
||||
+ "<input type='hidden' class='wym_dialog_type' value='"
|
||||
+ this.DIALOG_HISTORY
|
||||
+ "' />"
|
||||
+ "<legend>{Link}</legend>"
|
||||
+ "<div class='row'>"
|
||||
+ "<label>{Title}</label>"
|
||||
+ "<input type='text' class='wym_title' value='' size='40' />"
|
||||
+ "</div>"
|
||||
+ "<div class='row row-indent'>"
|
||||
+ "<input class='wym_submit' type='button'"
|
||||
+ " value='{Add}' />"
|
||||
+ "<input class='wym_cancel' type='button'"
|
||||
+ "value='{Cancel}' />"
|
||||
+ "</div>"
|
||||
+ "</fieldset>"
|
||||
+ "</form>"
|
||||
+ "</body>",
|
||||
};
|
||||
|
||||
|
||||
/********** JQUERY **********/
|
||||
|
||||
@@ -354,6 +414,7 @@ jQuery.fn.wymeditor = function(options) {
|
||||
+ "<div class='wym_area_right'>"
|
||||
+ WYMeditor.CONTAINERS
|
||||
+ WYMeditor.CLASSES
|
||||
+ Galaxy.TOOLS
|
||||
+ "</div>"
|
||||
+ "<div class='wym_area_main'>"
|
||||
+ WYMeditor.HTML
|
||||
@@ -384,6 +445,8 @@ jQuery.fn.wymeditor = function(options) {
|
||||
+ "<h2>{Tools}</h2>"
|
||||
+ "<ul>"
|
||||
+ WYMeditor.TOOLS_ITEMS
|
||||
// Add Galaxy Tools.
|
||||
+ Galaxy.TOOLS_ITEMS
|
||||
+ "</ul>"
|
||||
+ "</div>",
|
||||
|
||||
@@ -758,6 +821,7 @@ WYMeditor.editor.prototype.init = function() {
|
||||
|
||||
boxHtml = h.replaceAll(boxHtml, WYMeditor.LOGO, this._options.logoHtml);
|
||||
boxHtml = h.replaceAll(boxHtml, WYMeditor.TOOLS, this._options.toolsHtml);
|
||||
boxHtml = h.replaceAll(boxHtml, Galaxy.TOOLS, Galaxy.toolsHtml);
|
||||
boxHtml = h.replaceAll(boxHtml, WYMeditor.CONTAINERS,this._options.containersHtml);
|
||||
boxHtml = h.replaceAll(boxHtml, WYMeditor.CLASSES, this._options.classesHtml);
|
||||
boxHtml = h.replaceAll(boxHtml, WYMeditor.HTML, this._options.htmlHtml);
|
||||
@@ -781,6 +845,24 @@ WYMeditor.editor.prototype.init = function() {
|
||||
}
|
||||
|
||||
boxHtml = h.replaceAll(boxHtml, WYMeditor.TOOLS_ITEMS, sTools);
|
||||
|
||||
// Construct Galaxy tools list.
|
||||
var galaxyTools = eval(Galaxy.toolsItems);
|
||||
sTools = "";
|
||||
for(var i = 0; i < galaxyTools.length; i++) {
|
||||
var galaxyTool = galaxyTools[i];
|
||||
if(galaxyTool.name && galaxyTool.title) {
|
||||
var sTool = this._options.toolsItemHtml;
|
||||
var sTool = h.replaceAll(sTool, WYMeditor.TOOL_NAME, galaxyTool.name);
|
||||
sTool = h.replaceAll(sTool, WYMeditor.TOOL_TITLE, this._options.stringDelimiterLeft
|
||||
+ galaxyTool.title
|
||||
+ this._options.stringDelimiterRight);
|
||||
sTool = h.replaceAll(sTool, WYMeditor.TOOL_CLASS, galaxyTool.css);
|
||||
sTools += sTool;
|
||||
}
|
||||
}
|
||||
|
||||
boxHtml = h.replaceAll(boxHtml, Galaxy.TOOLS_ITEMS, sTools);
|
||||
|
||||
//construct classes list
|
||||
var aClasses = eval(this._options.classesItems);
|
||||
@@ -947,6 +1029,10 @@ WYMeditor.editor.prototype.exec = function(cmd) {
|
||||
this.dialog(WYMeditor.PREVIEW, this._options.dialogFeaturesPreview);
|
||||
break;
|
||||
|
||||
case Galaxy.INSERT_HISTORY:
|
||||
this.dialog(Galaxy.DIALOG_HISTORY);
|
||||
break;
|
||||
|
||||
default:
|
||||
this._exec(cmd);
|
||||
break;
|
||||
@@ -1153,7 +1239,6 @@ WYMeditor.editor.prototype.update = function() {
|
||||
* @description Opens a dialog box
|
||||
*/
|
||||
WYMeditor.editor.prototype.dialog = function( dialogType, dialogFeatures, bodyHtml ) {
|
||||
|
||||
var features = dialogFeatures || this._wym._options.dialogFeatures;
|
||||
var wDialog = window.open('', 'dialog', features);
|
||||
|
||||
@@ -1178,11 +1263,13 @@ WYMeditor.editor.prototype.dialog = function( dialogType, dialogFeatures, bodyHt
|
||||
case(WYMeditor.PREVIEW):
|
||||
sBodyHtml = this._options.dialogPreviewHtml;
|
||||
break;
|
||||
case(Galaxy.DIALOG_HISTORY):
|
||||
sBodyHtml = Galaxy.dialogHistoryHtml;
|
||||
break;
|
||||
|
||||
default:
|
||||
sBodyHtml = bodyHtml;
|
||||
}
|
||||
|
||||
var h = WYMeditor.Helper;
|
||||
|
||||
//construct the dialog
|
||||
@@ -1197,7 +1284,6 @@ WYMeditor.editor.prototype.dialog = function( dialogType, dialogFeatures, bodyHt
|
||||
dialogHtml = h.replaceAll(dialogHtml, WYMeditor.INDEX, this._index);
|
||||
|
||||
dialogHtml = this.replaceStrings(dialogHtml);
|
||||
|
||||
var doc = wDialog.document;
|
||||
doc.write(dialogHtml);
|
||||
doc.close();
|
||||
|
||||
@@ -40,6 +40,10 @@ WYMeditor.STRINGS['en'] = {
|
||||
Containers: 'Containers',
|
||||
Classes: 'Classes',
|
||||
Status: 'Status',
|
||||
Source_Code: 'Source code'
|
||||
Source_Code: 'Source code',
|
||||
|
||||
// Galaxy replacements.
|
||||
History: 'History',
|
||||
Dataset: 'Dataset',
|
||||
};
|
||||
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 3.6 KiB After Width: | Height: | Size: 2.9 KiB |
@@ -109,6 +109,7 @@
|
||||
.wym_skin_galaxy .wym_buttons li.wym_tools_paste a { background-position: 0 -552px;}
|
||||
.wym_skin_galaxy .wym_buttons li.wym_tools_html a { background-position: 0 -193px;}
|
||||
.wym_skin_galaxy .wym_buttons li.wym_tools_preview a { background-position: 0 -408px;}
|
||||
.wym_skin_galaxy .wym_buttons li.galaxy_tools_insert_history_link a { background-position: 0 -622px;}
|
||||
|
||||
/*DECORATION*/
|
||||
.wym_skin_galaxy .wym_section h2 { background: #f0f0f0; border: solid gray; border-width: 0 0 1px;}
|
||||
|
||||
@@ -167,43 +167,12 @@
|
||||
</style>
|
||||
</%def>
|
||||
|
||||
<div class="grid-header">
|
||||
<h2>${grid.title}</h2>
|
||||
<%namespace file="../grid_common.mako" import="*" />
|
||||
|
||||
## Print grid filter.
|
||||
<form name="dataset_actions" action="javascript:add_tag_to_grid_filter($('#input-tag-filter').attr('value'))" method="get" >
|
||||
<strong>Filter: </strong>
|
||||
%for column in grid.columns:
|
||||
%if column.filterable:
|
||||
<span> by ${column.label.lower()}:</span>
|
||||
## For now, include special case to handle tags.
|
||||
%if column.key == "tags":
|
||||
%if cur_filter_dict[column.key] != "All":
|
||||
<span class="filter" "style='font-style: italic'">
|
||||
${cur_filter_dict[column.key]}
|
||||
</span>
|
||||
<span>|</span>
|
||||
%endif
|
||||
<input id="input-tag-filter" name="f-tags" type="text" value="" size="15"/>
|
||||
<span>|</span>
|
||||
%endif
|
||||
|
||||
## Handle other columns.
|
||||
%for i, filter in enumerate( column.get_accepted_filters() ):
|
||||
%if i > 0:
|
||||
<span>|</span>
|
||||
%endif
|
||||
%if cur_filter_dict[column.key] == filter.args[column.key]:
|
||||
<span class="filter" "style='font-style: italic'">${filter.label}</span>
|
||||
%else:
|
||||
<span class="filter"><a href="${url( filter.get_url_args() )}">${filter.label}</a></span>
|
||||
%endif
|
||||
%endfor
|
||||
<span> </span>
|
||||
%endif
|
||||
%endfor
|
||||
</form>
|
||||
</div>
|
||||
## Print grid header.
|
||||
${render_grid_filters()}
|
||||
|
||||
## Print grid.
|
||||
<form name="dataset_actions" action="${url()}" method="post" >
|
||||
<table class="grid">
|
||||
<thead>
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
<%! from galaxy.web.framework.helpers.grids import TextColumn, GridColumnFilter %>
|
||||
|
||||
## Render a filter UI for a grid column. Filter is rendered as a table row.
|
||||
<%def name="render_grid_column_filter(column)">
|
||||
<tr>
|
||||
<%
|
||||
column_label = column.label
|
||||
if column.filterable == "advanced":
|
||||
column_label = column_label.lower()
|
||||
%>
|
||||
<td align="left" style="padding-left: 10px">${column_label}:</td>
|
||||
<td>
|
||||
%if isinstance(column, TextColumn):
|
||||
<form name="history_actions" action="${url( dict() )}"
|
||||
method="get" >
|
||||
## Carry forward filtering criteria with hidden inputs.
|
||||
%for temp_column in grid.columns:
|
||||
%if temp_column.key in cur_filter_dict:
|
||||
<% value = cur_filter_dict[ temp_column.key ] %>
|
||||
%if value != "All":
|
||||
<%
|
||||
if isinstance( temp_column, TextColumn ):
|
||||
value = h.to_json_string( value )
|
||||
%>
|
||||
<input type="hidden" id="${temp_column.key}" name="f-${temp_column.key}" value='${value}'/>
|
||||
%endif
|
||||
%endif
|
||||
%endfor
|
||||
|
||||
## Print current filtering criteria and links to delete.
|
||||
%if column.key in cur_filter_dict:
|
||||
<% column_filter = cur_filter_dict[column.key] %>
|
||||
%if isinstance( column_filter, basestring ):
|
||||
%if column_filter != "All":
|
||||
<span style="font-style: italic">${cur_filter_dict[column.key]}</span>
|
||||
<% filter_all = GridColumnFilter( "", { column.key : "All" } ) %>
|
||||
<a href="${url( filter_all.get_url_args() )}"><img src="${h.url_for('/static/images/delete_tag_icon_gray.png')}"/></a>
|
||||
|
|
||||
%endif
|
||||
%elif isinstance( column_filter, list ):
|
||||
%for i, filter in enumerate( column_filter ):
|
||||
%if i > 0:
|
||||
,
|
||||
%endif
|
||||
<span style="font-style: italic">${filter}</span>
|
||||
<%
|
||||
new_filter = list( column_filter )
|
||||
del new_filter[ i ]
|
||||
new_column_filter = GridColumnFilter( "", { column.key : h.to_json_string( new_filter ) } )
|
||||
%>
|
||||
<a href="${url( new_column_filter.get_url_args() )}"><img src="${h.url_for('/static/images/delete_tag_icon_gray.png')}"/></a>
|
||||
%endfor
|
||||
|
||||
%endif
|
||||
%endif
|
||||
<span><input id="input-${column.key}-filter" name="f-${column.key}" type="text" value="" size="15"/></span>
|
||||
</form>
|
||||
%else:
|
||||
%for i, filter in enumerate( column.get_accepted_filters() ):
|
||||
%if i > 0:
|
||||
<span>|</span>
|
||||
%endif
|
||||
%if cur_filter_dict[column.key] == filter.args[column.key]:
|
||||
<span class="filter" style="font-style: italic">${filter.label}</span>
|
||||
%else:
|
||||
<span class="filter"><a href="${url( filter.get_url_args() )}">${filter.label}</a></span>
|
||||
%endif
|
||||
%endfor
|
||||
%endif
|
||||
</td>
|
||||
</tr>
|
||||
</%def>
|
||||
|
||||
## Print grid search/filtering UI.
|
||||
<%def name="render_grid_filters()">
|
||||
<div class="grid-header">
|
||||
<h2>${grid.title}</h2>
|
||||
|
||||
## Default search.
|
||||
<div>
|
||||
<table><tr>
|
||||
<td>
|
||||
<table>
|
||||
%for column in grid.columns:
|
||||
%if column.filterable == "default":
|
||||
${render_grid_column_filter(column)}
|
||||
%endif
|
||||
%endfor
|
||||
</table>
|
||||
</td>
|
||||
<td>
|
||||
##|
|
||||
##<% filter_all = GridColumnFilter( "", { column.key : "All" } ) %>
|
||||
##<a href="${url( filter_all.get_url_args() )}">Clear All</a>
|
||||
| <a href="" onclick="javascript:$('#more-search-options').slideToggle('fast');return false;">Advanced Search</a>
|
||||
</td>
|
||||
</tr></table>
|
||||
</div>
|
||||
|
||||
|
||||
## Advanced search.
|
||||
<div id="more-search-options" style="display: none; padding-top: 5px">
|
||||
<table style="border: 1px solid gray;">
|
||||
<tr><td style="text-align: left" colspan="100">
|
||||
Advanced Search |
|
||||
<a href=""# onclick="javascript:$('#more-search-options').slideToggle('fast');return false;">Close</a> |
|
||||
## Link to clear all filters.
|
||||
<%
|
||||
no_filter = GridColumnFilter("Clear All", default_filter_dict)
|
||||
%>
|
||||
<a href="${url( no_filter.get_url_args() )}">${no_filter.label}</a>
|
||||
</td></tr>
|
||||
%for column in grid.columns:
|
||||
%if column.filterable == "advanced":
|
||||
## Show div if current filter has value that is different from the default filter.
|
||||
%if column.key in cur_filter_dict and column.key in default_filter_dict and \
|
||||
cur_filter_dict[column.key] != default_filter_dict[column.key]:
|
||||
<script type="text/javascript">
|
||||
$('#more-search-options').css("display", "block");
|
||||
</script>
|
||||
%endif
|
||||
|
||||
${render_grid_column_filter(column)}
|
||||
%endif
|
||||
%endfor
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</%def>
|
||||
@@ -1,4 +1,4 @@
|
||||
<%! from galaxy.web.framework.helpers.grids import GridColumnFilter %>
|
||||
<%! from galaxy.web.framework.helpers.grids import TextColumn, GridColumnFilter %>
|
||||
|
||||
<%inherit file="/base.mako"/>
|
||||
<%def name="title()">${grid.title}</%def>
|
||||
@@ -153,98 +153,12 @@
|
||||
</style>
|
||||
</%def>
|
||||
|
||||
<div class="grid-header">
|
||||
<h2>${grid.title}</h2>
|
||||
|
||||
## Search box and more options filter at top of grid.
|
||||
<div>
|
||||
## Grid search. TODO: use more elegant way to get free text search column.
|
||||
<% column = grid.columns[-1] %>
|
||||
<% use_form = False %>
|
||||
%for i, filter in enumerate( column.get_accepted_filters() ):
|
||||
%if i > 0:
|
||||
<span>|</span>
|
||||
%endif
|
||||
%if column.key in cur_filter_dict and cur_filter_dict[column.key] == filter.args[column.key]:
|
||||
<span class="filter" "style='font-style: italic'">${filter.label}</span>
|
||||
%elif filter.label == "FREETEXT":
|
||||
<form name="history_actions"
|
||||
action="javascript:add_condition_to_grid_filter($('#input-${column.key}-filter').attr('name'),$('#input-${column.key}-filter').attr('value'),false)"
|
||||
method="get" >
|
||||
${column.label}:
|
||||
%if column.key in cur_filter_dict and cur_filter_dict[column.key] != "All":
|
||||
<span style="font-style: italic">${cur_filter_dict[column.key]}</span>
|
||||
<% filter_all = GridColumnFilter( "", { column.key : "All" } ) %>
|
||||
<a href="${url( filter_all.get_url_args() )}"><img src="${h.url_for('/static/images/delete_tag_icon_gray.png')}"/></a>
|
||||
|
|
||||
%endif
|
||||
<span><input id="input-${column.key}-filter" name="${column.key}" type="text" value="" size="15"/></span>
|
||||
<% use_form = True %>
|
||||
%else:
|
||||
<span class="filter"><a href="${url( filter.get_url_args() )}">${filter.label}</a></span>
|
||||
%endif
|
||||
%endfor
|
||||
| <a href="" onclick="javascript:$('#more-search-options').slideToggle('fast');return false;">Advanced Search</a>
|
||||
%if use_form:
|
||||
</form>
|
||||
%endif
|
||||
</div>
|
||||
|
||||
## Advanced Search
|
||||
<div id="more-search-options" style="display: none; padding-top: 5px">
|
||||
<table style="border: 1px solid gray;">
|
||||
<tr><td style="text-align: left" colspan="100">
|
||||
Advanced Search |
|
||||
<a href=""# onclick="javascript:$('#more-search-options').slideToggle('fast');return false;">Close</a> |
|
||||
## Link to clear all filters.
|
||||
<%
|
||||
no_filter = GridColumnFilter("Clear All", default_filter_dict)
|
||||
%>
|
||||
<a href="${url( no_filter.get_url_args() )}">${no_filter.label}</a>
|
||||
</td></tr>
|
||||
%for column in grid.columns:
|
||||
%if column.filterable:
|
||||
<tr>
|
||||
## Show div if current filter has value that is different from the default filter.
|
||||
%if cur_filter_dict[column.key] != default_filter_dict[column.key]:
|
||||
<script type="text/javascript">
|
||||
$('#more-search-options').css("display", "block");
|
||||
</script>
|
||||
%endif
|
||||
<td style="padding-left: 10px">${column.label.lower()}:</td>
|
||||
<td>
|
||||
<% use_form = False %>
|
||||
%for i, filter in enumerate( column.get_accepted_filters() ):
|
||||
%if i > 0:
|
||||
<span>|</span>
|
||||
%endif
|
||||
%if cur_filter_dict[column.key] == filter.args[column.key]:
|
||||
<span class="filter" style="font-style: italic">${filter.label}</span>
|
||||
%elif filter.label == "FREETEXT":
|
||||
<form name="history_actions" action="javascript:add_condition_to_grid_filter($('#input-${column.key}-filter').attr('name'),$('#input-${column.key}-filter').attr('value'),true)"
|
||||
method="get" >
|
||||
%if column.key in cur_filter_dict and cur_filter_dict[column.key] != "All":
|
||||
<span style="font-style: italic">${cur_filter_dict[column.key]}</span>
|
||||
<% filter_all = GridColumnFilter( "", { column.key : "All" } ) %>
|
||||
<a href="${url( filter_all.get_url_args() )}"><img src="${h.url_for('/static/images/delete_tag_icon_gray.png')}"/></a>
|
||||
|
|
||||
%endif
|
||||
<span><input id="input-${column.key}-filter" name="${column.key}" type="text" value="" size="15"/></span>
|
||||
<% use_form = True %>
|
||||
%else:
|
||||
<span class="filter"><a href="${url( filter.get_url_args() )}">${filter.label}</a></span>
|
||||
%endif
|
||||
%endfor
|
||||
%if use_form:
|
||||
</form>
|
||||
%endif
|
||||
</td>
|
||||
</tr>
|
||||
%endif
|
||||
%endfor
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<%namespace file="../grid_common.mako" import="*" />
|
||||
|
||||
## Print grid header.
|
||||
${render_grid_filters()}
|
||||
|
||||
## Print grid.
|
||||
<form name="history_actions" action="${url()}" method="post" >
|
||||
<input type="hidden" name="page" value="${cur_page_num}">
|
||||
<table class="grid">
|
||||
|
||||
Reference in New Issue
Block a user