diff --git a/client/src/components/Grid/GridList.vue b/client/src/components/Grid/GridList.vue index d5eb85074aa1..11ba2e2387be 100644 --- a/client/src/components/Grid/GridList.vue +++ b/client/src/components/Grid/GridList.vue @@ -326,7 +326,7 @@ watch(operationMessage, () => { :title="rowData[fieldEntry.key]" @execute="onOperation($event, rowData)" /> - + { - url_data[`f-${k[0]}`] = k[1]; - }); - return url_data; - }, - - // Return URL for obtaining a new grid - get_url: function (args) { - return `${this.get("url_base")}?${$.param(this.get_url_data())}&${$.param(args)}`; - }, -}); diff --git a/client/src/mvc/grid/grid-template.js b/client/src/mvc/grid/grid-template.js deleted file mode 100644 index cedaa1a07e92..000000000000 --- a/client/src/mvc/grid/grid-template.js +++ /dev/null @@ -1,556 +0,0 @@ -import { sanitize } from "dompurify"; -import $ from "jquery"; -import _ from "underscore"; - -// grid view templates -export default { - // template - grid: function (options) { - let tmpl; - if (options.embedded) { - tmpl = this.grid_header(options) + this.grid_table(options); - } else { - tmpl = ` -
- - - - - - - - - - - -
${this.grid_header(options)}
- ${this.grid_table(options)} - `; - } - - // add info text - if (options.info_text) { - tmpl += `
${options.info_text}
`; - } - - // return - return tmpl; - }, - - // template - grid_table: function () { - return ` -
- - - - -
-
`; - }, - - // template - grid_header: function (options) { - var tmpl = '
'; - if (!options.embedded) { - let id_str = ""; - if (options.title_id) { - id_str += ` id="${options.title_id}"`; - } - tmpl += `${options.title}`; - } - if (options.global_actions) { - tmpl += '
    '; - var show_popup = options.global_actions.length >= 3; - if (show_popup) { - tmpl += - '
  • Actions
  • ' + - '
    '; - } - for (const action of options.global_actions) { - tmpl += `
  • ${action.label}
  • `; - } - if (show_popup) { - tmpl += "
    "; - } - tmpl += "
"; - } - if (options.insert) { - tmpl += options.insert; - } - - // add grid filters - tmpl += this.grid_filters(options); - tmpl += "
"; - - // return template - return tmpl; - }, - - // template - header: function (options) { - // start - var tmpl = ""; - - // add checkbox - if (options.show_item_checkboxes) { - tmpl += ""; - if (options.items.length > 0) { - tmpl += - '' + - ''; - } - tmpl += ""; - } - - // create header elements - for (const column of options.columns) { - if (column.visible) { - tmpl += ``; - if (column.sortable) { - tmpl += `${column.label}`; - } else { - tmpl += column.label; - } - tmpl += `${column.extra}`; - } - } - - // finalize - tmpl += ""; - - // return template - return tmpl; - }, - - // template - body: function (options) { - // initialize - var tmpl = ""; - var items_length = options.items.length; - - // empty grid? - if (items_length === 0) { - // No results. - const filters = options.filters; - const searchTerm = filters["free-text-search"] || ""; - const tags = filters.tags && filters.tags !== "All" ? `tags:${filters.tags} ` : ""; - const name = filters.name && filters.name !== "All" ? `name:${filters.name}` : ""; - const searchMsg = searchTerm || `${tags}${name}`; - const noItemsMsg = searchMsg ? `No matching entries found for ${searchMsg}` : "No items"; - - tmpl += `${noItemsMsg}`; - } - - // create rows - for (const item of options.items) { - // Tag current - tmpl += "`; - } - - // Data columns - for (const column of options.columns) { - if (column.visible) { - // Nowrap - var nowrap = ""; - if (column.nowrap) { - nowrap = 'style="white-space:nowrap;"'; - } - - // get column settings - var column_settings = item.column_config[column.label]; - - // load attributes - var link = column_settings.link; - var value = column_settings.value; - var target = column_settings.target; - - // unescape value - if ($.type(value) === "string") { - value = value.replace(/\/\//g, "/"); - } - - // Attach popup menu? - var popup_id = ""; - if (column.attach_popup) { - popup_id = `grid-${item.encode_id}-popup`; - } - - // Check for row wrapping - tmpl += ``; - - // Determine cell content - if (column.delayed) { - tmpl += `
`; - } else if (column.attach_popup && link) { - tmpl += `
- -
`; - } else if (column.attach_popup) { - tmpl += ``; - } else if (link) { - tmpl += `${value}`; - } else { - tmpl += ``; - } - tmpl += ""; - } - } - tmpl += ""; - } - return tmpl; - }, - - // template - footer: function (options) { - // create template string - var tmpl = ""; - - // paging - if (options.use_paging && options.num_pages > 1) { - // get configuration - var num_page_links = options.num_page_links; - var cur_page_num = options.cur_page_num; - var num_pages = options.num_pages; - - // First pass on min page. - var page_link_range = num_page_links / 2; - var min_page = cur_page_num - page_link_range; - var min_offset = 0; - if (min_page <= 0) { - // Min page is too low. - min_page = 1; - min_offset = page_link_range - (cur_page_num - min_page); - } - - // Set max page. - var max_range = page_link_range + min_offset; - var max_page = cur_page_num + max_range; - var max_offset; - if (max_page <= num_pages) { - // Max page is fine. - max_offset = 0; - } else { - // Max page is too high. - max_page = num_pages; - // +1 to account for the +1 in the loop below. - max_offset = max_range - (max_page + 1 - cur_page_num); - } - - // Second and final pass on min page to add any unused - // offset from max to min. - if (max_offset !== 0) { - min_page -= max_offset; - if (min_page < 1) { - min_page = 1; - } - } - - // template header - tmpl += ''; - if (options.show_item_checkboxes) { - tmpl += ""; - } - tmpl += '' + '' + "Page:"; - - if (min_page > 1) { - tmpl += - '1 ...'; - } - - // create page urls - for (var page_index = min_page; page_index < max_page + 1; page_index++) { - if (page_index == options.cur_page_num) { - tmpl += `${page_index}`; - } else { - tmpl += `${page_index}`; - } - } - - // show last page - if (max_page < num_pages) { - tmpl += `...${num_pages}`; - } - tmpl += ""; - - // Show all link - tmpl += ` - | Show All - - `; - } - - // Grid operations for multiple items. - if (options.show_item_checkboxes) { - // start template - tmpl += ` - - - - - For selected items: - `; - - // configure buttons for operations - for (const operation of options.operations) { - if (operation.allow_multiple) { - tmpl += ` `; - } - } - - // finalize template - tmpl += "" + ""; - } - - // count global operations - var found_global = false; - for (const operation of options.operations) { - if (operation.global_operation) { - found_global = true; - break; - } - } - - // add global operations - if (found_global) { - tmpl += "" + ''; - for (const operation of options.operations) { - if (operation.global_operation) { - tmpl += `${operation.label}`; - } - } - tmpl += "" + ""; - } - - // add legend - if (options.legend) { - tmpl += `${options.legend}`; - } - - // return - return tmpl; - }, - - // template - message: function (options) { - var status = options.status; - if (["success", "ok"].indexOf(status) != -1) { - status = "done"; - } - return `

${_.escape( - options.message - )}

