diff --git a/.ci/validate_test_tools.sh b/.ci/validate_test_tools.sh index 9b43e7a6c46..258799dc95d 100755 --- a/.ci/validate_test_tools.sh +++ b/.ci/validate_test_tools.sh @@ -1,11 +1,11 @@ #!/bin/sh -cd `dirname $0`/.. +cd "$(dirname "$0")"/.. xsd_path="lib/galaxy/tools/xsd/galaxy.xsd" # Lint the XSD xmllint --noout "$xsd_path" test_tools_path='test/functional/tools' -tool_files_list=$(ls "$test_tools_path"/*xml | grep -v '_conf.xml$') +tool_files_list=$(ls "$test_tools_path"/*.xml | grep -v '_conf.xml$') sh scripts/validate_tools.sh $tool_files_list diff --git a/.circleci/config.yml b/.circleci/config.yml index 01b3105991a..32dfbce7036 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -25,7 +25,7 @@ variables: jobs: get_code: docker: - - image: circleci/python:2.7.15 + - image: circleci/python:3.5 <<: *set_workdir steps: # Replace standard code checkout with shallow clone to speed things up. @@ -83,7 +83,7 @@ jobs: - ~/repo py27_lint: docker: - - image: circleci/python:2.7.15 + - image: circleci/python:2.7 <<: *set_workdir steps: - *restore_repo_cache @@ -91,7 +91,7 @@ jobs: - run: tox -e py27-lint py27_unit: docker: - - image: circleci/python:2.7.15 + - image: circleci/python:2.7 <<: *set_workdir steps: - *restore_repo_cache @@ -99,7 +99,7 @@ jobs: - run: tox -e py27-unit py27_docstring: docker: - - image: circleci/python:2.7.15 + - image: circleci/python:2.7 <<: *set_workdir steps: - *restore_repo_cache @@ -107,7 +107,7 @@ jobs: - run: tox -e py27-lint_docstring_include_list py27_first_startup: docker: - - image: circleci/python:2.7.15 + - image: circleci/python:2.7 <<: *set_workdir steps: - *restore_repo_cache @@ -139,14 +139,14 @@ jobs: <<: *set_workdir steps: - *restore_repo_cache - - *install_tox - run: sudo apt-get update # For uwsgi - run: sudo apt-get install -y libpython3.5-dev + - *install_tox - run: tox -e py35-first_startup validate_test_tools: docker: - - image: circleci/python:2.7.15 + - image: circleci/python:3.5 <<: *set_workdir steps: - *restore_repo_cache @@ -154,9 +154,17 @@ jobs: - run: sudo apt-get install -y libxml2-utils - *install_tox - run: tox -e validate_test_tools + test_galaxy_packages: + docker: + - image: circleci/python:3.5 + <<: *set_workdir + steps: + - *restore_repo_cache + - *install_tox + - run: tox -e test_galaxy_packages check_py3_compatibility: docker: - - image: circleci/python:2.7.15 + - image: circleci/python:3.5 <<: *set_workdir steps: - *restore_repo_cache @@ -202,6 +210,8 @@ workflows: <<: *requires_get_code - py35_first_startup: <<: *requires_get_code + - test_galaxy_packages: + <<: *requires_get_code - validate_test_tools: <<: *requires_get_code - check_py3_compatibility: diff --git a/client/galaxy/scripts/bundleEntries.js b/client/galaxy/scripts/bundleEntries.js index c666d664310..0390749fb83 100644 --- a/client/galaxy/scripts/bundleEntries.js +++ b/client/galaxy/scripts/bundleEntries.js @@ -79,6 +79,7 @@ export function multiHistory(options) { export { default as panels } from "layout/panel"; export { show_in_overlay, hide_modal, show_message, show_modal, Modal } from "layout/modal"; export { make_popupmenu, make_popup_menus } from "ui/popupmenu"; +export { render_embedded_items } from "mvc/embedded-objects"; export { default as async_save_text } from "utils/async-save-text"; // Previously "chart" diff --git a/client/galaxy/scripts/components/PageEditor/PageEditor.vue b/client/galaxy/scripts/components/PageEditor/PageEditor.vue new file mode 100644 index 00000000000..f589b15e23a --- /dev/null +++ b/client/galaxy/scripts/components/PageEditor/PageEditor.vue @@ -0,0 +1,38 @@ + + + diff --git a/client/galaxy/scripts/components/PageEditor/PageEditorHtml.vue b/client/galaxy/scripts/components/PageEditor/PageEditorHtml.vue new file mode 100644 index 00000000000..bce1497ac72 --- /dev/null +++ b/client/galaxy/scripts/components/PageEditor/PageEditorHtml.vue @@ -0,0 +1,682 @@ + + + + + diff --git a/client/galaxy/scripts/components/PageEditor/index.js b/client/galaxy/scripts/components/PageEditor/index.js new file mode 100644 index 00000000000..326ba8fc57f --- /dev/null +++ b/client/galaxy/scripts/components/PageEditor/index.js @@ -0,0 +1 @@ +export { default as PageEditor } from "./PageEditor.vue"; diff --git a/client/galaxy/scripts/components/PageEditor/util.js b/client/galaxy/scripts/components/PageEditor/util.js new file mode 100644 index 00000000000..5339f5172f6 --- /dev/null +++ b/client/galaxy/scripts/components/PageEditor/util.js @@ -0,0 +1,24 @@ +import $ from "jquery"; +import _l from "utils/localization"; +import { getAppRoot } from "onload/loadConfig"; +import { show_modal, hide_modal } from "layout/modal"; + +export const save = (pageId, content) => { + console.log(`content is ${content}`); + + show_modal(_l("Saving page"), _l("progress")); + + // Do save. + $.ajax({ + url: `${getAppRoot()}page/save`, + type: "POST", + data: { + id: pageId, + content: content, + _: "true" + }, + success: function() { + hide_modal(); + } + }); +}; diff --git a/client/galaxy/scripts/galaxy.pages.js b/client/galaxy/scripts/galaxy.pages.js index d9a1cf470fd..fd31483c8cc 100644 --- a/client/galaxy/scripts/galaxy.pages.js +++ b/client/galaxy/scripts/galaxy.pages.js @@ -1,681 +1,31 @@ import $ from "jquery"; -import _l from "utils/localization"; +import axios from "axios"; +import { Toast } from "ui/toast"; +import { getAppRoot } from "onload/loadConfig"; +import { PageEditor } from "components/PageEditor"; +import Vue from "vue"; -import GridView from "mvc/grid/grid-view"; -import { show_modal, hide_modal } from "layout/modal"; -import { make_popupmenu } from "ui/popupmenu"; -import { getGalaxyInstance } from "app"; -import { buildConfig } from "utils/genericConfig"; - -// Built a generic config container for the properties that -// are passed in from python then used (formerly) globally -const configs = buildConfig(); - -// Turn these window variables into config props list_objects_url, -// set_accessible_url, get_name_and_link_url, editor_base_path, -// iframe_base_path, save_url, page_id, page_list_url -function setPageConfigs(incoming = {}) { - return Object.assign(configs, incoming); -} - -var WYMeditor = window.WYMeditor; - -var CONTROLS = { - // Item types. - ITEM_HISTORY: "item_history", - ITEM_DATASET: "item_dataset", - ITEM_WORKFLOW: "item_workflow", - ITEM_PAGE: "item_page", - ITEM_VISUALIZATION: "item_visualization", - - // Link dialogs. - DIALOG_HISTORY_LINK: "link_history", - DIALOG_DATASET_LINK: "link_dataset", - DIALOG_WORKFLOW_LINK: "link_workflow", - DIALOG_PAGE_LINK: "link_page", - DIALOG_VISUALIZATION_LINK: "link_visualization", - - // Embed dialogs. - DIALOG_EMBED_HISTORY: "embed_history", - DIALOG_EMBED_DATASET: "embed_dataset", - DIALOG_EMBED_WORKFLOW: "embed_workflow", - DIALOG_EMBED_PAGE: "embed_page", - DIALOG_EMBED_VISUALIZATION: "embed_visualization" -}; - -// Based on the dialog type, return a dictionary of information about an item -function get_item_info(dialog_type) { - var item_singular; - var item_plural; - var item_controller; - var item_class; - switch (dialog_type) { - case CONTROLS.ITEM_HISTORY: - item_singular = "History"; - item_plural = "Histories"; - item_controller = "history"; - item_class = "History"; - break; - case CONTROLS.ITEM_DATASET: - item_singular = "Dataset"; - item_plural = "Datasets"; - item_controller = "dataset"; - item_class = "HistoryDatasetAssociation"; - break; - case CONTROLS.ITEM_WORKFLOW: - item_singular = "Workflow"; - item_plural = "Workflows"; - item_controller = "workflow"; - item_class = "StoredWorkflow"; - break; - case CONTROLS.ITEM_PAGE: - item_singular = "Page"; - item_plural = "Pages"; - item_controller = "page"; - item_class = "Page"; - break; - case CONTROLS.ITEM_VISUALIZATION: - item_singular = "Visualization"; - item_plural = "Visualizations"; - item_controller = "visualization"; - item_class = "Visualization"; - break; - } - - // Build ajax URL that lists items for selection. - var item_list_action = `list_${item_plural.toLowerCase()}_for_selection`; - var ajax_url = configs.list_objects_url.replace("LIST_ACTION", item_list_action); - - // Set up and return dict. - return { - singular: item_singular, - plural: item_plural, - controller: item_controller, - iclass: item_class, - list_ajax_url: ajax_url - }; -} - -// Make an item importable. -function make_item_importable(item_controller, item_id, item_type) { - var ajax_url = configs.set_accessible_url.replace("ITEM_CONTROLLER", item_controller); - $.ajax({ - type: "POST", - url: ajax_url, - data: { id: item_id, accessible: "True" }, - error: function() { - alert(`Making ${item_type} accessible failed`); - } - }); -} - -// Completely replace WYM's dialog handling -WYMeditor.editor.prototype.dialog = function(dialogType, dialogFeatures, bodyHtml) { - var wym = this; - var sStamp = wym.uniqueStamp(); - var selected = wym.selected(); - - // Swap out URL attribute for id/name attribute in link creation to enable anchor creation in page. - function set_link_id() { - // When "set link id" link clicked, update UI. - $("#set_link_id").click(function() { - // Set label. - $("#link_attribute_label").text("ID/Name"); - - // Set input elt class, value. - var attribute_input = $(".wym_href"); - attribute_input.addClass("wym_id").removeClass("wym_href"); - if (selected) attribute_input.val($(selected).attr("id")); - - // Remove link. - $(this).remove(); - }); - } - - // LINK DIALOG - if (dialogType == WYMeditor.DIALOG_LINK) { - if (selected) { - $(wym._options.hrefSelector).val($(selected).attr(WYMeditor.HREF)); - $(wym._options.srcSelector).val($(selected).attr(WYMeditor.SRC)); - $(wym._options.titleSelector).val($(selected).attr(WYMeditor.TITLE)); - $(wym._options.altSelector).val($(selected).attr(WYMeditor.ALT)); - } - - // Get current URL, title. - var curURL; - - var curTitle; - if (selected) { - curURL = $(selected).attr("href"); - if (curURL == undefined) curURL = ""; - curTitle = $(selected).attr("title"); - if (curTitle == undefined) curTitle = ""; - } - show_modal( - "Create Link", - `


