mirror of
https://github.com/galaxyproject/galaxy.git
synced 2026-09-24 16:30:27 +08:00
Merge pull request #8047 from jmchilton/pages_api_and_component
Toward a more modern frontend and backend for Pages.
This commit is contained in:
@@ -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();
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -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;
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -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");
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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'<!\1', data)
|
||||
data = re.sub(r'<([^<>\s]+?)\s*/>', self._shorttag_replace, data)
|
||||
data = data.replace(''', "'")
|
||||
data = data.replace('"', '"')
|
||||
HTMLParser.feed(self, data)
|
||||
HTMLParser.close(self)
|
||||
|
||||
def handle_starttag(self, tag, attrs):
|
||||
"""
|
||||
Called for each start tag
|
||||
|
||||
attrs is a list of (attr, value) tuples, e.g. for <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('>', '>').replace('<', '<').replace('"', '"')
|
||||
value = self.bare_ampersand.sub("&", value)
|
||||
uattrs.append((key, value))
|
||||
strattrs = ''.join(' %s="%s"' % (k, v) for k, v in uattrs)
|
||||
if tag in self.elements_no_end_tag:
|
||||
self.pieces.append('<%s%s />' % (tag, strattrs))
|
||||
else:
|
||||
self.pieces.append('<%s%s>' % (tag, strattrs))
|
||||
|
||||
def handle_endtag(self, tag):
|
||||
"""
|
||||
Called for each end tag
|
||||
|
||||
E.g. for </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 ' ', ref will be '160'
|
||||
# Reconstruct the original character reference.
|
||||
ref = ref.lower()
|
||||
if ref.startswith('x'):
|
||||
value = int(ref[1:], 16)
|
||||
else:
|
||||
value = int(ref)
|
||||
|
||||
if value in _cp1252:
|
||||
self.pieces.append('&#%s;' % hex(ord(_cp1252[value]))[1:])
|
||||
else:
|
||||
self.pieces.append('&#%s;' % ref)
|
||||
|
||||
def handle_entityref(self, ref):
|
||||
# called for each entity reference, e.g. for '©', ref will be 'copy'
|
||||
# Reconstruct the original entity reference.
|
||||
if ref in name2codepoint or ref == 'apos':
|
||||
self.pieces.append('&%s;' % ref)
|
||||
else:
|
||||
self.pieces.append('&%s' % ref)
|
||||
|
||||
def handle_data(self, text):
|
||||
"""
|
||||
Called for each block of plain text
|
||||
|
||||
Called outside of any tag and not containing any character or entity
|
||||
references. Store the original text verbatim.
|
||||
"""
|
||||
if self.ignore_content:
|
||||
return
|
||||
self.pieces.append(text)
|
||||
|
||||
def handle_comment(self, text):
|
||||
# called for each HTML comment, e.g. <!-- 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,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")
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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'<!\1', data)
|
||||
data = re.sub(r'<([^<>\s]+?)\s*/>', self._shorttag_replace, data)
|
||||
data = data.replace(''', "'")
|
||||
data = data.replace('"', '"')
|
||||
HTMLParser.feed(self, data)
|
||||
HTMLParser.close(self)
|
||||
|
||||
def handle_starttag(self, tag, attrs):
|
||||
"""
|
||||
Called for each start tag
|
||||
|
||||
attrs is a list of (attr, value) tuples, e.g. for <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('>', '>').replace('<', '<').replace('"', '"')
|
||||
value = self.bare_ampersand.sub("&", value)
|
||||
uattrs.append((key, value))
|
||||
strattrs = ''.join(' %s="%s"' % (k, v) for k, v in uattrs)
|
||||
if tag in self.elements_no_end_tag:
|
||||
self.pieces.append('<%s%s />' % (tag, strattrs))
|
||||
else:
|
||||
self.pieces.append('<%s%s>' % (tag, strattrs))
|
||||
|
||||
def handle_endtag(self, tag):
|
||||
"""
|
||||
Called for each end tag
|
||||
|
||||
E.g. for </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 ' ', ref will be '160'
|
||||
# Reconstruct the original character reference.
|
||||
ref = ref.lower()
|
||||
if ref.startswith('x'):
|
||||
value = int(ref[1:], 16)
|
||||
else:
|
||||
value = int(ref)
|
||||
|
||||
if value in _cp1252:
|
||||
self.pieces.append('&#%s;' % hex(ord(_cp1252[value]))[1:])
|
||||
else:
|
||||
self.pieces.append('&#%s;' % ref)
|
||||
|
||||
def handle_entityref(self, ref):
|
||||
# called for each entity reference, e.g. for '©', ref will be 'copy'
|
||||
# Reconstruct the original entity reference.
|
||||
if ref in name2codepoint or ref == 'apos':
|
||||
self.pieces.append('&%s;' % ref)
|
||||
else:
|
||||
self.pieces.append('&%s' % ref)
|
||||
|
||||
def handle_data(self, text):
|
||||
"""
|
||||
Called for each block of plain text
|
||||
|
||||
Called outside of any tag and not containing any character or entity
|
||||
references. Store the original text verbatim.
|
||||
"""
|
||||
if self.ignore_content:
|
||||
return
|
||||
self.pieces.append(text)
|
||||
|
||||
def handle_comment(self, text):
|
||||
# called for each HTML comment, e.g. <!-- 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
|
||||
)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
@@ -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")
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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()
|
||||
Reference in New Issue
Block a user