`; - }, - - // template - grid_filters: function (options) { - // get filters - var default_filter_dict = options.default_filter_dict; - var filters = options.filters; - - // show advanced search if flag set or if there are filters for advanced search fields - var advanced_search_display = "none"; - if (options.advanced_search) { - advanced_search_display = "block"; - } - - // identify columns with advanced filtering - var show_advanced_search_link = false; - for (const column of options.columns) { - if (column.filterable == "advanced") { - var column_key = column.key; - var f_key = filters[column_key]; - var d_key = default_filter_dict[column_key]; - if (f_key && d_key && f_key != d_key) { - advanced_search_display = "block"; - } - show_advanced_search_link = true; - } - } - - // hide standard search if advanced is shown - var standard_search_display = "block"; - if (advanced_search_display == "block") { - standard_search_display = "none"; - } - - // - // standard search - // - var tmpl = `"; - - // - // advanced search - // - tmpl += `"; - - // return template - return tmpl; - }, - - // template - grid_column_filter: function (options, column) { - // collect parameters - var filters = options.filters; - var column_label = column.label; - var column_key = column.key; - if (column.filterable == "advanced") { - column_label = column_label.toLowerCase(); - } - - // start - var tmpl = ""; - - if (column.filterable == "advanced") { - tmpl += `${column_label}:`; - } - tmpl += ''; - if (column.is_text) { - tmpl += `
`; - // Carry forward filtering criteria with hidden inputs. - for (const column of options.columns) { - var filter_value = filters[column.key]; - if (filter_value && filter_value != "All") { - if (column.is_text) { - filter_value = JSON.stringify(filter_value); - } - tmpl += ``; - } - } - // Print current filtering criteria and links to delete. - tmpl += ``; - - // add filters - var column_filter = filters[column_key]; - if (column_filter) { - // identify type - var type = $.type(column_filter); - - // single filter value - if (type == "string") { - if (column_filter != "All") { - // append template - tmpl += this.filter_element(column_key, column_filter); - } - } - - // multiple filter values - if (type == "array") { - for (const i in column_filter) { - // copy filters and remove entry - var params = column_filter; - params = params.slice(i); - - // append template - tmpl += this.filter_element(column_key, column_filter[i]); - } - } - } - - // close span - tmpl += ""; - - // Set value, size of search input field. Minimum size is 20 characters. - var value = ""; - var size = 20; - if (column.filterable == "standard") { - value = column.label.toLowerCase(); - if (value.length < 20) { - size = value.length; - } - // +4 to account for space after placeholder - size = size + 4; - } - - // print input field for column - tmpl += ` - - - - -
`; - } else { - // filter criteria - tmpl += ``; - - // add category filters - var seperator = false; - for (var cf_label in options.categorical_filters[column_key]) { - // get category filter - var cf = options.categorical_filters[column_key][cf_label]; - - // each filter will have only a single argument, so get that single argument - var cf_key = ""; - var cf_arg = ""; - for (var key in cf) { - cf_key = key; - cf_arg = cf[key]; - } - - // add seperator - if (seperator) { - tmpl += " | "; - } - seperator = true; - - // add category - var filter = filters[column_key]; - if (filter && cf[column_key] && filter == cf_arg) { - tmpl += `${cf_label}`; - } else { - tmpl += `${cf_label}`; - } - } - tmpl += ""; - } - tmpl += "" + ""; - - // return template - return tmpl; - }, - - // template for filter items - filter_element: function (filter_key, filter_value) { - filter_value = sanitize(filter_value); - return `${filter_value}`; - }, -}; diff --git a/client/src/mvc/grid/grid-view.js b/client/src/mvc/grid/grid-view.js deleted file mode 100644 index 26ce8bd101d4..000000000000 --- a/client/src/mvc/grid/grid-view.js +++ /dev/null @@ -1,681 +0,0 @@ -import { getGalaxyInstance } from "app"; -import Backbone from "backbone"; -import $ from "jquery"; -import GridModel from "mvc/grid/grid-model"; -import Templates from "mvc/grid/grid-template"; -import PopupMenu from "mvc/ui/popup-menu"; -import { init_refresh_on_change } from "onload/globalInits/init_refresh_on_change"; -import slugify from "slugify"; -import LoadingIndicator from "ui/loading-indicator"; -import _ from "underscore"; -import Utils from "utils/utils"; - -// This is necessary so that, when nested arrays are used in ajax/post/get methods, square brackets ('[]') are -// not appended to the identifier of a nested array. -$.ajaxSettings.traditional = true; - -// grid view -export default Backbone.View.extend({ - // model - grid: null, - - // Initialize - initialize: function (grid_config) { - this.grid = new GridModel(); - this.title = grid_config.title; - this.active_tab = grid_config.active_tab; - var self = this; - - if (grid_config.url_base && !grid_config.items) { - LoadingIndicator.markViewAsLoading(this); - var url_data = grid_config.url_data || {}; - _.each(grid_config.filters, (v, k) => { - url_data[`f-${k}`] = v; - }); - $.ajax({ - url: `${grid_config.url_base}?${$.param(url_data)}`, - success: function (response) { - response.embedded = grid_config.embedded; - response.filters = grid_config.filters || {}; - self.init_grid(response); - }, - }); - } else { - // set element - this.setElement("
"); - this.init_grid(grid_config); - } - - // fix padding - if (grid_config.use_panels) { - $("#center").css({ - padding: "10px", - overflow: "auto", - }); - } - }, - - openAdvancedSearch: function () { - var isOpen = $("#advanced-search").is(":visible"); - if (!isOpen) { - $("#standard-search").slideToggle("fast"); - $("#advanced-search").slideToggle("fast"); - } - }, - - // refresh frames - handle_refresh: function (refresh_frames) { - if (refresh_frames) { - if ($.inArray("history", refresh_frames) > -1) { - const Galaxy = getGalaxyInstance(); - if (Galaxy && Galaxy.currHistoryPanel) { - Galaxy.currHistoryPanel.loadCurrentHistory(); - } - } - } - }, - - // Initialize - init_grid: function (grid_config) { - this.grid.set(grid_config); - - // get options - var options = this.grid.attributes; - - if (this.allow_title_display && options.title) { - Utils.setWindowTitle(options.title); - } - // handle refresh requests - this.handle_refresh(options.refresh_frames); - - // strip protocol and domain - var url = this.grid.get("url_base"); - url = url.replace(/^.*\/\/[^/]+/, ""); - this.grid.set("url_base", url); - - // append main template - this.$el.html(Templates.grid(options)); - - // add a class identifier for styling purposes - this.$el.addClass(this.getRootClassName(grid_config)); - - // update div contents - this.$el.find("#grid-table-header").html(Templates.header(options)); - this.$el.find("#grid-table-body").html(Templates.body(options)); - this.$el.find("#grid-table-footer").html(Templates.footer(options)); - - // update message - if (options.message) { - this.$el.find("#grid-message").html(Templates.message(options)); - var self = this; - if (options.use_hide_message) { - window.setTimeout(() => { - self.$el.find("#grid-message").html(""); - }, 5000); - } - } - - // configure elements - this.init_grid_elements(); - this.init_grid_controls(); - - // attach global event handler - // TODO: redundant (the onload/standard page handlers do this) - but needed because these are constructed after page ready - init_refresh_on_change(); - }, - - // Initialize grid controls - init_grid_controls: function () { - // link - var self = this; - - // Initialize grid operation button. - this.$el.find(".operation-button").each(function () { - $(this).off(); - $(this).click(function () { - self.submit_operation(this); - return false; - }); - }); - - // Initialize text filters to select text on click and use normal font when user is typing. - this.$el.find("input[type=text]").each(function () { - $(this).off(); - $(this) - .click(function () { - $(this).select(); - }) - .keyup(function () { - $(this).css("font-style", "normal"); - }); - }); - - // Initialize sort links. - this.$el.find(".sort-link").each(function () { - $(this).off(); - $(this).click(function () { - self.set_sort_condition($(this).attr("sort_key")); - return false; - }); - }); - - // Initialize text filters. - this.$el.find(".text-filter-form").each(function () { - $(this).off(); - $(this).submit(function () { - var column_key = $(this).attr("column_key"); - var text_input_obj = $(`#input-${column_key}-filter`); - var text_input = text_input_obj.val(); - text_input_obj.val(""); - self.add_filter_condition(column_key, text_input); - return false; - }); - }); - - // Initialize categorical filters. - this.$el.find(".text-filter-val > a").each(function () { - $(this).off(); - $(this).click(function () { - // Remove visible element. - $(this).parent().remove(); - - // Remove filter condition. - self.remove_filter_condition($(this).attr("filter_key"), $(this).attr("filter_val")); - - // Return - return false; - }); - }); - - // Initialize categorical filters. - this.$el.find(".categorical-filter > a").each(function () { - $(this).off(); - $(this).click(function () { - self.set_categorical_filter($(this).attr("filter_key"), $(this).attr("filter_val")); - return false; - }); - }); - - // Initialize standard, advanced search toggles. - this.$el.find(".advanced-search-toggle").each(function () { - $(this).off(); - $(this).click(() => { - self.$el.find("#standard-search").slideToggle("fast"); - self.$el.find("#advanced-search").slideToggle("fast"); - return false; - }); - }); - - // Add event to check all box - this.$el.find("#check_all").off(); - this.$el.find("#check_all").on("click", () => { - self.check_all_items(); - }); - }, - - // Initialize grid elements. - init_grid_elements: function () { - // Initialize grid selection checkboxes. - this.$el.find(".grid").each(function () { - var checkboxes = $(this).find("input.grid-row-select-checkbox"); - var check_count = $(this).find("span.grid-selected-count"); - var update_checked = () => { - check_count.text($(checkboxes).filter(":checked").length); - }; - - $(checkboxes).each(function () { - $(this).change(update_checked); - }); - update_checked(); - }); - - // Initialize ratings. - if (this.$el.find(".community_rating_star").length !== 0) { - this.$el.find(".community_rating_star").rating({}); - } - - // get options - var options = this.grid.attributes; - var self = this; - - // - // add page click events - // - this.$el.find(".page-link-grid > a").each(function () { - $(this).click(function () { - self.set_page($(this).attr("page_num")); - return false; - }); - }); - - // - // add inbound/outbound events - // - this.$el.find(".use-target").each(function () { - $(this).click(function () { - self.execute({ - href: $(this).attr("href"), - target: $(this).attr("target"), - }); - return false; - }); - }); - - // empty grid? - var items_length = options.items.length; - if (items_length === 0) { - return; - } - - // add operation popup menus - _.each(options.items, (item, index) => { - var button = self.$(`#grid-${item.encode_id}-popup`).off(); - var popup = new PopupMenu(button); - _.each(options.operations, (operation) => { - self.add_operation(popup, operation, item); - }); - }); - }, - - /** Add an operation to the items menu */ - add_operation: function (popup, operation, item) { - var self = this; - var settings = item.operation_config[operation.label]; - if (settings.allowed && operation.allow_popup) { - popup.addItem({ - html: operation.label, - href: settings.url_args, - target: settings.target, - confirmation_text: operation.confirm, - func: function (e) { - e.preventDefault(); - var label = $(e.target).html(); - if (operation.onclick) { - operation.onclick(item.encode_id); - } else { - self.execute(this.findItemByHtml(label)); - } - }, - }); - } - }, - - // Add a condition to the grid filter; this adds the condition and refreshes the grid. - add_filter_condition: function (name, value) { - // Do nothing is value is empty. - if (value === "") { - return false; - } - - // Add condition to grid. - this.grid.add_filter(name, value, true); - - this.render_filter_button(name, value); - - // execute - this.go_page_one(); - this.execute(); - }, - - render_filter_button: function (name, value) { - // Add button that displays filter and provides a button to delete it. - var t = $(Templates.filter_element(name, value)); - var self = this; - t.click(function () { - // Remove visible element. - $(this).remove(); - - // Remove filter condition. - self.remove_filter_condition(name, value); - }); - - // append to container - var container = this.$el.find(`#${name}-filtering-criteria`); - container.append(t); - }, - - // Remove a condition to the grid filter; this adds the condition and refreshes the grid. - remove_filter_condition: function (name, value) { - // Remove filter condition. - this.grid.remove_filter(name, value); - - // Execute - this.go_page_one(); - this.execute(); - }, - - // Set sort condition for grid. - set_sort_condition: function (col_key) { - // Set new sort condition. New sort is col_key if sorting new column; if reversing sort on - // currently sorted column, sort is reversed. - var cur_sort = this.grid.get("sort_key"); - var new_sort = col_key; - if (cur_sort.indexOf(col_key) !== -1) { - // Reverse sort. - if (cur_sort.substring(0, 1) !== "-") { - new_sort = `-${col_key}`; - } - } - - // Remove sort arrows elements. - this.$el.find(".sort-arrow").remove(); - - // Add sort arrow element to new sort column. - var sort_arrow = new_sort.substring(0, 1) == "-" ? "↑" : "↓"; - var t = $(`${sort_arrow}`).addClass("sort-arrow"); - - // Add to header - this.$el.find(`#${col_key}-header`).append(t); - - // Update grid. - this.grid.set("sort_key", new_sort); - this.go_page_one(); - this.execute(); - }, - - // Set new value for categorical filter. - set_categorical_filter: function (name, new_value) { - // Update filter hyperlinks to reflect new filter value. - var category_filter = this.grid.get("categorical_filters")[name]; - - var cur_value = this.grid.get("filters")[name]; - var self = this; - this.$el.find(`.${name}-filter`).each(function () { - var text = $.trim($(this).text()); - var filter = category_filter[text]; - var filter_value = filter[name]; - if (filter_value == new_value) { - // Remove filter link since grid will be using this filter. It is assumed that - // this element has a single child, a hyperlink/anchor with text. - $(this).empty(); - $(this).addClass("current-filter"); - $(this).append(text); - } else if (filter_value == cur_value) { - // Add hyperlink for this filter since grid will no longer be using this filter. It is assumed that - // this element has a single child, a hyperlink/anchor. - $(this).empty(); - var t = $(`${text}`); - t.click(() => { - self.set_categorical_filter(name, filter_value); - }); - $(this).removeClass("current-filter"); - $(this).append(t); - } - }); - - // Update grid. - this.grid.add_filter(name, new_value); - this.go_page_one(); - this.execute(); - }, - - // Set page to view. - set_page: function (new_page) { - // Update page hyperlink to reflect new page. - var self = this; - this.$el.find(".page-link").each(function () { - var id = $(this).attr("id"); - - var // Id has form 'page-link- - page_num = parseInt(id.split("-")[2], 10); - - var cur_page = self.grid.get("cur_page"); - var text; - if (page_num === new_page) { - // Remove link to page since grid will be on this page. It is assumed that - // this element has a single child, a hyperlink/anchor with text. - text = $(this).children().text(); - $(this).empty(); - $(this).addClass("inactive-link"); - $(this).text(text); - } else if (page_num === cur_page) { - // Add hyperlink to this page since grid will no longer be on this page. It is assumed that - // this element has a single child, a hyperlink/anchor. - text = $(this).text(); - $(this).empty(); - $(this).removeClass("inactive-link"); - var t = $(`${text}`); - t.click(() => { - self.set_page(page_num); - }); - $(this).append(t); - } - }); - - if (new_page === "all") { - this.grid.set("cur_page", new_page); - } else { - this.grid.set("cur_page", parseInt(new_page, 10)); - } - this.execute(); - }, - - // confirmation/submission of operation request - submit_operation: function (operation_button, confirmation_text) { - // identify operation - var operation_name = $(operation_button).val(); - - // verify any item is selected - var number_of_checked_ids = this.$el.find('input[name="id"]:checked').length; - if (number_of_checked_ids < 1) { - return false; - } - - // Check to see if there's grid confirmation text for this operation - var operation = _.findWhere(this.grid.attributes.operations, { - label: operation_name, - }); - if (operation && !confirmation_text) { - confirmation_text = operation.confirm || ""; - } - - // collect ids - var item_ids = []; - this.$el.find("input[name=id]:checked").each(function () { - item_ids.push($(this).val()); - }); - - // execute operation - var options = { - operation: operation_name, - id: item_ids, - confirmation_text: confirmation_text, - }; - if (operation.target == "top" || operation.target == "center") { - options = _.extend(options, { - href: operation.href, - target: operation.target, - }); - } - this.execute(options); - return true; - }, - - check_all_items: function () { - var check = this.$(".grid-row-select-checkbox"); - var state = this.$("#check_all").prop("checked"); - _.each(check, (c) => { - $(c).prop("checked", state); - }); - this.init_grid_elements(); - }, - - // Go back to page one; this is useful when a filter is applied. - go_page_one: function () { - // Need to go back to page 1 if not showing all. - var cur_page = this.grid.get("cur_page"); - if (cur_page !== null && cur_page !== undefined && cur_page !== "all") { - this.grid.set("cur_page", 1); - } - }, - - // - // execute operations and hyperlink requests - // - execute: function (options) { - // get url - var id = null; - var href = null; - var operation = null; - var confirmation_text = null; - var target = null; - - // check for options - if (options) { - // get options - href = options.href; - operation = options.operation; - id = options.id; - confirmation_text = options.confirmation_text; - target = options.target; - - // check if input contains the operation tag - if (href !== undefined && href.indexOf("operation=") != -1) { - // Get operation, id in hyperlink's href. - var href_parts = href.split("?"); - if (href_parts.length > 1) { - var href_parms_str = href_parts[1]; - var href_parms = href_parms_str.split("&"); - for (var index = 0; index < href_parms.length; index++) { - if (href_parms[index].indexOf("operation") != -1) { - // Found operation parm; get operation value. - operation = href_parms[index].split("=")[1]; - operation = operation.replace(/\+/g, " "); - } else if (href_parms[index].indexOf("id") != -1) { - // Found id parm; get id value. - id = href_parms[index].split("=")[1]; - } - } - } - } - } - - // check for operation details - if (operation && id) { - // show confirmation box - if ( - confirmation_text && - confirmation_text !== "" && - confirmation_text != "None" && - confirmation_text != "null" - ) { - if (!window.confirm(confirmation_text)) { - return false; - } - } - - // use small characters for operation?! - operation = operation.toLowerCase(); - - // Update grid. - this.grid.set({ - operation: operation, - item_ids: id, - }); - - // Do operation. If operation cannot be performed asynchronously, redirect to location. - if (target == "top") { - window.top.location = `${href}?${$.param(this.grid.get_url_data())}`; - } else if (target == "center") { - $("#galaxy_main").attr("src", `${href}?${$.param(this.grid.get_url_data())}`); - } else { - this.update_grid(); - } - - // done - return false; - } - - // refresh grid - if (href) { - this.go_to(target, href); - return false; - } - - // refresh grid - this.update_grid(); - - // done - return false; - }, - - // go to url - go_to: function (target, href) { - // get slide status - var advanced_search = this.$el.find("#advanced-search").is(":visible"); - this.grid.set("advanced_search", advanced_search); - - // get default url - if (!href) { - href = `${this.grid.get("url_base")}?${$.param(this.grid.get_url_data())}`; - } - - // clear grid of transient request attributes. - this.grid.set({ - operation: undefined, - item_ids: undefined, - }); - switch (target) { - case "center": - $("#galaxy_main").attr("src", href); - break; - case "top": - window.top.location = href; - break; - default: - window.location = href; - } - }, - - // Update grid. - update_grid: function () { - // If there's an operation, do POST; otherwise, do GET. - var method = this.grid.get("operation") ? "POST" : "GET"; - - // Show overlay to indicate loading and prevent user actions. - this.$el.find(".loading-elt-overlay").show(); - var self = this; - $.ajax({ - type: method, - url: self.grid.get("url_base"), - data: self.grid.get_url_data(), - error: function () { - alert("Grid refresh failed"); - }, - success: function (response_text) { - // backup - var embedded = self.grid.get("embedded"); - var insert = self.grid.get("insert"); - var advanced_search = self.$el.find("#advanced-search").is(":visible"); - - // request new configuration - var json = response_text; - - // update - json.embedded = embedded; - json.insert = insert; - json.advanced_search = advanced_search; - - // Initialize new grid config - self.init_grid(json); - - // Hide loading overlay. - self.$el.find(".loading-elt-overlay").hide(); - }, - complete: function () { - // Clear grid of transient request attributes. - self.grid.set({ - operation: undefined, - item_ids: undefined, - }); - }, - }); - }, - - // Generates a class name at the root of the view that we can - // use for conditional styling in the various kinds of grids - // instead of acres of if/then statements in javascript - getRootClassName({ title = "grid" }) { - return slugify(title).toLowerCase(); - }, -}); diff --git a/client/src/mvc/visualization/chart/chart-client.js b/client/src/mvc/visualization/chart/chart-client.js index 4369e1512b2d..4f7a25b2ff17 100644 --- a/client/src/mvc/visualization/chart/chart-client.js +++ b/client/src/mvc/visualization/chart/chart-client.js @@ -1,15 +1,16 @@ import { getGalaxyInstance } from "app"; import Backbone from "backbone"; import $ from "jquery"; -import Ui from "mvc/ui/ui-misc"; import Modal from "mvc/ui/ui-modal"; -import Chart from "mvc/visualization/chart/components/model"; -import Editor from "mvc/visualization/chart/views/editor"; -import Menu from "mvc/visualization/chart/views/menu"; -import Viewer from "mvc/visualization/chart/views/viewer"; import { getAppRoot } from "onload/loadConfig"; import Deferred from "utils/deferred"; +import Chart from "./components/model"; +import Editor from "./views/editor"; +import Menu from "./views/menu"; +import Ui from "./views/misc"; +import Viewer from "./views/viewer"; + /** Get boolean as string */ function asBoolean(value) { return String(value).toLowerCase() == "true"; diff --git a/client/src/mvc/ui/ui-buttons.js b/client/src/mvc/visualization/chart/views/buttons.js similarity index 100% rename from client/src/mvc/ui/ui-buttons.js rename to client/src/mvc/visualization/chart/views/buttons.js diff --git a/client/src/mvc/visualization/chart/views/editor.js b/client/src/mvc/visualization/chart/views/editor.js index d30911d94157..3c9c21bb3f32 100644 --- a/client/src/mvc/visualization/chart/views/editor.js +++ b/client/src/mvc/visualization/chart/views/editor.js @@ -4,11 +4,12 @@ */ import Backbone from "backbone"; import $ from "jquery"; -import Ui from "mvc/ui/ui-misc"; -import Tabs from "mvc/ui/ui-tabs"; -import Description from "mvc/visualization/chart/views/description"; -import Groups from "mvc/visualization/chart/views/groups"; -import Settings from "mvc/visualization/chart/views/settings"; + +import Description from "./description"; +import Groups from "./groups"; +import Ui from "./misc"; +import Settings from "./settings"; +import Tabs from "./tabs"; export default Backbone.View.extend({ initialize: function (app, options) { diff --git a/client/src/mvc/visualization/chart/views/menu.js b/client/src/mvc/visualization/chart/views/menu.js index 283c329767f9..8ed0575bb970 100644 --- a/client/src/mvc/visualization/chart/views/menu.js +++ b/client/src/mvc/visualization/chart/views/menu.js @@ -1,6 +1,7 @@ /** This class renders the chart menu options. */ import Backbone from "backbone"; -import Ui from "mvc/ui/ui-misc"; + +import Ui from "./misc"; export default Backbone.View.extend({ initialize: function (app) { diff --git a/client/src/mvc/ui/ui-misc.js b/client/src/mvc/visualization/chart/views/misc.js similarity index 99% rename from client/src/mvc/ui/ui-misc.js rename to client/src/mvc/visualization/chart/views/misc.js index af8d4a236540..64c5a100c871 100644 --- a/client/src/mvc/ui/ui-misc.js +++ b/client/src/mvc/visualization/chart/views/misc.js @@ -3,10 +3,11 @@ */ import Backbone from "backbone"; import $ from "jquery"; -import Buttons from "mvc/ui/ui-buttons"; import Modal from "mvc/ui/ui-modal"; import _ from "underscore"; +import Buttons from "./buttons"; + /** Displays messages used e.g. in the tool form */ export var Message = Backbone.View.extend({ initialize: function (options) { diff --git a/client/src/mvc/visualization/chart/views/portlet.js b/client/src/mvc/visualization/chart/views/portlet.js index 3dfd60c03a9a..6bc6a542fd0f 100644 --- a/client/src/mvc/visualization/chart/views/portlet.js +++ b/client/src/mvc/visualization/chart/views/portlet.js @@ -1,8 +1,9 @@ import Backbone from "backbone"; import $ from "jquery"; -import Ui from "mvc/ui/ui-misc"; import Utils from "utils/utils"; +import Ui from "./misc"; + export var View = Backbone.View.extend({ visible: false, initialize: function (options) { diff --git a/client/src/mvc/visualization/chart/views/repeat.js b/client/src/mvc/visualization/chart/views/repeat.js index 027494c138ad..f88059fdbd16 100644 --- a/client/src/mvc/visualization/chart/views/repeat.js +++ b/client/src/mvc/visualization/chart/views/repeat.js @@ -1,11 +1,11 @@ /** This class creates a ui component which enables the dynamic creation of portlets */ import Backbone from "backbone"; import $ from "jquery"; -import Ui from "mvc/ui/ui-misc"; import _ from "underscore"; import _l from "utils/localization"; import Utils from "utils/utils"; +import Ui from "./misc"; import Portlet from "./portlet"; export var View = Backbone.View.extend({ diff --git a/client/src/mvc/ui/ui-tabs.js b/client/src/mvc/visualization/chart/views/tabs.js similarity index 100% rename from client/src/mvc/ui/ui-tabs.js rename to client/src/mvc/visualization/chart/views/tabs.js diff --git a/client/src/mvc/ui/icon-button.js b/client/src/viz/icon-button.js similarity index 100% rename from client/src/mvc/ui/icon-button.js rename to client/src/viz/icon-button.js diff --git a/client/src/viz/trackster.js b/client/src/viz/trackster.js index 9b6fe3bbe86a..d3c3162479a4 100644 --- a/client/src/viz/trackster.js +++ b/client/src/viz/trackster.js @@ -16,7 +16,6 @@ import "ui/editable-text"; import { getGalaxyInstance } from "app"; import Backbone from "backbone"; import $ from "jquery"; -import IconButton from "mvc/ui/icon-button"; import { getAppRoot } from "onload/loadConfig"; import _ from "underscore"; import _l from "utils/localization"; @@ -24,6 +23,8 @@ import query_string from "utils/query-string-parsing"; import tracks from "viz/trackster/tracks"; import visualization from "viz/visualization"; +import IconButton from "./icon-button"; + //import "static/style/jquery-ui/smoothness/jquery-ui.css"; //import "static/style/library.css"; //import "static/style/trackster.css"; diff --git a/lib/galaxy/web/framework/helpers/grids.py b/lib/galaxy/web/framework/helpers/grids.py index 1306b7306f6f..98795511186d 100644 --- a/lib/galaxy/web/framework/helpers/grids.py +++ b/lib/galaxy/web/framework/helpers/grids.py @@ -1,40 +1,15 @@ import logging -import math -from json import ( - dumps, - loads, -) from typing import ( - Dict, List, Optional, ) from markupsafe import escape -from sqlalchemy.sql.expression import ( - and_, - false, - func, - null, - or_, - true, -) -from galaxy.model.item_attrs import ( - get_foreign_key, - UsesAnnotations, - UsesItemRatings, -) from galaxy.util import ( - restore_text, - sanitize_text, string_as_bool, unicodify, ) -from galaxy.web.framework import ( - decorators, - url_for, -) log = logging.getLogger(__name__) @@ -47,16 +22,6 @@ def __init__( model_class=None, method=None, format=None, - link=None, - attach_popup=False, - visible=True, - nowrap=False, - # Valid values for filterable are ['standard', 'advanced', None] - filterable=None, - sortable=True, - label_id_prefix=None, - target=None, - delayed=False, escape=True, ): """Create a grid column.""" @@ -65,17 +30,7 @@ def __init__( self.model_class = model_class self.method = method self.format = format - self.link = link - self.target = target - self.nowrap = nowrap - self.attach_popup = attach_popup - self.visible = visible - self.filterable = filterable - self.delayed = delayed self.escape = escape - # Column must have a key to be sortable. - self.sortable = self.key is not None and sortable - self.label_id_prefix = label_id_prefix or "" def get_value(self, trans, grid, item): if self.method: @@ -91,30 +46,6 @@ def get_value(self, trans, grid, item): else: return value - def get_link(self, trans, grid, item): - if self.link and self.link(item): - return self.link(item) - return None - - def filter(self, trans, user, query, column_filter): - """Modify query to reflect the column filter.""" - if column_filter == "All": - pass - if column_filter == "True": - query = query.filter_by(**{self.key: True}) - elif column_filter == "False": - query = query.filter_by(**{self.key: False}) - return query - - def get_accepted_filters(self): - """Returns a list of accepted filters for this column.""" - accepted_filters_vals = ["False", "True", "All"] - accepted_filters = [] - for val in accepted_filters_vals: - args = {self.key: val} - accepted_filters.append(GridColumnFilter(val, args)) - return accepted_filters - def sort(self, trans, query, ascending, column_name=None): """Sort query using this column.""" if column_name is None: @@ -129,931 +60,6 @@ def sort(self, trans, query, ascending, column_name=None): return query -class ReverseSortColumn(GridColumn): - """Column that reverses sorting; this is useful when the natural sort is descending.""" - - def sort(self, trans, query, ascending, column_name=None): - return GridColumn.sort(self, trans, query, (not ascending), column_name=column_name) - - -class TextColumn(GridColumn): - """Generic column that employs freetext and, hence, supports freetext, case-independent filtering.""" - - def filter(self, trans, user, 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(trans, user, column_filter)) - return query - - def get_filter(self, trans, user, column_filter): - """Returns a SQLAlchemy criterion derived from column_filter.""" - if isinstance(column_filter, str): - return self.get_single_filter(user, column_filter) - elif isinstance(column_filter, list): - clause_list = [] - for filter in column_filter: - clause_list.append(self.get_single_filter(user, filter)) - return and_(*clause_list) - - def get_single_filter(self, user, a_filter): - """ - Returns a SQLAlchemy criterion derived for a single filter. Single filter - is the most basic filter--usually a string--and cannot be a list. - """ - # Queries that include table joins cannot guarantee that table column names will be - # unique, so check to see if a_filter is of type .. - if self.key.find(".") > -1: - a_key = self.key.split(".")[1] - else: - a_key = self.key - model_class_key_field = getattr(self.model_class, a_key) - return func.lower(model_class_key_field).like(f"%{a_filter.lower()}%") - - def sort(self, trans, query, ascending, column_name=None): - """Sort column using case-insensitive alphabetical sorting.""" - if column_name is None: - column_name = self.key - if ascending: - query = query.order_by(func.lower(self.model_class.table.c.get(column_name)).asc()) - else: - query = query.order_by(func.lower(self.model_class.table.c.get(column_name)).desc()) - return query - - -class DateTimeColumn(TextColumn): - def sort(self, trans, query, ascending, column_name=None): - """Sort query using this column.""" - return GridColumn.sort(self, trans, query, ascending, column_name=column_name) - - -class BooleanColumn(TextColumn): - def sort(self, trans, query, ascending, column_name=None): - """Sort query using this column.""" - return GridColumn.sort(self, trans, query, ascending, column_name=column_name) - - def get_single_filter(self, user, a_filter): - if self.key.find(".") > -1: - a_key = self.key.split(".")[1] - else: - a_key = self.key - model_class_key_field = getattr(self.model_class, a_key) - return model_class_key_field == a_filter - - -class IntegerColumn(TextColumn): - """ - Integer column that employs freetext, but checks that the text is an integer, - so support filtering on integer values. - - IMPORTANT NOTE: grids that use this column type should not include the column - in the cols_to_filter list of MulticolFilterColumn ( i.e., searching on this - column type should not be performed in the grid's standard search - it won't - throw exceptions, but it also will not find what you're looking for ). Grids - that search on this column should use 'filterable="advanced"' so that searching - is only performed in the advanced search component, restricting the search to - the specific column. - - This is useful for searching on object ids or other integer columns. See the - JobIdColumn column in the SpecifiedDateListGrid class in the jobs controller of - the reports webapp for an example. - """ - - def get_single_filter(self, user, a_filter): - model_class_key_field = getattr(self.model_class, self.key) - assert int(a_filter), "The search entry must be an integer" - return model_class_key_field == int(a_filter) - - def sort(self, trans, query, ascending, column_name=None): - """Sort query using this column.""" - return GridColumn.sort(self, trans, query, ascending, column_name=column_name) - - -class CommunityRatingColumn(GridColumn, UsesItemRatings): - """Column that displays community ratings for an item.""" - - def get_value(self, trans, grid, item): - if not hasattr(item, "average_rating"): - # No prefetched column property, generate it on the fly. - ave_item_rating, num_ratings = self.get_ave_item_rating_data( - trans.sa_session, item, webapp_model=trans.model - ) - else: - ave_item_rating = item.average_rating - num_ratings = 2 # just used for pluralization - if not ave_item_rating: - ave_item_rating = 0 - return trans.fill_template( - "tool_shed_rating.mako", - ave_item_rating=ave_item_rating, - num_ratings=num_ratings, - item_id=trans.security.encode_id(item.id), - ) - - def sort(self, trans, query, ascending, column_name=None): - # Get the columns that connect item's table and item's rating association table. - item_rating_assoc_class = getattr(trans.model, f"{self.model_class.__name__}RatingAssociation") - foreign_key = get_foreign_key(item_rating_assoc_class, self.model_class) - fk_col = foreign_key.parent - referent_col = foreign_key.get_referent(self.model_class.table) - # Do sorting using a subquery. - # Subquery to get average rating for each item. - ave_rating_subquery = ( - trans.sa_session.query(fk_col, func.avg(item_rating_assoc_class.table.c.rating).label("avg_rating")) - .group_by(fk_col) - .subquery() - ) - # Integrate subquery into main query. - query = query.outerjoin((ave_rating_subquery, referent_col == ave_rating_subquery.columns[fk_col.name])) - # Sort using subquery results; use coalesce to avoid null values. - if ( - not ascending - ): # TODO: for now, reverse sorting b/c first sort is ascending, and that should be the natural sort. - query = query.order_by(func.coalesce(ave_rating_subquery.c.avg_rating, 0).asc()) - else: - query = query.order_by(func.coalesce(ave_rating_subquery.c.avg_rating, 0).desc()) - return query - - -class OwnerAnnotationColumn(TextColumn, UsesAnnotations): - """Column that displays and filters item owner's annotations.""" - - def __init__(self, col_name, key, model_class=None, model_annotation_association_class=None, filterable=None): - GridColumn.__init__(self, col_name, key=key, model_class=model_class, filterable=filterable) - self.sortable = False - self.model_annotation_association_class = model_annotation_association_class - - def get_value(self, trans, grid, item): - """Returns first 150 characters of annotation.""" - annotation = self.get_item_annotation_str(trans.sa_session, item.user, item) - if annotation: - ann_snippet = annotation[:155] - if len(annotation) > 155: - ann_snippet = ann_snippet[: ann_snippet.rfind(" ")] - ann_snippet += "..." - else: - ann_snippet = "" - return escape(ann_snippet) - - def get_single_filter(self, user, a_filter): - """Filter by annotation and annotation owner.""" - return self.model_class.annotations.any( - and_( - func.lower(self.model_annotation_association_class.annotation).like(f"%{a_filter.lower()}%"), - # TODO: not sure why, to filter by owner's annotations, we have to do this rather than - # 'self.model_class.user==self.model_annotation_association_class.user' - self.model_annotation_association_class.table.c.user_id == self.model_class.table.c.user_id, - ) - ) - - -class CommunityTagsColumn(TextColumn): - """Column that supports community tags.""" - - def __init__( - self, col_name, key, model_class=None, model_tag_association_class=None, filterable=None, grid_name=None - ): - GridColumn.__init__( - self, col_name, key=key, model_class=model_class, nowrap=True, filterable=filterable, sortable=False - ) - self.model_tag_association_class = model_tag_association_class - # Column-specific attributes. - self.grid_name = grid_name - - def get_value(self, trans, grid, item): - return trans.fill_template( - "/tagging_common.mako", - tag_type="community", - trans=trans, - user=trans.get_user(), - tagged_item=item, - elt_context=self.grid_name, - tag_click_fn="add_tag_to_grid_filter", - use_toggle_link=True, - ) - - def filter(self, trans, user, 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(trans, user, column_filter)) - return query - - def get_filter(self, trans, user, column_filter): - # Parse filter to extract multiple tags. - 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 = trans.app.tag_handler.parse_tags(column_filter) - clause_list = [] - for name, value in raw_tags: - if name: - # Filter by all tags. - clause_list.append( - self.model_class.tags.any( - func.lower(self.model_tag_association_class.user_tname).like(f"%{name.lower()}%") - ) - ) - if value: - # Filter by all values. - clause_list.append( - self.model_class.tags.any( - func.lower(self.model_tag_association_class.user_value).like(f"%{value.lower()}%") - ) - ) - return and_(*clause_list) - - -class IndividualTagsColumn(CommunityTagsColumn): - """Column that supports individual tags.""" - - def get_value(self, trans, grid, item): - return trans.fill_template( - "/tagging_common.mako", - tag_type="individual", - user=trans.user, - tagged_item=item, - elt_context=self.grid_name, - tag_click_fn="add_tag_to_grid_filter", - use_toggle_link=True, - ) - - def get_filter(self, trans, user, column_filter): - # Parse filter to extract multiple tags. - 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 = trans.app.tag_handler.parse_tags(column_filter) - clause_list = [] - for name, value in raw_tags: - if name: - # Filter by individual's tag names. - clause_list.append( - self.model_class.tags.any( - and_( - func.lower(self.model_tag_association_class.user_tname).like(f"%{name.lower()}%"), - self.model_tag_association_class.user == user, - ) - ) - ) - if value: - # Filter by individual's tag values. - clause_list.append( - self.model_class.tags.any( - and_( - func.lower(self.model_tag_association_class.user_value).like(f"%{value.lower()}%"), - self.model_tag_association_class.user == user, - ) - ) - ) - return and_(*clause_list) - - -class MulticolFilterColumn(TextColumn): - """Column that performs multicolumn filtering.""" - - 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, trans, user, 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): - clause_list = [] - for filter in column_filter: - part_clause_list = [] - for column in self.cols_to_filter: - part_clause_list.append(column.get_filter(trans, user, filter)) - clause_list.append(or_(*part_clause_list)) - complete_filter = and_(*clause_list) - else: - clause_list = [] - for column in self.cols_to_filter: - clause_list.append(column.get_filter(trans, user, column_filter)) - complete_filter = or_(*clause_list) - return query.filter(complete_filter) - - -class OwnerColumn(TextColumn): - """Column that lists item's owner.""" - - def get_value(self, trans, grid, item): - return item.user.username - - def sort(self, trans, query, ascending, column_name=None): - """Sort column using case-insensitive alphabetical sorting on item's username.""" - if ascending: - query = query.order_by(func.lower(self.model_class.username).asc()) - else: - query = query.order_by(func.lower(self.model_class.username).desc()) - return query - - -class PublicURLColumn(TextColumn): - """Column displays item's public URL based on username and slug.""" - - def get_link(self, trans, grid, item): - if item.user.username and item.slug: - return dict(action="display_by_username_and_slug", username=item.user.username, slug=item.slug) - elif not item.user.username: - # TODO: provide link to set username. - return None - elif not item.user.slug: - # TODO: provide link to set slug. - return None - - -class DeletedColumn(GridColumn): - """Column that tracks and filters for items with deleted attribute.""" - - def get_accepted_filters(self): - """Returns a list of accepted filters for this column.""" - accepted_filter_labels_and_vals = {"active": "False", "deleted": "True", "all": "All"} - accepted_filters = [] - for label, val in accepted_filter_labels_and_vals.items(): - args = {self.key: val} - accepted_filters.append(GridColumnFilter(label, args)) - return accepted_filters - - def filter(self, trans, user, query, column_filter): - """Modify query to filter self.model_class by state.""" - if column_filter == "All": - pass - elif column_filter in ["True", "False"]: - query = query.filter(self.model_class.deleted == (column_filter == "True")) - return query - - -class PurgedColumn(GridColumn): - """Column that tracks and filters for items with purged attribute.""" - - def get_accepted_filters(self): - """Returns a list of accepted filters for this column.""" - accepted_filter_labels_and_vals = {"nonpurged": "False", "purged": "True", "all": "All"} - accepted_filters = [] - for label, val in accepted_filter_labels_and_vals.items(): - args = {self.key: val} - accepted_filters.append(GridColumnFilter(label, args)) - return accepted_filters - - def filter(self, trans, user, query, column_filter): - """Modify query to filter self.model_class by state.""" - if column_filter == "All": - pass - elif column_filter in ["True", "False"]: - query = query.filter(self.model_class.purged == (column_filter == "True")) - return query - - -class StateColumn(GridColumn): - """ - Column that tracks and filters for items with state attribute. - - IMPORTANT NOTE: self.model_class must have a states Bunch or dict if - this column type is used in the grid. - """ - - def get_value(self, trans, grid, item): - return item.state - - def filter(self, trans, user, query, column_filter): - """Modify query to filter self.model_class by state.""" - if column_filter == "All": - pass - elif column_filter in [v for k, v in self.model_class.states.items()]: - query = query.filter(self.model_class.state == column_filter) - return query - - def get_accepted_filters(self): - """Returns a list of accepted filters for this column.""" - all = GridColumnFilter("all", {self.key: "All"}) - accepted_filters = [all] - for v in self.model_class.states.values(): - args = {self.key: v} - accepted_filters.append(GridColumnFilter(v, args)) - return accepted_filters - - -class SharingStatusColumn(GridColumn): - """Grid column to indicate sharing status.""" - - def __init__(self, *args, **kwargs): - self.use_shared_with_count = kwargs.pop("use_shared_with_count", False) - super().__init__(*args, **kwargs) - - def get_value(self, trans, grid, item): - # Delete items cannot be shared. - if item.deleted: - return "" - # Build a list of sharing for this item. - sharing_statuses = [] - if self._is_shared(item): - sharing_statuses.append("Shared") - if item.importable: - sharing_statuses.append("Accessible") - if item.published: - sharing_statuses.append("Published") - return ", ".join(sharing_statuses) - - def _is_shared(self, item): - if self.use_shared_with_count: - # optimization to skip join for users_shared_with and loading in that data. - return item.users_shared_with_count > 0 - - return item.users_shared_with - - def filter(self, trans, user, query, column_filter): - """Modify query to filter histories by sharing status.""" - if column_filter == "All": - pass - elif column_filter: - if column_filter == "private": - query = query.filter(self.model_class.users_shared_with == null()) - query = query.filter(self.model_class.importable == false()) - elif column_filter == "shared": - query = query.filter(self.model_class.users_shared_with != null()) - elif column_filter == "accessible": - query = query.filter(self.model_class.importable == true()) - elif column_filter == "published": - query = query.filter(self.model_class.published == true()) - return query - - def get_accepted_filters(self): - """Returns a list of accepted filters for this column.""" - accepted_filter_labels_and_vals = {} - accepted_filter_labels_and_vals["private"] = "private" - accepted_filter_labels_and_vals["shared"] = "shared" - accepted_filter_labels_and_vals["accessible"] = "accessible" - accepted_filter_labels_and_vals["published"] = "published" - 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(GridColumnFilter(label, args)) - return accepted_filters - - -class GridOperation: - def __init__( - self, - label, - key=None, - condition=None, - allow_multiple=True, - allow_popup=True, - target=None, - url_args=None, - async_compatible=False, - confirm=None, - global_operation=None, - ): - self.label = label - self.key = key - self.allow_multiple = allow_multiple - self.allow_popup = allow_popup - self.condition = condition - self.target = target - self.url_args = url_args - self.async_compatible = async_compatible - # if 'confirm' is set, then ask before completing the operation - self.confirm = confirm - # specify a general operation that acts on the full grid - # this should be a function returning a dictionary with parameters - # to pass to the URL, similar to GridColumn links: - # global_operation=(lambda: dict(operation="download") - self.global_operation = global_operation - - def get_url_args(self, item): - if self.url_args: - if callable(self.url_args): - url_args = self.url_args(item) - else: - url_args = dict(self.url_args) - url_args["id"] = item.id - return url_args - else: - return dict(operation=self.label, id=item.id) - - def allowed(self, item): - if self.condition: - return bool(self.condition(item)) - else: - return True - - -class DisplayByUsernameAndSlugGridOperation(GridOperation): - """Operation to display an item by username and slug.""" - - def get_url_args(self, item): - return {"action": "display_by_username_and_slug", "username": item.user.username, "slug": item.slug} - - -class GridAction: - def __init__(self, label=None, url_args=None, target=None): - self.label = label - self.url_args = url_args - self.target = target - - -class GridColumnFilter: - def __init__(self, label, args=None): - self.label = label - self.args = args - - def get_url_args(self): - rval = {} - for k, v in self.args.items(): - rval[f"f-{k}"] = v - return rval - - -class Grid: - """ - Specifies the content and format of a grid (data table). - """ - - title = "" - model_class: Optional[type] = None - show_item_checkboxes = False - use_hide_message = True - global_actions: List[GridAction] = [] - columns: List[GridColumn] = [] - operations: List[GridOperation] = [] - standard_filters: List[GridColumnFilter] = [] - # Any columns that are filterable (either standard or advanced) should have a default value set in the default filter. - default_filter: Dict[str, str] = {} - default_sort_key: Optional[str] = None - use_paging = False - num_rows_per_page = 25 - num_page_links = 10 - # Set preference names. - cur_filter_pref_name = ".filter" - cur_sort_key_pref_name = ".sort_key" - legend = None - info_text: Optional[str] = None - - def __init__(self): - # Determine if any multiple row operations are defined - self.has_multiple_item_operations = False - for operation in self.operations: - if operation.allow_multiple: - self.has_multiple_item_operations = True - break - - # If a column does not have a model class, set the column's model class - # to be the grid's model class. - for column in self.columns: - if not column.model_class: - column.model_class = self.model_class - - def __call__(self, trans, **kwargs): - # Get basics. - # FIXME: pretty sure this is only here to pass along, can likely be eliminated - status = kwargs.get("status", None) - message = kwargs.get("message", None) - # Build a base filter and sort key that is the combination of the saved state and defaults. - # Saved state takes preference over defaults. - base_filter = {} - if self.default_filter: - # default_filter is a dictionary that provides a default set of filters based on the grid's columns. - base_filter = self.default_filter.copy() - base_sort_key = self.default_sort_key - # Build initial query - query = self.build_initial_query(trans, **kwargs) - query = self.apply_query_filter(trans, query, **kwargs) - # Maintain sort state in generated urls - extra_url_args = {} - # Determine whether use_default_filter flag is set. - use_default_filter = False - if use_default_filter_str := kwargs.get("use_default_filter"): - use_default_filter = use_default_filter_str.lower() == "true" - # Process filtering arguments to (a) build a query that represents the filter and (b) build a - # dictionary that denotes the current filter. - cur_filter_dict = {} - for column in self.columns: - if column.key: - # Get the filter criterion for the column. Precedence is (a) if using default filter, only look there; otherwise, - # (b) look in kwargs; and (c) look in base filter. - column_filter = None - if use_default_filter: - if self.default_filter: - column_filter = self.default_filter.get(column.key) - elif f"f-{column.model_class.__name__}.{column.key}" in kwargs: - # Queries that include table joins cannot guarantee unique column names. This problem is - # handled by setting the column_filter value to .. - column_filter = kwargs.get(f"f-{column.model_class.__name__}.{column.key}") - elif f"f-{column.key}" in kwargs: - column_filter = kwargs.get(f"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 loads_recurse(item): - decoded_list = [] - if isinstance(item, str): - try: - # Not clear what we're decoding, so recurse to ensure that we catch everything. - decoded_item = loads(item) - if isinstance(decoded_item, list): - decoded_list = loads_recurse(decoded_item) - else: - decoded_list = [str(decoded_item)] - except ValueError: - decoded_list = [str(item)] - elif isinstance(item, list): - for element in item: - a_list = loads_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 = loads_recurse(column_filter) - if len(column_filter) == 1: - column_filter = column_filter[0] - # Interpret ',' as a separator for multiple terms. - if isinstance(column_filter, str) and column_filter.find(",") != -1: - column_filter = column_filter.split(",") - - # Check if filter is empty - if isinstance(column_filter, list): - # Remove empty strings from filter list - column_filter = [x for x in column_filter if x != ""] - if len(column_filter) == 0: - continue - elif isinstance(column_filter, str): - # If filter criterion is empty, do nothing. - if column_filter == "": - continue - - # Update query. - query = column.filter(trans, trans.user, query, column_filter) - # Upate current filter dict. - # Column filters are rendered in various places, sanitize them all here. - cur_filter_dict[column.key] = sanitize_text(column_filter) - # 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 isinstance(column_filter, list): - # Filter is a list; process each item. - extra_url_args[f"f-{column.key}"] = dumps(column_filter) - else: - # Process singleton filter. - extra_url_args[f"f-{column.key}"] = column_filter - # Process sort arguments. - sort_key = None - if "sort" in kwargs: - sort_key = kwargs["sort"] - elif base_sort_key: - sort_key = base_sort_key - if sort_key: - ascending = not (sort_key.startswith("-")) - # Queries that include table joins cannot guarantee unique column names. This problem is - # handled by setting the column_filter value to .. - table_name = None - if sort_key.find(".") > -1: - a_list = sort_key.split(".") - if ascending: - table_name = a_list[0] - else: - table_name = a_list[0][1:] - column_name = a_list[1] - elif ascending: - column_name = sort_key - else: - column_name = sort_key[1:] - # Sort key is a column key. - for column in self.columns: - if column.key and column.key.find(".") > -1: - column_key = column.key.split(".")[1] - else: - column_key = column.key - if (table_name is None or table_name == column.model_class.__name__) and column_key == column_name: - query = column.sort(trans, query, ascending, column_name=column_name) - break - extra_url_args["sort"] = sort_key - # There might be a current row - current_item = self.get_current_item(trans, **kwargs) - # Process page number. - num_pages = None - total_row_count_query = query # query without limit applied to get total number of rows. - if self.use_paging: - if "page" in kwargs: - if kwargs["page"] == "all": - page_num = 0 - else: - page_num = int(kwargs["page"]) - else: - page_num = 1 - if page_num == 0: - num_pages = 1 - page_num = 1 - else: - query = query.limit(self.num_rows_per_page).offset((page_num - 1) * self.num_rows_per_page) - else: - # Defaults. - page_num = 1 - # There are some places in grid templates where it's useful for a grid - # to have its current filter. - self.cur_filter_dict = cur_filter_dict - - # Log grid view. - context = str(self.__class__.__name__) - params = cur_filter_dict.copy() - params["sort"] = sort_key - - # Render grid. - def url(*args, **kwargs): - route_name = kwargs.pop("__route_name__", None) - # Only include sort/filter arguments if not linking to another - # page. This is a bit of a hack. - if "action" in kwargs: - new_kwargs = dict() - else: - new_kwargs = dict(extra_url_args) - # Extend new_kwargs with first argument if found - if len(args) > 0: - new_kwargs.update(args[0]) - new_kwargs.update(kwargs) - # We need to encode item ids - if "id" in new_kwargs: - id = new_kwargs["id"] - if isinstance(id, list): - new_kwargs["id"] = [trans.security.encode_id(i) for i in id] - else: - new_kwargs["id"] = trans.security.encode_id(id) - # The url_for invocation *must* include a controller and action. - if "controller" not in new_kwargs: - new_kwargs["controller"] = trans.controller - if "action" not in new_kwargs: - new_kwargs["action"] = trans.action - if route_name: - return url_for(route_name, **new_kwargs) - return url_for(**new_kwargs) - - self.use_panels = kwargs.get("use_panels", False) in [True, "True", "true"] - self.advanced_search = kwargs.get("advanced_search", False) in [True, "True", "true"] - # Currently, filling the template returns a str object; this requires decoding the string into a - # unicode object within mako templates. What probably should be done is to return the template as - # utf-8 unicode; however, this would require encoding the object as utf-8 before returning the grid - # results via a controller method, which is require substantial changes. Hence, for now, return grid - # as str. - grid_config = { - "title": self.title, - "title_id": getattr(self, "title_id", None), - "url_base": trans.request.path_url, - "async_ops": [], - "categorical_filters": {}, - "filters": cur_filter_dict, - "sort_key": sort_key, - "show_item_checkboxes": self.show_item_checkboxes - or kwargs.get("show_item_checkboxes", "") in ["True", "true"], - "cur_page_num": page_num, - "num_page_links": self.num_page_links, - "status": status, - "message": restore_text(message), - "global_actions": [], - "operations": [], - "items": [], - "columns": [], - "model_class": str(self.model_class), - "use_paging": self.use_paging, - "legend": self.legend, - "current_item_id": False, - "use_hide_message": self.use_hide_message, - "default_filter_dict": self.default_filter, - "advanced_search": self.advanced_search, - "info_text": self.info_text, - "url": url(dict()), - "refresh_frames": kwargs.get("refresh_frames", []), - } - if current_item: - grid_config["current_item_id"] = current_item.id - for column in self.columns: - extra = "" - if column.sortable: - if sort_key.endswith(column.key): - if not sort_key.startswith("-"): - extra = "↓" - else: - extra = "↑" - grid_config["columns"].append( - { - "key": column.key, - "visible": column.visible, - "nowrap": column.nowrap, - "attach_popup": column.attach_popup, - "label_id_prefix": column.label_id_prefix, - "sortable": column.sortable, - "label": column.label, - "filterable": column.filterable, - "delayed": column.delayed, - "is_text": isinstance(column, TextColumn), - "extra": extra, - } - ) - for operation in self.operations: - grid_config["operations"].append( - { - "allow_multiple": operation.allow_multiple, - "allow_popup": operation.allow_popup, - "target": operation.target, - "label": operation.label, - "confirm": operation.confirm, - "href": url(**operation.url_args) if isinstance(operation.url_args, dict) else None, - "global_operation": False, - } - ) - if operation.allow_multiple: - grid_config["show_item_checkboxes"] = True - if operation.global_operation: - grid_config["global_operation"] = url(**(operation.global_operation())) - for action in self.global_actions: - grid_config["global_actions"].append( - {"url_args": url(**action.url_args), "label": action.label, "target": action.target} - ) - for operation in [op for op in self.operations if op.async_compatible]: - grid_config["async_ops"].append(operation.label.lower()) - for column in self.columns: - if column.filterable is not None and not isinstance(column, TextColumn): - grid_config["categorical_filters"][column.key] = { - filter.label: filter.args for filter in column.get_accepted_filters() - } - for item in query: - item_dict = { - "id": item.id, - "encode_id": trans.security.encode_id(item.id), - "link": [], - "operation_config": {}, - "column_config": {}, - } - for column in self.columns: - if column.visible: - link = column.get_link(trans, self, item) - if link: - link = url(**link) - else: - link = None - target = column.target - value = unicodify(column.get_value(trans, self, item)) - if value: - value = value.replace("/", "//") - item_dict["column_config"][column.label] = {"link": link, "value": value, "target": target} - for operation in self.operations: - item_dict["operation_config"][operation.label] = { - "allowed": operation.allowed(item), - "url_args": url(**operation.get_url_args(item)), - "target": operation.target, - } - grid_config["items"].append(item_dict) - - if self.use_paging and num_pages is None: - # TODO: it would be better to just return this as None, render, and fire - # off a second request for this count I think. - total_num_rows = total_row_count_query.count() - num_pages = int(math.ceil(float(total_num_rows) / self.num_rows_per_page)) - - grid_config["num_pages"] = num_pages - - trans.log_action(trans.get_user(), "grid.view", context, params) - return grid_config - - def get_ids(self, **kwargs): - id = [] - if "id" in kwargs: - id = kwargs["id"] - # Coerce ids to list - if not isinstance(id, list): - id = id.split(",") - # Ensure ids are integers - try: - id = list(map(int, id)) - except Exception: - decorators.error("Invalid id") - return id - - # ---- Override these ---------------------------------------------------- - def handle_operation(self, trans, operation, ids, **kwargs): - pass - - def get_current_item(self, trans, **kwargs): - return None - - def build_initial_query(self, trans, **kwargs): - return trans.sa_session.query(self.model_class) - - def apply_query_filter(self, trans, query, **kwargs): - # Applies a database filter that holds for all items in the grid. - # (gvk) Is this method necessary? Why not simply build the entire query, - # including applying filters in the build_initial_query() method? - return query - - class GridData: """ Specifies the content a grid (data table). @@ -1075,7 +81,7 @@ def __call__(self, trans, **kwargs): offset = kwargs.get("offset", 0) # Build initial query - query = self.build_initial_query(trans, **kwargs) + query = trans.sa_session.query(self.model_class) query = self.apply_query_filter(query, **kwargs) # Process sort arguments. @@ -1104,13 +110,3 @@ def __call__(self, trans, **kwargs): row_dict[column.key] = value grid_config["rows"].append(row_dict) return grid_config - - # ---- Override these ---------------------------------------------------- - def handle_operation(self, trans, operation, ids, **kwargs): - pass - - def get_current_item(self, trans, **kwargs): - return None - - def build_initial_query(self, trans, **kwargs): - return trans.sa_session.query(self.model_class) diff --git a/lib/galaxy/webapps/galaxy/controllers/admin.py b/lib/galaxy/webapps/galaxy/controllers/admin.py index 4f3a20a30d4b..08bef7a9bb24 100644 --- a/lib/galaxy/webapps/galaxy/controllers/admin.py +++ b/lib/galaxy/webapps/galaxy/controllers/admin.py @@ -245,7 +245,7 @@ def get_value(self, trans, grid, group): grids.GridColumn("Name", key="name"), UsersColumn("Users", key="users"), RolesColumn("Roles", key="roles"), - grids.DeletedColumn("Deleted", key="deleted", escape=False), + grids.GridColumn("Deleted", key="deleted", escape=False), grids.GridColumn("Last Updated", key="update_time"), ] @@ -281,7 +281,7 @@ def apply_query_filter(self, query, **kwargs): class QuotaListGrid(grids.GridData): - class AmountColumn(grids.TextColumn): + class AmountColumn(grids.GridColumn): def get_value(self, trans, grid, quota): return quota.operation + quota.display_amount diff --git a/lib/galaxy/webapps/galaxy/controllers/forms.py b/lib/galaxy/webapps/galaxy/controllers/forms.py index 4214d3797ce6..f62ef7c272cb 100644 --- a/lib/galaxy/webapps/galaxy/controllers/forms.py +++ b/lib/galaxy/webapps/galaxy/controllers/forms.py @@ -33,15 +33,15 @@ class FormsGrid(grids.GridData): # Custom column types - class NameColumn(grids.TextColumn): + class NameColumn(grids.GridColumn): def get_value(self, trans, grid, form): return form.latest_form.name - class DescriptionColumn(grids.TextColumn): + class DescriptionColumn(grids.GridColumn): def get_value(self, trans, grid, form): return form.latest_form.desc - class TypeColumn(grids.TextColumn): + class TypeColumn(grids.GridColumn): def get_value(self, trans, grid, form): return form.latest_form.type diff --git a/lib/galaxy/webapps/galaxy/controllers/history.py b/lib/galaxy/webapps/galaxy/controllers/history.py index 50cb04bbb20f..844945d285ff 100644 --- a/lib/galaxy/webapps/galaxy/controllers/history.py +++ b/lib/galaxy/webapps/galaxy/controllers/history.py @@ -1,13 +1,7 @@ import logging from dateutil.parser import isoparse -from markupsafe import escape -from sqlalchemy import ( - false, - select, - true, -) -from sqlalchemy.orm import undefer +from sqlalchemy import select from galaxy import ( exceptions, @@ -27,224 +21,20 @@ listify, sanitize_text, string_as_bool, - unicodify, ) from galaxy.web import ( expose_api_anonymous, url_for, ) -from galaxy.web.framework.helpers import ( - grids, - iff, - time_ago, -) from galaxy.webapps.base.controller import ( BaseUIController, - ERROR, - INFO, SharableMixin, - SUCCESS, - WARNING, ) from ..api import depends log = logging.getLogger(__name__) -class NameColumn(grids.TextColumn): - def get_value(self, trans, grid, history): - return escape(history.get_display_name()) - - -class HistoryListGrid(grids.Grid): - # Custom column types - class ItemCountColumn(grids.GridColumn): - def get_value(self, trans, grid, history): - return str(history.hid_counter - 1) - - class HistoryListNameColumn(NameColumn): - def get_link(self, trans, grid, history): - link = None - if not history.deleted: - link = dict(operation="Switch", id=history.id, use_panels=grid.use_panels, async_compatible=True) - return link - - class StatusColumn(grids.GridColumn): - def get_accepted_filters(self): - """Returns a list of accepted filters for this column.""" - accepted_filter_labels_and_vals = { - "active": "active", - "deleted": "deleted", - "archived": "archived", - "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 - - def filter(self, trans, user, query, column_filter): - """Modify query to filter self.model_class by state.""" - if column_filter == "all": - return query - elif column_filter == "active": - return query.filter(self.model_class.deleted == false(), self.model_class.archived == false()) - elif column_filter == "deleted": - return query.filter(self.model_class.deleted == true()) - elif column_filter == "archived": - return query.filter(self.model_class.archived == true()) - - def get_value(self, trans, grid, history): - if history == trans.history: - return "current history" - if history.purged: - return "deleted permanently" - elif history.deleted: - return "deleted" - elif history.archived: - return "archived" - return "" - - def sort(self, trans, query, ascending, column_name=None): - if ascending: - query = query.order_by(self.model_class.table.c.purged.asc(), self.model_class.update_time.desc()) - else: - query = query.order_by(self.model_class.table.c.purged.desc(), self.model_class.update_time.desc()) - return query - - def build_initial_query(self, trans, **kwargs): - # Override to preload sharing information used when fetching data for grid. - query = super().build_initial_query(trans, **kwargs) - query = query.options(undefer(self.model_class.users_shared_with_count)) - return query - - # Grid definition - title = "Saved Histories" - model_class = model.History - default_sort_key = "-update_time" - columns = [ - HistoryListNameColumn("Name", key="name", attach_popup=True, filterable="advanced"), - ItemCountColumn("Items", key="item_count", sortable=False), - grids.GridColumn("Datasets", key="datasets_by_state", sortable=False, nowrap=True, delayed=True), - grids.IndividualTagsColumn( - "Tags", - key="tags", - model_tag_association_class=model.HistoryTagAssociation, - filterable="advanced", - grid_name="HistoryListGrid", - ), - grids.SharingStatusColumn( - "Sharing", key="sharing", filterable="advanced", sortable=False, use_shared_with_count=True - ), - grids.GridColumn("Size on Disk", key="disk_size", sortable=False, delayed=True), - grids.GridColumn("Created", key="create_time", format=time_ago), - grids.GridColumn("Last Updated", key="update_time", format=time_ago), - StatusColumn("Status", key="status", filterable="advanced"), - ] - columns.append( - grids.MulticolFilterColumn( - "search history names and tags", - cols_to_filter=[columns[0], columns[3]], - key="free-text-search", - visible=False, - filterable="standard", - ) - ) - global_actions = [grids.GridAction("Import history", dict(controller="", action="histories/import"))] - operations = [ - grids.GridOperation( - "Switch", allow_multiple=False, condition=(lambda item: not item.deleted), async_compatible=True - ), - grids.GridOperation("View", allow_multiple=False, url_args=dict(controller="", action="histories/view")), - grids.GridOperation( - "Share or Publish", - allow_multiple=False, - condition=(lambda item: not item.deleted), - url_args=dict(controller="", action="histories/sharing"), - ), - grids.GridOperation( - "Change Permissions", - allow_multiple=False, - condition=(lambda item: not item.deleted), - url_args=dict(controller="", action="histories/permissions"), - ), - grids.GridOperation( - "Copy", allow_multiple=False, condition=(lambda item: not item.deleted), async_compatible=False - ), - grids.GridOperation( - "Rename", - condition=(lambda item: not item.deleted), - url_args=dict(controller="", action="histories/rename"), - target="top", - ), - grids.GridOperation("Delete", condition=(lambda item: not item.deleted), async_compatible=True), - grids.GridOperation( - "Delete Permanently", - condition=(lambda item: not item.purged), - confirm="History contents will be removed from disk, this cannot be undone. Continue?", - async_compatible=True, - ), - grids.GridOperation( - "Undelete", condition=(lambda item: item.deleted and not item.purged), async_compatible=True - ), - ] - standard_filters = [ - grids.GridColumnFilter("Active", args=dict(deleted=False)), - grids.GridColumnFilter("Deleted", args=dict(deleted=True)), - grids.GridColumnFilter("All", args=dict(deleted="All")), - ] - default_filter = dict(name="All", status="active", tags="All", sharing="All") - num_rows_per_page = 15 - use_paging = True - info_text = "Histories that have been deleted for more than a time period specified by the Galaxy administrator(s) may be permanently deleted." - - def get_current_item(self, trans, **kwargs): - return trans.get_history() - - def apply_query_filter(self, trans, query, **kwargs): - return query.filter_by(user=trans.user, importing=False) - - -class SharedHistoryListGrid(grids.Grid): - # Custom column types - class DatasetsByStateColumn(grids.GridColumn): - def get_value(self, trans, grid, history): - rval = "" - for state in ("ok", "running", "queued", "error"): - total = sum(1 for d in history.active_datasets if d.state == state) - if total: - rval += f'
{total}
' - return rval - - class SharedByColumn(grids.GridColumn): - def get_value(self, trans, grid, history): - return escape(history.user.email) - - # Grid definition - title = "Histories shared with you by others" - model_class = model.History - default_sort_key = "-update_time" - columns = [ - grids.GridColumn("Name", key="name", attach_popup=True), - DatasetsByStateColumn("Datasets", sortable=False), - grids.GridColumn("Created", key="create_time", format=time_ago), - grids.GridColumn("Last Updated", key="update_time", format=time_ago), - SharedByColumn("Shared by", key="user_id"), - ] - operations = [ - grids.GridOperation("View", allow_multiple=False, url_args=dict(controller="", action="histories/view")), - grids.GridOperation("Copy", allow_multiple=False), - grids.GridOperation("Unshare", allow_multiple=False), - ] - - def build_initial_query(self, trans, **kwargs): - return trans.sa_session.query(self.model_class).join(self.model_class.users_shared_with) - - def apply_query_filter(self, trans, query, **kwargs): - return query.filter(model.HistoryUserShareAssociation.user == trans.user) - - class HistoryController(BaseUIController, SharableMixin, UsesAnnotations, UsesItemRatings): history_manager: histories.HistoryManager = depends(histories.HistoryManager) history_serializer: histories.HistorySerializer = depends(histories.HistorySerializer) @@ -263,193 +53,6 @@ def list_as_xml(self, trans): trans.response.set_content_type("text/xml") return trans.fill_template("/history/list_as_xml.mako") - # ......................................................................... lists - stored_list_grid = HistoryListGrid() - shared_list_grid = SharedHistoryListGrid() - - @web.legacy_expose_api - @web.require_login("work with multiple histories") - def list(self, trans, **kwargs): - """List all available histories""" - current_history = trans.get_history() - message = kwargs.get("message") - status = kwargs.get("status") - if "operation" in kwargs: - operation = kwargs["operation"].lower() - history_ids = listify(kwargs.get("id", [])) - # Display no message by default - status, message = None, None - # Load the histories and ensure they all belong to the current user - histories = [] - for history_id in history_ids: - history = self.history_manager.get_owned( - self.decode_id(history_id), trans.user, current_history=trans.history - ) - if history: - # Ensure history is owned by current user - if history.user_id is not None and trans.user: - assert trans.user.id == history.user_id, "History does not belong to current user" - histories.append(history) - else: - log.warning("Invalid history id '%r' passed to list", history_id) - if histories: - if operation == "switch": - status, message = self._list_switch(trans, histories) - # Take action to update UI to reflect history switch. If - # grid is using panels, it is standalone and hence a redirect - # to root is needed; if grid is not using panels, it is nested - # in the main Galaxy UI and refreshing the history frame - # is sufficient. - use_panels = kwargs.get("use_panels", False) == "True" - if use_panels: - return trans.response.send_redirect(url_for("/")) - else: - kwargs["refresh_frames"] = ["history"] - elif operation in ("delete", "delete permanently"): - status, message = self._list_delete(trans, histories, purge=(operation == "delete permanently")) - if current_history in histories: - # Deleted the current history, so a new, empty history was - # created automatically, and we need to refresh the history frame - kwargs["refresh_frames"] = ["history"] - elif operation == "undelete": - status, message = self._list_undelete(trans, histories) - - with transaction(trans.sa_session): - trans.sa_session.commit() - # Render the list view - if message and status: - kwargs["message"] = sanitize_text(message) - kwargs["status"] = status - return self.stored_list_grid(trans, **kwargs) - - def _list_delete(self, trans, histories, purge=False): - """Delete histories""" - n_deleted = 0 - deleted_current = False - message_parts = [] - status = SUCCESS - current_history = trans.get_history() - for history in histories: - try: - if history.users_shared_with: - raise exceptions.ObjectAttributeInvalidException( - f"History ({history.name}) has been shared with others, unshare it before deleting it." - ) - if purge: - self.history_manager.purge(history, user=trans.user) - else: - self.history_manager.delete(history) - if history == current_history: - deleted_current = True - except Exception as e: - message_parts.append(unicodify(e)) - status = ERROR - else: - trans.log_event(f"History ({history.name}) marked as deleted") - n_deleted += 1 - - if n_deleted: - part = "Deleted %d %s" % (n_deleted, iff(n_deleted != 1, "histories", "history")) - if purge and trans.app.config.allow_user_dataset_purge: - part += f" and removed {iff(n_deleted != 1, 'their', 'its')} dataset{iff(n_deleted != 1, 's', '')} from disk" - elif purge: - part += " but the datasets were not removed from disk because that feature is not enabled in this Galaxy instance" - message_parts.append(f"{part}. ") - if deleted_current: - # if this history is the current history for this session, - # - attempt to find the most recently used, undeleted history and switch to it. - # - If no suitable recent history is found, create a new one and switch - # note: this needs to come after commits above or will use an empty history that was deleted above - not_deleted_or_purged = [model.History.deleted == false(), model.History.purged == false()] - most_recent_history = self.history_manager.most_recent(user=trans.user, filters=not_deleted_or_purged) - if most_recent_history: - self.history_manager.set_current(trans, most_recent_history) - else: - trans.get_or_create_default_history() - message_parts.append("Your active history was deleted, a new empty history is now active. ") - status = INFO - return (status, " ".join(message_parts)) - - def _list_undelete(self, trans, histories): - """Undelete histories""" - n_undeleted = 0 - n_already_purged = 0 - for history in histories: - if history.purged: - n_already_purged += 1 - if history.deleted: - history.deleted = False - if not history.default_permissions: - # For backward compatibility - for a while we were deleting all DefaultHistoryPermissions on - # the history when we deleted the history. We are no longer doing this. - # Need to add default DefaultHistoryPermissions in case they were deleted when the history was deleted - default_action = trans.app.security_agent.permitted_actions.DATASET_MANAGE_PERMISSIONS - private_user_role = trans.app.security_agent.get_private_user_role(history.user) - default_permissions = {} - default_permissions[default_action] = [private_user_role] - trans.app.security_agent.history_set_default_permissions(history, default_permissions) - n_undeleted += 1 - trans.log_event("History (%s) %d marked as undeleted" % (history.name, history.id)) - status = SUCCESS - message_parts = [] - if n_undeleted: - message_parts.append("Undeleted %d %s. " % (n_undeleted, iff(n_undeleted != 1, "histories", "history"))) - if n_already_purged: - message_parts.append("%d histories have already been purged and cannot be undeleted." % n_already_purged) - status = WARNING - return status, "".join(message_parts) - - def _list_switch(self, trans, histories): - """Switch to a new different history""" - new_history = histories[0] - galaxy_session = trans.get_galaxy_session() - try: - stmt = ( - select(trans.app.model.GalaxySessionToHistoryAssociation) - .filter_by(session_id=galaxy_session.id, history_id=new_history.id) - .limit(1) - ) - association = trans.sa_session.scalars(stmt).first() - except Exception: - association = None - new_history.add_galaxy_session(galaxy_session, association=association) - trans.sa_session.add(new_history) - with transaction(trans.sa_session): - trans.sa_session.commit() - trans.set_history(new_history) - # No message - return None, None - - @web.expose - @web.json - @web.require_login("work with shared histories") - def list_shared(self, trans, **kwargs): - """List histories shared with current user by others""" - status = message = None - if "operation" in kwargs: - ids = listify(kwargs.get("id", [])) - operation = kwargs["operation"].lower() - if operation == "unshare": - if not ids: - message = "Select a history to unshare" - status = "error" - for id in ids: - # No need to check security, association below won't yield a - # hit if this user isn't having the history shared with her. - history = self.history_manager.by_id(self.decode_id(id)) - # Current user is the user with which the histories were shared - stmt = select(trans.app.model.HistoryUserShareAssociation).filter_by( - user=trans.user, history=history - ) - association = trans.sa_session.execute(select(stmt)).scalar_one() - trans.sa_session.delete(association) - with transaction(trans.sa_session): - trans.sa_session.commit() - message = "Unshared %d shared histories" % len(ids) - status = "done" - # Render the list view - return self.shared_list_grid(trans, status=status, message=message, **kwargs) - @web.expose def as_xml(self, trans, id=None, show_deleted=None, show_hidden=None): """ diff --git a/lib/galaxy/webapps/galaxy/controllers/page.py b/lib/galaxy/webapps/galaxy/controllers/page.py index 6b65c7917e77..0d4d11b993e6 100644 --- a/lib/galaxy/webapps/galaxy/controllers/page.py +++ b/lib/galaxy/webapps/galaxy/controllers/page.py @@ -1,16 +1,5 @@ -from markupsafe import escape -from sqlalchemy import ( - false, - true, -) -from sqlalchemy.orm import ( - joinedload, - undefer, -) - from galaxy import ( model, - util, web, ) from galaxy.managers.hdas import HDAManager @@ -20,7 +9,6 @@ ) from galaxy.managers.pages import ( get_page as get_page_, - get_shared_pages, page_exists, PageManager, ) @@ -32,14 +20,7 @@ from galaxy.schema.schema import CreatePagePayload from galaxy.structured_app import StructuredApp from galaxy.util.sanitize_html import sanitize_html -from galaxy.web import ( - error, - url_for, -) -from galaxy.web.framework.helpers import ( - grids, - time_ago, -) +from galaxy.web import error from galaxy.webapps.base.controller import ( BaseUIController, SharableMixin, @@ -49,317 +30,8 @@ from galaxy.webapps.galaxy.api import depends -def format_bool(b): - if b: - return "yes" - else: - return "" - - -class PageListGrid(grids.Grid): - # Custom column. - class URLColumn(grids.PublicURLColumn): - def get_value(self, trans, grid, item): - return url_for( - controller="page", action="display_by_username_and_slug", username=item.user.username, slug=item.slug - ) - - # Grid definition - use_panels = True - title = "Pages" - model_class = model.Page - default_filter = {"published": "All", "tags": "All", "title": "All", "sharing": "All"} - default_sort_key = "-update_time" - columns = [ - grids.TextColumn( - "Title", - key="title", - attach_popup=True, - filterable="advanced", - link=( - lambda item: dict(action="display_by_username_and_slug", username=item.user.username, slug=item.slug) - ), - ), - URLColumn("Permalink"), - grids.OwnerAnnotationColumn( - "Annotation", - key="annotation", - model_annotation_association_class=model.PageAnnotationAssociation, - filterable="advanced", - ), - grids.IndividualTagsColumn( - "Tags", - key="tags", - model_tag_association_class=model.PageTagAssociation, - filterable="advanced", - grid_name="PageListGrid", - ), - grids.SharingStatusColumn("Sharing", key="sharing", filterable="advanced", sortable=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="standard", - ) - ) - global_actions = [grids.GridAction("Add new page", dict(controller="", action="pages/create"))] - operations = [ - grids.DisplayByUsernameAndSlugGridOperation("View", allow_multiple=False), - grids.GridOperation("Edit content", allow_multiple=False, url_args=dict(controller="", action="pages/editor")), - grids.GridOperation("Edit attributes", allow_multiple=False, url_args=dict(controller="", action="pages/edit")), - grids.GridOperation( - "Share or Publish", - allow_multiple=False, - condition=(lambda item: not item.deleted), - url_args=dict(controller="", action="pages/sharing"), - ), - grids.GridOperation("Delete", confirm="Are you sure you want to delete this page?"), - ] - - def apply_query_filter(self, trans, query, **kwargs): - return query.filter_by(user=trans.user, deleted=False) - - -class PageAllPublishedGrid(grids.Grid): - # Grid definition - use_panels = True - title = "Published Pages" - model_class = model.Page - default_sort_key = "update_time" - default_filter = dict(title="All", username="All") - columns = [ - grids.PublicURLColumn("Title", key="title", filterable="advanced"), - grids.OwnerAnnotationColumn( - "Annotation", - key="annotation", - model_annotation_association_class=model.PageAnnotationAssociation, - filterable="advanced", - ), - grids.OwnerColumn("Owner", key="username", model_class=model.User, filterable="advanced"), - grids.CommunityRatingColumn("Community Rating", key="rating"), - grids.CommunityTagsColumn( - "Community Tags", - key="tags", - model_tag_association_class=model.PageTagAssociation, - filterable="advanced", - grid_name="PageAllPublishedGrid", - ), - grids.ReverseSortColumn("Last Updated", key="update_time", format=time_ago), - ] - columns.append( - grids.MulticolFilterColumn( - "Search title, annotation, owner, and tags", - cols_to_filter=[columns[0], columns[1], columns[2], columns[4]], - key="free-text-search", - visible=False, - filterable="standard", - ) - ) - - def build_initial_query(self, trans, **kwargs): - # See optimization description comments and TODO for tags in matching public histories query. - return ( - trans.sa_session.query(self.model_class) - .join("user") - .filter(model.User.deleted == false()) - .options( - joinedload(self.model_class.user).load_only(self.model_class.username), - joinedload(self.model_class.annotations), - undefer(self.model_class.average_rating), - ) - ) - - def apply_query_filter(self, trans, query, **kwargs): - return query.filter(self.model_class.deleted == false()).filter(self.model_class.published == true()) - - -class ItemSelectionGrid(grids.Grid): - """Base class for pages' item selection grids.""" - - # Custom columns. - class NameColumn(grids.TextColumn): - def get_value(self, trans, grid, item): - if hasattr(item, "get_display_name"): - return escape(item.get_display_name()) - else: - return escape(item.name) - - # Grid definition. - show_item_checkboxes = True - default_filter = {"deleted": "False", "sharing": "All"} - default_sort_key = "-update_time" - use_paging = True - num_rows_per_page = 10 - - def apply_query_filter(self, trans, query, **kwargs): - return query.filter_by(user=trans.user) - - -class HistorySelectionGrid(ItemSelectionGrid): - """Grid for selecting histories.""" - - # Grid definition. - title = "Saved Histories" - model_class = model.History - columns = [ - ItemSelectionGrid.NameColumn("Name", key="name", filterable="advanced"), - grids.IndividualTagsColumn( - "Tags", key="tags", model_tag_association_class=model.HistoryTagAssociation, filterable="advanced" - ), - grids.GridColumn("Last Updated", key="update_time", format=time_ago), - # Columns that are valid for filtering but are not visible. - grids.DeletedColumn("Deleted", key="deleted", visible=False, filterable="advanced"), - grids.SharingStatusColumn("Sharing", key="sharing", filterable="advanced", sortable=False, visible=False), - ] - columns.append( - grids.MulticolFilterColumn( - "Search", - cols_to_filter=[columns[0], columns[1]], - key="free-text-search", - visible=False, - filterable="standard", - ) - ) - - def apply_query_filter(self, trans, query, **kwargs): - return query.filter_by(user=trans.user, purged=False) - - -class HistoryDatasetAssociationSelectionGrid(ItemSelectionGrid): - """Grid for selecting HDAs.""" - - # Grid definition. - title = "Saved Datasets" - model_class = model.HistoryDatasetAssociation - columns = [ - ItemSelectionGrid.NameColumn("Name", key="name", filterable="advanced"), - grids.IndividualTagsColumn( - "Tags", - key="tags", - model_tag_association_class=model.HistoryDatasetAssociationTagAssociation, - filterable="advanced", - ), - grids.GridColumn("Last Updated", key="update_time", format=time_ago), - # Columns that are valid for filtering but are not visible. - grids.DeletedColumn("Deleted", key="deleted", visible=False, filterable="advanced"), - grids.SharingStatusColumn("Sharing", key="sharing", filterable="advanced", sortable=False, visible=False), - ] - columns.append( - grids.MulticolFilterColumn( - "Search", - cols_to_filter=[columns[0], columns[1]], - key="free-text-search", - visible=False, - filterable="standard", - ) - ) - - def apply_query_filter(self, trans, query, **kwargs): - # To filter HDAs by user, need to join HDA and History table and then filter histories by user. This is necessary because HDAs do not have - # a user relation. - return query.select_from(model.HistoryDatasetAssociation.table.join(model.History.table)).filter( - model.History.user == trans.user - ) - - -class WorkflowSelectionGrid(ItemSelectionGrid): - """Grid for selecting workflows.""" - - # Grid definition. - title = "Saved Workflows" - model_class = model.StoredWorkflow - columns = [ - ItemSelectionGrid.NameColumn("Name", key="name", filterable="advanced"), - grids.IndividualTagsColumn( - "Tags", key="tags", model_tag_association_class=model.StoredWorkflowTagAssociation, filterable="advanced" - ), - grids.GridColumn("Last Updated", key="update_time", format=time_ago), - # Columns that are valid for filtering but are not visible. - grids.DeletedColumn("Deleted", key="deleted", visible=False, filterable="advanced"), - grids.SharingStatusColumn("Sharing", key="sharing", filterable="advanced", sortable=False, visible=False), - ] - columns.append( - grids.MulticolFilterColumn( - "Search", - cols_to_filter=[columns[0], columns[1]], - key="free-text-search", - visible=False, - filterable="standard", - ) - ) - - -class PageSelectionGrid(ItemSelectionGrid): - """Grid for selecting pages.""" - - # Grid definition. - title = "Saved Pages" - model_class = model.Page - columns = [ - grids.TextColumn("Title", key="title", filterable="advanced"), - grids.IndividualTagsColumn( - "Tags", key="tags", model_tag_association_class=model.PageTagAssociation, filterable="advanced" - ), - grids.GridColumn("Last Updated", key="update_time", format=time_ago), - # Columns that are valid for filtering but are not visible. - grids.DeletedColumn("Deleted", key="deleted", visible=False, filterable="advanced"), - grids.SharingStatusColumn("Sharing", key="sharing", filterable="advanced", sortable=False, visible=False), - ] - columns.append( - grids.MulticolFilterColumn( - "Search", - cols_to_filter=[columns[0], columns[1]], - key="free-text-search", - visible=False, - filterable="standard", - ) - ) - - -class VisualizationSelectionGrid(ItemSelectionGrid): - """Grid for selecting visualizations.""" - - # Grid definition. - title = "Saved Visualizations" - model_class = model.Visualization - columns = [ - grids.TextColumn("Title", key="title", filterable="advanced"), - grids.TextColumn("Type", key="type"), - grids.IndividualTagsColumn( - "Tags", - key="tags", - model_tag_association_class=model.VisualizationTagAssociation, - filterable="advanced", - grid_name="VisualizationListGrid", - ), - grids.SharingStatusColumn("Sharing", key="sharing", filterable="advanced", sortable=False), - 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="standard", - ) - ) - - # Adapted from the _BaseHTMLProcessor class of https://github.com/kurtmckee/feedparser class PageController(BaseUIController, SharableMixin, UsesStoredWorkflowMixin, UsesVisualizationMixin, UsesItemRatings): - _page_list = PageListGrid() - _all_published_list = PageAllPublishedGrid() - _history_selection_grid = HistorySelectionGrid() - _workflow_selection_grid = WorkflowSelectionGrid() - _datasets_selection_grid = HistoryDatasetAssociationSelectionGrid() - _page_selection_grid = PageSelectionGrid() - _visualization_selection_grid = VisualizationSelectionGrid() page_manager: PageManager = depends(PageManager) history_manager: HistoryManager = depends(HistoryManager) history_serializer: HistorySerializer = depends(HistorySerializer) @@ -370,43 +42,6 @@ class PageController(BaseUIController, SharableMixin, UsesStoredWorkflowMixin, U def __init__(self, app: StructuredApp): super().__init__(app) - @web.expose - @web.json - @web.require_login() - def list(self, trans, *args, **kwargs): - """List user's pages.""" - # Handle operation - if "operation" in kwargs and "id" in kwargs: - session = trans.sa_session - operation = kwargs["operation"].lower() - ids = util.listify(kwargs["id"]) - for id in ids: - if operation == "delete": - item = session.get(model.Page, self.decode_id(id)) - self.security_check(trans, item, check_ownership=True) - item.deleted = True - with transaction(session): - session.commit() - - # Build grid dictionary. - grid = self._page_list(trans, *args, **kwargs) - grid["shared_by_others"] = self._get_shared(trans) - return grid - - @web.expose - @web.json - def list_published(self, trans, *args, **kwargs): - grid = self._all_published_list(trans, *args, **kwargs) - grid["shared_by_others"] = self._get_shared(trans) - return grid - - def _get_shared(self, trans): - """Identify shared pages""" - shared_by_others = get_shared_pages(trans.sa_session, trans.get_user()) - return [ - {"username": p.page.user.username, "slug": p.page.slug, "title": p.page.title} for p in shared_by_others - ] - @web.expose_api @web.require_login("create pages") def create(self, trans, payload=None, **kwd): @@ -560,41 +195,6 @@ def display_by_username_and_slug(self, trans, username, slug, **kwargs): ) ) - @web.expose - @web.json - @web.require_login("select a history from saved histories") - def list_histories_for_selection(self, trans, **kwargs): - """Returns HTML that enables a user to select one or more histories.""" - return self._history_selection_grid(trans, **kwargs) - - @web.expose - @web.json - @web.require_login("select a workflow from saved workflows") - def list_workflows_for_selection(self, trans, **kwargs): - """Returns HTML that enables a user to select one or more workflows.""" - return self._workflow_selection_grid(trans, **kwargs) - - @web.expose - @web.json - @web.require_login("select a visualization from saved visualizations") - def list_visualizations_for_selection(self, trans, **kwargs): - """Returns HTML that enables a user to select one or more visualizations.""" - return self._visualization_selection_grid(trans, **kwargs) - - @web.expose - @web.json - @web.require_login("select a page from saved pages") - def list_pages_for_selection(self, trans, **kwargs): - """Returns HTML that enables a user to select one or more pages.""" - return self._page_selection_grid(trans, **kwargs) - - @web.expose - @web.json - @web.require_login("select a dataset from saved datasets") - def list_datasets_for_selection(self, trans, **kwargs): - """Returns HTML that enables a user to select one or more datasets.""" - return self._datasets_selection_grid(trans, **kwargs) - def get_page(self, trans, id, check_ownership=True, check_accessible=False): """Get a page from the database by id.""" # Load history from database @@ -604,6 +204,3 @@ def get_page(self, trans, id, check_ownership=True, check_accessible=False): error("Page not found") else: return self.security_check(trans, page, check_ownership, check_accessible) - - def get_item(self, trans, id): - return self.get_page(trans, id) diff --git a/lib/galaxy/webapps/galaxy/controllers/workflow.py b/lib/galaxy/webapps/galaxy/controllers/workflow.py index 8cf8d5f629bc..2d2eec42e5d2 100644 --- a/lib/galaxy/webapps/galaxy/controllers/workflow.py +++ b/lib/galaxy/webapps/galaxy/controllers/workflow.py @@ -3,12 +3,7 @@ from markupsafe import escape from sqlalchemy import desc -from sqlalchemy.orm import ( - joinedload, - lazyload, - undefer, -) -from sqlalchemy.sql import expression +from sqlalchemy.orm import joinedload from galaxy import ( model, @@ -26,10 +21,6 @@ from galaxy.util import FILENAME_VALID_CHARS from galaxy.util.sanitize_html import sanitize_html from galaxy.web import url_for -from galaxy.web.framework.helpers import ( - grids, - time_ago, -) from galaxy.webapps.base.controller import ( BaseUIController, SharableMixin, @@ -44,131 +35,6 @@ log = logging.getLogger(__name__) -class StoredWorkflowListGrid(grids.Grid): - class StepsColumn(grids.GridColumn): - def get_value(self, trans, grid, workflow): - return len(workflow.latest_workflow.steps) - - # Grid definition - use_panels = True - title = "Saved Workflows" - model_class = model.StoredWorkflow - default_filter = {"name": "All", "tags": "All"} - default_sort_key = "-update_time" - columns = [ - grids.TextColumn("Name", key="name", attach_popup=True, filterable="advanced"), - grids.IndividualTagsColumn( - "Tags", - "tags", - model_tag_association_class=model.StoredWorkflowTagAssociation, - filterable="advanced", - grid_name="StoredWorkflowListGrid", - ), - StepsColumn("Steps"), - 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[1]], - key="free-text-search", - visible=False, - filterable="standard", - ) - ) - operations = [ - grids.GridOperation( - "Edit", allow_multiple=False, condition=(lambda item: not item.deleted), async_compatible=False - ), - grids.GridOperation("Run", condition=(lambda item: not item.deleted), async_compatible=False), - grids.GridOperation("Copy", condition=(lambda item: not item.deleted), async_compatible=False), - grids.GridOperation("Rename", condition=(lambda item: not item.deleted), async_compatible=False), - grids.GridOperation("Sharing", condition=(lambda item: not item.deleted), async_compatible=False), - grids.GridOperation("Delete", condition=(lambda item: item.deleted), async_compatible=True), - ] - - def apply_query_filter(self, trans, query, **kwargs): - return query.filter_by(user=trans.user, deleted=False) - - -class StoredWorkflowAllPublishedGrid(grids.Grid): - title = "Published Workflows" - model_class = model.StoredWorkflow - default_sort_key = "update_time" - default_filter = dict(public_url="All", username="All", tags="All") - columns = [ - grids.PublicURLColumn("Name", key="name", filterable="advanced", attach_popup=True), - grids.OwnerAnnotationColumn( - "Annotation", - key="annotation", - model_annotation_association_class=model.StoredWorkflowAnnotationAssociation, - filterable="advanced", - ), - grids.OwnerColumn("Owner", key="username", model_class=model.User, filterable="advanced"), - grids.CommunityRatingColumn("Community Rating", key="rating"), - grids.CommunityTagsColumn( - "Community Tags", - key="tags", - model_tag_association_class=model.StoredWorkflowTagAssociation, - filterable="advanced", - grid_name="PublicWorkflowListGrid", - ), - grids.ReverseSortColumn("Last Updated", key="update_time", format=time_ago), - ] - columns.append( - grids.MulticolFilterColumn( - "Search name, annotation, owner, and tags", - cols_to_filter=[columns[0], columns[1], columns[2], columns[4]], - key="free-text-search", - visible=False, - filterable="standard", - ) - ) - operations = [ - grids.GridOperation( - "Run", - condition=(lambda item: not item.deleted), - allow_multiple=False, - url_args=dict(controller="workflows", action="run"), - ), - grids.GridOperation( - "Import", condition=(lambda item: not item.deleted), allow_multiple=False, url_args=dict(action="imp") - ), - grids.GridOperation( - "Save as File", - condition=(lambda item: not item.deleted), - allow_multiple=False, - url_args=dict(action="export_to_file"), - ), - ] - num_rows_per_page = 50 - use_paging = True - - def build_initial_query(self, trans, **kwargs): - # See optimization description comments and TODO for tags in matching public histories query. - # In addition to that - be sure to lazyload the latest_workflow - it isn't needed and it causes all - # of its steps to be eagerly loaded. - return ( - trans.sa_session.query(self.model_class) - .join(self.model_class.user) - .options( - lazyload(self.model_class.latest_workflow), - joinedload(self.model_class.user).load_only(model.User.username), - joinedload(self.model_class.annotations), - undefer(self.model_class.average_rating), - ) - ) - - def apply_query_filter(self, trans, query, **kwargs): - # A public workflow is published, has a slug, and is not deleted. - return ( - query.filter(self.model_class.published == expression.true()) - .filter(self.model_class.slug.isnot(None)) - .filter(self.model_class.deleted == expression.false()) - ) - - # Simple HTML parser to get all content in a single tag. class SingleTagContentsParser(HTMLParser): def __init__(self, target_tag): @@ -189,29 +55,8 @@ def handle_data(self, text): class WorkflowController(BaseUIController, SharableMixin, UsesStoredWorkflowMixin, UsesItemRatings): - stored_list_grid = StoredWorkflowListGrid() - published_list_grid = StoredWorkflowAllPublishedGrid() slug_builder = SlugBuilder() - @web.expose - @web.require_login("use Galaxy workflows") - def list_grid(self, trans, **kwargs): - """List user's stored workflows.""" - # status = message = None - if "operation" in kwargs: - operation = kwargs["operation"].lower() - if operation == "rename": - return self.rename(trans, **kwargs) - workflow_ids = util.listify(kwargs.get("id", [])) - if operation == "sharing": - return self.sharing(trans, id=workflow_ids) - return self.stored_list_grid(trans, **kwargs) - - @web.expose - @web.json - def list_published(self, trans, **kwargs): - return self.published_list_grid(trans, **kwargs) - @web.expose def display_by_username_and_slug(self, trans, username, slug, format="html", **kwargs): """ @@ -647,6 +492,3 @@ def build_from_current_history( f'Workflow "{escape(workflow_name)}" created from current history. ' f'You can edit or run the workflow.' ) - - def get_item(self, trans, id): - return self.get_stored_workflow(trans, id)