Merge pull request #8047 from jmchilton/pages_api_and_component

Toward a more modern frontend and backend for Pages.
This commit is contained in:
Dannon
2019-06-10 09:08:48 -04:00
committed by GitHub
30 changed files with 1445 additions and 1330 deletions
+1
View File
@@ -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"
@@ -0,0 +1,38 @@
<template>
<span>
<div class="unified-panel-header" unselectable="on">
<div class="unified-panel-header-inner">
Page Editor: {{ title }}
<a id="save-button" class="btn btn-secondary fa fa-save float-right" @click="saveContent"></a>
</div>
</div>
<page-editor-html :page-id="pageId" :content="content" ref="contentEditor" />
</span>
</template>
<script>
import PageEditorHtml from "./PageEditorHtml";
export default {
props: {
pageId: {
required: true,
type: String
},
content: {
type: String
},
title: {
type: String
}
},
methods: {
saveContent: function() {
this.$refs.contentEditor.saveContent();
}
},
components: {
PageEditorHtml
}
};
</script>
@@ -0,0 +1,682 @@
<template>
<div class="unified-panel-body">
<textarea name="page_content" :val="content"></textarea>
</div>
</template>
<script>
import $ from "jquery";
import _l from "utils/localization";
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";
import { getAppRoot } from "onload/loadConfig";
import { save } from "./util";
// 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 editor = null;
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",
`<div><div><label id='link_attribute_label'>URL <span style='float: right; font-size: 90%'><a href='#' id='set_link_id'>Create in-page anchor</a></span></label><br><input type='text' class='wym_href' value='${curURL}' size='40' /></div><div><label>Title</label><br><input type='text' class='wym_title' value='${curTitle}' size='40' /></div><div>`,
{
"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",
"<div class='row'>" +
"<label>URL</label><br>" +
"<input type='text' class='wym_src' value='' size='40' />" +
"</div>" +
"<div class='row'>" +
"<label>Alt text</label><br>" +
"<input type='text' class='wym_alt' value='' size='40' />" +
"</div>" +
"<div class='row'>" +
"<label>Title</label><br>" +
"<input type='text' class='wym_title' value='' size='40' />" +
"</div>",
{
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",
"<div class='row'>" +
"<label>Caption</label><br>" +
"<input type='text' class='wym_caption' value='' size='40' />" +
"</div>" +
"<div class='row'>" +
"<label>Summary</label><br>" +
"<input type='text' class='wym_summary' value='' size='40' />" +
"</div>" +
"<div class='row'>" +
"<label>Number Of Rows<br></label>" +
"<input type='text' class='wym_rows' value='3' size='3' />" +
"</div>" +
"<div class='row'>" +
"<label>Number Of Cols<br></label>" +
"<input type='text' class='wym_cols' value='2' size='3' />" +
"</div>",
{
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();
},
Cancel: function() {
hide_modal();
}
}
);
}
// 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: $("<div/>")
.append(grid.$el)
.append(
$("<div/>")
.append('<input id="make-importable" type="checkbox" checked/>')
.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(
`<a href='${returned_item_info.link}'>${item_info.singular} '${
returned_item_info.name
}'</a>`
);
} 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: $("<div/>")
.append(grid.$el)
.append(
$("<div/>")
.append('<input id="make-importable" type="checkbox" checked/>')
.append(
`Make the selected ${item_info.plural.toLowerCase()} accessible so that they can viewed by everyone.`
)
),
closing_events: true,
extra_class: "pages-embed",
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 = [
"<div id='",
item_elt_id,
"' class='embedded-item ",
item_info.singular.toLowerCase(),
" placeholder'>",
"<p class='title'>",
"Embedded Galaxy ",
item_info.singular,
" '",
item_name,
"'",
"</p>",
"<p class='content'>",
"[Do not edit this block; Galaxy will fill it in with the annotated ",
item_info.singular.toLowerCase(),
" when it is displayed.]",
"</p>",
"</div>"
].join("");
// Insert embedded item into document.
wym.insert(item_embed_html);
});
Galaxy.modal.hide();
},
Close: function() {
Galaxy.modal.hide();
}
}
});
}
};
function renderEditorWithContent(pageId, content) {
const appRoot = getAppRoot();
const pageConfigs = {
page_id: pageId,
page_list_url: `${appRoot}pages/list`,
list_objects_url: `${appRoot}page/LIST_ACTION`,
set_accessible_url: `${appRoot}ITEM_CONTROLLER/set_accessible_async`,
get_name_and_link_url: `${appRoot}ITEM_CONTROLLER/get_name_and_link_async?id=`,
editor_base_path: `${appRoot}static/wymeditor/`,
iframe_base_path: `${appRoot}static/wymeditor/iframe/galaxy/`
};
setPageConfigs(pageConfigs);
// 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]")
.val(content)
.wymeditor({
skin: "galaxy",
basePath: configs.editor_base_path,
iframeBasePath: configs.iframe_base_path,
// + WYMeditor.HTML
boxHtml: `<table class='wym_box' width='100%' height='100%'><tr><td><div class='wym_area_top'>${
WYMeditor.TOOLS
}</div></td></tr><tr height='100%'><td><div class='wym_area_main' style='height: 100%;'>${
// + WYMeditor.HTML
WYMeditor.IFRAME
}${WYMeditor.STATUS}</div></div></td></tr></table>`,
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
editor = $.wymeditors(0);
//
// Containers, Galaxy style
//
var containers_menu = $(
"<div class='galaxy-page-editor-button'><a id='insert-galaxy-link' class='action-button popup' href='#'>Paragraph type</a></div>"
);
$(".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 = $(
"<div><a id='insert-galaxy-link' class='action-button popup' href='#'>Insert Link to Galaxy Object</a></div>"
).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 = $(
"<div><a id='embed-galaxy-object' class='action-button popup' href='#'>Embed Galaxy Object</a></div>"
).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);
//}
});
}
export default {
props: {
pageId: {
required: true,
type: String
},
content: {
type: String
}
},
created: function() {
this.$nextTick(() => {
renderEditorWithContent(this.pageId, this.content);
});
},
methods: {
saveContent: function() {
save(this.pageId, editor.xhtml());
}
}
};
</script>
<style lang="scss">
@import "embed_item";
.galaxy-page-editor-button {
position: relative;
float: left;
padding: 0.2em;
}
</style>
@@ -0,0 +1 @@
export { default as PageEditor } from "./PageEditor.vue";
@@ -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();
}
});
};
+25 -675
View File
@@ -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",
`<div><div><label id='link_attribute_label'>URL <span style='float: right; font-size: 90%'><a href='#' id='set_link_id'>Create in-page anchor</a></span></label><br><input type='text' class='wym_href' value='${curURL}' size='40' /></div><div><label>Title</label><br><input type='text' class='wym_title' value='${curTitle}' size='40' /></div><div>`,
{
"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",
"<div class='row'>" +
"<label>URL</label><br>" +
"<input type='text' class='wym_src' value='' size='40' />" +
"</div>" +
"<div class='row'>" +
"<label>Alt text</label><br>" +
"<input type='text' class='wym_alt' value='' size='40' />" +
"</div>" +
"<div class='row'>" +
"<label>Title</label><br>" +
"<input type='text' class='wym_title' value='' size='40' />" +
"</div>",
{
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",
"<div class='row'>" +
"<label>Caption</label><br>" +
"<input type='text' class='wym_caption' value='' size='40' />" +
"</div>" +
"<div class='row'>" +
"<label>Summary</label><br>" +
"<input type='text' class='wym_summary' value='' size='40' />" +
"</div>" +
"<div class='row'>" +
"<label>Number Of Rows<br></label>" +
"<input type='text' class='wym_rows' value='3' size='3' />" +
"</div>" +
"<div class='row'>" +
"<label>Number Of Cols<br></label>" +
"<input type='text' class='wym_cols' value='2' size='3' />" +
"</div>",
{
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: $("<div/>")
.append(grid.$el)
.append(
$("<div/>")
.append('<input id="make-importable" type="checkbox" checked/>')
.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(
`<a href='${returned_item_info.link}'>${item_info.singular} '${
returned_item_info.name
}'</a>`
);
} 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: $("<div/>")
.append(grid.$el)
.append(
$("<div/>")
.append('<input id="make-importable" type="checkbox" checked/>')
.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 = [
"<div id='",
item_elt_id,
"' class='embedded-item ",
item_info.singular.toLowerCase(),
" placeholder'>",
"<p class='title'>",
"Embedded Galaxy ",
item_info.singular,
" '",
item_name,
"'",
"</p>",
"<p class='content'>",
"[Do not edit this block; Galaxy will fill it in with the annotated ",
item_info.singular.toLowerCase(),
" when it is displayed.]",
"</p>",
"</div>"
].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: `<table class='wym_box' width='100%' height='100%'><tr><td><div class='wym_area_top'>${
WYMeditor.TOOLS
}</div></td></tr><tr height='100%'><td><div class='wym_area_main' style='height: 100%;'>${
// + WYMeditor.HTML
WYMeditor.IFRAME
}${WYMeditor.STATUS}</div></div></td></tr></table>`,
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 = $(
"<div class='galaxy-page-editor-button'><a id='insert-galaxy-link' class='action-button popup' href='#'>Paragraph type</a></div>"
);
$(".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 = $(
"<div><a id='insert-galaxy-link' class='action-button popup' href='#'>Insert Link to Galaxy Object</a></div>"
).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 = $(
"<div><a id='embed-galaxy-object' class='action-button popup' href='#'>Embed Galaxy Object</a></div>"
).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);
//}
});
}
@@ -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;
});
});
}
+3
View File
@@ -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");
+7
View File
@@ -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)
+351 -3
View File
@@ -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 + '></' + tag + '>'
def feed(self, data):
data = re.compile(r'<!((?!DOCTYPE|--|\[))', re.IGNORECASE).sub(r'&lt;!\1', data)
data = re.sub(r'<([^<>\s]+?)\s*/>', self._shorttag_replace, data)
data = data.replace('&#39;', "'")
data = data.replace('&#34;', '"')
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 <pre class='screen'>,
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 '<class_name>-<encoded_item_id>'
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('>', '&gt;').replace('<', '&lt;').replace('"', '&quot;')
value = self.bare_ampersand.sub("&amp;", 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 </pre>, 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("</%s>" % tag)
def handle_charref(self, ref):
# called for each character reference, e.g. for '&#160;', 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 '&copy;', 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('&amp;%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. <!-- insert Javascript code here -->
# Reconstruct the original comment.
self.pieces.append('<!--%s-->' % text)
def handle_decl(self, text):
# called for the DOCTYPE, if present, e.g.
# <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
# "http://www.w3.org/TR/html4/loose.dtd">
# Reconstruct original DOCTYPE
self.pieces.append('<!%s>' % text)
def handle_pi(self, text):
# called for each processing instruction, e.g. <?instruction>
# Reconstruct original processing instruction.
self.pieces.append('<?%s>' % 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 = '''<div class="embedded-item {class_shorthand_lower} placeholder" id="{item_class}-{item_id}"><p class="title">Embedded Galaxy {class_shorthand} - '{item_name}'</p><p class="content">[Do not edit this block; Galaxy will fill it in with the annotated {class_shorthand} when it is displayed]</p></div>'''
# 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
)
+2 -10
View File
@@ -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")
+16 -46
View File
@@ -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
+7 -51
View File
@@ -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
+22 -352
View File
@@ -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 + '></' + tag + '>'
def feed(self, data):
data = re.compile(r'<!((?!DOCTYPE|--|\[))', re.IGNORECASE).sub(r'&lt;!\1', data)
data = re.sub(r'<([^<>\s]+?)\s*/>', self._shorttag_replace, data)
data = data.replace('&#39;', "'")
data = data.replace('&#34;', '"')
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 <pre class='screen'>,
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 '<class_name>-<encoded_item_id>'
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('>', '&gt;').replace('<', '&lt;').replace('"', '&quot;')
value = self.bare_ampersand.sub("&amp;", 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 </pre>, 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("</%s>" % tag)
def handle_charref(self, ref):
# called for each character reference, e.g. for '&#160;', 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 '&copy;', 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('&amp;%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. <!-- insert Javascript code here -->
# Reconstruct the original comment.
self.pieces.append('<!--%s-->' % text)
def handle_decl(self, text):
# called for the DOCTYPE, if present, e.g.
# <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
# "http://www.w3.org/TR/html4/loose.dtd">
# Reconstruct original DOCTYPE
self.pieces.append('<!%s>' % text)
def handle_pi(self, text):
# called for each processing instruction, e.g. <?instruction>
# Reconstruct original processing instruction.
self.pieces.append('<?%s>' % 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 = '''<div class="embedded-item {class_shorthand_lower} placeholder" id="{item_class}-{item_id}"><p class="title">Embedded Galaxy {class_shorthand} - '{item_name}'</p><p class="content">[Do not edit this block; Galaxy will fill it in with the annotated {class_shorthand} when it is displayed]</p></div>'''
# 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
)
+2 -2
View File
@@ -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:
+1 -6
View File
@@ -11,7 +11,6 @@
${self.render_title( item )}
</div>
<div class='summary-content'>
${self.render_summary_content( item, item_data )}
</div>
<div class='expanded-content'>
<div class='item-content'></div>
@@ -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 )
%>
<div style="float: left">
<a class="display_in_embed icon-button toggle-expand" item_id="${trans.security.encode_id( item.id )}" item_class="$item.__class__.__name__" href="${display_href}"
<a class="display_in_embed icon-button toggle-expand" item_id="${trans.security.encode_id( item.id )}" item_class="${item.__class__.__name__}" href="${display_href}"
title="Show ${item_display_name} content"></a>
<a class="toggle icon-button" href="${display_href}" title="Hide ${item_display_name} content"></a>
</div>
@@ -57,7 +56,3 @@
## Use a hidden var to store the ajax URL for getting an item's content.
<input type="hidden" name="ajax-item-content-url" value="${h.url_for( controller=item_controller, action='get_item_content_async', id=trans.security.encode_id( item.id ) )}"/>
</%def>
## Methods to override to render summary content.
<%def name="render_summary_content( item, item_data )">
</%def>
@@ -1,8 +1,4 @@
<%inherit file="/embed_base.mako"/>
<%!
from galaxy.web.framework.helpers import iff
%>
<%def name="render_item_links( dataset )">
<a href="${h.url_for( controller='/dataset', action='display', dataset_id=trans.security.encode_id( dataset.id ), to_ext=dataset.ext )}"
title="Save dataset" class="icon-button disk"></a>
@@ -11,12 +7,4 @@
title="Import dataset" class="icon-button import"></a>
<a href="${h.url_for( controller='/dataset', action='display_by_username_and_slug', username=dataset.history.user.username, slug=trans.security.encode_id( dataset.id ) )}"
title="Go to dataset" class="icon-button go-to-full-screen"></a>
</%def>
<%def name="render_summary_content( dataset, data )">
## <ul>
## <li>Format : ${dataset.extension}
## <pre>${dataset.peek}</pre>
## </ul>
</%def>
+1 -73
View File
@@ -7,80 +7,8 @@
<script type="text/javascript">
config.addInitialization(function() {
console.log("page/display.mako, javascript_app", "Setup embedded content");
// Setup embedded content:
// (a) toggles for showing/hiding embedded content;
// (b) ...
$('.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();
window.bundleEntries.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;
});
});
window.bundleEntries.render_embedded_items();
});
</script>
</%def>
+3 -34
View File
@@ -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();
});
</script>
</%def>
<%def name="stylesheets()">
${parent.stylesheets()}
${h.css( "base", "embed_item" )}
<style type='text/css'>
.galaxy-page-editor-button
{
position: relative;
float: left;
padding: 0.2em;
}
</style>
</%def>
<%def name="center_panel()">
<div class="unified-panel-header" unselectable="on">
<div class="unified-panel-header-inner">
Page Editor: ${page.title | h}
<a id="save-button" class="btn btn-secondary fa fa-save float-right"></a>
</div>
</div>
<div class="unified-panel-body">
<textarea name="page_content">${ content }</textarea>
</div>
<span id="page-editor-content" class="inbound" page_id="${trans.security.encode_id(page.id)}">
</span>
</%def>
@@ -1,4 +1 @@
<%inherit file="/embed_base.mako"/>
<%def name="render_summary_content( workflow, steps )">
</%def>
@@ -24,10 +24,6 @@
<div class="annotation">${item.annotation}</div>
%endif
</div>
##<div class='summary-content'>
## currently, no summary content for visualization
## could do the title or caption, whatever...
##</div>
<div class='expanded-content'>
<div class='item-content'>
</div>
@@ -1,24 +1,6 @@
<%inherit file="/embed_base.mako"/>
<%!
from galaxy.web.framework.helpers import iff
%>
<%def name="render_item_links( workflow )">
<a href="${h.url_for( controller='workflow', action='display_by_username_and_slug', username=workflow.user.username,
slug=item.slug, format='json-download' )}" title="Save Worflow" class="icon-button disk">
## FIXME: find and set appropriate icon for linking to workflow.
<!--
<a href="${h.url_for( controller='workflow', action='display_by_username_and_slug', username=workflow.user.username,
slug=item.slug, format='json' )}" title="Worflow Link for Import" class="icon-button TODO ">
-->
${parent.render_item_links( workflow )}
</%def>
<%def name="render_summary_content( workflow, steps )">
## <ul>
## <% num_steps = len ( steps ) %>
## <li>${num_steps} step${iff( num_steps != 1, "s", "" )}
## <li>Operations: ...
## </ul>
</%def>
+3 -3
View File
@@ -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)
+23 -5
View File
@@ -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"] = '''<p>Page!<div class="embedded-item" id="History-invaidencodedid"></div></p>'''
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"] = '''<p>Page!<div class="embedded-item" id="CoolObject-%s"></div></p>''' % 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")
+17
View File
@@ -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',
+19 -2
View File
@@ -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:
@@ -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='''<p>Page!<div class="embedded-item" id="History-%s"></div></p>''' % 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
+6 -4
View File
@@ -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
+9 -27
View File
@@ -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')
+63
View File
@@ -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()