`, - { - "Make link": function() { - // Get URL, name/title. - var sUrl = $(wym._options.hrefSelector).val() || ""; - - var sId = $(".wym_id").val() || ""; - var sName = $(wym._options.titleSelector).val() || ""; - - if (sUrl || sId) { - // Create link. - wym._exec(WYMeditor.CREATE_LINK, sStamp); - - // Set link attributes. - var link = $(`a[href=${sStamp}]`, wym._doc.body); - link.attr(WYMeditor.HREF, sUrl) - .attr(WYMeditor.TITLE, sName) - .attr("id", sId); - - // If link's text is default (wym-...), change it to the title. - if (link.text().indexOf("wym-") === 0) { - link.text(sName); - } - } - hide_modal(); - }, - Cancel: function() { - hide_modal(); - } - }, - {}, - set_link_id - ); - } - - // IMAGE DIALOG - if (dialogType == WYMeditor.DIALOG_IMAGE) { - if (wym._selected_image) { - $(`${wym._options.dialogImageSelector} ${wym._options.srcSelector}`).val( - $(wym._selected_image).attr(WYMeditor.SRC) - ); - $(`${wym._options.dialogImageSelector} ${wym._options.titleSelector}`).val( - $(wym._selected_image).attr(WYMeditor.TITLE) - ); - $(`${wym._options.dialogImageSelector} ${wym._options.altSelector}`).val( - $(wym._selected_image).attr(WYMeditor.ALT) - ); - } - show_modal( - "Image", - "
" + - "
" + - "" + - "
" + - "
" + - "
" + - "" + - "
" + - "
" + - "
" + - "" + - "
", - { - Insert: function() { - var sUrl = $(wym._options.srcSelector).val(); - if (sUrl.length > 0) { - wym._exec(WYMeditor.INSERT_IMAGE, sStamp); - $(`img[src$=${sStamp}]`, wym._doc.body) - .attr(WYMeditor.SRC, sUrl) - .attr(WYMeditor.TITLE, $(wym._options.titleSelector).val()) - .attr(WYMeditor.ALT, $(wym._options.altSelector).val()); - } - hide_modal(); - }, - Cancel: function() { - hide_modal(); - } - } - ); - return; - } - - // TABLE DIALOG - if (dialogType == WYMeditor.DIALOG_TABLE) { - show_modal( - "Table", - "
" + - "
" + - "" + - "
" + - "
" + - "
" + - "" + - "
" + - "
" + - "" + - "" + - "
" + - "
" + - "" + - "" + - "
", - { - Insert: function() { - var iRows = $(wym._options.rowsSelector).val(); - var iCols = $(wym._options.colsSelector).val(); - - if (iRows > 0 && iCols > 0) { - var table = wym._doc.createElement(WYMeditor.TABLE); - var newRow = null; - - var sCaption = $(wym._options.captionSelector).val(); - - //we create the caption - var newCaption = table.createCaption(); - newCaption.innerHTML = sCaption; - - //we create the rows and cells - for (var x = 0; x < iRows; x++) { - newRow = table.insertRow(x); - for (var y = 0; y < iCols; y++) { - newRow.insertCell(y); - } - } - - //set the summary attr - $(table).attr("summary", $(wym._options.summarySelector).val()); - - //append the table after the selected container - var node = $(wym.findUp(wym.container(), WYMeditor.MAIN_CONTAINERS)).get(0); - if (!node || !node.parentNode) $(wym._doc.body).append(table); - else $(node).after(table); - } - hide_modal(); +export default function pagesEditorOnload() { + const pageId = $("#page-editor-content").attr("page_id"); + axios + .get(`${getAppRoot()}api/pages/${pageId}`) + .then(response => { + const pageEditorInstance = Vue.extend(PageEditor); + new pageEditorInstance({ + propsData: { + pageId: pageId, + content: response.data.content, + title: response.data.title }, - Cancel: function() { - hide_modal(); - } + el: "#page-editor-content" + }); + }) + .catch(e => { + const response = e.response; + if (typeof response.responseJSON !== "undefined") { + Toast.error(response.responseJSON.err_msg); + } else { + Toast.error("An error occurred."); } - ); - } - - // INSERT "GALAXY ITEM" LINK DIALOG - if ( - dialogType == CONTROLS.DIALOG_HISTORY_LINK || - dialogType == CONTROLS.DIALOG_DATASET_LINK || - dialogType == CONTROLS.DIALOG_WORKFLOW_LINK || - dialogType == CONTROLS.DIALOG_PAGE_LINK || - dialogType == CONTROLS.DIALOG_VISUALIZATION_LINK - ) { - // Based on item type, set useful vars. - var item_info; - switch (dialogType) { - case CONTROLS.DIALOG_HISTORY_LINK: - item_info = get_item_info(CONTROLS.ITEM_HISTORY); - break; - case CONTROLS.DIALOG_DATASET_LINK: - item_info = get_item_info(CONTROLS.ITEM_DATASET); - break; - case CONTROLS.DIALOG_WORKFLOW_LINK: - item_info = get_item_info(CONTROLS.ITEM_WORKFLOW); - break; - case CONTROLS.DIALOG_PAGE_LINK: - item_info = get_item_info(CONTROLS.ITEM_PAGE); - break; - case CONTROLS.DIALOG_VISUALIZATION_LINK: - item_info = get_item_info(CONTROLS.ITEM_VISUALIZATION); - break; - } - - var grid = new GridView({ - url_base: item_info.list_ajax_url, - embedded: true - }); - const Galaxy = getGalaxyInstance(); - Galaxy.modal.show({ - title: `Insert Link to ${item_info.singular}`, - body: $("
") - .append(grid.$el) - .append( - $("
") - .append('') - .append( - `Make the selected ${item_info.plural.toLowerCase()} accessible so that they can viewed by everyone.` - ) - ), - closing_events: true, - buttons: { - Insert: function() { - // Make selected items accessible (importable) ? - var make_importable = false; - if ($("#make-importable:checked").val() != null) make_importable = true; - - // Insert links to history for each checked item. - grid.$("input[name=id]:checked").each(function() { - var item_id = $(this).val(); - - // Make item importable? - if (make_importable) make_item_importable(item_info.controller, item_id, item_info.singular); - - // Insert link(s) to item(s). This is done by getting item info and then manipulating wym. - var url_template = configs.get_name_and_link_url + item_id; - var ajax_url = url_template.replace("ITEM_CONTROLLER", item_info.controller); - $.getJSON(ajax_url, returned_item_info => { - // Get link text. - wym._exec(WYMeditor.CREATE_LINK, sStamp); - var link_text = $(`a[href=${sStamp}]`, wym._doc.body).text(); - - // Insert link: need to do different actions depending on link text. - if ( - link_text == "" || // Firefox. - link_text == sStamp // Safari - ) { - // User selected no text; create link from scratch and use default text. - wym.insert( - `${item_info.singular} '${ - returned_item_info.name - }'` - ); - } else { - // Link created from selected text; add href and title. - $(`a[href=${sStamp}]`, wym._doc.body) - .attr(WYMeditor.HREF, returned_item_info.link) - .attr(WYMeditor.TITLE, item_info.singular + item_id); - } - }); - }); - Galaxy.modal.hide(); - }, - Close: function() { - Galaxy.modal.hide(); - } - } - }); - } - // EMBED GALAXY OBJECT DIALOGS - if ( - dialogType == CONTROLS.DIALOG_EMBED_HISTORY || - dialogType == CONTROLS.DIALOG_EMBED_DATASET || - dialogType == CONTROLS.DIALOG_EMBED_WORKFLOW || - dialogType == CONTROLS.DIALOG_EMBED_PAGE || - dialogType == CONTROLS.DIALOG_EMBED_VISUALIZATION - ) { - // Based on item type, set useful vars. - let item_info; - switch (dialogType) { - case CONTROLS.DIALOG_EMBED_HISTORY: - item_info = get_item_info(CONTROLS.ITEM_HISTORY); - break; - case CONTROLS.DIALOG_EMBED_DATASET: - item_info = get_item_info(CONTROLS.ITEM_DATASET); - break; - case CONTROLS.DIALOG_EMBED_WORKFLOW: - item_info = get_item_info(CONTROLS.ITEM_WORKFLOW); - break; - case CONTROLS.DIALOG_EMBED_PAGE: - item_info = get_item_info(CONTROLS.ITEM_PAGE); - break; - case CONTROLS.DIALOG_EMBED_VISUALIZATION: - item_info = get_item_info(CONTROLS.ITEM_VISUALIZATION); - break; - } - - const grid = new GridView({ - url_base: item_info.list_ajax_url, - embedded: true }); - const Galaxy = getGalaxyInstance(); - Galaxy.modal.show({ - title: `Insert Link to ${item_info.singular}`, - body: $("
") - .append(grid.$el) - .append( - $("
") - .append('') - .append( - `Make the selected ${item_info.plural.toLowerCase()} accessible so that they can viewed by everyone.` - ) - ), - closing_events: true, - buttons: { - Embed: function() { - // Make selected items accessible (importable) ? - var make_importable = false; - if ($("#make-importable:checked").val() != null) make_importable = true; - - grid.$("input[name=id]:checked").each(function() { - var elt = $(this); - // Get item ID and name. - var item_id = elt.val(); - // Use ':first' because there are many labels in table; the first one is the item name. - var item_name = elt - .closest("td") - .next("td") - .find("label") - .text(); - - if (make_importable) make_item_importable(item_info.controller, item_id, item_info.singular); - - // Embedded item HTML; item class is embedded in div container classes; this is necessary because the editor strips - // all non-standard attributes when it returns its content (e.g. it will not return an element attribute of the form - // item_class='History'). - var item_elt_id = `${item_info.iclass}-${item_id}`; - var item_embed_html = [ - "
", - "

", - "Embedded Galaxy ", - item_info.singular, - " '", - item_name, - "'", - "

", - "

", - "[Do not edit this block; Galaxy will fill it in with the annotated ", - item_info.singular.toLowerCase(), - " when it is displayed.]", - "

", - "
" - ].join(""); - - // Insert embedded item into document. - wym.insert(item_embed_html); - }); - Galaxy.modal.hide(); - }, - Close: function() { - Galaxy.modal.hide(); - } - } - }); - } -}; - -export default function pagesEditorOnload(renderedConfigs = {}) { - setPageConfigs(renderedConfigs); - - // Generic error handling - $(document).ajaxError((e, x) => { - // console.log( e, x ); - var message = x.responseText || x.statusText || "Could not connect to server"; - show_modal("Server error", message, { "Ignore error": hide_modal }); - return false; - }); - // Create editor - $("[name=page_content]").wymeditor({ - skin: "galaxy", - basePath: configs.editor_base_path, - iframeBasePath: configs.iframe_base_path, - // + WYMeditor.HTML - boxHtml: `
${ - WYMeditor.TOOLS - }
${ - // + WYMeditor.HTML - WYMeditor.IFRAME - }${WYMeditor.STATUS}
`, - toolsItems: [ - { name: "Bold", title: "Strong", css: "wym_tools_strong" }, - { name: "Italic", title: "Emphasis", css: "wym_tools_emphasis" }, - { - name: "Superscript", - title: _l("Superscript"), - css: "wym_tools_superscript" - }, - { - name: "Subscript", - title: _l("Subscript"), - css: "wym_tools_subscript" - }, - { - name: "InsertOrderedList", - title: "Ordered_List", - css: "wym_tools_ordered_list" - }, - { - name: "InsertUnorderedList", - title: "Unordered_List", - css: "wym_tools_unordered_list" - }, - { name: "Indent", title: "Indent", css: "wym_tools_indent" }, - { name: "Outdent", title: "Outdent", css: "wym_tools_outdent" }, - { name: "Undo", title: "Undo", css: "wym_tools_undo" }, - { name: "Redo", title: "Redo", css: "wym_tools_redo" }, - { name: "CreateLink", title: "Link", css: "wym_tools_link" }, - { name: "Unlink", title: "Unlink", css: "wym_tools_unlink" }, - { name: "InsertImage", title: "Image", css: "wym_tools_image" }, - { name: "InsertTable", title: "Table", css: "wym_tools_table" } - ] - }); - // Get the editor object - var editor = $.wymeditors(0); - var save = callback => { - show_modal("Saving page", "progress"); - - // Do save. - $.ajax({ - url: configs.save_url, - type: "POST", - data: { - id: configs.page_id, - content: editor.xhtml(), - annotations: JSON.stringify({}), - // annotations: JSON.stringify(annotations), - _: "true" - }, - success: function() { - callback(); - } - }); - }; - // Save button - $("#save-button").click(() => { - save(() => { - hide_modal(); - }); - }); - // Close button - $("#close-button").click(() => { - // var new_content = editor.xhtml(); - // var changed = ( initial_content != new_content ); - var changed = false; - if (changed) { - var do_close = () => { - window.onbeforeunload = undefined; - window.document.location = configs.page_list_url; - }; - show_modal( - "Close editor", - "There are unsaved changes to your page which will be lost.", - { - Cancel: hide_modal, - "Save Changes": function() { - save(do_close); - } - }, - { - "Don't Save": do_close - } - ); - } else { - window.document.location = configs.page_list_url; - } - }); - - // Initialize galaxy elements. - //init_galaxy_elts(editor); - - // - // Containers, Galaxy style - // - var containers_menu = $( - "" - ); - $(".wym_area_top").append(containers_menu); - - // Add menu options. - var items = {}; - $.each(editor._options.containersItems, (k, v) => { - var tagname = v.name; - items[v.title.replace("_", " ")] = () => { - editor.container(tagname); - }; - }); - make_popupmenu(containers_menu, items); - - // - // Create 'Insert Link to Galaxy Object' menu. - // - - // Add menu button. - var insert_link_menu_button = $( - "" - ).addClass("galaxy-page-editor-button"); - $(".wym_area_top").append(insert_link_menu_button); - - // Add menu options. - make_popupmenu(insert_link_menu_button, { - "Insert History Link": function() { - editor.dialog(CONTROLS.DIALOG_HISTORY_LINK); - }, - "Insert Dataset Link": function() { - editor.dialog(CONTROLS.DIALOG_DATASET_LINK); - }, - "Insert Workflow Link": function() { - editor.dialog(CONTROLS.DIALOG_WORKFLOW_LINK); - }, - "Insert Page Link": function() { - editor.dialog(CONTROLS.DIALOG_PAGE_LINK); - }, - "Insert Visualization Link": function() { - editor.dialog(CONTROLS.DIALOG_VISUALIZATION_LINK); - } - }); - - // - // Create 'Embed Galaxy Object' menu. - // - - // Add menu button. - var embed_object_button = $( - "" - ).addClass("galaxy-page-editor-button"); - $(".wym_area_top").append(embed_object_button); - - // Add menu options. - make_popupmenu(embed_object_button, { - "Embed History": function() { - editor.dialog(CONTROLS.DIALOG_EMBED_HISTORY); - }, - "Embed Dataset": function() { - editor.dialog(CONTROLS.DIALOG_EMBED_DATASET); - }, - "Embed Workflow": function() { - editor.dialog(CONTROLS.DIALOG_EMBED_WORKFLOW); - }, - "Embed Visualization": function() { - editor.dialog(CONTROLS.DIALOG_EMBED_VISUALIZATION); - } - //"Embed Page": function() { - // editor.dialog(CONTROLS.DIALOG_EMBED_PAGE); - //} - }); } diff --git a/client/galaxy/scripts/mvc/embedded-objects.js b/client/galaxy/scripts/mvc/embedded-objects.js new file mode 100644 index 00000000000..e89bbeb3e5f --- /dev/null +++ b/client/galaxy/scripts/mvc/embedded-objects.js @@ -0,0 +1,79 @@ +import $ from "jquery"; +import { make_popup_menus } from "ui/popupmenu"; + +export function render_embedded_items() { + $(".embedded-item").each(function() { + var container = $(this); + if (container.hasClass("history")) { + return; + } + //note: we can't do the same override for visualizations + // bc builtins (like trackster) need the handlers/ajax below to work. + // instead: (for registry visualizations) we'll clear the handlers below + // and add new ones (in embed_in_frame.mako) ...ugh. + + // Show embedded item. + var show_embedded_item = function() { + var ajax_url = container.find("input[type=hidden]").val(); + + // Only get item content if it's not already there. + var item_content = $.trim(container.find(".item-content").text()); + if (!item_content) { + $.ajax({ + type: "GET", + url: ajax_url, + error: function() { + alert("Getting item content failed."); + }, + success: function(item_content) { + container.find(".summary-content").hide("fast"); + container.find(".item-content").html(item_content); + container.find(".expanded-content").show("fast"); + container.find(".toggle-expand").hide(); + container.find(".toggle").show(); + + make_popup_menus(); + } + }); + } else { + container.find(".summary-content").hide("fast"); + container.find(".expanded-content").show("fast"); + container.find(".toggle-expand").hide(); + container.find(".toggle").show(); + } + }; + + // Hide embedded item. + var hide_embedded_item = function() { + container.find(".expanded-content").hide("fast"); + container.find(".summary-content").show("fast"); + container.find(".toggle").hide(); + container.find(".toggle-expand").show(); + }; + + // Setup toggle expand. + var toggle_expand = $(this).find(".toggle-expand"); + toggle_expand.click(function() { + show_embedded_item(); + return false; + }); + + // Setup toggle contract. + var toggle_contract = $(this).find(".toggle"); + toggle_contract.click(function() { + hide_embedded_item(); + return false; + }); + + // Setup toggle embed. + var toggle_embed = $(this).find(".toggle-embed"); + toggle_embed.click(function() { + if (container.find(".expanded-content").is(":visible")) { + hide_embedded_item(); + } else { + show_embedded_item(); + } + return false; + }); + }); +} diff --git a/client/galaxy/scripts/mvc/ui/ui-modal.js b/client/galaxy/scripts/mvc/ui/ui-modal.js index 09c44b8dfc2..25dc0aa7679 100644 --- a/client/galaxy/scripts/mvc/ui/ui-modal.js +++ b/client/galaxy/scripts/mvc/ui/ui-modal.js @@ -81,6 +81,9 @@ export var View = Backbone.View.extend({ // link elements this.$header = this.$(".modal-header"); this.$dialog = this.$(".modal-dialog"); + if (this.options.extra_class) { + this.$dialog.addClass(this.options.extra_class); + } this.$body = this.$(".modal-body"); this.$footer = this.$(".modal-footer"); this.$backdrop = this.$(".modal-backdrop"); diff --git a/client/galaxy/style/scss/overrides.scss b/client/galaxy/style/scss/overrides.scss index d823ca46904..456f65fc816 100644 --- a/client/galaxy/style/scss/overrides.scss +++ b/client/galaxy/style/scss/overrides.scss @@ -85,3 +85,18 @@ pre.code { color: white; padding: 1em; } + +// increase visibility of links within alert boxes +.alert-info, .alert-danger, .alert-warning, .alert-success { + a { + font-weight: bold; + } +} + +// increase visibility of dropdown menu section headers +.dropdown-header { + font-size: 1rem; + color: $brand-dark; + padding: .5rem 1.5rem .5rem 1rem; + font-weight: bold; +} diff --git a/client/galaxy/style/scss/ui.scss b/client/galaxy/style/scss/ui.scss index 85f3f6d025b..e0fc091a557 100644 --- a/client/galaxy/style/scss/ui.scss +++ b/client/galaxy/style/scss/ui.scss @@ -256,6 +256,8 @@ $ui-margin-horizontal-large: $margin-v * 2; .popover { max-width: 700px; .popover-header { + color: $brand-white; + background-color: $brand-primary; i { @extend .float-right; @extend .pl-4; diff --git a/create_db.sh b/create_db.sh index 8c77eff858d..3c34dd6c3e5 100755 --- a/create_db.sh +++ b/create_db.sh @@ -1,9 +1,9 @@ #!/bin/sh -cd `dirname $0` +cd "$(dirname "$0")" . ./scripts/common_startup_functions.sh setup_python -python ./scripts/create_db.py $@ +python ./scripts/create_db.py "$@" diff --git a/doc/Makefile b/doc/Makefile index 86166fce846..c4589e50479 100644 --- a/doc/Makefile +++ b/doc/Makefile @@ -42,8 +42,8 @@ help: @echo " doctest to run all doctests embedded in the documentation (if enabled)" @echo " updaterst to update Sphinx RST files for lib/ to reflect code structure changes" -source/dev/schema.md: parse_gx_xsd.py schema_template.md ../lib/galaxy/tools/xsd/galaxy.xsd ## Build Github-flavored Markdown from Galaxy Tool XSD (expects lxml in environment) - python parse_gx_xsd.py schema_template.md ../lib/galaxy/tools/xsd/galaxy.xsd > $@ +source/dev/schema.md: parse_gx_xsd.py schema_template.md ../lib/galaxy/tool_util/xsd/galaxy.xsd ## Build Github-flavored Markdown from Galaxy Tool XSD (expects lxml in environment) + python parse_gx_xsd.py schema_template.md ../lib/galaxy/tool_util/xsd/galaxy.xsd > $@ source/api/api.rst: source/lib/galaxy.webapps.galaxy.api.rst printf "Galaxy API\n==========\n" > $@ diff --git a/doc/source/releases/19.05.rst b/doc/source/releases/19.05.rst index 7166c497f17..160ab07a718 100644 --- a/doc/source/releases/19.05.rst +++ b/doc/source/releases/19.05.rst @@ -393,6 +393,11 @@ Enhancements Fixes ------------------------------- +* Explicitly specify system user under which Kubernetes jobs should run. + `Pull Request 8103`_ +* Ensure Kubernetes job querries filter the same namespace as defined in the + job destination. + `Pull Request 8092`_ * Fix client styleguide asset handling. `Pull Request 7664`_ * Handle subworkflows in view workflow mako diff --git a/doc/source/releases/19.05_announce.rst b/doc/source/releases/19.05_announce.rst index 955e75a83c0..557133feea8 100644 --- a/doc/source/releases/19.05_announce.rst +++ b/doc/source/releases/19.05_announce.rst @@ -17,7 +17,7 @@ Highlights user release notes instead. A huge thanks to `@erasche `__ for getting the ball rolling on this initiative. - Check out the `19.05 user release notes`__. + Check out the `19.05 user release notes `__. **Login and Registration Rewrite** The march toward replacing templated backend generated HTML with modern, reactive diff --git a/doc/source/releases/19.05_prs.rst b/doc/source/releases/19.05_prs.rst index 59ca8a5b131..4498056e1a8 100644 --- a/doc/source/releases/19.05_prs.rst +++ b/doc/source/releases/19.05_prs.rst @@ -1,5 +1,7 @@ .. github_links +.. _Pull Request 8103: https://github.com/galaxyproject/galaxy/pull/8103 +.. _Pull Request 8092: https://github.com/galaxyproject/galaxy/pull/8092 .. _Pull Request 4659: https://github.com/galaxyproject/galaxy/pull/4659 .. _Pull Request 5495: https://github.com/galaxyproject/galaxy/pull/5495 .. _Pull Request 6321: https://github.com/galaxyproject/galaxy/pull/6321 diff --git a/extract_dataset_parts.sh b/extract_dataset_parts.sh index f17ced8bcd5..7591b4a5ae3 100755 --- a/extract_dataset_parts.sh +++ b/extract_dataset_parts.sh @@ -1,6 +1,6 @@ #!/bin/sh -cd `dirname $0` +cd "$(dirname "$0")" . ./scripts/common_startup_functions.sh @@ -9,5 +9,5 @@ setup_python for file in $1/split_info*.json do # echo processing $file - python ./scripts/extract_dataset_part.py $file + python ./scripts/extract_dataset_part.py "$file" done diff --git a/lib/galaxy/authnz/managers.py b/lib/galaxy/authnz/managers.py index 96fcd9b52e3..8c4505d02d8 100644 --- a/lib/galaxy/authnz/managers.py +++ b/lib/galaxy/authnz/managers.py @@ -133,7 +133,7 @@ class AuthnzManager(object): def _unify_provider_name(self, provider): if provider.lower() in self.oidc_backends_config: return provider.lower() - for k, v in BACKENDS_NAME.iteritems(): + for k, v in BACKENDS_NAME.items(): if v == provider: return k.lower() return None diff --git a/lib/galaxy/authnz/psa_authnz.py b/lib/galaxy/authnz/psa_authnz.py index 9fb666586af..fef95925abb 100644 --- a/lib/galaxy/authnz/psa_authnz.py +++ b/lib/galaxy/authnz/psa_authnz.py @@ -86,7 +86,7 @@ DISCONNECT_PIPELINE = ( class PSAAuthnz(IdentityProvider): def __init__(self, provider, oidc_config, oidc_backend_config): self.config = {'provider': provider.lower()} - for key, value in oidc_config.iteritems(): + for key, value in oidc_config.items(): self.config[setting_name(key)] = value self.config[setting_name('USER_MODEL')] = 'models.User' diff --git a/lib/galaxy/config.py b/lib/galaxy/config.py index 3941bf7d879..6a5646a4439 100644 --- a/lib/galaxy/config.py +++ b/lib/galaxy/config.py @@ -50,7 +50,7 @@ PATH_DEFAULTS = dict( error_report_file=['config/error_report.yml', 'config/error_report.yml.sample'], oidc_config_file=['config/oidc_config.yml', 'config/oidc_config.yml.sample'], oidc_backends_config_file=['config/oidc_backends_config.yml', 'config/oidc_backends_config.yml.sample'], - dependency_resolvers_config_file=['config/dependency_resolvers_conf.xml', 'dependency_resolvers_conf.xml'], + dependency_resolvers_config_file=['config/dependency_resolvers_conf.xml', 'dependency_resolvers_conf.xml', None], job_resource_params_file=['config/job_resource_params_conf.xml', 'job_resource_params_conf.xml'], workflow_resource_params_file=['config/workflow_resource_params_conf.xml', 'workflow_resource_params_conf.xml'], migrated_tools_config=['migrated_tools_conf.xml', 'config/migrated_tools_conf.xml'], @@ -142,6 +142,9 @@ def find_path(kwargs, var, root): path = kwargs.get(var) else: for default in defaults: + if default is None: + # if None is the final default - just return that. + return None if os.path.exists(resolve_path(default, root)): path = default break @@ -506,6 +509,7 @@ class Configuration(object): self.use_cached_dependency_manager = use_cached_dependency_manager self.tool_dependency_cache_dir = tool_dependency_cache_dir self.precache_dependencies = precache_dependencies + self.dependency_resolvers = kwargs.get("dependency_resolvers") # Deployers may either specify a complete list of mapping files or get the default for free and just # specify a local mapping file to adapt and extend the default one. if "conda_mapping_files" in kwargs: @@ -938,7 +942,10 @@ def parse_dependency_options(kwargs, root, dependency_resolvers_config_file): precache_dependencies = string_as_bool(kwargs.get("precache_dependencies", 'True')) else: tool_dependency_dir = None - use_tool_dependencies = os.path.exists(dependency_resolvers_config_file) + if dependency_resolvers_config_file is None: + use_tool_dependencies = bool(kwargs.get("dependency_resolvers", None)) + else: + use_tool_dependencies = os.path.exists(dependency_resolvers_config_file) tool_dependency_cache_dir = None precache_dependencies = False use_cached_dependency_manager = False diff --git a/lib/galaxy/datatypes/sniff.py b/lib/galaxy/datatypes/sniff.py index 0e226346304..bf21e846004 100644 --- a/lib/galaxy/datatypes/sniff.py +++ b/lib/galaxy/datatypes/sniff.py @@ -76,6 +76,8 @@ def stream_to_open_named_file(stream, fd, filename, source_encoding=None, source else: # Compressed files must be encoded after they are uncompressed in the upload utility, # while binary files should not be encoded at all. + if isinstance(chunk, text_type): + chunk = chunk.encode(target_encoding, target_error) os.write(fd, chunk) os.close(fd) return filename diff --git a/lib/galaxy/dependencies/__init__.py b/lib/galaxy/dependencies/__init__.py index 274e1c69a80..e3c614ecdb0 100644 --- a/lib/galaxy/dependencies/__init__.py +++ b/lib/galaxy/dependencies/__init__.py @@ -69,7 +69,8 @@ class ConditionalDependencies(object): pass else: try: - job_conf_dict = yaml.safe_load(job_conf_path) + with open("job_conf_path", "r") as f: + job_conf_dict = yaml.safe_load(f) load_job_config_dict(job_conf_dict) except (OSError, IOError): pass diff --git a/lib/galaxy/dependencies/pipfiles/default/Pipfile b/lib/galaxy/dependencies/pipfiles/default/Pipfile index 6741c925615..8980b46000a 100644 --- a/lib/galaxy/dependencies/pipfiles/default/Pipfile +++ b/lib/galaxy/dependencies/pipfiles/default/Pipfile @@ -15,6 +15,7 @@ mock = "*" NoseHTML = "*" pygithub3 = {version = "*", markers = "python_version < '3'"} pytest = "*" +pytest-cov = "*" pytest-html = "*" pytest-pythonpath = "*" pytest-postgresql = "*" @@ -75,8 +76,8 @@ pyparsing = "*" "Fabric3" = "*" paramiko = "*" python-genomespaceclient = "<2.0" -social_auth_core = {version = "==3.1.0", extras = ['openidconnect']} -cloudauthz = "<=0.2.0" +social_auth_core = {version = "==3.1.0+gx0", extras = ['openidconnect']} +cloudauthz = "<=0.4.0" gxformat2 = "*" [requires] diff --git a/lib/galaxy/dependencies/pipfiles/default/pinned-dev-requirements.txt b/lib/galaxy/dependencies/pipfiles/default/pinned-dev-requirements.txt index 282c185ca8d..bb14cf2fbde 100644 --- a/lib/galaxy/dependencies/pipfiles/default/pinned-dev-requirements.txt +++ b/lib/galaxy/dependencies/pipfiles/default/pinned-dev-requirements.txt @@ -4,45 +4,49 @@ alabaster==0.7.12 argh==0.26.2 atomicwrites==1.3.0 attrs==19.1.0 -babel==2.6.0 -certifi==2019.3.9 +babel==2.7.0 +certifi==2019.6.16 chardet==3.0.4 -commonmark==0.8.1 +commonmark==0.9.0 +configparser==3.7.4 ; python_version < '3.2' +contextlib2==0.5.5 ; python_version < '3.5' +coverage==4.5.3 docutils==0.14 funcsigs==1.0.2 ; python_version < '3.3' future==0.17.1 gunicorn==19.9.0 idna==2.8 imagesize==1.1.0 +importlib-metadata==0.18 jinja2==2.10.1 -lxml==4.3.3 +lxml==4.3.4 markdown==2.6.11 markupsafe==1.1.1 mirakuru==1.1.0 -mock==2.0.0 +mock==3.0.5 more-itertools==5.0.0 nose==1.3.7 nosehtml==0.4.5 packaging==19.0 pathlib2==2.3.2 ; python_version < '3' pathtools==0.1.2 -pbr==5.1.3 -pluggy==0.9.0 +pluggy==0.12.0 port-for==0.4 -psutil==5.6.1 +psutil==5.6.3 py==1.8.0 pygithub3==0.5.1 ; python_version < '3' -pygments==2.3.1 +pygments==2.4.2 pyparsing==2.4.0 +pytest-cov==2.7.1 pytest-html==1.20.0 pytest-metadata==1.8.0 -pytest-postgresql==1.4.0 +pytest-postgresql==1.4.1 pytest-pythonpath==0.7.3 -pytest==4.4.1 +pytest==4.6.3 pytz==2019.1 -pyyaml==5.1 +pyyaml==5.1.1 recommonmark==0.5.0 -requests==2.21.0 +requests==2.22.0 scandir==1.10.0 ; python_version < '3.5' selenium==3.141.0 six==1.11.0 @@ -50,9 +54,11 @@ snowballstemmer==1.2.1 sphinx-markdown-tables==0.0.9 sphinx-rtd-theme==0.4.3 sphinx==1.8.5 -sphinxcontrib-websupport==1.1.0 -testfixtures==6.7.0 +sphinxcontrib-websupport==1.1.2 +testfixtures==6.9.0 twill==0.9.1 ; python_version < '3' typing==3.6.6 ; python_version < '3.5' -urllib3==1.24.2 ; python_version == '2.7' +urllib3==1.25.3 ; python_version == '2.7' watchdog==0.9.0 +wcwidth==0.1.7 ; sys_platform != 'win32' +zipp==0.5.1 diff --git a/lib/galaxy/dependencies/pipfiles/default/pinned-requirements.txt b/lib/galaxy/dependencies/pipfiles/default/pinned-requirements.txt index fd625aa8efb..8a4e6c136ad 100644 --- a/lib/galaxy/dependencies/pipfiles/default/pinned-requirements.txt +++ b/lib/galaxy/dependencies/pipfiles/default/pinned-requirements.txt @@ -1,9 +1,10 @@ -i https://wheels.galaxyproject.org/simple --extra-index-url https://pypi.python.org/simple adal==1.2.1 -amqp==2.4.2 +amqp==2.5.0 appdirs==1.4.3 asn1crypto==0.24.0 +attrs==19.1.0 avro==1.8.1 ; python_version < '3' azure-common==1.1.14 azure-cosmosdb-nspkg==2.0.2 @@ -16,9 +17,9 @@ azure-mgmt-resource==2.0.0 azure-mgmt-storage==2.0.0 azure-nspkg==3.0.2 azure-storage-blob==1.3.1 -azure-storage-common==1.4.0 +azure-storage-common==1.4.2 azure-storage-nspkg==3.1.0 -babel==2.6.0 +babel==2.7.0 bagit==1.6.4 bcrypt==3.1.6 bdbag==1.4.1 @@ -28,21 +29,21 @@ bleach==3.1.0 boltons==19.1.0 boto3==1.9.114 boto==2.49.0 -botocore==1.12.133 +botocore==1.12.169 bx-python==0.8.2 bz2file==0.98 ; python_version < '3.3' cachecontrol==0.11.7 -cachetools==3.1.0 -certifi==2019.3.9 +cachetools==3.1.1 +certifi==2019.6.16 cffi==1.12.3 chardet==3.0.4 -cheetah3==3.2.2 -cliff==2.14.1 -cloudauthz==0.2.0 +cheetah3==3.2.3 +cliff==2.15.0 +cloudauthz==0.4.0 cloudbridge==2.0.0 cmd2==0.8.9 contextlib2==0.5.5 ; python_version < '3.5' -cryptography==2.6.1 +cryptography==2.7 cwltool==1.0.20180721142728 debtcollector==1.21.0 decorator==4.4.0 @@ -65,7 +66,7 @@ google-auth-httplib2==0.0.3 google-auth==1.6.3 gxformat2==0.8.3 h5py==2.9.0 -httplib2==0.12.1 +httplib2==0.13.0 idna==2.8 ipaddress==1.0.22 ; python_version < '3.3' isa-rwval==0.10.7 @@ -74,12 +75,12 @@ isodate==0.6.0 jmespath==0.9.4 jsonpatch==1.23 jsonpointer==2.0 -jsonschema==2.6.0 +jsonschema==3.0.1 keystoneauth1==3.14.0 -kombu==4.5.0 +kombu==4.6.3 lockfile==0.12.2 -lxml==4.3.3 -mako==1.0.9 +lxml==4.3.4 +mako==1.0.12 markupsafe==1.1.1 mercurial==3.7.3 ; python_version < '3' mistune==0.8.4 @@ -94,35 +95,35 @@ netifaces==0.10.9 networkx==1.11 nodeenv==1.3.3 nose==1.3.7 -numpy==1.16.2 +numpy==1.16.4 oauth2client==4.1.3 oauthlib==3.0.1 openstacksdk==0.17.0 os-client-config==1.32.0 -os-service-types==1.6.0 +os-service-types==1.7.0 osc-lib==1.12.1 -oslo.config==6.8.1 +oslo.config==6.10.0 oslo.context==2.22.1 oslo.i18n==3.23.1 -oslo.log==3.42.3 -oslo.serialization==2.28.2 -oslo.utils==3.40.3 +oslo.log==3.44.0 +oslo.serialization==2.29.1 +oslo.utils==3.41.0 packaging==19.0 -paramiko==2.4.2 +paramiko==2.5.0 parsley==1.3 paste==3.0.8 pastedeploy==2.0.1 pastescript==3.1.0 pathlib2==2.3.2 ; python_version < '3' -pbr==5.1.3 +pbr==5.3.0 prettytable==0.7.2 prov==1.5.1 -psutil==5.6.1 -pulsar-galaxy-lib==0.11.0 -pyasn1-modules==0.2.4 +psutil==5.6.3 +pulsar-galaxy-lib==0.12.1 +pyasn1-modules==0.2.5 pyasn1==0.4.5 pycparser==2.19 -pycryptodome==3.8.1 +pycryptodome==3.8.2 pyeventsystem==0.1.0 pyinotify==0.9.6 ; sys_platform != 'win32' and sys_platform != 'darwin' and sys_platform != 'sunos5' pyjwt==1.7.1 @@ -131,6 +132,7 @@ pynacl==1.3.0 pyopenssl==19.0.0 pyparsing==2.4.0 pyperclip==1.7.0 +pyrsistent==0.15.2 pysam==0.15.2 pysftp==0.2.9 python-cinderclient==4.0.0 @@ -144,20 +146,20 @@ python-novaclient==11.0.0 python-openid==2.2.5 ; python_version < '3.0' python-swiftclient==3.6.0 pytz==2019.1 -pyyaml==5.1 +pyyaml==5.1.1 rdflib-jsonld==0.4.0 rdflib==4.2.2 repoze.lru==0.7 requests-oauthlib==1.2.0 requests-toolbelt==0.9.1 -requests==2.21.0 +requests==2.22.0 requestsexceptions==1.4.0 -rfc3986==1.2.0 +rfc3986==1.3.2 routes==2.4.1 rsa==4.0 ruamel.ordereddict==0.4.13 ; platform_python_implementation == 'CPython' and python_version <= '2.7' -ruamel.yaml==0.15.92 -s3transfer==0.2.0 +ruamel.yaml==0.15.97 +s3transfer==0.2.1 scandir==1.10.0 ; python_version < '3.5' schema-salad==2.7.20181126142424 shellescape==3.4.1 @@ -165,11 +167,11 @@ simplejson==3.16.0 six==1.11.0 social-auth-core[openidconnect]==3.1.0+gx0 sqlalchemy-migrate==0.12.0 -sqlalchemy-utils==0.33.11 -sqlalchemy==1.2.18 +sqlalchemy-utils==0.34.0 +sqlalchemy==1.3.4 sqlparse==0.3.0 stevedore==1.30.1 -subprocess32==3.5.3 ; python_version < '3.0' +subprocess32==3.5.4 ; python_version < '3.0' svgwrite==1.2.1 tempita==0.5.2 tenacity==4.12.0 @@ -178,10 +180,10 @@ typing==3.6.6 ; python_version < '3.5' tzlocal==1.5.1 unicodecsv==0.14.1 ; python_version < '3.0' uritemplate==3.0.0 -urllib3==1.24.2 ; python_version == '2.7' +urllib3==1.25.3 ; python_version == '2.7' uwsgi==2.0.18 vine==1.3.0 -warlock==1.3.0 +warlock==1.3.3 wcwidth==0.1.7 ; sys_platform != 'win32' webencodings==0.5.1 webob==1.8.5 diff --git a/lib/galaxy/dependencies/pipfiles/update.sh b/lib/galaxy/dependencies/pipfiles/update.sh index ff67d81805c..edafb36cb7e 100755 --- a/lib/galaxy/dependencies/pipfiles/update.sh +++ b/lib/galaxy/dependencies/pipfiles/update.sh @@ -65,6 +65,7 @@ for env in $ENVS; do -e "s/^futures==\([^ ;]\{1,\}\).*$/futures==\1 ; python_version == '2.6' or python_version == '2.7'/" \ -e "s/^monotonic==\([^ ;]\{1,\}\).*$/monotonic==\1/" \ -e "s/^more-itertools==\([^ ;]\{1,\}\).*$/more-itertools==\1/" \ + -e "s/^paste==\([^ ;]\{1,\}\).*$/paste==\1/" \ -e "s/^py2-ipaddress==\([^ ;]\{1,\}\).*$/py2-ipaddress==\1 ; python_version < '3'/" \ -e "s/^pyinotify==\([^ ;]\{1,\}\).*$/pyinotify==\1 ; sys_platform != 'win32' and sys_platform != 'darwin' and sys_platform != 'sunos5'/" \ -e "s/^python-dateutil==\([^ ;]\{1,\}\).*$/python-dateutil==\1/" \ diff --git a/lib/galaxy/job_metrics/formatting.py b/lib/galaxy/job_metrics/formatting.py index 6bda18418c2..0e0ce11fbc3 100644 --- a/lib/galaxy/job_metrics/formatting.py +++ b/lib/galaxy/job_metrics/formatting.py @@ -11,8 +11,8 @@ class JobMetricFormatter(object): def seconds_to_str(value): """Convert seconds to a simple simple string describing the amount of time.""" if value < 60: - return "%s seconds" % value + return "%s seconds" % round(value, 2) elif value < 3600: - return "%s minutes" % (value / 60) + return "%s minutes" % round(value / 60, 2) else: - return "%s hours and %s minutes" % (value / 3600, (value % 3600) / 60) + return "%s hours and %s minutes" % (round(value / 3600, 2), round((value % 3600) / 60, 2)) diff --git a/lib/galaxy/jobs/__init__.py b/lib/galaxy/jobs/__init__.py index 3dad1661442..291b7bc8d8a 100644 --- a/lib/galaxy/jobs/__init__.py +++ b/lib/galaxy/jobs/__init__.py @@ -1456,7 +1456,8 @@ class JobWrapper(HasResourceParameters): if not os.path.exists(version_filename): version_filename = self.get_version_string_path_legacy() if os.path.exists(version_filename): - self.version_string = open(version_filename).read() + with open(version_filename, 'rb') as fh: + self.version_string = galaxy.util.shrink_and_unicodify(fh.read()) os.unlink(version_filename) outputs_to_working_directory = util.asbool(self.get_destination_configuration("outputs_to_working_directory", False)) diff --git a/lib/galaxy/jobs/handler.py b/lib/galaxy/jobs/handler.py index 33a705caccc..f2202d4d6f4 100644 --- a/lib/galaxy/jobs/handler.py +++ b/lib/galaxy/jobs/handler.py @@ -434,7 +434,7 @@ class JobHandlerQueue(Monitors): model.Dataset.deleted, model.Dataset.purged, model.Dataset.state, - ).join(job_to_input) \ + ).join(job_to_input.job) \ .join(input_association) \ .join(model.Dataset) \ .filter(model.Job.id.in_(job_ids_to_check)) \ diff --git a/lib/galaxy/jobs/runners/kubernetes.py b/lib/galaxy/jobs/runners/kubernetes.py index 782c4a3dc29..7c94edb654b 100644 --- a/lib/galaxy/jobs/runners/kubernetes.py +++ b/lib/galaxy/jobs/runners/kubernetes.py @@ -54,6 +54,8 @@ class KubernetesJobRunner(AsynchronousJobRunner): k8s_job_api_version=dict(map=str, default="batch/v1"), k8s_supplemental_group_id=dict(map=str), k8s_pull_policy=dict(map=str, default="Default"), + k8s_run_as_user_id=dict(map=str, valid=lambda s: s == "$uid" or s.isdigit()), + k8s_run_as_group_id=dict(map=str, valid=lambda s: s == "$gid" or s.isdigit()), k8s_fs_group_id=dict(map=int), k8s_default_requests_cpu=dict(map=str, default=None), k8s_default_requests_memory=dict(map=str, default=None), @@ -76,6 +78,8 @@ class KubernetesJobRunner(AsynchronousJobRunner): self._galaxy_instance_id = self.__get_galaxy_instance_id() + self._run_as_user_id = self.__get_run_as_user_id() + self._run_as_group_id = self.__get_run_as_group_id() self._supplemental_group = self.__get_supplemental_group() self._fs_group = self.__get_fs_group() self._default_pull_policy = self.__get_pull_policy() @@ -169,6 +173,24 @@ class KubernetesJobRunner(AsynchronousJobRunner): return self.runner_params['k8s_pull_policy'] return None + def __get_run_as_user_id(self): + if "k8s_run_as_user_id" in self.runner_params: + run_as_user = self.runner_params["k8s_run_as_user_id"] + if run_as_user == "$uid": + return os.getuid() + else: + return int(self.runner_params["k8s_run_as_user_id"]) + return None + + def __get_run_as_group_id(self): + if "k8s_run_as_group_id" in self.runner_params: + run_as_group = self.runner_params["k8s_run_as_group_id"] + if run_as_group == "$gid": + return self.app.config.gid + else: + return int(self.runner_params["k8s_run_as_group_id"]) + return None + def __get_supplemental_group(self): if "k8s_supplemental_group_id" in self.runner_params: try: @@ -238,17 +260,21 @@ class KubernetesJobRunner(AsynchronousJobRunner): } # TODO include other relevant elements that people might want to use from # TODO http://kubernetes.io/docs/api-reference/v1/definitions/#_v1_podspec - - if self._supplemental_group and self._supplemental_group > 0: - k8s_spec_template["spec"]["securityContext"] = dict(supplementalGroups=[self._supplemental_group]) - if self._fs_group and self._fs_group > 0: - if "securityContext" in k8s_spec_template["spec"]: - k8s_spec_template["spec"]["securityContext"]["fsGroup"] = self._fs_group - else: - k8s_spec_template["spec"]["securityContext"] = dict(fsGroup=self._fs_group) - + k8s_spec_template["spec"]["securityContext"] = self.__get_k8s_security_context() return k8s_spec_template + def __get_k8s_security_context(self): + security_context = {} + if self._run_as_user_id: + security_context["runAsUser"] = self._run_as_user_id + if self._run_as_group_id: + security_context["runAsGroup"] = self._run_as_group_id + if self._supplemental_group and self._supplemental_group > 0: + security_context["supplementalGroups"] = [self._supplemental_group] + if self._fs_group and self._fs_group > 0: + security_context["fsGroup"] = self._fs_group + return security_context + def __get_k8s_restart_policy(self, job_wrapper): """The default Kubernetes restart policy for Jobs""" return "Never" @@ -475,7 +501,8 @@ class KubernetesJobRunner(AsynchronousJobRunner): marks the job for resubmission (resubmit logic is part of destinations). """ - pods = Pod.objects(self._pykube_api).filter(selector="app=%s" % job_state.job_id) + pods = Pod.objects(self._pykube_api).filter(selector="app=%s" % job_state.job_id, + namespace=self.runner_params['k8s_namespace']) pod = Pod(self._pykube_api, pods.response['items'][0]) if pod.obj['status']['phase'] == "Failed" and \ @@ -488,8 +515,9 @@ class KubernetesJobRunner(AsynchronousJobRunner): """Attempts to delete a dispatched job to the k8s cluster""" job = job_wrapper.get_job() try: - jobs = Job.objects(self._pykube_api).filter(selector="app=" + - self.__produce_unique_k8s_job_name(job.get_id_tag())) + jobs = Job.objects(self._pykube_api).filter( + selector="app=" + self.__produce_unique_k8s_job_name(job.get_id_tag()), + namespace=self.runner_params['k8s_namespace']) if len(jobs.response['items']) >= 0: job_to_delete = Job(self._pykube_api, jobs.response['items'][0]) job_to_delete.scale(replicas=0) diff --git a/lib/galaxy/jobs/runners/slurm.py b/lib/galaxy/jobs/runners/slurm.py index ec2ce131d45..9b5b3b7600b 100644 --- a/lib/galaxy/jobs/runners/slurm.py +++ b/lib/galaxy/jobs/runners/slurm.py @@ -10,6 +10,7 @@ import time from galaxy import model from galaxy.jobs.runners.drmaa import DRMAAJobRunner +from galaxy.util import unicodify from galaxy.util.logging import get_logger log = get_logger(__name__) @@ -51,7 +52,7 @@ class SlurmJobRunner(DRMAAJobRunner): p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) stdout, stderr = p.communicate() if p.returncode != 0: - stderr = stderr.strip() + stderr = unicodify(stderr).strip() if stderr == 'SLURM accounting storage is disabled': log.warning('SLURM accounting storage is not properly configured, unable to run sacct') return @@ -59,7 +60,7 @@ class SlurmJobRunner(DRMAAJobRunner): # First line is for 'job_id' # Second line is for 'job_id.batch' (only available after the batch job is complete) # Following lines are for the steps 'job_id.0', 'job_id.1', ... (but Galaxy does not use steps) - first_line = stdout.splitlines()[0] + first_line = unicodify(stdout).splitlines()[0] # Strip whitespaces and the final '+' (if present), only return the first word return first_line.strip().rstrip('+').split()[0] @@ -77,6 +78,7 @@ class SlurmJobRunner(DRMAAJobRunner): stdout, stderr = p.communicate() if p.returncode != 0: # Will need to be more clever here if this message is not consistent + stderr = unicodify(stderr) if stderr == 'slurm_load_jobs error: Invalid job id specified\n': # The job may be old, try to get its state with sacct job_state = _get_slurm_state_with_sacct(job_id, cluster) @@ -84,7 +86,7 @@ class SlurmJobRunner(DRMAAJobRunner): return job_state return 'NOT_FOUND' raise Exception('`%s` returned %s, stderr: %s' % (' '.join(cmd), p.returncode, stderr)) - stdout = stdout.strip() + stdout = unicodify(stdout).strip() # stdout is a single line in format "key1=value1 key2=value2 ..." job_info_keys = [] job_info_values = [] diff --git a/lib/galaxy/managers/base.py b/lib/galaxy/managers/base.py index e06ca8de029..26b0fe2e01b 100644 --- a/lib/galaxy/managers/base.py +++ b/lib/galaxy/managers/base.py @@ -1128,3 +1128,10 @@ class ModelFilterParser(HasAModelManager): def raise_filter_err(self, attr, op, val, msg): raise exceptions.RequestParameterInvalidException(msg, column=attr, operation=op, val=val) + + +def is_valid_slug(slug): + """Returns true iff slug is valid.""" + + VALID_SLUG_RE = re.compile(r"^[a-z0-9\-]+$") + return VALID_SLUG_RE.match(slug) diff --git a/lib/galaxy/managers/pages.py b/lib/galaxy/managers/pages.py index 08f2c9f46fc..8d2053acce3 100644 --- a/lib/galaxy/managers/pages.py +++ b/lib/galaxy/managers/pages.py @@ -6,14 +6,53 @@ Pages are markup created and saved by users that can contain Galaxy objects from within Galaxy. """ import logging +import re -from galaxy import model -from galaxy.managers import sharable +from six.moves.html_entities import name2codepoint +from six.moves.html_parser import HTMLParser + +from galaxy import exceptions, model +from galaxy.managers import base, sharable +from galaxy.managers.hdas import HDAManager +from galaxy.model.item_attrs import UsesAnnotations +from galaxy.util import unicodify +from galaxy.util.sanitize_html import sanitize_html log = logging.getLogger(__name__) +# Copied from https://github.com/kurtmckee/feedparser +_cp1252 = { + 128: u'\u20ac', # euro sign + 130: u'\u201a', # single low-9 quotation mark + 131: u'\u0192', # latin small letter f with hook + 132: u'\u201e', # double low-9 quotation mark + 133: u'\u2026', # horizontal ellipsis + 134: u'\u2020', # dagger + 135: u'\u2021', # double dagger + 136: u'\u02c6', # modifier letter circumflex accent + 137: u'\u2030', # per mille sign + 138: u'\u0160', # latin capital letter s with caron + 139: u'\u2039', # single left-pointing angle quotation mark + 140: u'\u0152', # latin capital ligature oe + 142: u'\u017d', # latin capital letter z with caron + 145: u'\u2018', # left single quotation mark + 146: u'\u2019', # right single quotation mark + 147: u'\u201c', # left double quotation mark + 148: u'\u201d', # right double quotation mark + 149: u'\u2022', # bullet + 150: u'\u2013', # en dash + 151: u'\u2014', # em dash + 152: u'\u02dc', # small tilde + 153: u'\u2122', # trade mark sign + 154: u'\u0161', # latin small letter s with caron + 155: u'\u203a', # single right-pointing angle quotation mark + 156: u'\u0153', # latin small ligature oe + 158: u'\u017e', # latin small letter z with caron + 159: u'\u0178', # latin capital letter y with diaeresis +} -class PageManager(sharable.SharableModelManager): + +class PageManager(sharable.SharableModelManager, UsesAnnotations): """ """ @@ -35,6 +74,88 @@ class PageManager(sharable.SharableModelManager): """ pass + def create(self, trans, payload): + user = trans.get_user() + + if not payload.get("title", None): + raise exceptions.ObjectAttributeMissingException("Page name is required") + elif not payload.get("slug", None): + raise exceptions.ObjectAttributeMissingException("Page id is required") + elif not base.is_valid_slug(payload["slug"]): + raise exceptions.ObjectAttributeInvalidException("Page identifier must consist of only lowercase letters, numbers, and the '-' character") + elif trans.sa_session.query(trans.app.model.Page).filter_by(user=user, slug=payload["slug"], deleted=False).first(): + raise exceptions.DuplicatedSlugException("Page identifier must be unique") + + content = payload.get("content", "") + content = self.rewrite_content_for_import(trans, content) + + # Create the new stored page + page = trans.app.model.Page() + page.title = payload['title'] + page.slug = payload['slug'] + page_annotation = payload.get("annotation", None) + if page_annotation is not None: + page_annotation = sanitize_html(page_annotation) + self.add_item_annotation(trans.sa_session, trans.get_user(), page, page_annotation) + + page.user = user + # And the first (empty) page revision + page_revision = trans.app.model.PageRevision() + page_revision.title = payload['title'] + page_revision.page = page + page.latest_revision = page_revision + page_revision.content = content + # Persist + session = trans.sa_session + session.add(page) + session.flush() + return page + + def save_new_revision(self, trans, page, payload): + # Assumes security has already been checked by caller. + content = payload.get("content", None) + if not content: + raise exceptions.ObjectAttributeMissingException("content undefined or empty") + + if 'title' in payload: + title = payload['title'] + else: + title = page.title + + content = self.rewrite_content_for_import(trans, content) + + page_revision = trans.app.model.PageRevision() + page_revision.title = title + page_revision.page = page + page.latest_revision = page_revision + page_revision.content = content + + # Persist + session = trans.sa_session + session.flush() + return page_revision + + def rewrite_content_for_import(self, trans, content): + try: + content = sanitize_html(content) + processor = PageContentProcessor(trans, placeholderRenderForSave) + processor.feed(content) + # Output is string, so convert to unicode for saving. + content = unicodify(processor.output(), 'utf-8') + except exceptions.MessageException: + raise + except Exception: + raise exceptions.RequestParameterInvalidException("problem with embedded HTML content [%s]" % content) + return content + + def rewrite_content_for_export(self, trans, as_dict): + content = as_dict["content"] + processor = PageContentProcessor(trans, placeholderRenderForEdit) + processor.feed(content) + content = unicodify(processor.output(), 'utf-8') + as_dict["content"] = content + return as_dict + class PageSerializer(sharable.SharableModelSerializer): """ @@ -73,3 +194,230 @@ class PageDeserializer(sharable.SharableModelDeserializer): self.deserializers.update({ }) self.deserializable_keyset.update(self.deserializers.keys()) + + +class PageContentProcessor(HTMLParser, object): + """ + Processes page content to produce HTML that is suitable for display. + For now, processor renders embedded objects. + """ + bare_ampersand = re.compile(r"&(?!#\d+;|#x[0-9a-fA-F]+;|\w+;)") + elements_no_end_tag = set([ + 'area', 'base', 'basefont', 'br', 'col', 'command', 'embed', 'frame', + 'hr', 'img', 'input', 'isindex', 'keygen', 'link', 'meta', 'param', + 'source', 'track', 'wbr' + ]) + + def __init__(self, trans, render_embed_html_fn): + HTMLParser.__init__(self) + self.trans = trans + self.ignore_content = False + self.num_open_tags_for_ignore = 0 + self.render_embed_html_fn = render_embed_html_fn + + def reset(self): + self.pieces = [] + HTMLParser.reset(self) + + def _shorttag_replace(self, match): + tag = match.group(1) + if tag in self.elements_no_end_tag: + return '<' + tag + ' />' + else: + return '<' + tag + '>' + + def feed(self, data): + data = re.compile(r'\s]+?)\s*/>', self._shorttag_replace, data) + data = data.replace(''', "'") + data = data.replace('"', '"') + HTMLParser.feed(self, data) + HTMLParser.close(self) + + def handle_starttag(self, tag, attrs): + """ + Called for each start tag + + attrs is a list of (attr, value) tuples, e.g. for
,
+        tag='pre', attrs=[('class', 'screen')]
+        """
+
+        # If ignoring content, just increment tag count and ignore.
+        if self.ignore_content:
+            self.num_open_tags_for_ignore += 1
+            return
+
+        # Not ignoring tag; look for embedded content.
+        embedded_item = False
+        for attribute in attrs:
+            if (attribute[0] == "class") and ("embedded-item" in attribute[1].split(" ")):
+                embedded_item = True
+                break
+        # For embedded content, set ignore flag to ignore current content and add new content for embedded item.
+        if embedded_item:
+            # Set processing attributes to ignore content.
+            self.ignore_content = True
+            self.num_open_tags_for_ignore = 1
+
+            # Insert content for embedded element.
+            for attribute in attrs:
+                name = attribute[0]
+                if name == "id":
+                    # ID has form '-'
+                    item_class, item_id = attribute[1].split("-")
+                    embed_html = self.render_embed_html_fn(self.trans, item_class, item_id)
+                    self.pieces.append(embed_html)
+            return
+
+        # Default behavior: not ignoring and no embedded content.
+        uattrs = []
+        strattrs = ''
+        if attrs:
+            for key, value in attrs:
+                value = value.replace('>', '>').replace('<', '<').replace('"', '"')
+                value = self.bare_ampersand.sub("&", value)
+                uattrs.append((key, value))
+            strattrs = ''.join(' %s="%s"' % (k, v) for k, v in uattrs)
+        if tag in self.elements_no_end_tag:
+            self.pieces.append('<%s%s />' % (tag, strattrs))
+        else:
+            self.pieces.append('<%s%s>' % (tag, strattrs))
+
+    def handle_endtag(self, tag):
+        """
+        Called for each end tag
+
+        E.g. for 
, tag will be 'pre' + """ + # If ignoring content, see if current tag is the end of content to ignore. + if self.ignore_content: + self.num_open_tags_for_ignore -= 1 + if self.num_open_tags_for_ignore == 0: + # Done ignoring content. + self.ignore_content = False + return + + # Default behavior: reconstruct the original end tag. + if tag not in self.elements_no_end_tag: + self.pieces.append("" % tag) + + def handle_charref(self, ref): + # called for each character reference, e.g. for ' ', ref will be '160' + # Reconstruct the original character reference. + ref = ref.lower() + if ref.startswith('x'): + value = int(ref[1:], 16) + else: + value = int(ref) + + if value in _cp1252: + self.pieces.append('&#%s;' % hex(ord(_cp1252[value]))[1:]) + else: + self.pieces.append('&#%s;' % ref) + + def handle_entityref(self, ref): + # called for each entity reference, e.g. for '©', ref will be 'copy' + # Reconstruct the original entity reference. + if ref in name2codepoint or ref == 'apos': + self.pieces.append('&%s;' % ref) + else: + self.pieces.append('&%s' % ref) + + def handle_data(self, text): + """ + Called for each block of plain text + + Called outside of any tag and not containing any character or entity + references. Store the original text verbatim. + """ + if self.ignore_content: + return + self.pieces.append(text) + + def handle_comment(self, text): + # called for each HTML comment, e.g. + # Reconstruct the original comment. + self.pieces.append('' % text) + + def handle_decl(self, text): + # called for the DOCTYPE, if present, e.g. + # + # Reconstruct original DOCTYPE + self.pieces.append('' % text) + + def handle_pi(self, text): + # called for each processing instruction, e.g. + # Reconstruct original processing instruction. + self.pieces.append('' % text) + + def output(self): + '''Return processed HTML as a single string''' + return ''.join(self.pieces) + + +PAGE_MAXRAW = 10**15 + + +def get_page_identifiers(item_id, app): + # Assume if item id is integer and less than 10**15, it's unencoded. + try: + decoded_id = int(item_id) + if decoded_id >= PAGE_MAXRAW: + raise ValueError("Identifier larger than maximum expected raw int, must be already encoded.") + encoded_id = app.security.encode_id(item_id) + except ValueError: + # It's an encoded id. + encoded_id = item_id + decoded_id = base.decode_id(app, item_id) + return (encoded_id, decoded_id) + + +# Utilities for encoding/decoding HTML content. +PLACEHOLDER_TEMPLATE = '''

Embedded Galaxy {class_shorthand} - '{item_name}'

[Do not edit this block; Galaxy will fill it in with the annotated {class_shorthand} when it is displayed]

''' + +# This is a mapping of the id portion of page contents to the cssclass/shortname. +PAGE_CLASS_MAPPING = { + 'History': 'History', + 'HistoryDatasetAssociation': 'Dataset', + 'StoredWorkflow': 'Workflow', + 'Visualization': 'Visualization' +} + + +def placeholderRenderForEdit(trans, item_class, item_id): + return placeholderRenderForSave(trans, item_class, item_id, encode=True) + + +def placeholderRenderForSave(trans, item_class, item_id, encode=False): + encoded_item_id, decoded_item_id = get_page_identifiers(item_id, trans.app) + item_name = '' + if item_class == 'History': + history = trans.sa_session.query(trans.model.History).get(decoded_item_id) + history = base.security_check(trans, history, False, True) + item_name = history.name + elif item_class == 'HistoryDatasetAssociation': + hda = trans.sa_session.query(trans.model.HistoryDatasetAssociation).get(decoded_item_id) + hda_manager = HDAManager(trans.app) + hda = hda_manager.get_accessible(decoded_item_id, trans.user) + item_name = hda.name + elif item_class == 'StoredWorkflow': + wf = trans.sa_session.query(trans.model.StoredWorkflow).get(decoded_item_id) + wf = base.security_check(trans, wf, False, True) + item_name = wf.name + elif item_class == 'Visualization': + visualization = trans.sa_session.query(trans.model.Visualization).get(decoded_item_id) + visualization = base.security_check(trans, visualization, False, True) + item_name = visualization.title + class_shorthand = PAGE_CLASS_MAPPING[item_class] + if encode: + item_id = encoded_item_id + else: + item_id = decoded_item_id + return PLACEHOLDER_TEMPLATE.format( + item_class=item_class, + class_shorthand=class_shorthand, + class_shorthand_lower=class_shorthand.lower(), + item_id=item_id, + item_name=item_name + ) diff --git a/lib/galaxy/model/__init__.py b/lib/galaxy/model/__init__.py index f37dda4fd8a..a65dad0caa3 100644 --- a/lib/galaxy/model/__init__.py +++ b/lib/galaxy/model/__init__.py @@ -289,11 +289,13 @@ class JobLike(object): def set_streams(self, tool_stdout, tool_stderr, job_stdout=None, job_stderr=None, job_messages=None): def shrink_and_unicodify(what, stream): - stream = galaxy.util.unicodify(stream) or u'' - if (len(stream) > galaxy.util.DATABASE_MAX_STRING_SIZE): - stream = galaxy.util.shrink_string_by_size(tool_stdout, galaxy.util.DATABASE_MAX_STRING_SIZE, join_by="\n..\n", left_larger=True, beginning_on_size_error=True) - log.info("%s for %s %d is greater than %s, only a portion will be logged to database", what, type(self), self.id, galaxy.util.DATABASE_MAX_STRING_SIZE_PRETTY) - return stream + if len(stream) > galaxy.util.DATABASE_MAX_STRING_SIZE: + log.info("%s for %s %d is greater than %s, only a portion will be logged to database", + what, + type(self), + self.id, + galaxy.util.DATABASE_MAX_STRING_SIZE_PRETTY) + return galaxy.util.shrink_and_unicodify(stream) self.tool_stdout = shrink_and_unicodify('tool_stdout', tool_stdout) self.tool_stderr = shrink_and_unicodify('tool_stderr', tool_stderr) @@ -1678,13 +1680,10 @@ class History(HasTags, Dictifiable, UsesAnnotations, HasName, RepresentById): set_genome = genome_build not in [None, '?'] for i, dataset in enumerate(datasets): dataset.hid = base_hid + i - # Don't let SA manage this. - delattr(dataset, "history") + dataset.history = self dataset.history_id = cached_id(self) if set_genome: self.genome_build = genome_build - for dataset in datasets: - dataset.history_id = cached_id(self) return datasets def add_dataset_collection(self, history_dataset_collection, set_hid=True): @@ -2444,6 +2443,14 @@ class DatasetInstance(object): self.parent_id = parent_id self.validation_errors = validation_errors + @property + def peek(self): + return self._peek + + @peek.setter + def peek(self, peek): + self._peek = unicodify(peek, strip_null=True) + def update(self): self.update_time = galaxy.model.orm.now.now() diff --git a/lib/galaxy/model/item_attrs.py b/lib/galaxy/model/item_attrs.py index bf32ed87a5a..d054c8bbc42 100644 --- a/lib/galaxy/model/item_attrs.py +++ b/lib/galaxy/model/item_attrs.py @@ -74,13 +74,10 @@ class UsesItemRatings(object): item_rating_assoc_class = '%sRatingAssociation' % item.__class__.__name__ return getattr(webapp_model, item_rating_assoc_class, None) - def _get_item_id_filter_str(self, item, item_rating_assoc_class, webapp_model=None): + def _get_item_id_filter_str(self, item, item_rating_assoc_class): # Get foreign key in item-rating association table that references item table. - if webapp_model is None: - webapp_model = galaxy.model item_fk = get_foreign_key(item_rating_assoc_class, item) - # TODO: can we provide a better filter than a raw string? - return "%s=%i" % (item_fk.parent.name, item.id) + return item_fk.parent == item.id class UsesAnnotations(object): @@ -122,7 +119,6 @@ def get_item_annotation_obj(db_session, user, item): # Get annotation association object. annotation_assoc = db_session.query(annotation_assoc_class).filter_by(user=user) - # TODO: use filtering like that in _get_item_id_filter_str() if item.__class__ == galaxy.model.History: annotation_assoc = annotation_assoc.filter_by(history=item) elif item.__class__ == galaxy.model.HistoryDatasetAssociation: diff --git a/lib/galaxy/model/mapping.py b/lib/galaxy/model/mapping.py index 98657d46b49..90caf7f413f 100644 --- a/lib/galaxy/model/mapping.py +++ b/lib/galaxy/model/mapping.py @@ -222,7 +222,7 @@ model.HistoryDatasetAssociation.table = Table( Column("name", TrimmedString(255)), Column("info", TrimmedString(255)), Column("blurb", TrimmedString(255)), - Column("peek", TEXT), + Column("peek", TEXT, key="_peek"), Column("tool_version", TEXT), Column("extension", TrimmedString(64)), Column("metadata", MetadataType(), key="_metadata"), @@ -506,7 +506,7 @@ model.LibraryDatasetDatasetAssociation.table = Table( Column("name", TrimmedString(255), index=True), Column("info", TrimmedString(255)), Column("blurb", TrimmedString(255)), - Column("peek", TEXT), + Column("peek", TEXT, key="_peek"), Column("tool_version", TEXT), Column("extension", TrimmedString(64)), Column("metadata", MetadataType(), key="_metadata"), @@ -2763,7 +2763,7 @@ def db_next_hid(self, n=1): trans = session.begin() try: if "postgres" not in session.bind.dialect.name: - next_hid = select([table.c.hid_counter], table.c.id == model.cached_id(self), for_update=True).scalar() + next_hid = select([table.c.hid_counter], table.c.id == model.cached_id(self)).with_for_update().scalar() table.update(table.c.id == self.id).execute(hid_counter=(next_hid + n)) else: stmt = table.update().where(table.c.id == model.cached_id(self)).values(hid_counter=(table.c.hid_counter + n)).returning(table.c.hid_counter) diff --git a/lib/galaxy/util/none_like.py b/lib/galaxy/model/none_like.py similarity index 78% rename from lib/galaxy/util/none_like.py rename to lib/galaxy/model/none_like.py index ff7c5de0475..293ecc5860c 100644 --- a/lib/galaxy/util/none_like.py +++ b/lib/galaxy/model/none_like.py @@ -2,7 +2,6 @@ Objects with No values """ -from galaxy.datatypes.registry import Registry from galaxy.model.metadata import MetadataCollection @@ -27,10 +26,7 @@ class NoneDataset(RecursiveNone): def __init__(self, datatypes_registry=None, ext='data', dbkey='?'): self.ext = self.extension = ext self.dbkey = dbkey - if datatypes_registry is None: - # Default Value Required for unit tests - datatypes_registry = Registry() - datatypes_registry.load_datatypes() + assert datatypes_registry is not None self.datatype = datatypes_registry.get_datatype_by_extension(ext) self._metadata = None self.metadata = MetadataCollection(self) diff --git a/lib/galaxy/tool_util/deps/__init__.py b/lib/galaxy/tool_util/deps/__init__.py index 1aab7f2e545..397a9a12faf 100644 --- a/lib/galaxy/tool_util/deps/__init__.py +++ b/lib/galaxy/tool_util/deps/__init__.py @@ -24,8 +24,6 @@ from .resolvers import ( ContainerDependency, NullDependency, ) -from .resolvers.conda import CondaDependencyResolver -from .resolvers.galaxy_packages import GalaxyPackageDependencyResolver from .resolvers.tool_shed_packages import ToolShedPackageDependencyResolver log = logging.getLogger(__name__) @@ -34,13 +32,26 @@ CONFIG_VAL_NOT_FOUND = object() def build_dependency_manager(config): - if getattr(config, "use_tool_dependencies", False): + config_dict = { + "use_tool_dependencies": getattr(config, "use_tool_dependencies", False), + "default_base_path": getattr(config, "tool_dependency_dir", None), + "conf_file": getattr(config, "dependency_resolvers_config_file", None), + "cache": getattr(config, "use_cached_dependency_manager", False), + "app_config": config, + "dependency_resolvers": getattr(config, "dependency_resolvers", None), + } + return build_dependency_manager_from_dict(config_dict) + + +def build_dependency_manager_from_dict(config_dict): + if config_dict.get("use_tool_dependencies", False): dependency_manager_kwds = { - 'default_base_path': config.tool_dependency_dir, - 'conf_file': config.dependency_resolvers_config_file, - 'app_config': config, + 'default_base_path': config_dict.get("default_base_path"), + 'conf_file': config_dict.get("conf_file"), + 'app_config': config_dict.get("app_config"), + "dependency_resolver_dicts": config_dict.get("dependency_resolvers"), } - if getattr(config, "use_cached_dependency_manager", False): + if config_dict.get("cache"): dependency_manager = CachedDependencyManager(**dependency_manager_kwds) else: dependency_manager = DependencyManager(**dependency_manager_kwds) @@ -61,8 +72,9 @@ class DependencyManager(object): and should each contain a file 'env.sh' which can be sourced to make the dependency available in the current shell environment. """ + cached = False - def __init__(self, default_base_path, conf_file=None, app_config={}): + def __init__(self, default_base_path, conf_file=None, app_config={}, dependency_resolver_dicts=None): """ Create a new dependency manager looking for packages under the paths listed in `base_paths`. The default base path is app.config.tool_dependency_dir. @@ -74,7 +86,13 @@ class DependencyManager(object): self.__app_config = app_config self.default_base_path = os.path.abspath(default_base_path) self.resolver_classes = self.__resolvers_dict() - self.dependency_resolvers = self.__build_dependency_resolvers(conf_file) + + plugin_source = None + if dependency_resolver_dicts is not None: + plugin_source = ('dict', dependency_resolver_dicts) + else: + plugin_source = self.__build_dependency_resolvers_plugin_source(conf_file) + self.dependency_resolvers = self.__parse_resolver_conf_plugins(plugin_source) self._enabled_container_types = [] self._destination_for_container_type = {} @@ -232,38 +250,50 @@ class DependencyManager(object): else: return NullDependency(name=name, version=version) - def __build_dependency_resolvers(self, conf_file): + def __build_dependency_resolvers_plugin_source(self, conf_file): if not conf_file: - return self.__default_dependency_resolvers() + return self.__default_dependency_resolvers_source() if not os.path.exists(conf_file): log.debug("Unable to find config file '%s'", conf_file) - return self.__default_dependency_resolvers() + return self.__default_dependency_resolvers_source() plugin_source = plugin_config.plugin_source_from_path(conf_file) - return self.__parse_resolver_conf_xml(plugin_source) + return plugin_source - def __default_dependency_resolvers(self): - return [ - ToolShedPackageDependencyResolver(self), - GalaxyPackageDependencyResolver(self), - CondaDependencyResolver(self), - GalaxyPackageDependencyResolver(self, versionless=True), - CondaDependencyResolver(self, versionless=True), - ] + def __default_dependency_resolvers_source(self): + return ('dict', [ + {"type": "tool_shed_packages"}, + {"type": "galaxy_packages"}, + {"type": "conda"}, + {"type": "galaxy_packages", "versionless": True}, + {"type": "conda", "versionless": True}, + ]) - def __parse_resolver_conf_xml(self, plugin_source): + def __parse_resolver_conf_plugins(self, plugin_source): """ """ extra_kwds = dict(dependency_manager=self) - return plugin_config.load_plugins(self.resolver_classes, plugin_source, extra_kwds) + # Use either 'type' from YAML definition or 'resolver_type' from to_dict definition. + return plugin_config.load_plugins(self.resolver_classes, plugin_source, extra_kwds, plugin_type_keys=['type', 'resolver_type']) def __resolvers_dict(self): import galaxy.tool_util.deps.resolvers return plugin_config.plugins_dict(galaxy.tool_util.deps.resolvers, 'resolver_type') + def to_dict(self): + return { + "cache": self.cached, + "use_tool_dependencies": True, + "default_base_path": self.default_base_path, + "dependency_resolvers": [m.to_dict() for m in self.dependency_resolvers], + "tool_dependency_cache_dir": getattr(self, "tool_dependency_cache_dir", None), + } + class CachedDependencyManager(DependencyManager): - def __init__(self, default_base_path, conf_file=None, app_config={}, tool_dependency_cache_dir=None): - super(CachedDependencyManager, self).__init__(default_base_path=default_base_path, conf_file=conf_file, app_config=app_config) + cached = True + + def __init__(self, default_base_path, **kwd): + super(CachedDependencyManager, self).__init__(default_base_path=default_base_path, **kwd) self.tool_dependency_cache_dir = self.get_app_option("tool_dependency_cache_dir") def build_cache(self, requirements, **kwds): @@ -338,3 +368,6 @@ class NullDependencyManager(DependencyManager): def find_dep(self, name, version=None, type='package', **kwds): return NullDependency(version=version, name=name) + + def to_dict(self): + return {"use_tool_dependencies": False} diff --git a/lib/galaxy/tool_util/deps/containers.py b/lib/galaxy/tool_util/deps/containers.py index d99a2e1c0aa..55a994ec78c 100644 --- a/lib/galaxy/tool_util/deps/containers.py +++ b/lib/galaxy/tool_util/deps/containers.py @@ -148,7 +148,7 @@ class ContainerFinder(object): key = "%s_image" % container_type if key in destination_info: return destination_info.get(key) - elif "%s_image_default" in destination_info: + elif "%s_image_default" % container_type in destination_info: return self.__build_container_id_from_parts(container_type, destination_info, mode="default") return None @@ -159,7 +159,7 @@ class ContainerFinder(object): # TODO: Right now this assumes all containers available when a # container type is - there should be more thought put into this. - # Checking which are availalbe - settings policies for what can be + # Checking which are available - settings policies for what can be # auto-fetched, etc.... return CONTAINER_CLASSES[container_type](container_id, self.app_info, tool_info, destination_info, job_info, container_description) diff --git a/lib/galaxy/tool_util/deps/resolvers/conda.py b/lib/galaxy/tool_util/deps/resolvers/conda.py index 28468ffb8ee..e7f4d3a25f0 100644 --- a/lib/galaxy/tool_util/deps/resolvers/conda.py +++ b/lib/galaxy/tool_util/deps/resolvers/conda.py @@ -65,7 +65,7 @@ log = logging.getLogger(__name__) class CondaDependencyResolver(DependencyResolver, MultipleDependencyResolver, ListableDependencyResolver, InstallableDependencyResolver, SpecificationPatternDependencyResolver, MappableDependencyResolver): - dict_collection_visible_keys = DependencyResolver.dict_collection_visible_keys + ['conda_prefix', 'versionless', 'ensure_channels', 'auto_install'] + dict_collection_visible_keys = DependencyResolver.dict_collection_visible_keys + ['conda_prefix', 'versionless', 'ensure_channels', 'auto_install', 'auto_init'] resolver_type = "conda" config_options = { 'prefix': None, diff --git a/lib/galaxy/tool_util/deps/resolvers/modules.py b/lib/galaxy/tool_util/deps/resolvers/modules.py index fc7e17b62da..37ac3d70b8b 100644 --- a/lib/galaxy/tool_util/deps/resolvers/modules.py +++ b/lib/galaxy/tool_util/deps/resolvers/modules.py @@ -41,7 +41,7 @@ UNKNOWN_FIND_BY_MESSAGE = "ModuleDependencyResolver does not know how to find mo class ModuleDependencyResolver(DependencyResolver, MappableDependencyResolver): - dict_collection_visible_keys = DependencyResolver.dict_collection_visible_keys + ['base_path', 'modulepath'] + dict_collection_visible_keys = DependencyResolver.dict_collection_visible_keys + ['base_path', 'modulepath', 'modulecmd', 'prefetch', 'default_indicator', 'find_by'] resolver_type = "modules" def __init__(self, dependency_manager, **kwds): @@ -50,6 +50,7 @@ class ModuleDependencyResolver(DependencyResolver, MappableDependencyResolver): self._setup_mapping(dependency_manager, **kwds) self.versionless = _string_as_bool(kwds.get('versionless', 'false')) find_by = kwds.get('find_by', 'avail') + self.find_by = find_by prefetch = _string_as_bool(kwds.get('prefetch', DEFAULT_MODULE_PREFETCH)) self.modulecmd = kwds.get('modulecmd', DEFAULT_MODULECMD_PATH) self.modulepath = kwds.get('modulepath', self.__default_modulespath()) diff --git a/lib/galaxy/tools/xsd/LICENSE b/lib/galaxy/tool_util/xsd/LICENSE similarity index 100% rename from lib/galaxy/tools/xsd/LICENSE rename to lib/galaxy/tool_util/xsd/LICENSE diff --git a/lib/galaxy/tools/xsd/README.md b/lib/galaxy/tool_util/xsd/README.md similarity index 100% rename from lib/galaxy/tools/xsd/README.md rename to lib/galaxy/tool_util/xsd/README.md diff --git a/lib/galaxy/tools/xsd/galaxy.jxb b/lib/galaxy/tool_util/xsd/galaxy.jxb similarity index 100% rename from lib/galaxy/tools/xsd/galaxy.jxb rename to lib/galaxy/tool_util/xsd/galaxy.jxb diff --git a/lib/galaxy/tools/xsd/galaxy.xsd b/lib/galaxy/tool_util/xsd/galaxy.xsd similarity index 100% rename from lib/galaxy/tools/xsd/galaxy.xsd rename to lib/galaxy/tool_util/xsd/galaxy.xsd diff --git a/lib/galaxy/tools/actions/__init__.py b/lib/galaxy/tools/actions/__init__.py index 3125f399355..def2be977ee 100644 --- a/lib/galaxy/tools/actions/__init__.py +++ b/lib/galaxy/tools/actions/__init__.py @@ -10,12 +10,12 @@ from galaxy import model from galaxy.jobs.actions.post import ActionBox from galaxy.model import LibraryDatasetDatasetAssociation, WorkflowRequestInputParameter from galaxy.model.dataset_collections.builder import CollectionBuilder +from galaxy.model.none_like import NoneDataset from galaxy.objectstore import ObjectStorePopulator from galaxy.tools.parameters import update_dataset_ids from galaxy.tools.parameters.basic import DataCollectionToolParameter, DataToolParameter, RuntimeValue from galaxy.tools.parameters.wrapped import WrappedParameters from galaxy.util import ExecutionTimer -from galaxy.util.none_like import NoneDataset from galaxy.util.odict import odict from galaxy.util.template import fill_template from galaxy.web import url_for @@ -518,9 +518,8 @@ class DefaultToolAction(object): add_datasets_timer = ExecutionTimer() # Add all the top-level (non-child) datasets to the history unless otherwise specified datasets_to_persist = [] - for name in out_data.keys(): + for name, data in out_data.items(): if name not in child_dataset_names and name not in incoming: # don't add children; or already existing datasets, i.e. async created - data = out_data[name] datasets_to_persist.append(data) # Set HID and add to history. # This is brand new and certainly empty so don't worry about quota. diff --git a/lib/galaxy/tools/evaluation.py b/lib/galaxy/tools/evaluation.py index e90216968a0..f861cfce2ed 100644 --- a/lib/galaxy/tools/evaluation.py +++ b/lib/galaxy/tools/evaluation.py @@ -8,6 +8,7 @@ from six import string_types from galaxy import model from galaxy.jobs.datasets import dataset_path_rewrites +from galaxy.model.none_like import NoneDataset from galaxy.tools import global_tool_errors from galaxy.tools.parameters import ( visit_input_values, @@ -38,7 +39,6 @@ from galaxy.util import ( unicodify, ) from galaxy.util.bunch import Bunch -from galaxy.util.none_like import NoneDataset from galaxy.util.object_wrapper import wrap_with_safe_string from galaxy.util.template import fill_template from galaxy.work.context import WorkRequestContext diff --git a/lib/galaxy/tools/linters/citations.py b/lib/galaxy/tools/linters/citations.py index 95c03d078ab..23d8b63cdcb 100644 --- a/lib/galaxy/tools/linters/citations.py +++ b/lib/galaxy/tools/linters/citations.py @@ -21,12 +21,15 @@ def lint_citations(tool_xml, lint_ctx): for citation in citations[0]: if citation.tag != "citation": lint_ctx.warn("Unknown tag discovered in citations block [%s], will be ignored." % citation.tag) - if "type" in citation.attrib: - citation_type = citation.attrib.get("type") - if citation_type not in ["doi", "bibtex"]: - lint_ctx.warn("Unknown citation type discovered [%s], will be ignored.", citation_type) - else: - valid_citations += 1 + continue + citation_type = citation.attrib.get("type") + if citation_type not in ('bibtex', 'doi'): + lint_ctx.warn("Unknown citation type discovered [%s], will be ignored.", citation_type) + continue + if citation.text is None or not citation.text.strip(): + lint_ctx.error('Empty %s citation.' % citation_type) + continue + valid_citations += 1 if valid_citations > 0: lint_ctx.valid("Found %d likely valid citations.", valid_citations) diff --git a/lib/galaxy/tools/linters/cwl.py b/lib/galaxy/tools/linters/cwl.py new file mode 100644 index 00000000000..58390b5ddd4 --- /dev/null +++ b/lib/galaxy/tools/linters/cwl.py @@ -0,0 +1,48 @@ +"""Linter for CWL tools.""" + +lint_tool_types = ["cwl"] + +from galaxy.tool_util.cwl.schema import schema_loader + + +def lint_cwl_validation(tool_source, lint_ctx): + """Determine in CWL tool validates against spec.""" + raw_reference = schema_loader.raw_process_reference(tool_source._source_path) + validation_exception = None + try: + schema_loader.process_definition(raw_reference) + except Exception as e: + validation_exception = e + if validation_exception: + lint_ctx.error("Failed to valdiate CWL artifact [%s]", validation_exception) + else: + lint_ctx.info("CWL appears to be valid.") + + +def lint_new_draft(tool_source, lint_ctx): + """Determine in CWL tool is valid, modern draft.""" + raw_reference = schema_loader.raw_process_reference(tool_source._source_path) + cwl_version = raw_reference.process_object.get("cwlVersion", None) + if cwl_version is None: + lint_ctx.error("CWL file does not contain a 'cwlVersion'") + if cwl_version not in ["v1.0"]: + lint_ctx.warn("CWL version [%s] is unknown, we recommend the v1.0 the stable release." % cwl_version) + else: + lint_ctx.info("Modern CWL version [%s]", cwl_version) + + +def lint_docker_image(tool_source, lint_ctx): + _, containers = tool_source.parse_requirements_and_containers() + if len(containers) == 0: + lint_ctx.warn("Tool does not specify a DockerPull source.") + else: + identifier = containers[0].identifier + lint_ctx.info("Tool will run in Docker image [%s]." % identifier) + + +def lint_description(tool_source, lint_ctx): + help = tool_source.parse_help() + if not help: + lint_ctx.warn("Description of tool is empty or absent.") + elif "TODO" in help: + lint_ctx.warn("Help contains TODO text.") diff --git a/lib/galaxy/tools/test.py b/lib/galaxy/tools/test.py index a4acd0f5ee2..e8c74b6e725 100644 --- a/lib/galaxy/tools/test.py +++ b/lib/galaxy/tools/test.py @@ -21,7 +21,7 @@ log = logging.getLogger(__name__) @nottest def parse_tests(tool, tests_source): """ - Build ToolTestBuilder objects for each "" elements and + Build ToolTestDescription objects for each "" elements and return default interactor (if any). """ raw_tests_dict = tests_source.parse_tests_to_dict() diff --git a/lib/galaxy/tools/verify/interactor.py b/lib/galaxy/tools/verify/interactor.py index 90207d3bc65..235afc5e1ea 100644 --- a/lib/galaxy/tools/verify/interactor.py +++ b/lib/galaxy/tools/verify/interactor.py @@ -42,13 +42,12 @@ log = getLogger(__name__) VERBOSE_ERRORS = util.asbool(os.environ.get("GALAXY_TEST_VERBOSE_ERRORS", False)) UPLOAD_ASYNC = util.asbool(os.environ.get("GALAXY_TEST_UPLOAD_ASYNC", True)) ERROR_MESSAGE_DATASET_SEP = "--------------------------------------" -DEFAULT_TOOL_TEST_WAIT = os.environ.get("GALAXY_TEST_DEFAULT_WAIT", 86400) +DEFAULT_TOOL_TEST_WAIT = int(os.environ.get("GALAXY_TEST_DEFAULT_WAIT", 86400)) DEFAULT_FTYPE = 'auto' # This following default dbkey was traditionally hg17 before Galaxy 18.05, # restore this behavior by setting GALAXY_TEST_DEFAULT_DBKEY to hg17. DEFAULT_DBKEY = os.environ.get("GALAXY_TEST_DEFAULT_DBKEY", "?") -DEFAULT_MAX_SECS = DEFAULT_TOOL_TEST_WAIT class OutputsDict(OrderedDict): @@ -73,7 +72,7 @@ class OutputsDict(OrderedDict): return item -def stage_data_in_history(galaxy_interactor, tool_id, all_test_data, history=None, force_path_paste=False): +def stage_data_in_history(galaxy_interactor, tool_id, all_test_data, history=None, force_path_paste=False, maxseconds=DEFAULT_TOOL_TEST_WAIT): # Upload any needed files upload_waits = [] @@ -81,12 +80,20 @@ def stage_data_in_history(galaxy_interactor, tool_id, all_test_data, history=Non if UPLOAD_ASYNC: for test_data in all_test_data: - upload_waits.append(galaxy_interactor.stage_data_async(test_data, history, tool_id, force_path_paste=force_path_paste)) + upload_waits.append(galaxy_interactor.stage_data_async(test_data, + history, + tool_id, + force_path_paste=force_path_paste, + maxseconds=maxseconds)) for upload_wait in upload_waits: upload_wait() else: for test_data in all_test_data: - upload_wait = galaxy_interactor.stage_data_async(test_data, history, tool_id, force_path_paste=force_path_paste) + upload_wait = galaxy_interactor.stage_data_async(test_data, + history, + tool_id, + force_path_paste=force_path_paste, + maxseconds=maxseconds) upload_wait() @@ -240,7 +247,7 @@ class GalaxyInteractorApi(object): def wait_for(self, func, **kwd): sleep_amount = 0.2 slept = 0 - walltime_exceeded = kwd.get("maxseconds", DEFAULT_TOOL_TEST_WAIT) + walltime_exceeded = int(kwd.get("maxseconds", DEFAULT_TOOL_TEST_WAIT)) while slept <= walltime_exceeded: result = func() @@ -307,7 +314,7 @@ class GalaxyInteractorApi(object): output_id = output_data return output_id - def stage_data_async(self, test_data, history_id, tool_id, force_path_paste=False): + def stage_data_async(self, test_data, history_id, tool_id, force_path_paste=False, maxseconds=DEFAULT_TOOL_TEST_WAIT): fname = test_data['fname'] tool_input = { "file_type": test_data['ftype'], @@ -365,7 +372,7 @@ class GalaxyInteractorApi(object): assert "jobs" in submit_response, "Invalid response from server [%s], expecting jobs in response." % submit_response jobs = submit_response["jobs"] assert len(jobs) > 0, "Invalid response from server [%s], expecting a job." % submit_response - return lambda: self.wait_for_job(jobs[0]["id"], history_id, DEFAULT_TOOL_TEST_WAIT) + return lambda: self.wait_for_job(jobs[0]["id"], history_id, maxseconds=maxseconds) def run_tool(self, testdef, history_id, resource_parameters={}): # We need to handle the case where we've uploaded a valid compressed file since the upload @@ -726,21 +733,25 @@ def verify_tool(tool_id, quiet=False, test_history=None, force_path_paste=False, - maxseconds=None, + maxseconds=DEFAULT_TOOL_TEST_WAIT, tool_test_dicts=None): if resource_parameters is None: resource_parameters = {} tool_test_dicts = tool_test_dicts or galaxy_interactor.get_tool_tests(tool_id, tool_version=tool_version) tool_test_dict = tool_test_dicts[test_index] + tool_test_dict.setdefault('maxseconds', maxseconds) testdef = ToolTestDescription(tool_test_dict) - if maxseconds is not None: - testdef.maxseconds = int(maxseconds) _handle_def_errors(testdef) if test_history is None: test_history = galaxy_interactor.new_history() - stage_data_in_history(galaxy_interactor, tool_id, testdef.test_data(), history=test_history, force_path_paste=force_path_paste) + stage_data_in_history(galaxy_interactor, + tool_id, + testdef.test_data(), + history=test_history, + force_path_paste=force_path_paste, + maxseconds=maxseconds) # Once data is ready, run the tool and check the outputs - record API # input, job info, tool run exception, as well as exceptions related to @@ -1027,7 +1038,7 @@ class ToolTestDescription(object): def __init__(self, processed_test_dict): test_index = processed_test_dict["test_index"] name = processed_test_dict.get('name', 'Test-%d' % (test_index + 1)) - maxseconds = processed_test_dict.get('maxseconds', DEFAULT_MAX_SECS) + maxseconds = processed_test_dict.get('maxseconds', DEFAULT_TOOL_TEST_WAIT) if maxseconds is not None: maxseconds = int(maxseconds) diff --git a/lib/galaxy/tools/wrappers.py b/lib/galaxy/tools/wrappers.py index 8e23cb72585..9952f301308 100644 --- a/lib/galaxy/tools/wrappers.py +++ b/lib/galaxy/tools/wrappers.py @@ -6,8 +6,8 @@ from six import string_types, text_type from six.moves import shlex_quote from galaxy import exceptions +from galaxy.model.none_like import NoneDataset from galaxy.util import odict -from galaxy.util.none_like import NoneDataset from galaxy.util.object_wrapper import wrap_with_safe_string log = logging.getLogger(__name__) diff --git a/lib/galaxy/util/__init__.py b/lib/galaxy/util/__init__.py index 93c76f36003..686946e9c96 100644 --- a/lib/galaxy/util/__init__.py +++ b/lib/galaxy/util/__init__.py @@ -402,6 +402,17 @@ def shrink_stream_by_size(value, size, join_by=b"..", left_larger=True, beginnin return unicodify(rval) +def shrink_and_unicodify(stream): + stream = unicodify(stream, strip_null=True) or u'' + if (len(stream) > DATABASE_MAX_STRING_SIZE): + stream = shrink_string_by_size(stream, + DATABASE_MAX_STRING_SIZE, + join_by="\n..\n", + left_larger=True, + beginning_on_size_error=True) + return stream + + def shrink_string_by_size(value, size, join_by="..", left_larger=True, beginning_on_size_error=False, end_on_size_error=False): if len(value) > size: len_join_by = len(join_by) @@ -993,7 +1004,7 @@ def roundify(amount, sfs=2): return amount[0:sfs] + '0' * (len(amount) - sfs) -def unicodify(value, encoding=DEFAULT_ENCODING, error='replace'): +def unicodify(value, encoding=DEFAULT_ENCODING, error='replace', strip_null=False): u""" Returns a Unicode string or None. @@ -1008,7 +1019,7 @@ def unicodify(value, encoding=DEFAULT_ENCODING, error='replace'): >>> s = u'lâtín strìñg'; assert unicodify(s.encode('latin-1')) == u'l\ufffdt\ufffdn str\ufffd\ufffdg' >>> s = u'lâtín strìñg'; assert unicodify(s.encode('latin-1'), error='ignore') == u'ltn strg' """ - if value is None or isinstance(value, text_type): + if value is None: return value try: if isinstance(value, bytearray): @@ -1025,6 +1036,8 @@ def unicodify(value, encoding=DEFAULT_ENCODING, error='replace'): msg = "Value '%s' could not be coerced to Unicode" % value log.exception(msg) raise Exception(msg) + if strip_null: + return value.replace('\0', '') return value diff --git a/lib/galaxy/util/jstree.py b/lib/galaxy/util/jstree.py index 49906c52db9..f5b3001c939 100644 --- a/lib/galaxy/util/jstree.py +++ b/lib/galaxy/util/jstree.py @@ -47,26 +47,17 @@ class Node(dictobj.DictionaryObject): For example, users may want to pass "attr" or some other valid jsTree options. Example: - >>> import jstree - >>> node = jstree.Node('a', None) - >>> print(node) - Node({'text': 'a', 'children': MutableDictionaryObject({})}) - >>> print(node.jsonData()) - {'text': 'a'} + >>> node = Node('a', None) + >>> assert node._items == {'text': 'a', 'children': dictobj.MutableDictionaryObject({})} + >>> assert node.jsonData() == {'text': 'a'} - >>> import jstree - >>> node = jstree.Node('a', 1) - >>> print(node) - Node({'text': 'a', 'children': MutableDictionaryObject({}), 'li_attr': DictionaryObject({'id': 1}), 'id': 1}) - >>> print(node.jsonData()) - {'text': 'a', 'id': 1, 'li_attr': {'id': 1}} + >>> node = Node('a', 1) + >>> assert node._items == {'text': 'a', 'children': dictobj.MutableDictionaryObject({}), 'li_attr': dictobj.DictionaryObject({'id': 1}), 'id': 1} + >>> assert node.jsonData() == {'text': 'a', 'id': 1, 'li_attr': {'id': 1}} - >>> import jstree - >>> node = jstree.Node('a', 5, icon="folder", state = {'opened': True}) - >>> print(node) - Node({'text': 'a', 'id': 5, 'state': DictionaryObject({'opened': True}), 'children': MutableDictionaryObject({}), 'li_attr': DictionaryObject({'id': 5}), 'icon': 'folder'}) - >>> print(node.jsonData()) - {'text': 'a', 'state': {'opened': True}, 'id': 5, 'li_attr': {'id': 5}, 'icon': 'folder'} + >>> node = Node('a', 5, icon="folder", state = {'opened': True}) + >>> assert node._items == {'text': 'a', 'id': 5, 'state': dictobj.DictionaryObject({'opened': True}), 'children': dictobj.MutableDictionaryObject({}), 'li_attr': dictobj.DictionaryObject({'id': 5}), 'icon': 'folder'} + >>> assert node.jsonData() == {'text': 'a', 'state': {'opened': True}, 'id': 5, 'li_attr': {'id': 5}, 'icon': 'folder'} """ super(Node, self).__init__() diff --git a/lib/galaxy/util/monitors.py b/lib/galaxy/util/monitors.py index 04a1f76359a..ee43d940ff3 100644 --- a/lib/galaxy/util/monitors.py +++ b/lib/galaxy/util/monitors.py @@ -3,8 +3,8 @@ from __future__ import absolute_import import logging import threading -from galaxy.web.stack import register_postfork_function from .sleeper import Sleeper +from .web_compat import register_postfork_function log = logging.getLogger(__name__) diff --git a/lib/galaxy/util/plugin_config.py b/lib/galaxy/util/plugin_config.py index 1a4ce87f212..894601e7f75 100644 --- a/lib/galaxy/util/plugin_config.py +++ b/lib/galaxy/util/plugin_config.py @@ -30,12 +30,14 @@ def plugins_dict(module, plugin_type_identifier): return plugin_dict -def load_plugins(plugins_dict, plugin_source, extra_kwds={}): +def load_plugins(plugins_dict, plugin_source, extra_kwds=None, plugin_type_keys=('type',)): + if extra_kwds is None: + extra_kwds = {} source_type, source = plugin_source if source_type == "xml": return __load_plugins_from_element(plugins_dict, source, extra_kwds) else: - return __load_plugins_from_dicts(plugins_dict, source, extra_kwds) + return __load_plugins_from_dicts(plugins_dict, source, extra_kwds, plugin_type_keys=plugin_type_keys) def __load_plugins_from_element(plugins_dict, plugins_element, extra_kwds): @@ -58,11 +60,16 @@ def __load_plugins_from_element(plugins_dict, plugins_element, extra_kwds): return plugins -def __load_plugins_from_dicts(plugins_dict, configs, extra_kwds): +def __load_plugins_from_dicts(plugins_dict, configs, extra_kwds, plugin_type_keys): plugins = [] for config in configs: - plugin_type = config["type"] + plugin_type = None + for plugin_type_key in plugin_type_keys: + if plugin_type_key in config: + plugin_type = config[plugin_type_key] + break + assert plugin_type is not None, "Could not determine plugin type for [%s]" % config plugin_kwds = config plugin_kwds.update(extra_kwds) plugin = plugins_dict[plugin_type](**plugin_kwds) diff --git a/lib/galaxy/util/watcher.py b/lib/galaxy/util/watcher.py index e90141756cc..b8417b34fa1 100644 --- a/lib/galaxy/util/watcher.py +++ b/lib/galaxy/util/watcher.py @@ -21,7 +21,7 @@ except ImportError: can_watch = False from galaxy.util.hash_util import md5_hash_file -from galaxy.web.stack import register_postfork_function +from .web_compat import register_postfork_function log = logging.getLogger(__name__) diff --git a/lib/galaxy/util/web_compat.py b/lib/galaxy/util/web_compat.py new file mode 100644 index 00000000000..2c71d0e2b7f --- /dev/null +++ b/lib/galaxy/util/web_compat.py @@ -0,0 +1,10 @@ +"""Work around for gross circular dependency between galaxy.util and galaxy.web.stack. + +Provide a function that will delay to forking in a uwsgi environment but run immediately +otherwise. +""" +try: + from galaxy.web.stack import register_postfork_function +except ImportError: + def register_postfork_function(f, *args, **kwargs): + f(*args, **kwargs) diff --git a/lib/galaxy/version.py b/lib/galaxy/version.py index 701b600c93d..0f46519cd48 100644 --- a/lib/galaxy/version.py +++ b/lib/galaxy/version.py @@ -1,3 +1,3 @@ -VERSION_MAJOR = "19.05" +VERSION_MAJOR = "19.09" VERSION_MINOR = "dev" VERSION = VERSION_MAJOR + ('.' + VERSION_MINOR if VERSION_MINOR else '') diff --git a/lib/galaxy/visualization/plugins/registry.py b/lib/galaxy/visualization/plugins/registry.py index 88b10178aa5..8442484bad2 100644 --- a/lib/galaxy/visualization/plugins/registry.py +++ b/lib/galaxy/visualization/plugins/registry.py @@ -223,7 +223,7 @@ class VisualizationsRegistry(object): def get_plugins(self): result = [] - for plugin in self.plugins.itervalues(): + for plugin in self.plugins.values(): result.append(plugin.to_dict()) return sorted(result, key=lambda k: k.get('html')) diff --git a/lib/galaxy/web/base/controller.py b/lib/galaxy/web/base/controller.py index da39c15233e..49e209c8f5e 100644 --- a/lib/galaxy/web/base/controller.py +++ b/lib/galaxy/web/base/controller.py @@ -2,7 +2,6 @@ Contains functionality needed in every web interface """ import logging -import re from six import string_types from sqlalchemy import true @@ -54,13 +53,6 @@ log = logging.getLogger(__name__) SUCCESS, INFO, WARNING, ERROR = "done", "info", "warning", "error" -def _is_valid_slug(slug): - """ Returns true if slug is valid. """ - - VALID_SLUG_RE = re.compile(r"^[a-z0-9\-]+$") - return VALID_SLUG_RE.match(slug) - - class BaseController(object): """ Base class for Galaxy web application controllers. @@ -1118,7 +1110,7 @@ class UsesVisualizationMixin(UsesLibraryMixinItems): title_err = slug_err = "" if not title: title_err = "visualization name is required" - elif slug and not _is_valid_slug(slug): + elif slug and not managers_base.is_valid_slug(slug): slug_err = "visualization identifier must consist of only lowercase letters, numbers, and the '-' character" elif slug and trans.sa_session.query(trans.model.Visualization).filter_by(user=user, slug=slug, deleted=False).first(): slug_err = "visualization identifier must be unique" @@ -1361,7 +1353,7 @@ class SharableMixin(object): def _is_valid_slug(self, slug): """ Returns true if slug is valid. """ - return _is_valid_slug(slug) + return managers_base.is_valid_slug(slug) @web.expose @web.require_login("modify Galaxy items") diff --git a/lib/galaxy/webapps/galaxy/api/job_files.py b/lib/galaxy/webapps/galaxy/api/job_files.py index 813db8bca9c..5f571b94049 100644 --- a/lib/galaxy/webapps/galaxy/api/job_files.py +++ b/lib/galaxy/webapps/galaxy/api/job_files.py @@ -99,6 +99,8 @@ class JobFilesAPIController(BaseAPIController): else: input_file = payload.get("file", payload.get("__file", None)).file + target_dir = os.path.dirname(path) + util.safe_makedirs(target_dir) try: shutil.move(input_file.name, path) finally: diff --git a/lib/galaxy/webapps/galaxy/api/page_revisions.py b/lib/galaxy/webapps/galaxy/api/page_revisions.py index faf2be37035..f240f6e9155 100644 --- a/lib/galaxy/webapps/galaxy/api/page_revisions.py +++ b/lib/galaxy/webapps/galaxy/api/page_revisions.py @@ -3,9 +3,9 @@ API for updating Galaxy Pages """ import logging -from galaxy import exceptions +from galaxy.managers.base import get_object +from galaxy.managers.pages import PageManager from galaxy.model.item_attrs import UsesAnnotations -from galaxy.util.sanitize_html import sanitize_html from galaxy.web import expose_api from galaxy.web.base.controller import ( BaseAPIController, @@ -18,6 +18,10 @@ log = logging.getLogger(__name__) class PageRevisionsController(BaseAPIController, SharableItemSecurityMixin, UsesAnnotations, SharableMixin): + def __init__(self, app): + super(PageRevisionsController, self).__init__(app) + self.manager = PageManager(app) + @expose_api def index(self, trans, page_id, **kwd): """ @@ -30,13 +34,13 @@ class PageRevisionsController(BaseAPIController, SharableItemSecurityMixin, Uses :rtype: list :returns: dictionaries containing different revisions of the page """ - page = self._get_page(trans, page_id) - self._verify_page_ownership(trans, page) - - r = trans.sa_session.query(trans.app.model.PageRevision).filter_by(page_id=trans.security.decode_id(page_id)) + page = get_object(trans, page_id, 'Page', check_ownership=False, check_accessible=True) + r = trans.sa_session.query(trans.app.model.PageRevision).filter_by(page_id=page.id) out = [] for page in r: - out.append(self.encode_all_ids(trans, page.to_dict(), True)) + as_dict = self.encode_all_ids(trans, page.to_dict(), True) + self.manager.rewrite_content_for_export(trans, as_dict) + out.append(as_dict) return out @expose_api @@ -54,42 +58,8 @@ class PageRevisionsController(BaseAPIController, SharableItemSecurityMixin, Uses :rtype: dictionary :returns: Dictionary with 'success' or 'error' element to indicate the result of the request """ - content = payload.get("content", None) - if not content: - raise exceptions.ObjectAttributeMissingException("content undefined or empty") - - page = self._get_page(trans, page_id) - self._verify_page_ownership(trans, page) - - if 'title' in payload: - title = payload['title'] - else: - title = page.title - - content = sanitize_html(content) - - page_revision = trans.app.model.PageRevision() - page_revision.title = title - page_revision.page = page - page.latest_revision = page_revision - page_revision.content = content - - # Persist - session = trans.sa_session - session.flush() - - return page_revision.to_dict(view="element") - - def _get_page(self, trans, page_id): - page = None - try: - page = trans.sa_session.query(trans.app.model.Page).get(trans.security.decode_id(page_id)) - except Exception: - pass - if not page: - raise exceptions.ObjectNotFound() - return page - - def _verify_page_ownership(self, trans, page): - if not self.security_check(trans, page, True, True): - raise exceptions.ItemOwnershipException() + page = get_object(trans, page_id, 'Page', check_ownership=True) + page_revision = self.manager.save_new_revision(trans, page, payload) + rval = self.encode_all_ids(trans, page_revision.to_dict(view="element"), True) + self.manager.rewrite_content_for_export(trans, rval) + return rval diff --git a/lib/galaxy/webapps/galaxy/api/pages.py b/lib/galaxy/webapps/galaxy/api/pages.py index a5a8ac3d75e..c86090a29f0 100644 --- a/lib/galaxy/webapps/galaxy/api/pages.py +++ b/lib/galaxy/webapps/galaxy/api/pages.py @@ -3,13 +3,12 @@ API for updating Galaxy Pages """ import logging -from galaxy import exceptions +from galaxy.managers.base import get_object from galaxy.managers.pages import ( PageManager, PageSerializer ) from galaxy.model.item_attrs import UsesAnnotations -from galaxy.util.sanitize_html import sanitize_html from galaxy.web import expose_api from galaxy.web.base.controller import ( BaseAPIController, @@ -81,39 +80,10 @@ class PagesController(BaseAPIController, SharableItemSecurityMixin, UsesAnnotati :rtype: dict :returns: Dictionary return of the Page.to_dict call """ - user = trans.get_user() - - if not payload.get("title", None): - raise exceptions.ObjectAttributeMissingException("Page name is required") - elif not payload.get("slug", None): - raise exceptions.ObjectAttributeMissingException("Page id is required") - elif not self._is_valid_slug(payload["slug"]): - raise exceptions.ObjectAttributeInvalidException("Page identifier must consist of only lowercase letters, numbers, and the '-' character") - elif trans.sa_session.query(trans.app.model.Page).filter_by(user=user, slug=payload["slug"], deleted=False).first(): - raise exceptions.DuplicatedSlugException("Page slug must be unique") - - content = payload.get("content", "") - content = sanitize_html(content) - - # Create the new stored page - page = trans.app.model.Page() - page.title = payload['title'] - page.slug = payload['slug'] - page_annotation = sanitize_html(payload.get("annotation", "")) - self.add_item_annotation(trans.sa_session, trans.get_user(), page, page_annotation) - page.user = user - # And the first (empty) page revision - page_revision = trans.app.model.PageRevision() - page_revision.title = payload['title'] - page_revision.page = page - page.latest_revision = page_revision - page_revision.content = content - # Persist - session = trans.sa_session - session.add(page) - session.flush() - + page = self.manager.create(trans, payload) rval = self.encode_all_ids(trans, page.to_dict(), True) + rval['content'] = page.latest_revision.content + self.manager.rewrite_content_for_export(trans, rval) return rval @expose_api @@ -128,7 +98,7 @@ class PagesController(BaseAPIController, SharableItemSecurityMixin, UsesAnnotati :rtype: dict :returns: Dictionary with 'success' or 'error' element to indicate the result of the request """ - page = self._get_page(trans, id) + page = get_object(trans, id, 'Page', check_ownership=True) # Mark a page as deleted page.deleted = True @@ -147,22 +117,8 @@ class PagesController(BaseAPIController, SharableItemSecurityMixin, UsesAnnotati :rtype: dict :returns: Dictionary return of the Page.to_dict call with the 'content' field populated by the most recent revision """ - page = self._get_page(trans, id) - self.security_check(trans, page, check_ownership=False, check_accessible=True) + page = get_object(trans, id, 'Page', check_ownership=False, check_accessible=True) rval = self.encode_all_ids(trans, page.to_dict(), True) rval['content'] = page.latest_revision.content + self.manager.rewrite_content_for_export(trans, rval) return rval - - def _get_page(self, trans, id): # Fetches page object and verifies security. - try: - page = trans.sa_session.query(trans.app.model.Page).get(trans.security.decode_id(id)) - except Exception: - page = None - - if not page: - raise exceptions.ObjectNotFound() - - if page.user != trans.user and not trans.user_is_admin: - raise exceptions.ItemOwnershipException() - - return page diff --git a/lib/galaxy/webapps/galaxy/config_schema.yml b/lib/galaxy/webapps/galaxy/config_schema.yml index 91b3482b4e1..91875eff914 100644 --- a/lib/galaxy/webapps/galaxy/config_schema.yml +++ b/lib/galaxy/webapps/galaxy/config_schema.yml @@ -2364,6 +2364,13 @@ mapping: job_config: !include job_config_schema.yml + dependency_resolvers: + type: seq + desc: | + Rather than specifying a dependency_resolvers_config_file, the definition of the + resolvers to enable can be embedded into Galaxy's config with this option. + This has no effect if a dependency_resolvers_config_file is used. + default_job_resubmission_condition: type: str required: false diff --git a/lib/galaxy/webapps/galaxy/controllers/_create_history_template.py b/lib/galaxy/webapps/galaxy/controllers/_create_history_template.py index 24a2b8c0deb..290c8f4495f 100644 --- a/lib/galaxy/webapps/galaxy/controllers/_create_history_template.py +++ b/lib/galaxy/webapps/galaxy/controllers/_create_history_template.py @@ -142,7 +142,7 @@ def inputs_recursive(trans, input_params, param_values, depth=1, upgrade_message if upgrade_messages is None: upgrade_messages = {} - for input_index, input in enumerate(input_params.itervalues()): + for input_index, input in enumerate(input_params.values()): if input.name in param_values: if input.type == "repeat": for i in range(len(param_values[input.name])): diff --git a/lib/galaxy/webapps/galaxy/controllers/admin.py b/lib/galaxy/webapps/galaxy/controllers/admin.py index 175fff3589b..bbc077ddc46 100644 --- a/lib/galaxy/webapps/galaxy/controllers/admin.py +++ b/lib/galaxy/webapps/galaxy/controllers/admin.py @@ -566,7 +566,7 @@ class AdminGalaxy(controller.JSAppLauncher, AdminActions, UsesQuotaMixin, QuotaP ) for data_table_elem_name, data_table in sorted_data_tables: - for filename, file_dict in data_table.filenames.iteritems(): + for filename, file_dict in data_table.filenames.items(): file_missing = ['file missing'] \ if not file_dict.get('found') else [] data.append({ diff --git a/lib/galaxy/webapps/galaxy/controllers/data_manager.py b/lib/galaxy/webapps/galaxy/controllers/data_manager.py index 493731321fc..fb5c47ce231 100644 --- a/lib/galaxy/webapps/galaxy/controllers/data_manager.py +++ b/lib/galaxy/webapps/galaxy/controllers/data_manager.py @@ -23,7 +23,7 @@ class DataManager(BaseUIController): message = kwd.get('message', '') status = kwd.get('status', 'info') data_managers = [] - for data_manager_id, data_manager in sorted(trans.app.data_managers.data_managers.iteritems(), + for data_manager_id, data_manager in sorted(trans.app.data_managers.data_managers.items(), key=lambda data_manager: data_manager[1].name): data_managers.append({'toolUrl': web.url_for(controller='root', tool_id=data_manager.tool.id), diff --git a/lib/galaxy/webapps/galaxy/controllers/page.py b/lib/galaxy/webapps/galaxy/controllers/page.py index 78b00076b42..c5254697430 100644 --- a/lib/galaxy/webapps/galaxy/controllers/page.py +++ b/lib/galaxy/webapps/galaxy/controllers/page.py @@ -1,9 +1,5 @@ -import re -from json import loads from markupsafe import escape -from six.moves.html_entities import name2codepoint -from six.moves.html_parser import HTMLParser from sqlalchemy import ( and_, desc, @@ -16,11 +12,18 @@ from sqlalchemy.orm import ( ) from galaxy import ( - managers, + exceptions, model, util, web ) +from galaxy.managers.hdas import HDAManager +from galaxy.managers.histories import HistoryManager, HistorySerializer +from galaxy.managers.pages import ( + get_page_identifiers, + PageContentProcessor, + PageManager, +) from galaxy.model.item_attrs import UsesItemRatings from galaxy.util import unicodify from galaxy.util.sanitize_html import sanitize_html @@ -40,55 +43,6 @@ from galaxy.web.framework.helpers import ( ) -# Copied from https://github.com/kurtmckee/feedparser -_cp1252 = { - 128: u'\u20ac', # euro sign - 130: u'\u201a', # single low-9 quotation mark - 131: u'\u0192', # latin small letter f with hook - 132: u'\u201e', # double low-9 quotation mark - 133: u'\u2026', # horizontal ellipsis - 134: u'\u2020', # dagger - 135: u'\u2021', # double dagger - 136: u'\u02c6', # modifier letter circumflex accent - 137: u'\u2030', # per mille sign - 138: u'\u0160', # latin capital letter s with caron - 139: u'\u2039', # single left-pointing angle quotation mark - 140: u'\u0152', # latin capital ligature oe - 142: u'\u017d', # latin capital letter z with caron - 145: u'\u2018', # left single quotation mark - 146: u'\u2019', # right single quotation mark - 147: u'\u201c', # left double quotation mark - 148: u'\u201d', # right double quotation mark - 149: u'\u2022', # bullet - 150: u'\u2013', # en dash - 151: u'\u2014', # em dash - 152: u'\u02dc', # small tilde - 153: u'\u2122', # trade mark sign - 154: u'\u0161', # latin small letter s with caron - 155: u'\u203a', # single right-pointing angle quotation mark - 156: u'\u0153', # latin small ligature oe - 158: u'\u017e', # latin small letter z with caron - 159: u'\u0178', # latin capital letter y with diaeresis -} - - -PAGE_MAXRAW = 10**15 - - -def _get_page_identifiers(item_id, app): - # Assume if item id is integer and less than 10**15, it's unencoded. - try: - decoded_id = int(item_id) - if decoded_id >= PAGE_MAXRAW: - raise ValueError("Identifier larger than maximum expected raw int, must be already encoded.") - encoded_id = app.security.encode_id(item_id) - except ValueError: - # It's an encoded id. - encoded_id = item_id - decoded_id = app.security.decode_id(item_id) - return (encoded_id, decoded_id) - - def format_bool(b): if b: return "yes" @@ -300,166 +254,6 @@ class VisualizationSelectionGrid(ItemSelectionGrid): # Adapted from the _BaseHTMLProcessor class of https://github.com/kurtmckee/feedparser -class _PageContentProcessor(HTMLParser, object): - """ - Processes page content to produce HTML that is suitable for display. - For now, processor renders embedded objects. - """ - bare_ampersand = re.compile(r"&(?!#\d+;|#x[0-9a-fA-F]+;|\w+;)") - elements_no_end_tag = set([ - 'area', 'base', 'basefont', 'br', 'col', 'command', 'embed', 'frame', - 'hr', 'img', 'input', 'isindex', 'keygen', 'link', 'meta', 'param', - 'source', 'track', 'wbr' - ]) - - def __init__(self, trans, render_embed_html_fn): - HTMLParser.__init__(self) - self.trans = trans - self.ignore_content = False - self.num_open_tags_for_ignore = 0 - self.render_embed_html_fn = render_embed_html_fn - - def reset(self): - self.pieces = [] - HTMLParser.reset(self) - - def _shorttag_replace(self, match): - tag = match.group(1) - if tag in self.elements_no_end_tag: - return '<' + tag + ' />' - else: - return '<' + tag + '>' - - def feed(self, data): - data = re.compile(r'\s]+?)\s*/>', self._shorttag_replace, data) - data = data.replace(''', "'") - data = data.replace('"', '"') - HTMLParser.feed(self, data) - HTMLParser.close(self) - - def handle_starttag(self, tag, attrs): - """ - Called for each start tag - - attrs is a list of (attr, value) tuples, e.g. for
,
-        tag='pre', attrs=[('class', 'screen')]
-        """
-
-        # If ignoring content, just increment tag count and ignore.
-        if self.ignore_content:
-            self.num_open_tags_for_ignore += 1
-            return
-
-        # Not ignoring tag; look for embedded content.
-        embedded_item = False
-        for attribute in attrs:
-            if (attribute[0] == "class") and ("embedded-item" in attribute[1].split(" ")):
-                embedded_item = True
-                break
-        # For embedded content, set ignore flag to ignore current content and add new content for embedded item.
-        if embedded_item:
-            # Set processing attributes to ignore content.
-            self.ignore_content = True
-            self.num_open_tags_for_ignore = 1
-
-            # Insert content for embedded element.
-            for attribute in attrs:
-                name = attribute[0]
-                if name == "id":
-                    # ID has form '-'
-                    item_class, item_id = attribute[1].split("-")
-                    embed_html = self.render_embed_html_fn(self.trans, item_class, item_id)
-                    self.pieces.append(embed_html)
-            return
-
-        # Default behavior: not ignoring and no embedded content.
-        uattrs = []
-        strattrs = ''
-        if attrs:
-            for key, value in attrs:
-                value = value.replace('>', '>').replace('<', '<').replace('"', '"')
-                value = self.bare_ampersand.sub("&", value)
-                uattrs.append((key, value))
-            strattrs = ''.join(' %s="%s"' % (k, v) for k, v in uattrs)
-        if tag in self.elements_no_end_tag:
-            self.pieces.append('<%s%s />' % (tag, strattrs))
-        else:
-            self.pieces.append('<%s%s>' % (tag, strattrs))
-
-    def handle_endtag(self, tag):
-        """
-        Called for each end tag
-
-        E.g. for 
, tag will be 'pre' - """ - # If ignoring content, see if current tag is the end of content to ignore. - if self.ignore_content: - self.num_open_tags_for_ignore -= 1 - if self.num_open_tags_for_ignore == 0: - # Done ignoring content. - self.ignore_content = False - return - - # Default behavior: reconstruct the original end tag. - if tag not in self.elements_no_end_tag: - self.pieces.append("" % tag) - - def handle_charref(self, ref): - # called for each character reference, e.g. for ' ', ref will be '160' - # Reconstruct the original character reference. - ref = ref.lower() - if ref.startswith('x'): - value = int(ref[1:], 16) - else: - value = int(ref) - - if value in _cp1252: - self.pieces.append('&#%s;' % hex(ord(_cp1252[value]))[1:]) - else: - self.pieces.append('&#%s;' % ref) - - def handle_entityref(self, ref): - # called for each entity reference, e.g. for '©', ref will be 'copy' - # Reconstruct the original entity reference. - if ref in name2codepoint or ref == 'apos': - self.pieces.append('&%s;' % ref) - else: - self.pieces.append('&%s' % ref) - - def handle_data(self, text): - """ - Called for each block of plain text - - Called outside of any tag and not containing any character or entity - references. Store the original text verbatim. - """ - if self.ignore_content: - return - self.pieces.append(text) - - def handle_comment(self, text): - # called for each HTML comment, e.g. - # Reconstruct the original comment. - self.pieces.append('' % text) - - def handle_decl(self, text): - # called for the DOCTYPE, if present, e.g. - # - # Reconstruct original DOCTYPE - self.pieces.append('' % text) - - def handle_pi(self, text): - # called for each processing instruction, e.g. - # Reconstruct original processing instruction. - self.pieces.append('' % text) - - def output(self): - '''Return processed HTML as a single string''' - return ''.join(self.pieces) - - class PageController(BaseUIController, SharableMixin, UsesStoredWorkflowMixin, UsesVisualizationMixin, UsesItemRatings): @@ -473,9 +267,10 @@ class PageController(BaseUIController, SharableMixin, def __init__(self, app): super(PageController, self).__init__(app) - self.history_manager = managers.histories.HistoryManager(app) - self.history_serializer = managers.histories.HistorySerializer(self.app) - self.hda_manager = managers.hdas.HDAManager(app) + self.page_manager = PageManager(app) + self.history_manager = HistoryManager(app) + self.history_serializer = HistorySerializer(self.app) + self.hda_manager = HDAManager(app) @web.expose @web.json @@ -541,37 +336,11 @@ class PageController(BaseUIController, SharableMixin, }] } else: - user = trans.get_user() - p_title = payload.get('title') - p_slug = payload.get('slug') - p_annotation = payload.get('annotation') - if not p_title: - return self.message_exception(trans, 'Please provide a page name is required.') - elif not p_slug: - return self.message_exception(trans, 'Please provide a unique identifier.') - elif not self._is_valid_slug(p_slug): - return self.message_exception(trans, 'Page identifier can only contain lowercase letters, numbers, and dashes (-).') - elif trans.sa_session.query(model.Page).filter_by(user=user, slug=p_slug, deleted=False).first(): - return self.message_exception(trans, 'Page id must be unique.') - else: - # Create the new stored page - p = model.Page() - p.title = p_title - p.slug = p_slug - p.user = user - if p_annotation: - p_annotation = sanitize_html(p_annotation) - self.add_item_annotation(trans.sa_session, user, p, p_annotation) - # And the first (empty) page revision - p_revision = model.PageRevision() - p_revision.title = p_title - p_revision.page = p - p.latest_revision = p_revision - p_revision.content = "" - # Persist - trans.sa_session.add(p) - trans.sa_session.flush() - return {'message': 'Page \'%s\' successfully created.' % p.title, 'status': 'success'} + try: + page = self.page_manager.create(trans, payload) + except exceptions.MessageException as e: + return self.message_exception(trans, str(e)) + return {'message': 'Page \'%s\' successfully created.' % page.title, 'status': 'success'} @web.legacy_expose_api @web.require_login("edit pages") @@ -637,11 +406,7 @@ class PageController(BaseUIController, SharableMixin, id = self.decode_id(id) page = trans.sa_session.query(model.Page).get(id) assert page.user == trans.user - content = page.latest_revision.content - processor = _PageContentProcessor(trans, _placeholderRenderForEdit) - processor.feed(content) - content = unicodify(processor.output(), 'utf-8') - return trans.fill_template("page/editor.mako", page=page, content=content) + return trans.fill_template("page/editor.mako", page=page) @web.expose @web.require_login("use Galaxy pages") @@ -685,57 +450,11 @@ class PageController(BaseUIController, SharableMixin, @web.expose @web.require_login() - def save(self, trans, id, content, annotations): + def save(self, trans, id, content): id = self.decode_id(id) page = trans.sa_session.query(model.Page).get(id) assert page.user == trans.user - - # Sanitize content - content = sanitize_html(content) - processor = _PageContentProcessor(trans, _placeholderRenderForSave) - processor.feed(content) - # Output is string, so convert to unicode for saving. - content = unicodify(processor.output(), 'utf-8') - - # Add a new revision to the page with the provided content. - page_revision = model.PageRevision() - page_revision.title = page.title - page_revision.page = page - page.latest_revision = page_revision - page_revision.content = content - - # Save annotations. - annotations = loads(annotations) - for annotation_dict in annotations: - item_id = self.decode_id(annotation_dict['item_id']) - item_class = self.get_class(annotation_dict['item_class']) - item = trans.sa_session.query(item_class).filter_by(id=item_id).first() - if not item: - raise RuntimeError("cannot find annotated item") - text = sanitize_html(annotation_dict['text']) - - # Add/update annotation. - if item_id and item_class and text: - # Get annotation association. - annotation_assoc_class = eval("model.%sAnnotationAssociation" % item_class.__name__) - annotation_assoc = trans.sa_session.query(annotation_assoc_class).filter_by(user=trans.get_user()) - if item_class == model.History.__class__: - annotation_assoc = annotation_assoc.filter_by(history=item) - elif item_class == model.HistoryDatasetAssociation.__class__: - annotation_assoc = annotation_assoc.filter_by(hda=item) - elif item_class == model.StoredWorkflow.__class__: - annotation_assoc = annotation_assoc.filter_by(stored_workflow=item) - elif item_class == model.WorkflowStep.__class__: - annotation_assoc = annotation_assoc.filter_by(workflow_step=item) - annotation_assoc = annotation_assoc.first() - if not annotation_assoc: - # Create association. - annotation_assoc = annotation_assoc_class() - item.annotations.append(annotation_assoc) - annotation_assoc.user = trans.get_user() - # Set annotation user text. - annotation_assoc.annotation = text - trans.sa_session.flush() + self.page_manager.save_new_revision(trans, page, {"content": content}) @web.expose @web.require_login() @@ -760,7 +479,7 @@ class PageController(BaseUIController, SharableMixin, self.security_check(trans, page, False, True) # Process page content. - processor = _PageContentProcessor(trans, self._get_embed_html) + processor = PageContentProcessor(trans, self._get_embed_html) processor.feed(page.latest_revision.content) # Output is string, so convert to unicode for display. page_content = unicodify(processor.output(), 'utf-8') @@ -942,7 +661,7 @@ class PageController(BaseUIController, SharableMixin, def _get_embed_html(self, trans, item_class, item_id): """ Returns HTML for embedding an item in a page. """ item_class = self.get_class(item_class) - encoded_id, decoded_id = _get_page_identifiers(item_id, trans.app) + encoded_id, decoded_id = get_page_identifiers(item_id, trans.app) if item_class == model.History: return self._get_embedded_history_html(trans, decoded_id) @@ -967,52 +686,3 @@ class PageController(BaseUIController, SharableMixin, elif item_class == model.Page: pass - - -PLACEHOLDER_TEMPLATE = '''

Embedded Galaxy {class_shorthand} - '{item_name}'

[Do not edit this block; Galaxy will fill it in with the annotated {class_shorthand} when it is displayed]

''' - -# This is a mapping of the id portion of page contents to the cssclass/shortname. -PAGE_CLASS_MAPPING = { - 'History': 'History', - 'HistoryDatasetAssociation': 'Dataset', - 'StoredWorkflow': 'Workflow', - 'Visualization': 'Visualization' -} - - -def _placeholderRenderForEdit(trans, item_class, item_id): - return _placeholderRenderForSave(trans, item_class, item_id, encode=True) - - -def _placeholderRenderForSave(trans, item_class, item_id, encode=False): - encoded_item_id, decoded_item_id = _get_page_identifiers(item_id, trans.app) - item_name = '' - if item_class == 'History': - history = trans.sa_session.query(trans.model.History).get(decoded_item_id) - history = managers.base.security_check(trans, history, False, True) - item_name = history.name - elif item_class == 'HistoryDatasetAssociation': - hda = trans.sa_session.query(trans.model.HistoryDatasetAssociation).get(decoded_item_id) - hda_manager = managers.hdas.HDAManager(trans.app) - hda = hda_manager.get_accessible(decoded_item_id, trans.user) - item_name = hda.name - elif item_class == 'StoredWorkflow': - wf = trans.sa_session.query(trans.model.StoredWorkflow).get(decoded_item_id) - wf = managers.base.security_check(trans, wf, False, True) - item_name = wf.name - elif item_class == 'Visualization': - visualization = trans.sa_session.query(trans.model.Visualization).get(decoded_item_id) - visualization = managers.base.security_check(trans, visualization, False, True) - item_name = visualization.title - class_shorthand = PAGE_CLASS_MAPPING[item_class] - if encode: - item_id = encoded_item_id - else: - item_id = decoded_item_id - return PLACEHOLDER_TEMPLATE.format( - item_class=item_class, - class_shorthand=class_shorthand, - class_shorthand_lower=class_shorthand.lower(), - item_id=item_id, - item_name=item_name - ) diff --git a/lib/tool_shed/grids/repository_grids.py b/lib/tool_shed/grids/repository_grids.py index be1b51994e8..ba664184fab 100644 --- a/lib/tool_shed/grids/repository_grids.py +++ b/lib/tool_shed/grids/repository_grids.py @@ -364,10 +364,9 @@ class MatchedRepositoryGrid(grids.Grid): if match_tuples: for match_tuple in match_tuples: repository_id, changeset_revision = match_tuple - clause_list.append("%s=%d and %s='%s'" % (model.RepositoryMetadata.table.c.repository_id, - int(repository_id), - model.RepositoryMetadata.table.c.changeset_revision, - changeset_revision)) + clause_list.append(and_( + model.RepositoryMetadata.repository_id == int(repository_id), + model.RepositoryMetadata.changeset_revision == changeset_revision)) return trans.sa_session.query(model.RepositoryMetadata) \ .join(model.Repository) \ .filter(and_(model.Repository.table.c.deleted == false(), diff --git a/packages/containers/HISTORY.rst b/packages/containers/HISTORY.rst new file mode 100644 index 00000000000..ec2f0e746e9 --- /dev/null +++ b/packages/containers/HISTORY.rst @@ -0,0 +1,12 @@ +.. :changelog: + +History +------- + +.. to_doc + +--------------------- +19.9.0.dev0 +--------------------- + +* Initial import from dev branch of Galaxy during 19.09 development cycle. diff --git a/packages/containers/LICENSE b/packages/containers/LICENSE new file mode 120000 index 00000000000..1ef648f64b3 --- /dev/null +++ b/packages/containers/LICENSE @@ -0,0 +1 @@ +../../LICENSE.txt \ No newline at end of file diff --git a/packages/containers/MANIFEST.in b/packages/containers/MANIFEST.in new file mode 100644 index 00000000000..c74cefe0942 --- /dev/null +++ b/packages/containers/MANIFEST.in @@ -0,0 +1 @@ +include *.rst LICENSE diff --git a/packages/containers/Makefile b/packages/containers/Makefile new file mode 120000 index 00000000000..37af8bae5ba --- /dev/null +++ b/packages/containers/Makefile @@ -0,0 +1 @@ +../package.Makefile \ No newline at end of file diff --git a/packages/containers/README.rst b/packages/containers/README.rst new file mode 100644 index 00000000000..ed360aeef60 --- /dev/null +++ b/packages/containers/README.rst @@ -0,0 +1,14 @@ + +.. image:: https://badge.fury.io/py/galaxy-containers.svg + :target: https://pypi.python.org/pypi/galaxy-containers/ + + +Overview +-------- + +The Galaxy_ containers module - interfaces for modeling and interacting with container backends (docker and docker swarm currently). + +* Free software: Academic Free License version 3.0 +* Code: https://github.com/galaxyproject/galaxy + +.. _Galaxy: http://galaxyproject.org/ diff --git a/packages/containers/dev-requirements.txt b/packages/containers/dev-requirements.txt new file mode 120000 index 00000000000..467b90d7a23 --- /dev/null +++ b/packages/containers/dev-requirements.txt @@ -0,0 +1 @@ +../package-dev-requirements.txt \ No newline at end of file diff --git a/packages/containers/galaxy/__init__.py b/packages/containers/galaxy/__init__.py new file mode 100644 index 00000000000..69e3be50dac --- /dev/null +++ b/packages/containers/galaxy/__init__.py @@ -0,0 +1 @@ +__path__ = __import__('pkgutil').extend_path(__path__, __name__) diff --git a/packages/containers/galaxy/containers b/packages/containers/galaxy/containers new file mode 120000 index 00000000000..bc2353d5e12 --- /dev/null +++ b/packages/containers/galaxy/containers @@ -0,0 +1 @@ +../../../lib/galaxy/containers \ No newline at end of file diff --git a/packages/containers/galaxy/project_galaxy_containers.py b/packages/containers/galaxy/project_galaxy_containers.py new file mode 100644 index 00000000000..6442c8261bb --- /dev/null +++ b/packages/containers/galaxy/project_galaxy_containers.py @@ -0,0 +1,13 @@ +# -*- coding: utf-8 -*- + +__version__ = '19.9.0.dev0' + +PROJECT_NAME = "galaxy-containers" +PROJECT_OWNER = PROJECT_USERAME = "galaxyproject" +PROJECT_URL = "https://github.com/galaxyproject/galaxy" +PROJECT_AUTHOR = 'Galaxy Project and Community' +PROJECT_DESCRIPTION = 'Galaxy Container Modeling and Interaction Abstractions' +PROJECT_EMAIL = 'jmchilton@gmail.com' +RAW_CONTENT_URL = "https://raw.github.com/%s/%s/master/" % ( + PROJECT_USERAME, PROJECT_NAME +) diff --git a/packages/containers/requirements.txt b/packages/containers/requirements.txt new file mode 100644 index 00000000000..3a4655d7abd --- /dev/null +++ b/packages/containers/requirements.txt @@ -0,0 +1,3 @@ +galaxy-util +requests + diff --git a/packages/containers/scripts b/packages/containers/scripts new file mode 120000 index 00000000000..9aec9dc5a06 --- /dev/null +++ b/packages/containers/scripts @@ -0,0 +1 @@ +../build_scripts \ No newline at end of file diff --git a/packages/containers/setup.cfg b/packages/containers/setup.cfg new file mode 120000 index 00000000000..eb7cf09393f --- /dev/null +++ b/packages/containers/setup.cfg @@ -0,0 +1 @@ +../../setup.cfg \ No newline at end of file diff --git a/packages/containers/setup.py b/packages/containers/setup.py new file mode 100644 index 00000000000..4d4fb9caec1 --- /dev/null +++ b/packages/containers/setup.py @@ -0,0 +1,102 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +import ast +import os +import re +try: + from setuptools import setup +except ImportError: + from distutils.core import setup + +SOURCE_DIR = "galaxy" + +_version_re = re.compile(r'__version__\s+=\s+(.*)') + +with open('%s/project_galaxy_containers.py' % SOURCE_DIR, 'rb') as f: + init_contents = f.read().decode('utf-8') + + def get_var(var_name): + pattern = re.compile(r'%s\s+=\s+(.*)' % var_name) + match = pattern.search(init_contents).group(1) + return str(ast.literal_eval(match)) + + version = get_var("__version__") + PROJECT_NAME = get_var("PROJECT_NAME") + PROJECT_URL = get_var("PROJECT_URL") + PROJECT_AUTHOR = get_var("PROJECT_AUTHOR") + PROJECT_EMAIL = get_var("PROJECT_EMAIL") + PROJECT_DESCRIPTION = get_var("PROJECT_DESCRIPTION") + +TEST_DIR = 'tests' +PACKAGES = [ + 'galaxy', + 'galaxy.containers', +] +ENTRY_POINTS = ''' + [console_scripts] +''' +PACKAGE_DATA = { + # Be sure to update MANIFEST.in for source dist. + 'galaxy': [ + ], +} +PACKAGE_DIR = { + SOURCE_DIR: SOURCE_DIR, +} + +readme = open('README.rst').read() +history = open('HISTORY.rst').read().replace('.. :changelog:', '') + +if os.path.exists("requirements.txt"): + requirements = open("requirements.txt").read().split("\n") +else: + # In tox, it will cover them anyway. + requirements = [] + + +test_requirements = [ + # TODO: put package test requirements here +] + + +setup( + name=PROJECT_NAME, + version=version, + description=PROJECT_DESCRIPTION, + long_description=readme + '\n\n' + history, + long_description_content_type='text/x-rst', + author=PROJECT_AUTHOR, + author_email=PROJECT_EMAIL, + url=PROJECT_URL, + packages=PACKAGES, + entry_points=ENTRY_POINTS, + package_data=PACKAGE_DATA, + package_dir=PACKAGE_DIR, + include_package_data=True, + install_requires=requirements, + extras_require={ + 'docker': ['docker'], + }, + license="AFL", + zip_safe=False, + keywords='galaxy', + classifiers=[ + 'Development Status :: 5 - Production/Stable', + 'Intended Audience :: Developers', + 'Environment :: Console', + 'License :: OSI Approved :: Academic Free License (AFL)', + 'Operating System :: POSIX', + 'Topic :: Software Development', + 'Topic :: Software Development :: Code Generators', + 'Topic :: Software Development :: Testing', + 'Natural Language :: English', + "Programming Language :: Python :: 2", + 'Programming Language :: Python :: 2.7', + 'Programming Language :: Python :: 3.5', + 'Programming Language :: Python :: 3.6', + 'Programming Language :: Python :: 3.7', + ], + test_suite=TEST_DIR, + tests_require=test_requirements +) diff --git a/packages/data/requirements.txt b/packages/data/requirements.txt index 0c13610d96a..f2b5c80f06e 100644 --- a/packages/data/requirements.txt +++ b/packages/data/requirements.txt @@ -1,7 +1,7 @@ galaxy-objectstore galaxy-util bdbag -bx +bx-python h5py isa-rwval numpy diff --git a/packages/data/tests/unittest_utils b/packages/data/tests/unittest_utils deleted file mode 120000 index 6b6233367e6..00000000000 --- a/packages/data/tests/unittest_utils +++ /dev/null @@ -1 +0,0 @@ -../../../test/unit/unittest_utils \ No newline at end of file diff --git a/packages/data/tests/unittest_utils/__init__.py b/packages/data/tests/unittest_utils/__init__.py new file mode 120000 index 00000000000..2115ad35783 --- /dev/null +++ b/packages/data/tests/unittest_utils/__init__.py @@ -0,0 +1 @@ +../../test/unit/unittest_utils/__init__.py \ No newline at end of file diff --git a/packages/data/tests/unittest_utils/tempfilecache.py b/packages/data/tests/unittest_utils/tempfilecache.py new file mode 120000 index 00000000000..93fb83f1df7 --- /dev/null +++ b/packages/data/tests/unittest_utils/tempfilecache.py @@ -0,0 +1 @@ +../../test/unit/unittest_utils/tempfilecache.py \ No newline at end of file diff --git a/packages/data/tests/unittest_utils/utility.py b/packages/data/tests/unittest_utils/utility.py new file mode 120000 index 00000000000..54dd4ca7a3b --- /dev/null +++ b/packages/data/tests/unittest_utils/utility.py @@ -0,0 +1 @@ +../../test/unit/unittest_utils/utility.py \ No newline at end of file diff --git a/packages/job_metrics/galaxy/project_galaxy_job_metrics.py b/packages/job_metrics/galaxy/project_galaxy_job_metrics.py index 755ea7bf08c..5c5c5411ab8 100644 --- a/packages/job_metrics/galaxy/project_galaxy_job_metrics.py +++ b/packages/job_metrics/galaxy/project_galaxy_job_metrics.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -__version__ = '19.9.0.dev1' +__version__ = '19.9.0.dev2' PROJECT_NAME = "galaxy-job-metrics" PROJECT_OWNER = PROJECT_USERAME = "galaxyproject" diff --git a/packages/objectstore/galaxy/project_galaxy_objectstore.py b/packages/objectstore/galaxy/project_galaxy_objectstore.py index 9aae9df3e85..825abc23062 100644 --- a/packages/objectstore/galaxy/project_galaxy_objectstore.py +++ b/packages/objectstore/galaxy/project_galaxy_objectstore.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -__version__ = '19.9.0.dev1' +__version__ = '19.9.0.dev2' PROJECT_NAME = "galaxy-objectstore" PROJECT_OWNER = PROJECT_USERAME = "galaxyproject" diff --git a/packages/test.sh b/packages/test.sh new file mode 100755 index 00000000000..d311a4d5ed1 --- /dev/null +++ b/packages/test.sh @@ -0,0 +1,43 @@ +#!/bin/bash + +set -e + +# Change to packages directory. +cd "$(dirname "$0")" + +# Use a throw-away virtualenv +TEST_PYTHON=${TEST_PYTHON:-"python"} +TEST_ENV_DIR=${TEST_ENV_DIR:-$(mktemp -d -t gxpkgtestenvXXXXXX)} + +virtualenv -p "$TEST_PYTHON" "$TEST_ENV_DIR" +. "${TEST_ENV_DIR}/bin/activate" +pip install pytest + +# ensure ordered by dependency dag +PACKAGE_DIRS=( + util + objectstore + job_metrics + containers + tool_util + data +) +# containers has no tests, tool_util not yet working 100%, +# data has many problems quota, tool shed install database, etc.. +RUN_TESTS=(1 1 1 0 0 0 0) + +for ((i=0; i<${#PACKAGE_DIRS[@]}; i++)); do + package_dir=${PACKAGE_DIRS[$i]} + run_tests=${RUN_TESTS[$i]} + + cd "$package_dir" + pip install -e . + if [ "$package_dir" = "util" ]; then + pip install -e '.[template,jstree]' + fi + + if [[ "$run_tests" == "1" ]]; then + pytest --doctest-modules galaxy tests + fi + cd .. +done diff --git a/packages/tool_util/MANIFEST.in b/packages/tool_util/MANIFEST.in index 2bf148a5764..baf7e4b1ce2 100644 --- a/packages/tool_util/MANIFEST.in +++ b/packages/tool_util/MANIFEST.in @@ -1,2 +1,4 @@ include *.rst LICENSE - +include galaxy/tool_util/deps/mulled/invfile.lua +include galaxy/tool_util/deps/resolvers/default_conda_mapping.yml +include galaxy/tool_util/xsd/* diff --git a/packages/tool_util/galaxy/project_galaxy_tool_util.py b/packages/tool_util/galaxy/project_galaxy_tool_util.py index c6440bf685a..4f526958884 100644 --- a/packages/tool_util/galaxy/project_galaxy_tool_util.py +++ b/packages/tool_util/galaxy/project_galaxy_tool_util.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -__version__ = '19.9.0.dev1' +__version__ = '19.9.0.dev2' PROJECT_NAME = "galaxy-tool-util" PROJECT_OWNER = PROJECT_USERAME = "galaxyproject" diff --git a/packages/tool_util/setup.py b/packages/tool_util/setup.py index 9645f9942fd..33bf10b7794 100644 --- a/packages/tool_util/setup.py +++ b/packages/tool_util/setup.py @@ -53,6 +53,7 @@ PACKAGE_DATA = { 'galaxy': [ 'tool_util/deps/mulled/invfile.lua', 'tool_util/deps/resolvers/default_conda_mapping.yml', + 'tool_util/xsd/galaxy.xsd', ], } PACKAGE_DIR = { diff --git a/packages/util/galaxy/project_galaxy_util.py b/packages/util/galaxy/project_galaxy_util.py index 1333aebadd2..c5a4d0ce99a 100644 --- a/packages/util/galaxy/project_galaxy_util.py +++ b/packages/util/galaxy/project_galaxy_util.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -__version__ = '19.9.0.dev2' +__version__ = '19.9.0.dev3' PROJECT_NAME = "galaxy-util" PROJECT_OWNER = PROJECT_USERAME = "galaxyproject" diff --git a/packages/util/requirements.txt b/packages/util/requirements.txt index 3290e4ad496..e15e5db251e 100644 --- a/packages/util/requirements.txt +++ b/packages/util/requirements.txt @@ -1,7 +1,6 @@ bleach boltons bz2file; python_version < '3.3' -Cheetah3 docutils markupsafe packaging diff --git a/packages/util/setup.py b/packages/util/setup.py index ee54c34e225..1227150c7b1 100644 --- a/packages/util/setup.py +++ b/packages/util/setup.py @@ -80,6 +80,10 @@ setup( package_dir=PACKAGE_DIR, include_package_data=True, install_requires=requirements, + extras_require={ + 'template': ['future', 'Cheetah3'], + 'jstree': ['dictobj'], + }, license="AFL", zip_safe=False, keywords='galaxy', diff --git a/run_tests.sh b/run_tests.sh index 0efac9e7111..1236e8742cc 100755 --- a/run_tests.sh +++ b/run_tests.sh @@ -1,7 +1,6 @@ #!/bin/sh -pwd_dir=$(pwd) -cd `dirname $0` +cd "$(dirname "$0")" rm -f run_functional_tests.log @@ -318,8 +317,8 @@ then -e "GALAXY_TEST_DATABASE_TYPE=$db_type" \ -e "LC_ALL=C" \ --rm \ - -v `pwd`:/galaxy \ - -v `pwd`/test/docker/base/run_test_wrapper.sh:/usr/local/bin/run_test_wrapper.sh $DOCKER_IMAGE "$@" + -v "$(pwd)":/galaxy \ + -v "$(pwd)"/test/docker/base/run_test_wrapper.sh:/usr/local/bin/run_test_wrapper.sh "$DOCKER_IMAGE" "$@" exit $? fi @@ -374,7 +373,6 @@ do api_script="./test/api" shift 1 fi - coverage_file="api_coverage.xml" ;; -selenium|--selenium) GALAXY_TEST_TOOL_CONF="config/tool_conf.xml.sample,test/functional/tools/samples_tool_conf.xml" @@ -412,15 +410,15 @@ do shift ;; --external_url) - GALAXY_TEST_EXTERNAL=$2 + export GALAXY_TEST_EXTERNAL=$2 shift 2 ;; --external_master_key) - GALAXY_CONFIG_MASTER_KEY=$2 + export GALAXY_CONFIG_MASTER_API_KEY=$2 shift 2 ;; --external_user_key) - GALAXY_TEST_USER_API_KEY=$2 + export GALAXY_TEST_USER_API_KEY=$2 shift 2 ;; -f|-framework|--framework) @@ -428,7 +426,6 @@ do marker="-m tool" test_script="pytest" report_file="run_framework_tests.html" - coverage_file="framework_coverage.xml" framework_test=1; shift 1 ;; @@ -437,7 +434,6 @@ do marker="-m tool" test_script="pytest" report_file="run_framework_tests.html" - coverage_file="main_tools_coverage.xml" framework_test=1; shift 1 ;; @@ -445,7 +441,6 @@ do marker="-m data_manager" test_script="pytest" report_file="run_data_managers_tests.html" - coverage_file="data_managers_coverage.xml" data_managers_test=1; shift 1 ;; @@ -454,7 +449,6 @@ do marker="-m tool" test_script="pytest" report_file="run_migrated_tests.html" - coverage_file="migrated_coverage.xml" migrated_test=1; shift ;; @@ -463,7 +457,6 @@ do marker="-m tool" test_script="pytest" report_file="run_installed_tests.html" - coverage_file="installed_coverage.xml" installed_test=1; shift ;; @@ -503,7 +496,6 @@ do # Must have coverage installed (try `which coverage`) - only valid with --unit # for now. Would be great to get this to work with functional tests though. coverage_arg="--with-coverage" - NOSE_WITH_COVERAGE=true shift ;; --debug) @@ -515,7 +507,7 @@ do -u|-unit|--unit) report_file="run_unit_tests.html" test_script="pytest" - unit_extra='--doctest-modules --ignore lib/galaxy/web/proxy/js/node_modules/ --ignore lib/galaxy/webapps/tool_shed/controllers --ignore lib/galaxy/jobs/runners/chronos.py --ignore lib/galaxy/webapps/tool_shed/model/migrate --ignore lib/galaxy/util/jstree.py' + unit_extra='--doctest-modules --ignore lib/galaxy/web/proxy/js/node_modules/ --ignore lib/galaxy/webapps/tool_shed/controllers --ignore lib/galaxy/jobs/runners/chronos.py --ignore lib/galaxy/webapps/tool_shed/model/migrate' if [ $# -gt 1 ]; then unit_extra="$unit_extra $2" shift 2 @@ -523,7 +515,6 @@ do unit_extra="$unit_extra lib test/unit" shift 1 fi - coverage_file="unit_coverage.xml" ;; -i|-integration|--integration) GALAXY_TEST_TOOL_CONF="config/tool_conf.xml.sample,test/functional/tools/samples_tool_conf.xml" @@ -535,7 +526,6 @@ do else integration_extra="./test/integration" shift 1 - coverage_file="integration_coverage.xml" fi ;; --no_cleanup) @@ -616,7 +606,7 @@ elif [ -n "$toolshed_script" ]; then elif [ -n "$api_script" ]; then extra_args="$api_script" elif [ -n "$section_id" ]; then - extra_args=`python tool_list.py $section_id` + extra_args=$(python tool_list.py "$section_id") elif [ -n "$unit_extra" ]; then extra_args="$unit_extra" elif [ -n "$integration_extra" ]; then @@ -645,8 +635,8 @@ else fi export GALAXY_TEST_TOOL_CONF if [ "$test_script" = 'pytest' ]; then - if [ "$coverage_arg" = "--with_coverage" ]; then - coverage_arg="--cov-report term --cov-report xml:cov-unit.xml --cov=lib" + if [ "$coverage_arg" = '--with-coverage' ]; then + coverage_arg="--cov-report term --cov=lib" fi "$test_script" -v --html "$report_file" $coverage_arg $xunit_args $extra_args "$@" else diff --git a/scripts/cleanup_datasets/delete_datasets.sh b/scripts/cleanup_datasets/delete_datasets.sh index 501c234a8d5..e7dfb3f6874 100644 --- a/scripts/cleanup_datasets/delete_datasets.sh +++ b/scripts/cleanup_datasets/delete_datasets.sh @@ -1,4 +1,4 @@ #!/bin/sh -cd `dirname $0`/../.. -python ./scripts/cleanup_datasets/cleanup_datasets.py -d 10 -6 -r $@ >> ./scripts/cleanup_datasets/delete_datasets.log +cd "$(dirname "$0")"/../.. +python ./scripts/cleanup_datasets/cleanup_datasets.py -d 10 -6 -r "$@" >> ./scripts/cleanup_datasets/delete_datasets.log diff --git a/scripts/cleanup_datasets/delete_userless_histories.sh b/scripts/cleanup_datasets/delete_userless_histories.sh index 72d68db72ec..9bb8c70882f 100644 --- a/scripts/cleanup_datasets/delete_userless_histories.sh +++ b/scripts/cleanup_datasets/delete_userless_histories.sh @@ -1,4 +1,4 @@ #!/bin/sh -cd `dirname $0`/../.. -python ./scripts/cleanup_datasets/cleanup_datasets.py -d 10 -1 $@ >> ./scripts/cleanup_datasets/delete_userless_histories.log +cd "$(dirname "$0")"/../.. +python ./scripts/cleanup_datasets/cleanup_datasets.py -d 10 -1 "$@" >> ./scripts/cleanup_datasets/delete_userless_histories.log diff --git a/scripts/cleanup_datasets/populate_uuid.sh b/scripts/cleanup_datasets/populate_uuid.sh index ac2028462cf..c12c5240c19 100644 --- a/scripts/cleanup_datasets/populate_uuid.sh +++ b/scripts/cleanup_datasets/populate_uuid.sh @@ -1,5 +1,5 @@ #!/bin/sh -cd `dirname $0`/../.. +cd "$(dirname "$0")"/../.. export PYTHONPATH=./lib/ -python ./scripts/cleanup_datasets/populate_uuid.py ./config/galaxy.ini $@ +python ./scripts/cleanup_datasets/populate_uuid.py ./config/galaxy.ini "$@" diff --git a/scripts/cleanup_datasets/purge_datasets.sh b/scripts/cleanup_datasets/purge_datasets.sh index e4405015c50..6d5c12bb880 100644 --- a/scripts/cleanup_datasets/purge_datasets.sh +++ b/scripts/cleanup_datasets/purge_datasets.sh @@ -1,4 +1,4 @@ #!/bin/sh -cd `dirname $0`/../.. -python ./scripts/cleanup_datasets/cleanup_datasets.py -d 10 -3 -r $@ >> ./scripts/cleanup_datasets/purge_datasets.log +cd "$(dirname "$0")"/../.. +python ./scripts/cleanup_datasets/cleanup_datasets.py -d 10 -3 -r "$@" >> ./scripts/cleanup_datasets/purge_datasets.log diff --git a/scripts/cleanup_datasets/purge_folders.sh b/scripts/cleanup_datasets/purge_folders.sh index ff970d0ac8e..d02b7671e6f 100644 --- a/scripts/cleanup_datasets/purge_folders.sh +++ b/scripts/cleanup_datasets/purge_folders.sh @@ -1,4 +1,4 @@ #!/bin/sh -cd `dirname $0`/../.. -python ./scripts/cleanup_datasets/cleanup_datasets.py -d 10 -5 -r $@ >> ./scripts/cleanup_datasets/purge_folders.log +cd "$(dirname "$0")"/../.. +python ./scripts/cleanup_datasets/cleanup_datasets.py -d 10 -5 -r "$@" >> ./scripts/cleanup_datasets/purge_folders.log diff --git a/scripts/cleanup_datasets/purge_histories.sh b/scripts/cleanup_datasets/purge_histories.sh index 66e9f3d285f..8c7ff274d4a 100644 --- a/scripts/cleanup_datasets/purge_histories.sh +++ b/scripts/cleanup_datasets/purge_histories.sh @@ -1,4 +1,4 @@ #!/bin/sh -cd `dirname $0`/../.. -python ./scripts/cleanup_datasets/cleanup_datasets.py -d 10 -2 -r $@ >> ./scripts/cleanup_datasets/purge_histories.log +cd "$(dirname "$0")"/../.. +python ./scripts/cleanup_datasets/cleanup_datasets.py -d 10 -2 -r "$@" >> ./scripts/cleanup_datasets/purge_histories.log diff --git a/scripts/cleanup_datasets/purge_libraries.sh b/scripts/cleanup_datasets/purge_libraries.sh index 6a96b30268d..9e66e05cb5c 100644 --- a/scripts/cleanup_datasets/purge_libraries.sh +++ b/scripts/cleanup_datasets/purge_libraries.sh @@ -1,4 +1,4 @@ #!/bin/sh -cd `dirname $0`/../.. -python ./scripts/cleanup_datasets/cleanup_datasets.py -d 10 -4 -r $@ >> ./scripts/cleanup_datasets/purge_libraries.log +cd "$(dirname "$0")"/../.. +python ./scripts/cleanup_datasets/cleanup_datasets.py -d 10 -4 -r "$@" >> ./scripts/cleanup_datasets/purge_libraries.log diff --git a/scripts/cleanup_datasets/update_metadata.sh b/scripts/cleanup_datasets/update_metadata.sh index 7933a0d9ebb..a78325d8ee7 100644 --- a/scripts/cleanup_datasets/update_metadata.sh +++ b/scripts/cleanup_datasets/update_metadata.sh @@ -6,4 +6,4 @@ . ./scripts/get_python.sh . ./setup_paths.sh -$GALAXY_PYTHON ./scripts/cleanup_datasets/update_metadata.py ./config/galaxy.ini $@ +"$GALAXY_PYTHON" ./scripts/cleanup_datasets/update_metadata.py ./config/galaxy.ini "$@" diff --git a/scripts/db_shell.py b/scripts/db_shell.py index 2ec27eb2db4..ddf85059796 100644 --- a/scripts/db_shell.py +++ b/scripts/db_shell.py @@ -58,8 +58,7 @@ def printquery(statement, bind=None): import sqlalchemy.orm if isinstance(statement, sqlalchemy.orm.Query): if bind is None: - bind = statement.session.get_bind( - statement._mapper_zero_or_none()) + bind = statement.session.get_bind() statement = statement.statement elif bind is None: bind = statement.bind diff --git a/scripts/maintenance.sh b/scripts/maintenance.sh index 8ca4771ed54..e45d11bb603 100755 --- a/scripts/maintenance.sh +++ b/scripts/maintenance.sh @@ -6,7 +6,7 @@ set -e display_help(){ - scriptname=`basename $0` + scriptname=$(basename "$0") printf "./$scriptname [--help] [--no-dry-run] [--days 10] Will run the galaxy cleanup scripts in the recommend order. By default a 'dry-run' is started. Specify --no-dry-run to do the actual cleanup. @@ -52,7 +52,7 @@ do esac done -cd "$(cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd)"/../ +cd "$(dirname "$0")"/.. . scripts/common_startup_functions.sh diff --git a/scripts/migrate_tools/0002_tools.sh b/scripts/migrate_tools/0002_tools.sh index 926aba9529d..7262196b5a9 100644 --- a/scripts/migrate_tools/0002_tools.sh +++ b/scripts/migrate_tools/0002_tools.sh @@ -1,4 +1,4 @@ #!/bin/sh -cd `dirname $0`/../.. -python ./scripts/migrate_tools/migrate_tools.py 0002_tools.xml $@ +cd "$(dirname "$0")"/../.. +python ./scripts/migrate_tools/migrate_tools.py 0002_tools.xml "$@" diff --git a/scripts/migrate_tools/0003_tools.sh b/scripts/migrate_tools/0003_tools.sh index dfc3bde56b3..f8a1ba8bb16 100644 --- a/scripts/migrate_tools/0003_tools.sh +++ b/scripts/migrate_tools/0003_tools.sh @@ -1,4 +1,4 @@ #!/bin/sh -cd `dirname $0`/../.. -python ./scripts/migrate_tools/migrate_tools.py 0003_tools.xml $@ +cd "$(dirname "$0")"/../.. +python ./scripts/migrate_tools/migrate_tools.py 0003_tools.xml "$@" diff --git a/scripts/migrate_tools/0004_tools.sh b/scripts/migrate_tools/0004_tools.sh index 40b76956fa2..7ae608f3683 100644 --- a/scripts/migrate_tools/0004_tools.sh +++ b/scripts/migrate_tools/0004_tools.sh @@ -1,4 +1,4 @@ #!/bin/sh -cd `dirname $0`/../.. -python ./scripts/migrate_tools/migrate_tools.py 0004_tools.xml $@ +cd "$(dirname "$0")"/../.. +python ./scripts/migrate_tools/migrate_tools.py 0004_tools.xml "$@" diff --git a/scripts/migrate_tools/0005_tools.sh b/scripts/migrate_tools/0005_tools.sh index c9f1f3c61d6..da7f9c09098 100644 --- a/scripts/migrate_tools/0005_tools.sh +++ b/scripts/migrate_tools/0005_tools.sh @@ -1,4 +1,4 @@ #!/bin/sh -cd `dirname $0`/../.. -python ./scripts/migrate_tools/migrate_tools.py 0005_tools.xml $@ +cd "$(dirname "$0")"/../.. +python ./scripts/migrate_tools/migrate_tools.py 0005_tools.xml "$@" diff --git a/scripts/migrate_tools/0006_tools.sh b/scripts/migrate_tools/0006_tools.sh index 1f000ebeca6..ff85522a5f9 100644 --- a/scripts/migrate_tools/0006_tools.sh +++ b/scripts/migrate_tools/0006_tools.sh @@ -1,4 +1,4 @@ #!/bin/sh -cd `dirname $0`/../.. -python ./scripts/migrate_tools/migrate_tools.py 0006_tools.xml $@ +cd "$(dirname "$0")"/../.. +python ./scripts/migrate_tools/migrate_tools.py 0006_tools.xml "$@" diff --git a/scripts/migrate_tools/0007_tools.sh b/scripts/migrate_tools/0007_tools.sh index a6cf948593d..8a88f44420c 100644 --- a/scripts/migrate_tools/0007_tools.sh +++ b/scripts/migrate_tools/0007_tools.sh @@ -1,4 +1,4 @@ #!/bin/sh -cd `dirname $0`/../.. -python ./scripts/migrate_tools/migrate_tools.py 0007_tools.xml $@ +cd "$(dirname "$0")"/../.. +python ./scripts/migrate_tools/migrate_tools.py 0007_tools.xml "$@" diff --git a/scripts/migrate_tools/0008_tools.sh b/scripts/migrate_tools/0008_tools.sh index 50cafd19936..d312237023b 100644 --- a/scripts/migrate_tools/0008_tools.sh +++ b/scripts/migrate_tools/0008_tools.sh @@ -1,4 +1,4 @@ #!/bin/sh -cd `dirname $0`/../.. -python ./scripts/migrate_tools/migrate_tools.py 0008_tools.xml $@ +cd "$(dirname "$0")"/../.. +python ./scripts/migrate_tools/migrate_tools.py 0008_tools.xml "$@" diff --git a/scripts/migrate_tools/0009_tools.sh b/scripts/migrate_tools/0009_tools.sh index 36f162b141c..78242fe2422 100644 --- a/scripts/migrate_tools/0009_tools.sh +++ b/scripts/migrate_tools/0009_tools.sh @@ -1,4 +1,4 @@ #!/bin/sh -cd `dirname $0`/../.. -python ./scripts/migrate_tools/migrate_tools.py 0009_tools.xml $@ +cd "$(dirname "$0")"/../.. +python ./scripts/migrate_tools/migrate_tools.py 0009_tools.xml "$@" diff --git a/scripts/migrate_tools/0010_tools.sh b/scripts/migrate_tools/0010_tools.sh index fde17704dd3..03c65dd9973 100644 --- a/scripts/migrate_tools/0010_tools.sh +++ b/scripts/migrate_tools/0010_tools.sh @@ -1,4 +1,4 @@ #!/bin/sh -cd `dirname $0`/../.. -python ./scripts/migrate_tools/migrate_tools.py 0010_tools.xml $@ +cd "$(dirname "$0")"/../.. +python ./scripts/migrate_tools/migrate_tools.py 0010_tools.xml "$@" diff --git a/scripts/migrate_tools/0011_tools.sh b/scripts/migrate_tools/0011_tools.sh index a31365804fd..e06a6be40b3 100644 --- a/scripts/migrate_tools/0011_tools.sh +++ b/scripts/migrate_tools/0011_tools.sh @@ -1,4 +1,4 @@ #!/bin/sh -cd `dirname $0`/../.. -python ./scripts/migrate_tools/migrate_tools.py 0011_tools.xml $@ +cd "$(dirname "$0")"/../.. +python ./scripts/migrate_tools/migrate_tools.py 0011_tools.xml "$@" diff --git a/scripts/migrate_tools/0012_tools.sh b/scripts/migrate_tools/0012_tools.sh index 97fbe354d04..2df312f980a 100644 --- a/scripts/migrate_tools/0012_tools.sh +++ b/scripts/migrate_tools/0012_tools.sh @@ -1,4 +1,4 @@ #!/bin/sh -cd `dirname $0`/../.. -python ./scripts/migrate_tools/migrate_tools.py 0012_tools.xml $@ +cd "$(dirname "$0")"/../.. +python ./scripts/migrate_tools/migrate_tools.py 0012_tools.xml "$@" diff --git a/scripts/pages_identifier_conversion.py b/scripts/pages_identifier_conversion.py index ce914938f21..b9cec401071 100644 --- a/scripts/pages_identifier_conversion.py +++ b/scripts/pages_identifier_conversion.py @@ -9,12 +9,12 @@ sys.path.insert(1, os.path.abspath(os.path.join(os.path.dirname(__file__), os.pa import galaxy import galaxy.app import galaxy.config +from galaxy.managers.pages import PageContentProcessor, placeholderRenderForSave from galaxy.objectstore import build_object_store_from_config from galaxy.security.idencoding import IdEncodingHelper from galaxy.util import unicodify from galaxy.util.bunch import Bunch from galaxy.util.script import app_properties_from_args, populate_config_args -from galaxy.webapps.galaxy.controllers.page import _PageContentProcessor, _placeholderRenderForSave def main(argv): @@ -37,7 +37,7 @@ def main(argv): mock_trans = Bunch(app=Bunch(security=security_helper), model=model, user_is_admin=lambda: True, sa_session=session) for p in pagerevs: try: - processor = _PageContentProcessor(mock_trans, _placeholderRenderForSave) + processor = PageContentProcessor(mock_trans, placeholderRenderForSave) processor.feed(p.content) newcontent = unicodify(processor.output(), 'utf-8') if p.content != newcontent: diff --git a/scripts/tool_shed/migrate_tools_to_repositories.sh b/scripts/tool_shed/migrate_tools_to_repositories.sh index e1108e20307..bbab9b1b333 100644 --- a/scripts/tool_shed/migrate_tools_to_repositories.sh +++ b/scripts/tool_shed/migrate_tools_to_repositories.sh @@ -1,4 +1,4 @@ #!/bin/sh -cd `dirname $0`/../.. +cd "$(dirname "$0")"/../.. python ./scripts/tool_shed/migrate_tools_to_repositories.py ./community_wsgi.ini >> ./scripts/tool_shed/migrate_tools_to_repositories.log diff --git a/scripts/validate_tools.sh b/scripts/validate_tools.sh index 16502e530d8..f57c1cb814a 100755 --- a/scripts/validate_tools.sh +++ b/scripts/validate_tools.sh @@ -1,33 +1,31 @@ #!/bin/sh -cd `dirname $0`/.. +cd "$(dirname "$0")"/.. -./scripts/common_startup.sh +GALAXY_SKIP_CLIENT_BUILD=1 ./scripts/common_startup.sh GALAXY_VIRTUAL_ENV="${GALAXY_VIRTUAL_ENV:-.venv}" -if [ -d "$GALAXY_VIRTUAL_ENV" ]; -then +if [ -d "$GALAXY_VIRTUAL_ENV" ]; then printf "Activating virtualenv at $GALAXY_VIRTUAL_ENV\n" . "$GALAXY_VIRTUAL_ENV/bin/activate" fi -xsd_path="lib/galaxy/tools/xsd/galaxy.xsd" +xsd_path="lib/galaxy/tool_util/xsd/galaxy.xsd" -err_tmp=`mktemp` +err_tmp=$(mktemp) count=0 exit=0 for p in "$@"; do count=$((count+1)) path="$p" - echo $path + echo "$path" PYTHONPATH=lib:$PYTHONPATH export PYTHONPATH - result=`python -c "import galaxy.tool_util.loader; import xml.etree; xml.etree.ElementTree.dump(galaxy.tool_util.loader.load_tool('$path').getroot())" | xmllint --nowarning --noout --schema "$xsd_path" - 2> "$err_tmp"` - if [ $? -eq 0 ] - then + python -c "import galaxy.tool_util.loader; import xml.etree; xml.etree.ElementTree.dump(galaxy.tool_util.loader.load_tool('$path').getroot())" | xmllint --nowarning --noout --schema "$xsd_path" - 2> "$err_tmp" + if [ $? -eq 0 ]; then echo "ok $count"; else echo "not ok $count $path"; - cat "$err_tmp" | sed 's/^/ /' + sed 's/^/ /' "$err_tmp" exit=1 fi done diff --git a/templates/base.mako b/templates/base.mako index 062d988ede5..8c6e3f2ba1d 100644 --- a/templates/base.mako +++ b/templates/base.mako @@ -65,7 +65,9 @@ ${ galaxy_client.load( app=self.js_app ) } ${ galaxy_client.config_sentry( app=self.js_app ) } - ${ galaxy_client.config_google_analytics( app=self.js_app ) } + %if self.js_app and self.js_app.config and self.js_app.config.ga_code: + ${ galaxy_client.config_google_analytics(self.js_app.config.ga_code) } + %endif %if not form_input_auto_focus is UNDEFINED and form_input_auto_focus: %if t.webapp.name == 'galaxy' and app.config.ga_code: - ${galaxy_client.config_google_analytics(app)} + ${galaxy_client.config_google_analytics(app.config.ga_code)} %endif diff --git a/templates/embed_base.mako b/templates/embed_base.mako index 1e7c74b2622..163f293392a 100644 --- a/templates/embed_base.mako +++ b/templates/embed_base.mako @@ -11,7 +11,6 @@ ${self.render_title( item )}
- ${self.render_summary_content( item, item_data )}
@@ -42,7 +41,7 @@ display_href = h.url_for( controller=item_controller, action='display_by_username_and_slug', username=item_user.username, slug=item_slug ) %>
-
@@ -57,7 +56,3 @@ ## Use a hidden var to store the ajax URL for getting an item's content. - -## Methods to override to render summary content. -<%def name="render_summary_content( item, item_data )"> - diff --git a/templates/galaxy_client_app.mako b/templates/galaxy_client_app.mako index 4b4cb51d032..7c22f5d77fa 100644 --- a/templates/galaxy_client_app.mako +++ b/templates/galaxy_client_app.mako @@ -39,8 +39,6 @@ ${ h.dumps( dictionary, indent=( 2 if trans.debug else 0 ) ) } %if app and app.config: - %endif + %else: + console.warn("Missing google analytics code"); + %endif + diff --git a/templates/js-app.mako b/templates/js-app.mako index 1f25ff30a46..f606b272ba2 100644 --- a/templates/js-app.mako +++ b/templates/js-app.mako @@ -74,8 +74,10 @@ %endif - ## ${ galaxy_client.config_sentry(app)} - ## ${ galaxy_client.config_google_analytics(app)} + ${ galaxy_client.config_sentry(app) } + %if app.config.ga_code: + ${ galaxy_client.config_google_analytics(app.config.ga_code) } + %endif diff --git a/templates/webapps/galaxy/dataset/embed.mako b/templates/webapps/galaxy/dataset/embed.mako index b31c04e2f27..74948f29527 100644 --- a/templates/webapps/galaxy/dataset/embed.mako +++ b/templates/webapps/galaxy/dataset/embed.mako @@ -1,8 +1,4 @@ <%inherit file="/embed_base.mako"/> -<%! - from galaxy.web.framework.helpers import iff -%> - <%def name="render_item_links( dataset )"> @@ -11,12 +7,4 @@ title="Import dataset" class="icon-button import"> - - - -<%def name="render_summary_content( dataset, data )"> -##
    -##
  • Format : ${dataset.extension} -##
    ${dataset.peek}
    -##
diff --git a/templates/webapps/galaxy/galaxy.panels.mako b/templates/webapps/galaxy/galaxy.panels.mako index 324ca1d25d8..39fc84ec0e7 100644 --- a/templates/webapps/galaxy/galaxy.panels.mako +++ b/templates/webapps/galaxy/galaxy.panels.mako @@ -225,7 +225,7 @@ ${ galaxy_client.load() } - ## ${ galaxy_client.config_sentry(app) } + ${ galaxy_client.config_sentry(app) } diff --git a/templates/webapps/galaxy/page/display.mako b/templates/webapps/galaxy/page/display.mako index 1ed74810217..5d3d29f613c 100644 --- a/templates/webapps/galaxy/page/display.mako +++ b/templates/webapps/galaxy/page/display.mako @@ -7,80 +7,8 @@ diff --git a/templates/webapps/galaxy/page/editor.mako b/templates/webapps/galaxy/page/editor.mako index 27e2eb0307f..858774b54ef 100644 --- a/templates/webapps/galaxy/page/editor.mako +++ b/templates/webapps/galaxy/page/editor.mako @@ -16,46 +16,15 @@ // Apparently pages() relies on these variables being defined // in window. config.addInitialization(function(){ - var pagesConfig = { - page_id: "${trans.security.encode_id(page.id)}", - page_list_url: '${h.url_for( controller='pages', action='list' )}', - list_objects_url: "${h.url_for(controller='page', action='LIST_ACTION' )}", - set_accessible_url: "${h.url_for( controller='ITEM_CONTROLLER', action='set_accessible_async' )}", - get_name_and_link_url: "${h.url_for( controller='ITEM_CONTROLLER', action='get_name_and_link_async' )}?id=", - editor_base_path: "${h.url_for('/static/wymeditor')}/", - iframe_base_path: "${h.url_for('/static/wymeditor/iframe/galaxy')}/", - save_url: "${h.url_for(controller='page', action='save' )}" - }; console.log("editor.mako, javascript_app", "define variables needed by galaxy.pages script"); - window.bundleEntries.pages(pagesConfig); + window.bundleEntries.pages(); }); -<%def name="stylesheets()"> - ${parent.stylesheets()} - ${h.css( "base", "embed_item" )} - - - <%def name="center_panel()"> -
-
- Page Editor: ${page.title | h} - -
-
- -
- -
+ + diff --git a/templates/webapps/galaxy/visualization/embed.mako b/templates/webapps/galaxy/visualization/embed.mako index 242d6df7eea..9a6037dcfae 100644 --- a/templates/webapps/galaxy/visualization/embed.mako +++ b/templates/webapps/galaxy/visualization/embed.mako @@ -1,4 +1 @@ <%inherit file="/embed_base.mako"/> - -<%def name="render_summary_content( workflow, steps )"> - diff --git a/templates/webapps/galaxy/visualization/embed_in_frame.mako b/templates/webapps/galaxy/visualization/embed_in_frame.mako index b239bc1ba3a..2bd7d24c5ec 100644 --- a/templates/webapps/galaxy/visualization/embed_in_frame.mako +++ b/templates/webapps/galaxy/visualization/embed_in_frame.mako @@ -24,10 +24,6 @@
${item.annotation}
%endif
- ##
- ## currently, no summary content for visualization - ## could do the title or caption, whatever... - ##
diff --git a/templates/webapps/galaxy/workflow/embed.mako b/templates/webapps/galaxy/workflow/embed.mako index 2297b99c5fc..dee48db4490 100644 --- a/templates/webapps/galaxy/workflow/embed.mako +++ b/templates/webapps/galaxy/workflow/embed.mako @@ -1,24 +1,6 @@ <%inherit file="/embed_base.mako"/> -<%! - from galaxy.web.framework.helpers import iff -%> - <%def name="render_item_links( workflow )"> - ## FIXME: find and set appropriate icon for linking to workflow. - ${parent.render_item_links( workflow )} - -<%def name="render_summary_content( workflow, steps )"> - -##
    -## <% num_steps = len ( steps ) %> -##
  • ${num_steps} step${iff( num_steps != 1, "s", "" )} -##
  • Operations: ... -##
- diff --git a/templates/webapps/tool_shed/base_panels.mako b/templates/webapps/tool_shed/base_panels.mako index 295f897679f..dd4c983abe4 100644 --- a/templates/webapps/tool_shed/base_panels.mako +++ b/templates/webapps/tool_shed/base_panels.mako @@ -46,7 +46,7 @@ <%def name="masthead()"> %if app.config.ga_code: - ${galaxy_client.config_google_analytics(app)} + ${ galaxy_client.config_google_analytics(app.config.ga_code)} %endif ## start main tag diff --git a/test/api/test_page_revisions.py b/test/api/test_page_revisions.py index 33d29d74fe6..fb89c90d680 100644 --- a/test/api/test_page_revisions.py +++ b/test/api/test_page_revisions.py @@ -28,8 +28,8 @@ class PageRevisionsApiTestCase(BasePageApiTestCase): revisions_json = revisions_response.json() assert len(revisions_json) == 2 # Original revision and new one - def test_404_if_index_unknown_page(self): + def test_malformed_id_if_index_unknown_page(self): revisions_url = "pages/%s/revisions" % self._random_key() revisions_response = self._get(revisions_url) - self._assert_status_code_is(revisions_response, 404) - self._assert_error_code_is(revisions_response, error_codes.USER_OBJECT_NOT_FOUND) + self._assert_status_code_is(revisions_response, 400) + self._assert_error_code_is(revisions_response, error_codes.MALFORMED_ID) diff --git a/test/api/test_pages.py b/test/api/test_pages.py index 6cabf05526a..4126fa0a46e 100644 --- a/test/api/test_pages.py +++ b/test/api/test_pages.py @@ -1,6 +1,7 @@ from requests import delete from base import api # noqa: I100,I202 +from base.populators import DatasetPopulator from galaxy.exceptions import error_codes # noqa: I201 @@ -53,7 +54,7 @@ class PageApiTestCase(BasePageApiTestCase): self._assert_error_code_is(page_response_2, error_codes.USER_SLUG_DUPLICATE) def test_page_requires_name(self): - page_request = self._test_page_payload() + page_request = self._test_page_payload(slug="requires-name") del page_request['title'] page_response = self._post("pages", page_request) self._assert_status_code_is(page_response, 400) @@ -70,10 +71,10 @@ class PageApiTestCase(BasePageApiTestCase): delete_response = delete(self._api_url("pages/%s" % response_json['id'], use_key=True)) self._assert_status_code_is(delete_response, 200) - def test_404_on_delete_unknown_page(self): + def test_400_on_delete_invalid_page_id(self): delete_response = delete(self._api_url("pages/%s" % self._random_key(), use_key=True)) - self._assert_status_code_is(delete_response, 404) - self._assert_error_code_is(delete_response, error_codes.USER_OBJECT_NOT_FOUND) + self._assert_status_code_is(delete_response, 400) + self._assert_error_code_is(delete_response, error_codes.MALFORMED_ID) def test_403_on_delete_unowned_page(self): page_response = self._create_valid_page_as("others_page@bx.psu.edu", "otherspage") @@ -81,6 +82,23 @@ class PageApiTestCase(BasePageApiTestCase): self._assert_status_code_is(delete_response, 403) self._assert_error_code_is(delete_response, error_codes.USER_DOES_NOT_OWN_ITEM) + def test_400_on_invalid_id_encoding(self): + page_request = self._test_page_payload(slug="invalid-id-encding") + page_request["content"] = '''

Page!

''' + page_response = self._post("pages", page_request) + self._assert_status_code_is(page_response, 400) + self._assert_error_code_is(page_response, error_codes.MALFORMED_ID) + + def test_400_on_invalid_embedded_content(self): + dataset_populator = DatasetPopulator(self.galaxy_interactor) + valid_id = dataset_populator.new_history() + page_request = self._test_page_payload(slug="invalid-id-encding") + page_request["content"] = '''

Page!

''' % valid_id + page_response = self._post("pages", page_request) + self._assert_status_code_is(page_response, 400) + self._assert_error_code_is(page_response, error_codes.USER_REQUEST_INVALID_PARAMETER) + assert "embedded HTML content" in page_response.content + def test_show(self): response_json = self._create_valid_page_with_slug("pagetoshow") show_response = self._get("pages/%s" % response_json['id']) @@ -95,7 +113,7 @@ class PageApiTestCase(BasePageApiTestCase): response_json = self._create_valid_page_as("others_page_show@bx.psu.edu", "otherspageshow") show_response = self._get("pages/%s" % response_json['id']) self._assert_status_code_is(show_response, 403) - self._assert_error_code_is(show_response, error_codes.USER_DOES_NOT_OWN_ITEM) + self._assert_error_code_is(show_response, error_codes.USER_CANNOT_ACCESS_ITEM) def _users_index_has_page_with_id(self, id): index_response = self._get("pages") diff --git a/test/functional/tools/unicode_stream.xml b/test/functional/tools/unicode_stream.xml index 7fb2b00a30d..623b83a61be 100644 --- a/test/functional/tools/unicode_stream.xml +++ b/test/functional/tools/unicode_stream.xml @@ -1,21 +1,26 @@ + echo "\x00" '$out_file1'; +#if $include_null: + echo "\x00" > $out_file1; +#end if +echo '$input1' >> '$out_file1'; cat '$cf'; +echo "\x00"; >&2 cat '$cf'; sh -c "exit $exit" ]]> - ვეპხის ტყაოსანი შოთა რუსთაველი - + ვეპხის ტყაოსანი შოთა რუსთაველი + @@ -34,6 +39,10 @@ sh -c "exit $exit" + + + + diff --git a/test/galaxy_selenium/navigates_galaxy.py b/test/galaxy_selenium/navigates_galaxy.py index 7b9359ae4f7..a38b2a4fd80 100644 --- a/test/galaxy_selenium/navigates_galaxy.py +++ b/test/galaxy_selenium/navigates_galaxy.py @@ -331,6 +331,23 @@ class NavigatesGalaxy(HasDriver): raise self.prepend_timeout_message(e, message) return history_item_selector_state + def click_grid_popup_option(self, item_name, option_label): + item_button = None + grid = self.components.grids.body.wait_for_visible() + for row in grid.find_elements_by_tag_name('tr'): + name_cell = row.find_elements_by_tag_name('td')[1] + if name_cell.text == item_name: + item_button = name_cell + break + + if item_button is None: + raise AssertionError('Failed to find item with name [%s]' % item_name) + + popup_menu_button = item_button.find_element_by_css_selector('.dropdown-toggle') + popup_menu_button.click() + popup_option = self.driver.find_element_by_link_text(option_label) + popup_option.click() + def published_grid_search_for(self, search_term=None): return self._inline_search_for( '#input-free-text-search-filter', diff --git a/test/galaxy_selenium/navigation.yml b/test/galaxy_selenium/navigation.yml index 96b2fdd7e62..eae430b7eed 100644 --- a/test/galaxy_selenium/navigation.yml +++ b/test/galaxy_selenium/navigation.yml @@ -60,6 +60,7 @@ masthead: logout: 'Logout' custom_builds: 'Custom Builds' histories: 'Histories' + pages: 'Pages' # Shared data libraries: 'Data Libraries' @@ -201,6 +202,21 @@ histories: labels: unshare: 'Unshare' +pages: + selectors: + create: '.manage-table-actions .action-button' + submit: '#submit' + editor: + selectors: + wym_iframe: 'div.wym_iframe iframe' + wym_iframe_content: '.text-content' + save: '#save-button' + embed_button: '#embed-galaxy-object' + dataset_selector: '.saved-datasets' + embed_dialog_add_button: '.pages-embed .buttons #button-0' + + labels: + embed_dataset: 'Embed Dataset' login: selectors: @@ -365,9 +381,10 @@ libraries: table: '.dataset_table' table_rows: '.dataset_table table tbody tr' -published_grids: +grids: selectors: - search: '#input-free-text-search-filter' + body: '#grid-table-body' + free_text_search: '#input-free-text-search-filter' gies: jupyter: diff --git a/test/integration/test_cli_runners.py b/test/integration/test_cli_runners.py index 64605642e7a..370c37da79f 100644 --- a/test/integration/test_cli_runners.py +++ b/test/integration/test_cli_runners.py @@ -67,7 +67,7 @@ def cli_job_config(remote_connection, shell_plugin='ParamikoShell', job_plugin=' job_conf_str = job_conf_template.substitute(shell_plugin=shell_plugin, job_plugin=job_plugin, **remote_connection._asdict()) - with tempfile.NamedTemporaryFile(suffix="_slurm_integration_job_conf", mode="w", delete=False) as job_conf: + with tempfile.NamedTemporaryFile(suffix="_slurm_integration_job_conf.xml", mode="w", delete=False) as job_conf: job_conf.write(job_conf_str) return job_conf.name diff --git a/test/integration/test_kubernetes_runner.py b/test/integration/test_kubernetes_runner.py index 766260f599c..34e469ee716 100644 --- a/test/integration/test_kubernetes_runner.py +++ b/test/integration/test_kubernetes_runner.py @@ -111,7 +111,7 @@ def job_config(jobs_directory): tool_directory=TOOL_DIR, k8s_config_path=os.environ.get('GALAXY_TEST_KUBE_CONFIG_PATH', '~/.kube/config'), ) - with tempfile.NamedTemporaryFile(suffix="_kubernetes_integration_job_conf", mode="w", delete=False) as job_conf: + with tempfile.NamedTemporaryFile(suffix="_kubernetes_integration_job_conf.xml", mode="w", delete=False) as job_conf: job_conf.write(job_conf_str) return Config(job_conf.name) diff --git a/test/integration/test_page_revision_json_encoding.py b/test/integration/test_page_revision_json_encoding.py new file mode 100644 index 00000000000..f509c9b8c10 --- /dev/null +++ b/test/integration/test_page_revision_json_encoding.py @@ -0,0 +1,40 @@ +"""Test pages save JSON with unencoded IDs. + +Verifies the database doesn't get saved with encoded IDs (that would be bad because +the security parameter to encode IDs may be changed by admins). Test case also verifies +exported API values are encoded though. +""" + +from base import api_asserts +from base import integration_util +from base.populators import ( + DatasetPopulator, +) +from galaxy import model # noqa: I101,I201 + + +class PageJsonEncodingIntegrationTestCase(integration_util.IntegrationTestCase): + + def setUp(self): + super(PageJsonEncodingIntegrationTestCase, self).setUp() + self.dataset_populator = DatasetPopulator(self.galaxy_interactor) + + def test_page_encoding(self): + history_id = self.dataset_populator.new_history() + request = dict( + slug="mypage", + title="MY PAGE", + content='''

Page!

''' % history_id, + ) + page_response = self._post("pages", request) + api_asserts.assert_status_code_is_ok(page_response) + sa_session = self._app.model.context + page_revision = sa_session.query(model.PageRevision).all()[0] + assert '''id="History-1"''' in page_revision.content, page_revision.content + assert '''id="History-%s"''' % history_id not in page_revision.content, page_revision.content + + show_page_response = self._get("pages/%s" % page_response.json()["id"]) + api_asserts.assert_status_code_is_ok(show_page_response) + content = show_page_response.json()["content"] + assert '''id="History-1"''' not in content, content + assert '''id="History-%s"''' % history_id in content, content diff --git a/test/selenium_tests/framework.py b/test/selenium_tests/framework.py index 385e7469132..853c2a04ad1 100644 --- a/test/selenium_tests/framework.py +++ b/test/selenium_tests/framework.py @@ -92,10 +92,12 @@ def managed_history(f): f(self, *args, **kwds) finally: if "GALAXY_TEST_NO_CLEANUP" not in os.environ: - current_history_id = self.current_history_id() - self.dataset_populator.cancel_history_jobs(current_history_id) - self.api_delete("histories/%s" % current_history_id) - + try: + current_history_id = self.current_history_id() + self.dataset_populator.cancel_history_jobs(current_history_id) + self.api_delete("histories/%s" % current_history_id) + except Exception: + print("Faild to cleanup managed history, selenium connection corrupted somehow?") return func_wrapper diff --git a/test/selenium_tests/test_histories_list.py b/test/selenium_tests/test_histories_list.py index c250f4f1877..8f200f2640b 100644 --- a/test/selenium_tests/test_histories_list.py +++ b/test/selenium_tests/test_histories_list.py @@ -19,7 +19,7 @@ class SavedHistoriesTestCase(SharedStateSeleniumTestCase): self._login() self.navigate_to_histories_page() self.screenshot("histories_saved_grid") - self.click_popup_option(self.history2_name, 'Switch') + self.click_grid_popup_option(self.history2_name, 'Switch') self.sleep_for(self.wait_types.UX_RENDER) @retry_assertion_during_transitions @@ -32,7 +32,7 @@ class SavedHistoriesTestCase(SharedStateSeleniumTestCase): def test_history_view(self): self._login() self.navigate_to_histories_page() - self.click_popup_option(self.history2_name, 'View') + self.click_grid_popup_option(self.history2_name, 'View') history_name = self.wait_for_selector('.name.editable-text') self.assertEqual(history_name.text, self.history2_name) @@ -42,7 +42,7 @@ class SavedHistoriesTestCase(SharedStateSeleniumTestCase): self.navigate_to_histories_page() # Publish the history - self.click_popup_option(self.history2_name, 'Share or Publish') + self.click_grid_popup_option(self.history2_name, 'Share or Publish') self.components.histories.sharing.make_accessible_and_publish.wait_for_and_click() self.navigate_to_histories_page() @@ -58,7 +58,7 @@ class SavedHistoriesTestCase(SharedStateSeleniumTestCase): self._login() self.navigate_to_histories_page() - self.click_popup_option('Unnamed history', 'Rename') + self.click_grid_popup_option('Unnamed history', 'Rename') # Rename the history history_name_input = self.wait_for_selector('.ui-form-element input.ui-input') @@ -77,7 +77,7 @@ class SavedHistoriesTestCase(SharedStateSeleniumTestCase): self.navigate_to_histories_page() # Delete the history - self.click_popup_option(self.history2_name, 'Delete') + self.click_grid_popup_option(self.history2_name, 'Delete') self.assert_histories_in_grid([self.history2_name], False) @@ -86,7 +86,7 @@ class SavedHistoriesTestCase(SharedStateSeleniumTestCase): self.sleep_for(self.wait_types.UX_RENDER) # Restore the history - self.click_popup_option(self.history2_name, 'Undelete') + self.click_grid_popup_option(self.history2_name, 'Undelete') self.assert_grid_histories_are([]) self.select_filter('deleted', 'False') @@ -101,7 +101,7 @@ class SavedHistoriesTestCase(SharedStateSeleniumTestCase): self.navigate_to_histories_page() self.assert_histories_in_grid([self.history4_name]) - self.click_popup_option(self.history4_name, 'Delete Permanently') + self.click_grid_popup_option(self.history4_name, 'Delete Permanently') alert = self.driver.switch_to.alert alert.accept() @@ -164,15 +164,14 @@ class SavedHistoriesTestCase(SharedStateSeleniumTestCase): self._login() self.navigate_to_histories_page() - input_selector = '#input-free-text-search-filter' - search_input = self.wait_for_selector(input_selector) + search_input = self.components.grids.free_text_search.wait_for_visible() search_input.send_keys(self.history2_name) self.send_enter(search_input) self.assert_grid_histories_are([self.history2_name]) self.unset_filter('free-text-search', self.history2_name) - search_input = self.wait_for_selector(input_selector) + search_input = self.components.grids.free_text_search.wait_for_visible() search_input.send_keys(self.history4_name) self.send_enter(search_input) @@ -299,23 +298,6 @@ class SavedHistoriesTestCase(SharedStateSeleniumTestCase): (filter_key, filter_value) self.wait_for_and_click_selector(filter_selector) - def click_popup_option(self, history_name, option_label): - history_menu_button = None - grid = self.wait_for_selector('#grid-table-body') - for row in grid.find_elements_by_tag_name('tr'): - name_cell = row.find_elements_by_tag_name('td')[1] - if name_cell.text == history_name: - history_menu_button = name_cell - break - - if history_menu_button is None: - raise AssertionError('Failed to find history with name [%s]' % history_name) - - popup_menu_button = history_menu_button.find_element_by_css_selector('.dropdown-toggle') - popup_menu_button.click() - popup_option = self.driver.find_element_by_link_text(option_label) - popup_option.click() - def get_history_tags_cell(self, history_name): tags_cell = None grid = self.wait_for_selector('#grid-table-body') diff --git a/test/selenium_tests/test_pages.py b/test/selenium_tests/test_pages.py new file mode 100644 index 00000000000..922ed4ce351 --- /dev/null +++ b/test/selenium_tests/test_pages.py @@ -0,0 +1,63 @@ +from .framework import ( + managed_history, + selenium_test, + SeleniumTestCase, +) + + +class PagesTestCase(SeleniumTestCase): + + ensure_registered = True + + @selenium_test + @managed_history + def test_simple_page_creation_edit_and_view(self): + # Upload a file to test embedded object stuff + test_path = self.get_filename("1.fasta") + self.perform_upload(test_path) + self.history_panel_wait_for_hid_ok(1) + self.navigate_to_pages() + self.screenshot("pages_grid") + + self.components.pages.create.wait_for_and_click() + name = self._get_random_name(prefix="page") + slug = self._get_random_name(prefix="pageslug") + self.tool_set_value("title", name) + self.tool_set_value("slug", slug) + self.screenshot("pages_create_form") + + # Sometimes 'submit' button not yet hooked up? + self.sleep_for(self.wait_types.UX_RENDER) + + self.components.pages.submit.wait_for_and_click() + + self.click_grid_popup_option(name, "Edit content") + self.components.pages.editor.wym_iframe.wait_for_visible() + self.screenshot("pages_editor_new") + self.driver.switch_to.frame(0) + try: + self.components.pages.editor.wym_iframe_content.wait_for_and_send_keys("moo\n\n\ncow\n\n") + finally: + self.driver.switch_to.default_content() + + self.components.pages.editor.embed_button.wait_for_and_click() + self.screenshot("pages_editor_embed_menu") + self.components.pages.editor.embed_dataset.wait_for_and_click() + saved_datasets_element = self.components.pages.editor.dataset_selector.wait_for_and_click() + self.screenshot("pages_editor_embed_dataset_dialog") + checkboxes = saved_datasets_element.find_elements_by_css_selector("input[type='checkbox']") + assert len(checkboxes) > 0 + checkboxes[0].click() + self.components.pages.editor.embed_dialog_add_button.wait_for_and_click() + + self.sleep_for(self.wait_types.UX_RENDER) + self.components.pages.editor.save.wait_for_and_click() + self.screenshot("pages_editor_saved") + self.home() + self.navigate_to_pages() + self.click_grid_popup_option(name, "View") + self.screenshot("pages_view_simple") + + def navigate_to_pages(self): + self.click_masthead_user() # Open masthead menu + self.components.masthead.pages.wait_for_and_click() diff --git a/test/unit/tools/test_cwl_util.py b/test/unit/tools/test_cwl_util.py new file mode 100644 index 00000000000..24c2ac95cf7 --- /dev/null +++ b/test/unit/tools/test_cwl_util.py @@ -0,0 +1,25 @@ +import tempfile + +from galaxy.tool_util.cwl.util import output_properties + + +def test_output_properties_in_memory(): + props = output_properties(content=b"hello world", basename="hello.txt") + assert props["basename"] == "hello.txt" + assert props["nameroot"] == "hello" + assert props["nameext"] == ".txt" + assert props["size"] == 11 + assert props["checksum"] == "sha1$2aae6c35c94fcfb415dbe95f408b9ce91ee846ed" + + +def test_output_properties_path(): + f = tempfile.NamedTemporaryFile(mode="w") + f.write("hello world") + f.flush() + + props = output_properties(path=f.name, basename="hello.txt") + assert props["basename"] == "hello.txt" + assert props["nameroot"] == "hello" + assert props["nameext"] == ".txt" + assert props["size"] == 11 + assert props["checksum"] == "sha1$2aae6c35c94fcfb415dbe95f408b9ce91ee846ed" diff --git a/test/unit/tools/test_tool_deps.py b/test/unit/tools/test_tool_deps.py index 5230466f802..0f609201a77 100644 --- a/test/unit/tools/test_tool_deps.py +++ b/test/unit/tools/test_tool_deps.py @@ -12,7 +12,7 @@ from shutil import rmtree from stat import S_IXUSR from subprocess import PIPE, Popen -from galaxy.tool_util.deps import DependencyManager +from galaxy.tool_util.deps import build_dependency_manager_from_dict, DependencyManager from galaxy.tool_util.deps.requirements import ( ToolRequirement, ToolRequirements @@ -23,6 +23,9 @@ from galaxy.tool_util.deps.resolvers.lmod import LmodDependency, LmodDependencyR from galaxy.tool_util.deps.resolvers.modules import ModuleDependency, ModuleDependencyResolver from galaxy.util.bunch import Bunch +# If true, test created DependencyManager objects by serializing out to json and re-constituting. +ROUND_TRIP_TEST_DEPENDENCY_MANAGER_SERIALIZATION = True + def test_tool_dependencies(): # Setup directories @@ -560,6 +563,13 @@ def test_config_module_defaults(): module_resolver = dependency_resolvers[0] assert module_resolver.module_checker.__class__.__name__ == "AvailModuleChecker" + with __parse_resolvers(''' +- type: modules + prefetch: false +''', extension=".yml") as dependency_resolvers: + module_resolver = dependency_resolvers[0] + assert module_resolver.module_checker.__class__.__name__ == "AvailModuleChecker" + def test_config_modulepath(): # Test reads and splits MODULEPATH if modulepath is not specified. @@ -623,23 +633,27 @@ def __environ(values, remove=[]): @contextmanager -def __parse_resolvers(xml_content): - with __dependency_manager(xml_content) as dm: +def __parse_resolvers(file_content, extension=".xml"): + with __dependency_manager(file_content, extension=extension) as dm: yield dm.dependency_resolvers @contextmanager -def __dependency_manager(xml_content): +def __dependency_manager(file_content, extension=".xml"): with __test_base_path() as base_path: - with tempfile.NamedTemporaryFile('w+') as tmp: - tmp.write(xml_content) + with tempfile.NamedTemporaryFile('w+', suffix=extension) as tmp: + tmp.write(file_content) tmp.flush() dm = __dependency_manager_for_base_path(default_base_path=base_path, conf_file=tmp.name) yield dm def __dependency_manager_for_base_path(default_base_path, conf_file=None): - return DependencyManager(default_base_path=default_base_path, conf_file=conf_file, app_config={"conda_auto_init": False}) + dm = DependencyManager(default_base_path=default_base_path, conf_file=conf_file, app_config={"conda_auto_init": False}) + if ROUND_TRIP_TEST_DEPENDENCY_MANAGER_SERIALIZATION: + as_dict = dm.to_dict() + dm = build_dependency_manager_from_dict(as_dict) + return dm class _SimpleDependencyManager(object): diff --git a/tox.ini b/tox.ini index 9904d2a82f1..0aec466cf97 100644 --- a/tox.ini +++ b/tox.ini @@ -1,7 +1,7 @@ [tox] # envlist is the list of environments that are tested when `tox` is run without any option # hyphens in an environment name are used to delimit factors -envlist = check_py3_compatibility, py{27,35}-first_startup, py{27,35}-lint, py27-lint_docstring_include_list, py{27,35}-unit, validate_test_tools +envlist = check_py3_compatibility, py{27,35}-first_startup, py{27,35}-lint, py27-lint_docstring_include_list, py{27,35}-unit, test_galaxy_packages, validate_test_tools skipsdist = True [testenv] @@ -35,6 +35,9 @@ commands = bash .ci/flake8_wrapper_docstrings.sh --exclude [testenv:py27-lint_docstring_include_list] commands = bash .ci/flake8_wrapper_docstrings.sh --include +[testenv:test_galaxy_packages] +commands = bash packages/test.sh + [testenv:validate_test_tools] commands = bash .ci/validate_test_tools.sh