diff --git a/client/galaxy/scripts/bundleEntries.js b/client/galaxy/scripts/bundleEntries.js
index 21bfd39e932..6dce4ad39a6 100644
--- a/client/galaxy/scripts/bundleEntries.js
+++ b/client/galaxy/scripts/bundleEntries.js
@@ -94,6 +94,7 @@ export { mountMakoTags } from "components/Tags";
export { mountJobMetrics } from "components/JobMetrics";
export { mountJobParameters } from "components/JobParameters";
export { mountWorkflowEditor } from "components/Workflow/Editor/mount";
+export { mountPageDisplay } from "components/PageDisplay";
// Used in common.mako
export { default as store } from "storemodern";
diff --git a/client/galaxy/scripts/components/DataDialog/DataDialog.test.js b/client/galaxy/scripts/components/DataDialog/DataDialog.test.js
index bf3b01019c9..1b254b98638 100644
--- a/client/galaxy/scripts/components/DataDialog/DataDialog.test.js
+++ b/client/galaxy/scripts/components/DataDialog/DataDialog.test.js
@@ -1,10 +1,10 @@
-import { mount } from "@vue/test-utils";
import DataDialog from "./DataDialog.vue";
+import SelectionDialog from "components/SelectionDialog/SelectionDialog.vue";
import { __RewireAPI__ as rewire } from "./DataDialog";
import { Model } from "./model.js";
import { UrlTracker } from "./utilities.js";
import { Services } from "./services";
-import Vue from "vue";
+import { mount, createLocalVue } from "@vue/test-utils";
const mockOptions = {
callback: () => {},
@@ -80,9 +80,9 @@ describe("services/Services:isDataset", () => {
expect(services.isDataset({ history_content_type: "dataset" })).to.equals(true);
expect(services.isDataset({ history_content_type: "xyz" })).to.equals(false);
expect(services.isDataset({ type: "file" })).to.equals(true);
- expect(services.getRecord({ hid: 1, history_content_type: "dataset" }).isDataset).to.equals(true);
- expect(services.getRecord({ hid: 2, history_content_type: "xyz" }).isDataset).to.equals(false);
- expect(services.getRecord({ type: "file" }).isDataset).to.equals(true);
+ expect(services.getRecord({ hid: 1, history_content_type: "dataset" }).isLeaf).to.equals(true);
+ expect(services.getRecord({ hid: 2, history_content_type: "xyz" }).isLeaf).to.equals(false);
+ expect(services.getRecord({ type: "file" }).isLeaf).to.equals(true);
});
});
@@ -137,18 +137,32 @@ describe("DataDialog.vue", () => {
};
beforeEach(() => {
- rewire.__Rewire__("Services", mockServices);
+ rewire.__Rewire__("getGalaxyInstance", () => {
+ root: "root";
+ });
+ const localVue = createLocalVue();
wrapper = mount(DataDialog, {
- propsData: mockOptions
+ propsData: mockOptions,
+ attachToDocument: true,
+ localVue
});
});
- it("loads correctly, shows datasets and folders", async () => {
- expect(wrapper.find(":first-child div").classes()).contain("data-dialog-modal");
- expect(wrapper.find(".fa-spinner").text()).to.equals("");
- expect(wrapper.contains(".fa-spinner")).to.equals(true);
- await Vue.nextTick();
- expect(wrapper.findAll(".fa-folder").length).to.equals(2);
- expect(wrapper.findAll(".fa-file-o").length).to.equals(2);
+ it("loads correctly, embeds a SelectionDialog", async () => {
+ expect(wrapper.find(SelectionDialog).is(SelectionDialog)).to.equals(true);
+ // Cannot get nested slot templates to render into the wrapper
+ /// Lots of open issues around this
+ /// ... Maybe because named slots have many issues
+ // - https://github.com/vuejs/vue-test-utils/issues?utf8=%E2%9C%93&q=is%3Aissue+is%3Aopen+slot
+ // ... Maybe because it is a modal and no longer inside the template DOM element?
+ // - https://github.com/vuejs/vue-test-utils/issues/1333
+
+ // expect(wrapper.find(".fa-spinner").text()).to.equals("");
+ // expect(wrapper.contains(".fa-spinner")).to.equals(true);
+ // await Vue.nextTick();
+ // expect(wrapper.findAll(".fa-folder").length).to.equals(2);
+ // expect(wrapper.findAll(".fa-file-o").length).to.equals(2);
+
+ // SelectionDialog DOM properties are tested now in SelectionDialog.test.js however.
});
});
diff --git a/client/galaxy/scripts/components/DataDialog/DataDialog.vue b/client/galaxy/scripts/components/DataDialog/DataDialog.vue
index 73c4b8cd250..11319f2d468 100644
--- a/client/galaxy/scripts/components/DataDialog/DataDialog.vue
+++ b/client/galaxy/scripts/components/DataDialog/DataDialog.vue
@@ -1,10 +1,14 @@
-
-
+
+
-
-
-
-
-
-
- Back
-
-
- Ok
-
-
Cancel
-
-
+
+
+
+ Back
+
+
+ Ok
+
+
+
-
diff --git a/client/galaxy/scripts/components/DataDialog/services.js b/client/galaxy/scripts/components/DataDialog/services.js
index f2d45262739..7b260f8988f 100644
--- a/client/galaxy/scripts/components/DataDialog/services.js
+++ b/client/galaxy/scripts/components/DataDialog/services.js
@@ -53,7 +53,7 @@ export class Services {
getRecord(record) {
record.details = record.extension || record.description;
record.time = record.update_time || record.create_time;
- record.isDataset = this.isDataset(record);
+ record.isLeaf = this.isDataset(record);
if (record.time) {
record.time = record.time.substring(0, 16).replace("T", " ");
}
diff --git a/client/galaxy/scripts/components/Markdown/Markdown.vue b/client/galaxy/scripts/components/Markdown/Markdown.vue
index 5d49d1fcac4..4f05319126f 100644
--- a/client/galaxy/scripts/components/Markdown/Markdown.vue
+++ b/client/galaxy/scripts/components/Markdown/Markdown.vue
@@ -16,6 +16,7 @@ import MarkdownIt from "markdown-it";
import JOB_STATES_MODEL from "mvc/history/job-states-model";
import HDCAModel from "mvc/history/hdca-model";
import HDCAListItemEdit from "mvc/history/hdca-li-edit";
+import HDCAListItem from "mvc/history/hdca-li";
const FUNCTION_CALL_LINE_TEMPLATE = /\s*(\w+)\s*\((\s*\w+\s*=\s*\w+\s*)\)\s*/m;
@@ -140,6 +141,10 @@ export default {
props: {
markdownConfig: {
type: Object
+ },
+ readOnly: {
+ type: Boolean,
+ default: true
}
},
data() {
@@ -190,8 +195,8 @@ export default {
jobStateSummariesCollection.historyId = hdca["history_id"];
jobStateSummariesCollection.monitor();
jobStateSummariesCollection.trackModel(hdcaModel);
-
- return new HDCAListItemEdit.HDCAListItemEdit({
+ const viewClass = this.readOnly ? HDCAListItem.HDCAListItemView : HDCAListItemEdit.HDCAListItemEdit;
+ return new viewClass({
model: hdcaModel,
el: $(el),
linkTarget: "galaxy_main",
diff --git a/client/galaxy/scripts/components/Markdown/MarkdownEditor.vue b/client/galaxy/scripts/components/Markdown/MarkdownEditor.vue
index 9b25d6daf13..e20c5b5bec7 100644
--- a/client/galaxy/scripts/components/Markdown/MarkdownEditor.vue
+++ b/client/galaxy/scripts/components/Markdown/MarkdownEditor.vue
@@ -1,9 +1,87 @@
-
+
-
diff --git a/client/galaxy/scripts/components/Markdown/help.js b/client/galaxy/scripts/components/Markdown/help.js
new file mode 100644
index 00000000000..a3172c133a8
--- /dev/null
+++ b/client/galaxy/scripts/components/Markdown/help.js
@@ -0,0 +1,26 @@
+export const markdownGeneralHelpHtml = `
+
+ For an overview of standard Markdown visit the
+ commonmark.org tutorial.
+
+
+
+ The Galaxy extensions to Markdown are represented as code blocks, these blocks start
+ with the line \`\`\`galaxy and end with the line \`\`\` and have a
+ command with arguments that reference parts of the workflow in the middle.
+
`;
+
+export const datasetCommandsHtml = `
+history_dataset_display
+Embed a dataset description in the resulting document.
+history_dataset_collection_display
+Embed a dataset collection description in the resulting document.
+history_dataset_as_image
+Embed a dataset as an image in the resulting document - the dataset should be an image datatype.
+history_dataset_peek
+Embed Galaxy's metadata attribute 'peek' into the resulting document - this is datatype dependent metadata but usually this is a few lines from the start of a file.
+history_dataset_info
+Embed Galaxy's metadata attribute 'info' into the resulting document - this is datatype dependent metadata but usually this is the program output that generated the dataset.
+
+
+`;
diff --git a/client/galaxy/scripts/components/PageDisplay/PageDisplay.vue b/client/galaxy/scripts/components/PageDisplay/PageDisplay.vue
new file mode 100644
index 00000000000..8fdef2f9d9c
--- /dev/null
+++ b/client/galaxy/scripts/components/PageDisplay/PageDisplay.vue
@@ -0,0 +1,43 @@
+
+
+
+
+
diff --git a/client/galaxy/scripts/components/PageDisplay/index.js b/client/galaxy/scripts/components/PageDisplay/index.js
new file mode 100644
index 00000000000..1a732b7cdfe
--- /dev/null
+++ b/client/galaxy/scripts/components/PageDisplay/index.js
@@ -0,0 +1,4 @@
+export { default as PageDisplay } from "./PageDisplay";
+
+// function for mounting page display in non-Vue environments
+export { mountPageDisplay } from "./mount";
diff --git a/client/galaxy/scripts/components/PageDisplay/mount.js b/client/galaxy/scripts/components/PageDisplay/mount.js
new file mode 100644
index 00000000000..4c83fe91d5c
--- /dev/null
+++ b/client/galaxy/scripts/components/PageDisplay/mount.js
@@ -0,0 +1,15 @@
+/**
+ * Endpoint for mounting PageDisplay from non-Vue environment (annotated DOMs).
+ */
+import $ from "jquery";
+import Vue from "vue";
+import PageDisplay from "./PageDisplay.vue";
+
+export const mountPageDisplay = (propsData = {}) => {
+ $("#page-display-content").each((index, el) => {
+ const pageId = $(el).attr("page_id");
+ const component = Vue.extend(PageDisplay);
+ propsData.pageId = pageId;
+ return new component({ propsData: propsData }).$mount(el);
+ });
+};
diff --git a/client/galaxy/scripts/components/PageEditor/PageEditor.vue b/client/galaxy/scripts/components/PageEditor/PageEditor.vue
index f589b15e23a..f77e804f259 100644
--- a/client/galaxy/scripts/components/PageEditor/PageEditor.vue
+++ b/client/galaxy/scripts/components/PageEditor/PageEditor.vue
@@ -3,15 +3,34 @@
-
+
+
diff --git a/client/galaxy/scripts/components/PageEditor/PageEditorMarkdown.vue b/client/galaxy/scripts/components/PageEditor/PageEditorMarkdown.vue
new file mode 100644
index 00000000000..d876f64dcc3
--- /dev/null
+++ b/client/galaxy/scripts/components/PageEditor/PageEditorMarkdown.vue
@@ -0,0 +1,53 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/client/galaxy/scripts/components/PageEditor/markdownHelp.js b/client/galaxy/scripts/components/PageEditor/markdownHelp.js
new file mode 100644
index 00000000000..bd008b5b7f6
--- /dev/null
+++ b/client/galaxy/scripts/components/PageEditor/markdownHelp.js
@@ -0,0 +1,87 @@
+import { datasetCommandsHtml, markdownGeneralHelpHtml } from "components/Markdown/help";
+
+const markdownHelp = `
+
+
Overview
+
+
+ This document Markdown document will be used to generate your Galaxy Page.
+ This document should be Markdown with embedded command for extracting and
+ displaying Galaxy objects and their metadata.
+
+
+${markdownGeneralHelpHtml}
+
+
History Contents Commands
+
+
+ These commands reference a dataset. For instance, the following examples would display
+ the dataset collection corresponding to a Galaxy dataset collection ID and display a
+ single dataset as an image corresponding to a Galaxy dataset ID.
+
+
+
+\`\`\`galaxy
+history_dataset_collection_display(history_dataset_collection_id=33b43b4e7093c91f)
+\`\`\`
+
+
+
+\`\`\`galaxy
+history_dataset_as_image(history_dataset_id=33b43b4e7093c91f)
+\`\`\`
+
+
+
+${datasetCommandsHtml}
+
+
Workflow Commands
+
+
+ These commands reference a workflow (currently only one). The following example would
+ display a representation of the workflow in the resulting Galaxy Page:
+
+
+
+\`\`\`galaxy
+workflow_display(workflow_id=33b43b4e7093c91f>)
+\`\`\`
+
+
+
+- workflow_display
+- Embed a description of the workflow itself in the resulting document.
+
+
+
Job Commands
+
+
+ These commands reference a Galaxy job.For instance, the
+ following example would show the job parameters the job ID 33b43b4e7093c91f:
+
+
+
+\`\`\`galaxy
+job_parameters(job_id=33b43b4e7093c91f)
+\`\`\`
+
+
+
tool_stderr
+
Embed the tool standard error stream for this job in the resulting document.
+
tool_stdout
+
Embed the tool standard output stream for this job in the resulting document.
+
job_metrics
+
Embed the job metrics for this job in the resulting document (if Galaxy is configured and you have permission).
+
job_parameters
+
Embed the tool parameters for this job in the resulting document.
+`;
+
+import $ from "jquery";
+import { hide_modal, show_modal } from "layout/modal";
+
+export function showMarkdownHelp() {
+ const markdownHelpBody = $(markdownHelp);
+ show_modal("Pages Markdown Help", markdownHelpBody, {
+ Ok: hide_modal
+ });
+}
diff --git a/client/galaxy/scripts/components/PageEditor/util.js b/client/galaxy/scripts/components/PageEditor/util.js
index 5339f5172f6..de6c96c92a4 100644
--- a/client/galaxy/scripts/components/PageEditor/util.js
+++ b/client/galaxy/scripts/components/PageEditor/util.js
@@ -1,24 +1,15 @@
-import $ from "jquery";
+import axios from "axios";
import _l from "utils/localization";
import { getAppRoot } from "onload/loadConfig";
import { show_modal, hide_modal } from "layout/modal";
+import { Toast } from "ui/toast";
+import { errorMessageAsString } from "utils/simple-error";
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();
- }
- });
+ axios
+ .post(`${getAppRoot()}api/pages/${pageId}/revisions`, { content: content })
+ .finally(hide_modal)
+ .catch(error_message => Toast.error("Failed to save page: " + errorMessageAsString(error_message)));
};
diff --git a/client/galaxy/scripts/components/DataDialog/DataDialogSearch.vue b/client/galaxy/scripts/components/SelectionDialog/DataDialogSearch.vue
similarity index 100%
rename from client/galaxy/scripts/components/DataDialog/DataDialogSearch.vue
rename to client/galaxy/scripts/components/SelectionDialog/DataDialogSearch.vue
diff --git a/client/galaxy/scripts/components/DataDialog/DataDialogTable.vue b/client/galaxy/scripts/components/SelectionDialog/DataDialogTable.vue
similarity index 92%
rename from client/galaxy/scripts/components/DataDialog/DataDialogTable.vue
rename to client/galaxy/scripts/components/SelectionDialog/DataDialogTable.vue
index 6c13d70b4b0..fa4c076b752 100644
--- a/client/galaxy/scripts/components/DataDialog/DataDialogTable.vue
+++ b/client/galaxy/scripts/components/SelectionDialog/DataDialogTable.vue
@@ -12,7 +12,7 @@
@filtered="filtered"
>
-
+
{{ data.value ? data.value : "-" }}
@@ -52,6 +52,10 @@ export default {
multiple: {
type: Boolean,
default: false
+ },
+ leafIcon: {
+ type: String,
+ default: "fa fa-file-o"
}
},
data() {
diff --git a/client/galaxy/scripts/components/SelectionDialog/DatasetCollectionDialog.test.js b/client/galaxy/scripts/components/SelectionDialog/DatasetCollectionDialog.test.js
new file mode 100644
index 00000000000..e7e57530401
--- /dev/null
+++ b/client/galaxy/scripts/components/SelectionDialog/DatasetCollectionDialog.test.js
@@ -0,0 +1,68 @@
+import MockAdapter from "axios-mock-adapter";
+import axios from "axios";
+import DatasetCollectionDialog from "./DatasetCollectionDialog.vue";
+import { __RewireAPI__ as rewire } from "./DatasetCollectionDialog";
+import SelectionDialog from "./SelectionDialog.vue";
+
+import { shallowMount, createLocalVue } from "@vue/test-utils";
+
+const mockOptions = {
+ callback: () => {},
+ modalStatic: true,
+ history: "f2db41e1fa331b3e"
+};
+
+const galaxyInstance = {
+ root: "/"
+};
+
+describe("DatasetCollectionDialog.vue", () => {
+ let wrapper;
+ let localVue;
+ let axiosMock;
+
+ beforeEach(() => {
+ axiosMock = new MockAdapter(axios);
+ rewire.__Rewire__("getGalaxyInstance", () => galaxyInstance);
+ localVue = createLocalVue();
+ wrapper = shallowMount(DatasetCollectionDialog, {
+ propsData: mockOptions,
+ localVue: localVue
+ });
+ });
+
+ afterEach(() => {
+ axiosMock.restore();
+ });
+
+ it("loads correctly in loading state, shows options when optionsShow becomes true", async () => {
+ // Initially in loading state.
+ const collectionsResponse = [{ id: "f2db41e1fa331b3e", name: "Awesome Collection" }];
+ axiosMock
+ .onGet(`/api/histories/${mockOptions.history}/contents?type=dataset_collection`)
+ .reply(200, collectionsResponse);
+
+ expect(wrapper.find(SelectionDialog).is(SelectionDialog)).to.equals(true);
+ expect(wrapper.vm.optionsShow).to.equals(false);
+
+ await localVue.nextTick();
+ await localVue.nextTick();
+ await localVue.nextTick();
+
+ // why not shown?
+ expect(wrapper.vm.errorMessage).to.equals(null);
+ expect(wrapper.vm.optionsShow).to.equals(true);
+ });
+
+ it("error message set on dataset collection fetch problems", async () => {
+ expect(wrapper.vm.errorMessage).to.equals(null);
+ axiosMock
+ .onGet(`/api/histories/${mockOptions.history}/contents?type=dataset_collection`)
+ .reply(403, { err_msg: "Bad error" });
+ await localVue.nextTick();
+ await localVue.nextTick();
+ await localVue.nextTick();
+ await localVue.nextTick();
+ expect(wrapper.vm.errorMessage).to.equals("Bad error");
+ });
+});
diff --git a/client/galaxy/scripts/components/SelectionDialog/DatasetCollectionDialog.vue b/client/galaxy/scripts/components/SelectionDialog/DatasetCollectionDialog.vue
new file mode 100644
index 00000000000..beeca791c7d
--- /dev/null
+++ b/client/galaxy/scripts/components/SelectionDialog/DatasetCollectionDialog.vue
@@ -0,0 +1,84 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/client/galaxy/scripts/components/SelectionDialog/SelectionDialog.test.js b/client/galaxy/scripts/components/SelectionDialog/SelectionDialog.test.js
new file mode 100644
index 00000000000..85e7c7ebfab
--- /dev/null
+++ b/client/galaxy/scripts/components/SelectionDialog/SelectionDialog.test.js
@@ -0,0 +1,53 @@
+import SelectionDialog from "./SelectionDialog.vue";
+import { mount, createLocalVue } from "@vue/test-utils";
+
+const mockOptions = {
+ callback: () => {},
+ modalStatic: true
+};
+
+describe("SelectionDialog.vue", () => {
+ let wrapper;
+ let localVue;
+ let calledHide = false;
+
+ beforeEach(() => {
+ localVue = createLocalVue();
+ mockOptions.hideModal = () => {
+ calledHide = true;
+ };
+ wrapper = mount(SelectionDialog, {
+ slots: {
+ options: "",
+ search: ""
+ },
+ propsData: mockOptions,
+ localVue
+ });
+ });
+
+ it("loads correctly in loading state, shows options when optionsShow becomes true", async () => {
+ expect(wrapper.find(".fa-spinner").text()).to.equals("");
+ expect(wrapper.contains(".fa-spinner")).to.equals(true);
+ expect(wrapper.contains("tree-options")).to.equals(false);
+ wrapper.setProps({ optionsShow: true });
+ await localVue.nextTick();
+ expect(wrapper.contains(".fa-spinner")).to.equals(false);
+ expect(wrapper.contains("tree-options")).to.equals(true);
+ });
+
+ it("loads search correctly", async () => {
+ await localVue.nextTick();
+ expect(wrapper.contains("cool-search")).to.equals(true);
+ });
+
+ it("hideModal called on click cancel", async () => {
+ expect(calledHide).to.equals(false);
+ expect(wrapper.contains(".selection-dialog-modal-cancel")).to.equals(true);
+ wrapper.setProps({ optionsShow: true });
+ await localVue.nextTick();
+ wrapper.find(".selection-dialog-modal-cancel").trigger("click");
+ await localVue.nextTick();
+ expect(calledHide).to.equals(true);
+ });
+});
diff --git a/client/galaxy/scripts/components/SelectionDialog/SelectionDialog.vue b/client/galaxy/scripts/components/SelectionDialog/SelectionDialog.vue
new file mode 100644
index 00000000000..9028f992145
--- /dev/null
+++ b/client/galaxy/scripts/components/SelectionDialog/SelectionDialog.vue
@@ -0,0 +1,69 @@
+
+
+
+
+
+
+
+
+
+
+ Cancel
+
+
+
+
+
+
+
+
diff --git a/client/galaxy/scripts/components/SelectionDialog/SelectionDialogMixin.js b/client/galaxy/scripts/components/SelectionDialog/SelectionDialogMixin.js
new file mode 100644
index 00000000000..9529f860c56
--- /dev/null
+++ b/client/galaxy/scripts/components/SelectionDialog/SelectionDialogMixin.js
@@ -0,0 +1,17 @@
+import DataDialogSearch from "./DataDialogSearch.vue";
+import DataDialogTable from "./DataDialogTable.vue";
+import SelectionDialog from "./SelectionDialog.vue";
+
+export default {
+ components: {
+ "selection-dialog": SelectionDialog,
+ "data-dialog-table": DataDialogTable,
+ "data-dialog-search": DataDialogSearch
+ },
+ props: {
+ callback: {
+ type: Function,
+ required: true
+ }
+ }
+};
diff --git a/client/galaxy/scripts/components/SelectionDialog/WorkflowDialog.test.js b/client/galaxy/scripts/components/SelectionDialog/WorkflowDialog.test.js
new file mode 100644
index 00000000000..804220802d7
--- /dev/null
+++ b/client/galaxy/scripts/components/SelectionDialog/WorkflowDialog.test.js
@@ -0,0 +1,69 @@
+import MockAdapter from "axios-mock-adapter";
+import axios from "axios";
+import WorkflowDialog from "./WorkflowDialog.vue";
+import { __RewireAPI__ as rewire } from "components/Workflow/services";
+import SelectionDialog from "./SelectionDialog.vue";
+
+import { shallowMount, createLocalVue } from "@vue/test-utils";
+
+const mockOptions = {
+ callback: () => {},
+ modalStatic: true
+};
+
+const userModel = {
+ get: () => "username"
+};
+
+const galaxyInstance = {
+ root: "/",
+ user: userModel
+};
+
+describe("WorkflowDialog.vue", () => {
+ let wrapper;
+ let localVue;
+ let axiosMock;
+
+ beforeEach(() => {
+ axiosMock = new MockAdapter(axios);
+ rewire.__Rewire__("getGalaxyInstance", () => galaxyInstance);
+ rewire.__Rewire__("_addAttributes", workflow => workflow);
+ localVue = createLocalVue();
+ wrapper = shallowMount(WorkflowDialog, {
+ propsData: mockOptions,
+ localVue: localVue
+ });
+ });
+
+ afterEach(() => {
+ axiosMock.restore();
+ });
+
+ it("loads correctly in loading state, shows options when optionsShow becomes true", async () => {
+ // Initially in loading state.
+ const workflowsResponse = [{ id: "f2db41e1fa331b3e", name: "Awesome Workflow" }];
+ axiosMock.onGet("/api/workflows").reply(200, workflowsResponse);
+
+ expect(wrapper.find(SelectionDialog).is(SelectionDialog)).to.equals(true);
+ expect(wrapper.vm.optionsShow).to.equals(false);
+
+ await localVue.nextTick();
+ await localVue.nextTick();
+ await localVue.nextTick();
+
+ // why not shown?
+ expect(wrapper.vm.errorMessage).to.equals(null);
+ expect(wrapper.vm.optionsShow).to.equals(true);
+ });
+
+ it("error message set on workflow fetch problems", async () => {
+ expect(wrapper.vm.errorMessage).to.equals(null);
+ axiosMock.onGet("/api/workflows").reply(403, { err_msg: "Bad error" });
+ await localVue.nextTick();
+ await localVue.nextTick();
+ await localVue.nextTick();
+ await localVue.nextTick();
+ expect(wrapper.vm.errorMessage).to.equals("Bad error");
+ });
+});
diff --git a/client/galaxy/scripts/components/SelectionDialog/WorkflowDialog.vue b/client/galaxy/scripts/components/SelectionDialog/WorkflowDialog.vue
new file mode 100644
index 00000000000..3f6715c1608
--- /dev/null
+++ b/client/galaxy/scripts/components/SelectionDialog/WorkflowDialog.vue
@@ -0,0 +1,76 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/client/galaxy/scripts/components/Workflow/Editor/WorkflowEditor.vue b/client/galaxy/scripts/components/Workflow/Editor/WorkflowEditor.vue
index 26c08525f2e..5eff71dff15 100644
--- a/client/galaxy/scripts/components/Workflow/Editor/WorkflowEditor.vue
+++ b/client/galaxy/scripts/components/Workflow/Editor/WorkflowEditor.vue
@@ -100,7 +100,7 @@
-
+
diff --git a/client/galaxy/scripts/components/Workflow/Editor/reportHelp.js b/client/galaxy/scripts/components/Workflow/Editor/reportHelp.js
index bda7287cb14..e968384d1aa 100644
--- a/client/galaxy/scripts/components/Workflow/Editor/reportHelp.js
+++ b/client/galaxy/scripts/components/Workflow/Editor/reportHelp.js
@@ -1,3 +1,5 @@
+import { datasetCommandsHtml, markdownGeneralHelpHtml } from "components/Markdown/help";
+
const reportHelp = `
Overview
@@ -6,16 +8,10 @@ const reportHelp = `
This document Markdown document will be used to generate a report
for invocations of this workflow. This document should be Markdown
with embedded command for extracting and displaying parts of the workflow,
- its invocation metadata, its inputs and outputs, etc.. For an overview
- of standard Markdown visit the
commonmark.org
- tutorial.
+ its invocation metadata, its inputs and outputs, etc..
-
- The Galaxy extensions to Markdown are represented as code blocks, these blocks start
- with the line \`\`\`galaxy and end with the line \`\`\` and have a
- command with arguments that reference parts of the workflow in the middle.
-
+${markdownGeneralHelpHtml}
Workflow Commands
@@ -78,18 +74,7 @@ history_dataset_collection_display(output="Merged Bam")
\`\`\`
-
history_dataset_display
-
Embed a dataset description in the resulting report.
-
history_dataset_collection_display
-
Embed a dataset collection description in the resulting report.
-
history_dataset_as_image
-
Embed a dataset as an image in the resulting report - the dataset should be an image datatype.
-
history_dataset_peek
-
Embed Galaxy's metadata attribute 'peek' into the resulting report - this is datatype dependent metadata but usually this is a few lines from the start of a file.
-
history_dataset_info
-
Embed Galaxy's metadata attribute 'info' into the resulting report - this is datatype dependent metadata but usually this is the program output that generated the dataset.
-
-
+${datasetCommandsHtml}
`;
import $ from "jquery";
diff --git a/client/galaxy/scripts/components/Workflow/services.js b/client/galaxy/scripts/components/Workflow/services.js
index cb357d720d8..a628c50a1b7 100644
--- a/client/galaxy/scripts/components/Workflow/services.js
+++ b/client/galaxy/scripts/components/Workflow/services.js
@@ -5,7 +5,7 @@ import { rethrowSimple } from "utils/simple-error";
/** Workflow data request helper **/
export class Services {
constructor(options = {}) {
- this.root = options.root;
+ this.root = options.root || getGalaxyInstance().root;
}
async copyWorkflow(workflow) {
diff --git a/client/galaxy/scripts/galaxy.pages.js b/client/galaxy/scripts/galaxy.pages.js
index fd31483c8cc..8ab624c80cb 100644
--- a/client/galaxy/scripts/galaxy.pages.js
+++ b/client/galaxy/scripts/galaxy.pages.js
@@ -10,12 +10,15 @@ export default function pagesEditorOnload() {
axios
.get(`${getAppRoot()}api/pages/${pageId}`)
.then(response => {
+ const data = response.data;
const pageEditorInstance = Vue.extend(PageEditor);
new pageEditorInstance({
propsData: {
+ publicUrl: `u/${data.username}/p/${data.slug}`,
pageId: pageId,
- content: response.data.content,
- title: response.data.title
+ content: data.content,
+ contentFormat: data.content_format,
+ title: data.title
},
el: "#page-editor-content"
});
diff --git a/client/galaxy/scripts/mvc/base/controlled-fetch-collection.js b/client/galaxy/scripts/mvc/base/controlled-fetch-collection.js
index 9b5af96b2ac..848d85504b8 100644
--- a/client/galaxy/scripts/mvc/base/controlled-fetch-collection.js
+++ b/client/galaxy/scripts/mvc/base/controlled-fetch-collection.js
@@ -1,7 +1,6 @@
import jQuery from "jquery";
import _ from "underscore";
import Backbone from "backbone";
-import { getGalaxyInstance } from "app";
import BASE_MVC from "mvc/base-mvc";
//=============================================================================
@@ -28,15 +27,11 @@ var ControlledFetchCollection = Backbone.Collection.extend({
*/
fetch: function(options) {
options = this._buildFetchOptions(options);
- const Galaxy = getGalaxyInstance();
- Galaxy.debug("fetch options:", options);
return Backbone.Collection.prototype.fetch.call(this, options);
},
/** build ajax data/parameters from options */
_buildFetchOptions: function(options) {
- const Galaxy = getGalaxyInstance();
-
// note: we normally want options passed in to override the defaults built here
// so most of these fns will generate defaults
options = _.clone(options) || {};
@@ -52,15 +47,12 @@ var ControlledFetchCollection = Backbone.Collection.extend({
// (i.e. this.on( 'sync', function( options ){ if( options.limit ){ ... } }))
// however, when we send to xhr/jquery we copy them to data also so that they become API query params
options.data = options.data || this._buildFetchData(options);
- Galaxy.debug("data:", options.data);
// options.data.filters --> options.data.q, options.data.qv
var filters = this._buildFetchFilters(options);
- Galaxy.debug("filters:", filters);
if (!_.isEmpty(filters)) {
_.extend(options.data, this._fetchFiltersToAjaxData(filters));
}
- Galaxy.debug("data:", options.data);
return options;
},
@@ -271,8 +263,6 @@ var InfinitelyScrollingCollection = ControlledFetchCollection.extend({
/** fetch the first 'page' of data */
fetchFirst: function(options) {
- const Galaxy = getGalaxyInstance();
- Galaxy.debug("ControlledFetchCollection.fetchFirst:", options);
options = options ? _.clone(options) : {};
this.allFetched = false;
this.lastFetched = 0;
@@ -286,13 +276,9 @@ var InfinitelyScrollingCollection = ControlledFetchCollection.extend({
/** fetch the next page of data */
fetchMore: function(options) {
- const Galaxy = getGalaxyInstance();
-
- Galaxy.debug("ControlledFetchCollection.fetchMore:", options);
options = _.clone(options || {});
var collection = this;
- Galaxy.debug("fetchMore, options.reset:", options.reset);
if (!options.reset && collection.allFetched) {
return jQuery.when();
}
@@ -305,7 +291,6 @@ var InfinitelyScrollingCollection = ControlledFetchCollection.extend({
options.offset = collection.lastFetched;
}
var limit = (options.limit = options.limit || collection.limitPerFetch || null);
- Galaxy.debug("fetchMore, limit:", limit, "offset:", options.offset);
collection.trigger("fetching-more");
return (
@@ -318,7 +303,6 @@ var InfinitelyScrollingCollection = ControlledFetchCollection.extend({
.done(function _postFetchMore(fetchedData) {
var numFetched = _.isArray(fetchedData) ? fetchedData.length : 0;
collection.lastFetched += numFetched;
- Galaxy.debug("fetchMore, lastFetched:", collection.lastFetched);
// anything less than a full page means we got all there is to get
if (!limit || numFetched < limit) {
collection.allFetched = true;
diff --git a/client/galaxy/scripts/utils/data.js b/client/galaxy/scripts/utils/data.js
index 959e358a4a9..6800bd12f9b 100644
--- a/client/galaxy/scripts/utils/data.js
+++ b/client/galaxy/scripts/utils/data.js
@@ -2,6 +2,8 @@ import $ from "jquery";
import axios from "axios";
import Vue from "vue";
import DataDialog from "components/DataDialog/DataDialog.vue";
+import WorkflowDialog from "components/SelectionDialog/WorkflowDialog.vue";
+import DatasetCollectionDialog from "components/SelectionDialog/DatasetCollectionDialog.vue";
import { getGalaxyInstance } from "app";
import { getAppRoot } from "onload/loadConfig";
@@ -41,15 +43,44 @@ export function dialog(callback, options = {}) {
root: galaxy.root,
host: host
});
- const instance = Vue.extend(DataDialog);
- const vm = document.createElement("div");
- $("body").append(vm);
- new instance({
- propsData: options
- }).$mount(vm);
+ _mountSelectionDialog(DataDialog, options);
});
}
+/**
+ * Opens a modal dialog for workflow selection
+ * @param {function} callback - Result function called with selection
+ */
+export function workflowDialog(callback, options = {}) {
+ Object.assign(options, {
+ callback: callback
+ });
+ _mountSelectionDialog(WorkflowDialog, options);
+}
+
+/**
+ * Opens a modal dialog for dataset collection selection
+ * @param {function} callback - Result function called with selection
+ */
+export function datasetCollectionDialog(callback, options = {}) {
+ getCurrentGalaxyHistory().then(history_id => {
+ Object.assign(options, {
+ callback: callback,
+ history: history_id
+ });
+ _mountSelectionDialog(DatasetCollectionDialog, options);
+ });
+}
+
+function _mountSelectionDialog(clazz, options) {
+ const instance = Vue.extend(clazz);
+ const vm = document.createElement("div");
+ $("body").append(vm);
+ new instance({
+ propsData: options
+ }).$mount(vm);
+}
+
/**
* Creates a history dataset by submitting an upload request
* TODO: This should live somewhere else.
diff --git a/client/galaxy/scripts/utils/simple-error.js b/client/galaxy/scripts/utils/simple-error.js
index 1c4dc9453e7..1341c1301f4 100644
--- a/client/galaxy/scripts/utils/simple-error.js
+++ b/client/galaxy/scripts/utils/simple-error.js
@@ -1,9 +1,13 @@
-export function rethrowSimple(e) {
+export function errorMessageAsString(e) {
let message = "Request failed.";
if (e.response) {
message = e.response.data.err_msg || `${e.response.statusText} (${e.response.status})`;
} else if (typeof e == "string") {
message = e;
}
- throw message;
+ return message;
+}
+
+export function rethrowSimple(e) {
+ throw errorMessageAsString(e);
}
diff --git a/lib/galaxy/managers/markdown_util.py b/lib/galaxy/managers/markdown_util.py
index 04629abea27..438c9b965f0 100644
--- a/lib/galaxy/managers/markdown_util.py
+++ b/lib/galaxy/managers/markdown_util.py
@@ -13,6 +13,7 @@ context specific processing.
import logging
import re
+from galaxy.exceptions import MalformedContents, MalformedId
from galaxy.managers.collections import DatasetCollectionManager
from galaxy.managers.hdas import HDAManager
from galaxy.managers.hdcas import HDCASerializer
@@ -53,7 +54,8 @@ OUTPUT_LABEL_PATTERN = re.compile(r'output=\s*%s\s*' % ARG_VAL_CAPTURED_REGEX)
INPUT_LABEL_PATTERN = re.compile(r'input=\s*%s\s*' % ARG_VAL_CAPTURED_REGEX)
STEP_LABEL_PATTERN = re.compile(r'step=\s*%s\s*' % ARG_VAL_CAPTURED_REGEX)
# STEP_OUTPUT_LABEL_PATTERN = re.compile(r'step_output=([\w_\-]+)/([\w_\-]+)')
-ID_PATTERN = re.compile(r'(workflow_id|history_dataset_id|history_dataset_collection_id|job_id)=([\d]+)')
+UNENCODED_ID_PATTERN = re.compile(r'(workflow_id|history_dataset_id|history_dataset_collection_id|job_id)=([\d]+)')
+ENCODED_ID_PATTERN = re.compile(r'(workflow_id|history_dataset_id|history_dataset_collection_id|job_id)=([a-z0-9]+)')
GALAXY_FLAVORED_MARKDOWN_CONTAINER_LINE_PATTERN = re.compile(
r"```\s*galaxy\s*"
)
@@ -66,6 +68,27 @@ VALID_CONTAINER_END_PATTERN = re.compile(r"^```\s*$")
WHITE_SPACE_ONLY_PATTERN = re.compile(r"^[\s]+$")
+def ready_galaxy_markdown_for_import(trans, external_galaxy_markdown):
+ """Convert from encoded IDs to decoded numeric IDs for storing in the DB."""
+
+ validate_galaxy_markdown(external_galaxy_markdown, internal=False)
+
+ def _remap(container, line):
+ id_match = re.search(ENCODED_ID_PATTERN, line)
+ object_id = None
+ if id_match:
+ object_id = id_match.group(2)
+ try:
+ decoded_id = trans.security.decode_id(object_id)
+ except Exception:
+ raise MalformedId("Invalid encoded ID %s" % object_id)
+ line = line.replace(id_match.group(), "%s=%d" % (id_match.group(1), decoded_id))
+ return (line, False)
+
+ internal_markdown = _remap_galaxy_markdown_calls(_remap, external_galaxy_markdown)
+ return internal_markdown
+
+
def ready_galaxy_markdown_for_export(trans, internal_galaxy_markdown):
"""Fill in details needed to render Galaxy flavored markdown.
@@ -76,10 +99,13 @@ def ready_galaxy_markdown_for_export(trans, internal_galaxy_markdown):
"""
hdas_manager = HDAManager(trans.app)
workflows_manager = WorkflowsManager(trans.app)
+ job_manager = JobManager(trans.app)
+ collection_manager = DatasetCollectionManager(trans.app)
+
extra_rendering_data = {}
def _remap(container, line):
- id_match = re.search(ID_PATTERN, line)
+ id_match = re.search(UNENCODED_ID_PATTERN, line)
object_id = None
encoded_id = None
if id_match:
@@ -112,13 +138,12 @@ def ready_galaxy_markdown_for_export(trans, internal_galaxy_markdown):
elif container == "history_dataset_info":
hda = hdas_manager.get_accessible(object_id, trans.user)
info = hda.info
- extend_history_dataset_rendering_data("info", info, "*No Dataset Peek Available*")
+ extend_history_dataset_rendering_data("info", info, "*No Dataset Info Available*")
elif container == "workflow_display":
# TODO: should be workflow id...
stored_workflow = workflows_manager.get_stored_accessible_workflow(trans, encoded_id)
ensure_rendering_data_for("workflows", encoded_id)["name"] = stored_workflow.name
elif container == "history_dataset_collection_display":
- collection_manager = DatasetCollectionManager(trans.app)
hdca = collection_manager.get_dataset_collection_instance(trans, "history", encoded_id)
hdca_serializer = HDCASerializer(trans.app)
hdca_view = hdca_serializer.serialize_to_view(
@@ -128,11 +153,9 @@ def ready_galaxy_markdown_for_export(trans, internal_galaxy_markdown):
extra_rendering_data["history_dataset_collections"] = {}
ensure_rendering_data_for("history_dataset_collections", encoded_id).update(hdca_view)
elif container == "tool_stdout":
- job_manager = JobManager(trans.app)
job = job_manager.get_accessible_job(trans, object_id)
ensure_rendering_data_for("jobs", encoded_id)["tool_stdout"] = job.tool_stdout or "*No Standard Output Available*"
elif container == "tool_stderr":
- job_manager = JobManager(trans.app)
job = job_manager.get_accessible_job(trans, object_id)
ensure_rendering_data_for("jobs", encoded_id)["tool_stderr"] = job.tool_stderr or "*No Standard Error Available*"
return (line, False)
@@ -238,13 +261,11 @@ history_dataset_collection_display(input=%s)
line = line.replace(target_match.group(), "%s_id=%s" % (ref_object_type, ref_object.id))
return (line, False)
- log.debug("workflow markdown is \n%s" % workflow_markdown)
workflow_markdown = _remap_galaxy_markdown_calls(
_section_remap,
workflow_markdown,
)
galaxy_markdown = _remap_galaxy_markdown_calls(_remap, workflow_markdown)
- log.debug("galaxy markdown is \n%s" % galaxy_markdown)
return galaxy_markdown
@@ -258,7 +279,7 @@ def validate_galaxy_markdown(galaxy_markdown, internal=True):
def invalid_line(template, **kwd):
if "line" in kwd:
kwd["line"] = line.rstrip("\r\n")
- raise Exception("Invalid line %d: %s" % (line_no + 1, template.format(**kwd)))
+ raise MalformedContents("Invalid line %d: %s" % (line_no + 1, template.format(**kwd)))
expecting_container_close = expecting_container_close_for is not None
if not fenced and expecting_container_close:
@@ -267,13 +288,16 @@ def validate_galaxy_markdown(galaxy_markdown, internal=True):
elif not fenced:
continue
elif fenced and expecting_container_close and BLOCK_FENCE_END.match(line):
+ # reset
expecting_container_close_for = None
+ function_calls = 0
elif open_fence and GALAXY_FLAVORED_MARKDOWN_CONTAINER_LINE_PATTERN.match(line):
if expecting_container_close:
if not VALID_CONTAINER_END_PATTERN.match(line):
invalid_line("Invalid command close line [{line}] for [{expected_for}]", line=line, expected_for=expecting_container_close_for)
# else closing container and we're done
expecting_container_close_for = None
+ function_calls = 0
continue
expecting_container_close_for = line
@@ -291,7 +315,7 @@ def validate_galaxy_markdown(galaxy_markdown, internal=True):
continue
if expecting_container_close_for:
- raise Exception("Invalid line %d: %s" % (last_line_no, "close of block for [{expected_for}] expected".format(expected_for=expecting_container_close_for)))
+ raise MalformedContents("Invalid line %d: %s" % (last_line_no, "close of block for [{expected_for}] expected".format(expected_for=expecting_container_close_for)))
def _remap_galaxy_markdown_containers(func, markdown):
@@ -360,5 +384,6 @@ def _split_markdown_lines(markdown):
__all__ = (
'ready_galaxy_markdown_for_export',
+ 'ready_galaxy_markdown_for_import',
'resolve_invocation_markdown',
)
diff --git a/lib/galaxy/managers/pages.py b/lib/galaxy/managers/pages.py
index 8d2053acce3..8edf18ddbb4 100644
--- a/lib/galaxy/managers/pages.py
+++ b/lib/galaxy/managers/pages.py
@@ -14,6 +14,10 @@ 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.managers.markdown_util import (
+ ready_galaxy_markdown_for_export,
+ ready_galaxy_markdown_for_import,
+)
from galaxy.model.item_attrs import UsesAnnotations
from galaxy.util import unicodify
from galaxy.util.sanitize_html import sanitize_html
@@ -87,7 +91,8 @@ class PageManager(sharable.SharableModelManager, UsesAnnotations):
raise exceptions.DuplicatedSlugException("Page identifier must be unique")
content = payload.get("content", "")
- content = self.rewrite_content_for_import(trans, content)
+ content_format = payload.get("content_format", "html")
+ content = self.rewrite_content_for_import(trans, content, content_format)
# Create the new stored page
page = trans.app.model.Page()
@@ -105,6 +110,7 @@ class PageManager(sharable.SharableModelManager, UsesAnnotations):
page_revision.page = page
page.latest_revision = page_revision
page_revision.content = content
+ page_revision.content_format = content_format
# Persist
session = trans.sa_session
session.add(page)
@@ -114,46 +120,66 @@ class PageManager(sharable.SharableModelManager, UsesAnnotations):
def save_new_revision(self, trans, page, payload):
# Assumes security has already been checked by caller.
content = payload.get("content", None)
+ content_format = payload.get("content_format", None)
if not content:
raise exceptions.ObjectAttributeMissingException("content undefined or empty")
+ if content_format not in [None, "html", "markdown"]:
+ raise exceptions.RequestParameterInvalidException("content_format [%s], if specified, must be either html or markdown" % content_format)
if 'title' in payload:
title = payload['title']
else:
title = page.title
- content = self.rewrite_content_for_import(trans, content)
+ if content_format is None:
+ content_format = page.latest_revision.content_format
+ content = self.rewrite_content_for_import(trans, content, content_format=content_format)
page_revision = trans.app.model.PageRevision()
page_revision.title = title
page_revision.page = page
page.latest_revision = page_revision
page_revision.content = content
+ page_revision.content_format = content_format
# 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)
+ def rewrite_content_for_import(self, trans, content, content_format):
+ if content_format == "html":
+ 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)
+ elif content_format == "markdown":
+ content = ready_galaxy_markdown_for_import(trans, content)
+ else:
+ raise exceptions.RequestParameterInvalidException("content_format [%s] must be either html or markdown" % content_format)
+
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
+ content_format = as_dict.get("content_format", "html")
+ if content_format == "html":
+ processor = PageContentProcessor(trans, placeholderRenderForEdit)
+ processor.feed(content)
+ content = unicodify(processor.output(), 'utf-8')
+ as_dict["content"] = content
+ elif content_format == "markdown":
+ content, extra_attributes = ready_galaxy_markdown_for_export(trans, content)
+ as_dict["content"] = content
+ as_dict.update(extra_attributes)
+ else:
+ raise exceptions.RequestParameterInvalidException("content_format [%s] must be either html or markdown" % content_format)
return as_dict
diff --git a/lib/galaxy/model/__init__.py b/lib/galaxy/model/__init__.py
index dc96f4502d6..e687b521df0 100644
--- a/lib/galaxy/model/__init__.py
+++ b/lib/galaxy/model/__init__.py
@@ -5767,7 +5767,8 @@ class CloudAuthz(RepresentById):
class Page(Dictifiable, RepresentById):
- dict_element_visible_keys = ['id', 'title', 'latest_revision_id', 'slug', 'published', 'importable', 'deleted']
+ # username needed for slug generation
+ dict_element_visible_keys = ['id', 'title', 'latest_revision_id', 'slug', 'published', 'importable', 'deleted', 'username']
def __init__(self):
self.id = None
@@ -5787,14 +5788,20 @@ class Page(Dictifiable, RepresentById):
rval['revision_ids'] = rev
return rval
+ @property
+ def username(self):
+ return self.user.username
+
class PageRevision(Dictifiable, RepresentById):
- dict_element_visible_keys = ['id', 'page_id', 'title', 'content']
+ DEFAULT_CONTENT_FORMAT = 'html'
+ dict_element_visible_keys = ['id', 'page_id', 'title', 'content', 'content_format']
def __init__(self):
self.user = None
self.title = None
self.content = None
+ self.content_format = PageRevision.DEFAULT_CONTENT_FORMAT
def to_dict(self, view='element'):
rval = super(PageRevision, self).to_dict(view=view)
diff --git a/lib/galaxy/model/mapping.py b/lib/galaxy/model/mapping.py
index c64cb75bdd7..d245a258a6a 100644
--- a/lib/galaxy/model/mapping.py
+++ b/lib/galaxy/model/mapping.py
@@ -1246,7 +1246,8 @@ model.PageRevision.table = Table(
Column("update_time", DateTime, default=now, onupdate=now),
Column("page_id", Integer, ForeignKey("page.id"), index=True, nullable=False),
Column("title", TEXT),
- Column("content", TEXT))
+ Column("content", TEXT),
+ Column("content_format", TrimmedString(32)))
model.PageUserShareAssociation.table = Table(
"page_user_share_association", metadata,
diff --git a/lib/galaxy/model/migrate/versions/0164_page_format.py b/lib/galaxy/model/migrate/versions/0164_page_format.py
new file mode 100644
index 00000000000..4517939df02
--- /dev/null
+++ b/lib/galaxy/model/migrate/versions/0164_page_format.py
@@ -0,0 +1,30 @@
+"""
+Adds page content format.
+"""
+import datetime
+import logging
+
+from sqlalchemy import Column, MetaData
+
+from galaxy.model.custom_types import TrimmedString
+from galaxy.model.migrate.versions.util import add_column, drop_column
+
+now = datetime.datetime.utcnow
+log = logging.getLogger(__name__)
+metadata = MetaData()
+
+
+def upgrade(migrate_engine):
+ metadata.bind = migrate_engine
+ print(__doc__)
+ metadata.reflect()
+
+ content_format_column = Column('content_format', TrimmedString(32), default='html', server_default="html", nullable=False)
+ add_column(content_format_column, 'page_revision', metadata)
+
+
+def downgrade(migrate_engine):
+ metadata.bind = migrate_engine
+ metadata.reflect()
+
+ drop_column('content_format', 'page_revision', metadata)
diff --git a/lib/galaxy/webapps/galaxy/api/page_revisions.py b/lib/galaxy/webapps/galaxy/api/page_revisions.py
index 51b26f9bb82..d859bd85ddc 100644
--- a/lib/galaxy/webapps/galaxy/api/page_revisions.py
+++ b/lib/galaxy/webapps/galaxy/api/page_revisions.py
@@ -52,8 +52,7 @@ class PageRevisionsController(BaseAPIController, SharableItemSecurityMixin, Uses
:param page_id: Add revision to Page with ID=page_id
:param payload: A dictionary containing::
- 'title' = New title of the page
- 'content' = New content of the page
+ 'content' = New content of new page revision
:rtype: dictionary
:returns: Dictionary with 'success' or 'error' element to indicate the result of the request
diff --git a/lib/galaxy/webapps/galaxy/api/pages.py b/lib/galaxy/webapps/galaxy/api/pages.py
index 52fb6e6ffa8..9f737ce73dc 100644
--- a/lib/galaxy/webapps/galaxy/api/pages.py
+++ b/lib/galaxy/webapps/galaxy/api/pages.py
@@ -72,10 +72,11 @@ class PagesController(BaseAPIController, SharableItemSecurityMixin, UsesAnnotati
Create a page and return dictionary containing Page summary
:param payload: dictionary structure containing::
- 'slug' = The title slug for the page URL, must be unique
- 'title' = Title of the page
- 'content' = HTML contents of the page
- 'annotation' = Annotation that will be attached to the page
+ 'slug' = The title slug for the page URL, must be unique
+ 'title' = Title of the page
+ 'content' = contents of the first page revision (type dependent on content_format)
+ 'content_format' = 'html' or 'markdown'
+ 'annotation' = Annotation that will be attached to the page
:rtype: dict
:returns: Dictionary return of the Page.to_dict call
@@ -120,5 +121,6 @@ class PagesController(BaseAPIController, SharableItemSecurityMixin, UsesAnnotati
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
+ rval['content_format'] = page.latest_revision.content_format
self.manager.rewrite_content_for_export(trans, rval)
return rval
diff --git a/lib/galaxy/webapps/galaxy/controllers/page.py b/lib/galaxy/webapps/galaxy/controllers/page.py
index e7501e4e2c0..c97498c3cbb 100644
--- a/lib/galaxy/webapps/galaxy/controllers/page.py
+++ b/lib/galaxy/webapps/galaxy/controllers/page.py
@@ -333,6 +333,12 @@ class PageController(BaseUIController, SharableMixin,
'name' : 'annotation',
'label' : 'Annotation',
'help' : 'A description of the page. The annotation is shown alongside published pages.'
+ }, {
+ 'name' : 'content_format',
+ 'label' : 'Content Format',
+ 'type' : 'select',
+ 'options' : [('HTML', 'html'), ('Markdown', 'markdown')],
+ 'help' : 'Use the traditional rich HTML editor or the newer experimental Markdown editor to create the page content. The HTML editor has several known bugs, is unmaintained and pages created with it will be read-only in future releases of Galaxy.'
}]
}
else:
@@ -448,14 +454,6 @@ class PageController(BaseUIController, SharableMixin,
email=email,
use_panels=use_panels)
- @web.expose
- @web.require_login()
- 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
- self.page_manager.save_new_revision(trans, page, {"content": content})
-
@web.expose
@web.require_login()
def display(self, trans, id):
@@ -478,11 +476,17 @@ class PageController(BaseUIController, SharableMixin,
# Security check raises error if user cannot access page.
self.security_check(trans, page, False, True)
- # Process page content.
- 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')
+ latest_revision = page.latest_revision
+ if latest_revision.content_format == "html":
+ # Process page content.
+ 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')
+ template = "page/display.mako"
+ else:
+ page_content = trans.security.encode_id(page.id)
+ template = "page/display_markdown.mako"
# Get rating data.
user_item_rating = 0
@@ -494,7 +498,7 @@ class PageController(BaseUIController, SharableMixin,
user_item_rating = 0
ave_item_rating, num_ratings = self.get_ave_item_rating_data(trans.sa_session, page)
- return trans.fill_template_mako("page/display.mako", item=page,
+ return trans.fill_template_mako(template, item=page,
item_data=page_content,
user_item_rating=user_item_rating,
ave_item_rating=ave_item_rating,
diff --git a/lib/galaxy_test/api/test_pages.py b/lib/galaxy_test/api/test_pages.py
index 18b2201143f..4de64b2750d 100644
--- a/lib/galaxy_test/api/test_pages.py
+++ b/lib/galaxy_test/api/test_pages.py
@@ -22,10 +22,16 @@ class BasePageApiTestCase(ApiTestCase):
return page_response.json()
def _test_page_payload(self, **kwds):
+ content_format = kwds.get("content_format", "html")
+ if content_format == "html":
+ content = "Page!
"
+ else:
+ content = "*Page*\n\n"
request = dict(
slug="mypage",
title="MY PAGE",
- content="Page!
",
+ content=content,
+ content_format=content_format,
)
request.update(**kwds)
return request
@@ -41,7 +47,7 @@ class PageApiTestCase(BasePageApiTestCase):
create_response_json = self._create_valid_page_with_slug("indexpage")
assert self._users_index_has_page_with_id(create_response_json["id"])
- def test_index_doesnt_show_unavailable_pages(self):
+ def test_index_does_not_show_unavailable_pages(self):
create_response_json = self._create_valid_page_as("others_page_index@bx.psu.edu", "otherspageindex")
assert not self._users_index_has_page_with_id(create_response_json["id"])
@@ -53,6 +59,13 @@ class PageApiTestCase(BasePageApiTestCase):
self._assert_status_code_is(page_response_2, 400)
self._assert_error_code_is(page_response_2, error_codes.USER_SLUG_DUPLICATE)
+ def test_cannot_create_page_with_invalid_content_format(self):
+ page_request = self._test_page_payload(slug="mypageinvalidformat")
+ page_request["content_format"] = "xml"
+ page_response_1 = self._post("pages", page_request)
+ self._assert_status_code_is(page_response_1, 400)
+ self._assert_error_code_is(page_response_1, error_codes.USER_REQUEST_INVALID_PARAMETER)
+
def test_page_requires_name(self):
page_request = self._test_page_payload(slug="requires-name")
del page_request['title']
@@ -89,16 +102,30 @@ class PageApiTestCase(BasePageApiTestCase):
self._assert_status_code_is(page_response, 400)
self._assert_error_code_is(page_response, error_codes.MALFORMED_ID)
+ def test_400_on_invalid_id_encoding_markdown(self):
+ page_request = self._test_page_payload(slug="invalid-id-encding-markdown", content_format="markdown")
+ page_request["content"] = '''```galaxy\nhistory_dataset_display(history_dataset_id=badencoding)\n```\n'''
+ 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 = self._test_page_payload(slug="invalid-embed-content")
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.text
+ def test_400_on_invalid_markdown_call(self):
+ page_request = self._test_page_payload(slug="invalid-markdown-call", content_format="markdown")
+ page_request["content"] = '''```galaxy\njob_metrics(job_id)\n```\n'''
+ 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_CONTENTS)
+
def test_show(self):
response_json = self._create_valid_page_with_slug("pagetoshow")
show_response = self._get("pages/%s" % response_json['id'])
@@ -108,6 +135,7 @@ class PageApiTestCase(BasePageApiTestCase):
self.assertEqual(show_json["slug"], "pagetoshow")
self.assertEqual(show_json["title"], "MY PAGE")
self.assertEqual(show_json["content"], "Page!
")
+ self.assertEqual(show_json["content_format"], "html")
def test_403_on_unowner_show(self):
response_json = self._create_valid_page_as("others_page_show@bx.psu.edu", "otherspageshow")
diff --git a/templates/webapps/galaxy/dataset/display.mako b/templates/webapps/galaxy/dataset/display.mako
index 04bb46d2889..0e3ccc90bf6 100644
--- a/templates/webapps/galaxy/dataset/display.mako
+++ b/templates/webapps/galaxy/dataset/display.mako
@@ -71,8 +71,10 @@
%endif
## TODO: why is the default font size so small?
${ data_to_render | h }
+ %elif data.get_size() == 0:
+ Dataset is empty.
%else:
- Cannot show dataset content
+ Problem displaying dataset content.
%endif
%def>
diff --git a/templates/webapps/galaxy/page/display_markdown.mako b/templates/webapps/galaxy/page/display_markdown.mako
new file mode 100644
index 00000000000..a40051afe4c
--- /dev/null
+++ b/templates/webapps/galaxy/page/display_markdown.mako
@@ -0,0 +1,30 @@
+<%inherit file="/display_base.mako"/>
+
+<%def name="javascript_app()">
+
+ ${parent.javascript_app()}
+
+
+%def>
+
+<%def name="stylesheets()">
+ ${parent.stylesheets()}
+ ${h.css( "base" )}
+%def>
+
+<%def name="render_item_header( item )">
+ ## No header for pages.
+%def>
+
+<%def name="render_item_links( page )">
+%def>
+
+<%def name="render_item( page, page_data=None )">
+
+
+%def>
diff --git a/test/integration/test_page_revision_json_encoding.py b/test/integration/test_page_revision_json_encoding.py
index f238057e040..c8ed2b644ce 100644
--- a/test/integration/test_page_revision_json_encoding.py
+++ b/test/integration/test_page_revision_json_encoding.py
@@ -18,23 +18,53 @@ class PageJsonEncodingIntegrationTestCase(integration_util.IntegrationTestCase):
def setUp(self):
super(PageJsonEncodingIntegrationTestCase, self).setUp()
self.dataset_populator = DatasetPopulator(self.galaxy_interactor)
+ self.history_id = self.dataset_populator.new_history()
def test_page_encoding(self):
- history_id = self.dataset_populator.new_history()
request = dict(
slug="mypage",
title="MY PAGE",
- content='''Page!
''' % history_id,
+ content='''Page!
''' % self.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]
+ page_revision = sa_session.query(model.PageRevision).filter_by(content_format="html").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
+ assert '''id="History-%s"''' % self.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
+ assert '''id="History-%s"''' % self.history_id in content, content
+
+ def test_page_encoding_markdown(self):
+ dataset = self.dataset_populator.new_dataset(self.history_id)
+ dataset_id = dataset["id"]
+ request = dict(
+ slug="mypage-markdown",
+ title="MY PAGE",
+ content='''```galaxy
+history_dataset_display(history_dataset_id=%s)
+```''' % dataset["id"],
+ content_format="markdown",
+ )
+ 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).filter_by(content_format="markdown").all()[0]
+ assert '''```galaxy
+history_dataset_display(history_dataset_id=1)
+```''' in page_revision.content, page_revision.content
+ assert '''::: history_dataset_display history_dataset_id=%s''' % dataset_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 '''```galaxy
+history_dataset_display(history_dataset_id=1)
+```''' not in content, content
+ assert '''```galaxy
+history_dataset_display(history_dataset_id=%s)
+```''' % dataset_id in content, content
diff --git a/test/unit/test_markdown_validate.py b/test/unit/test_markdown_validate.py
index 6feea6bd96d..00339444bc7 100644
--- a/test/unit/test_markdown_validate.py
+++ b/test/unit/test_markdown_validate.py
@@ -36,6 +36,20 @@ printf('hello')
job_metrics(job_id=THISFAKEID)
```
""")
+ # assert multiple valid container is fine.
+ assert_markdown_valid("""
+```galaxy
+job_metrics(job_id=THISFAKEID)
+```
+
+Markdown between directives.
+
+```galaxy
+job_metrics(job_id=THISFAKEID)
+```
+
+""")
+
# assert valid container is fine at end of document.
assert_markdown_valid("""
```galaxy