From a7d0fc96289e6235e9d43802f3eb26da13b59f5f Mon Sep 17 00:00:00 2001 From: John Chilton Date: Sat, 25 May 2019 20:48:08 -0400 Subject: [PATCH 01/19] clean-fix-page-api - remove unused pages code, annotation param never set. --- client/galaxy/scripts/galaxy.pages.js | 2 -- lib/galaxy/webapps/galaxy/controllers/page.py | 33 +------------------ 2 files changed, 1 insertion(+), 34 deletions(-) diff --git a/client/galaxy/scripts/galaxy.pages.js b/client/galaxy/scripts/galaxy.pages.js index d9a1cf470fd..ecca493eb26 100644 --- a/client/galaxy/scripts/galaxy.pages.js +++ b/client/galaxy/scripts/galaxy.pages.js @@ -557,8 +557,6 @@ export default function pagesEditorOnload(renderedConfigs = {}) { data: { id: configs.page_id, content: editor.xhtml(), - annotations: JSON.stringify({}), - // annotations: JSON.stringify(annotations), _: "true" }, success: function() { diff --git a/lib/galaxy/webapps/galaxy/controllers/page.py b/lib/galaxy/webapps/galaxy/controllers/page.py index 78b00076b42..669bdb24f9f 100644 --- a/lib/galaxy/webapps/galaxy/controllers/page.py +++ b/lib/galaxy/webapps/galaxy/controllers/page.py @@ -685,7 +685,7 @@ 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 @@ -704,37 +704,6 @@ class PageController(BaseUIController, SharableMixin, 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() @web.expose From 31aa773066464cdcfd69a85b043b4d3229c7e9ae Mon Sep 17 00:00:00 2001 From: John Chilton Date: Sat, 25 May 2019 21:16:20 -0400 Subject: [PATCH 02/19] clean-fix-page-api - page_revisions reuse manager util for checking security and decoding ids --- .../webapps/galaxy/api/page_revisions.py | 26 ++++--------------- 1 file changed, 5 insertions(+), 21 deletions(-) diff --git a/lib/galaxy/webapps/galaxy/api/page_revisions.py b/lib/galaxy/webapps/galaxy/api/page_revisions.py index faf2be37035..9d2233f282b 100644 --- a/lib/galaxy/webapps/galaxy/api/page_revisions.py +++ b/lib/galaxy/webapps/galaxy/api/page_revisions.py @@ -5,6 +5,7 @@ import logging from galaxy import exceptions from galaxy.model.item_attrs import UsesAnnotations +from galaxy.managers.base import get_object from galaxy.util.sanitize_html import sanitize_html from galaxy.web import expose_api from galaxy.web.base.controller import ( @@ -30,10 +31,8 @@ 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)) @@ -54,13 +53,12 @@ class PageRevisionsController(BaseAPIController, SharableItemSecurityMixin, Uses :rtype: dictionary :returns: Dictionary with 'success' or 'error' element to indicate the result of the request """ + page = get_object(trans, page_id, 'Page', check_ownership=True) + 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: @@ -79,17 +77,3 @@ class PageRevisionsController(BaseAPIController, SharableItemSecurityMixin, Uses 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() From 86d21ab5695deb193a794bf1026bf9bfd40ea86c Mon Sep 17 00:00:00 2001 From: John Chilton Date: Sat, 25 May 2019 22:25:24 -0400 Subject: [PATCH 03/19] clean-fix-page-api - pages API reuse manager util for checking security and decoding ids --- lib/galaxy/webapps/galaxy/api/pages.py | 20 +++----------------- 1 file changed, 3 insertions(+), 17 deletions(-) diff --git a/lib/galaxy/webapps/galaxy/api/pages.py b/lib/galaxy/webapps/galaxy/api/pages.py index a5a8ac3d75e..de69766d5ef 100644 --- a/lib/galaxy/webapps/galaxy/api/pages.py +++ b/lib/galaxy/webapps/galaxy/api/pages.py @@ -4,6 +4,7 @@ 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 @@ -128,7 +129,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 +148,7 @@ 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 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 From d324ad430e24bc5e1fdae946a91698abcff6b0e4 Mon Sep 17 00:00:00 2001 From: John Chilton Date: Sat, 25 May 2019 17:39:49 -0400 Subject: [PATCH 04/19] clean-fix-page-api - refactor shared page creation code into manager. De-duplication between web and API controllers. --- lib/galaxy/managers/base.py | 7 +++ lib/galaxy/managers/pages.py | 47 ++++++++++++++- lib/galaxy/web/base/controller.py | 11 +--- .../webapps/galaxy/api/page_revisions.py | 5 ++ lib/galaxy/webapps/galaxy/api/pages.py | 37 +----------- lib/galaxy/webapps/galaxy/controllers/page.py | 57 ++++++------------- 6 files changed, 79 insertions(+), 85 deletions(-) diff --git a/lib/galaxy/managers/base.py b/lib/galaxy/managers/base.py index e06ca8de029..26b0fe2e01b 100644 --- a/lib/galaxy/managers/base.py +++ b/lib/galaxy/managers/base.py @@ -1128,3 +1128,10 @@ class ModelFilterParser(HasAModelManager): def raise_filter_err(self, attr, op, val, msg): raise exceptions.RequestParameterInvalidException(msg, column=attr, operation=op, val=val) + + +def is_valid_slug(slug): + """Returns true iff slug is valid.""" + + VALID_SLUG_RE = re.compile(r"^[a-z0-9\-]+$") + return VALID_SLUG_RE.match(slug) diff --git a/lib/galaxy/managers/pages.py b/lib/galaxy/managers/pages.py index 08f2c9f46fc..c8422cc638d 100644 --- a/lib/galaxy/managers/pages.py +++ b/lib/galaxy/managers/pages.py @@ -7,13 +7,16 @@ from within Galaxy. """ import logging -from galaxy import model +from galaxy import exceptions, model from galaxy.managers import sharable +from galaxy.managers.base import is_valid_slug +from galaxy.model.item_attrs import UsesAnnotations +from galaxy.util.sanitize_html import sanitize_html log = logging.getLogger(__name__) -class PageManager(sharable.SharableModelManager): +class PageManager(sharable.SharableModelManager, UsesAnnotations): """ """ @@ -35,6 +38,46 @@ 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 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 = sanitize_html(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_revision(self, trans, page_id, payload): + pass + class PageSerializer(sharable.SharableModelSerializer): """ diff --git a/lib/galaxy/web/base/controller.py b/lib/galaxy/web/base/controller.py index da39c15233e..98468a54e3e 100644 --- a/lib/galaxy/web/base/controller.py +++ b/lib/galaxy/web/base/controller.py @@ -54,13 +54,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 +1111,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 +1354,7 @@ class SharableMixin(object): def _is_valid_slug(self, slug): """ Returns true if slug is valid. """ - return _is_valid_slug(slug) + return managers_base.is_valid_slug(slug) @web.expose @web.require_login("modify Galaxy items") diff --git a/lib/galaxy/webapps/galaxy/api/page_revisions.py b/lib/galaxy/webapps/galaxy/api/page_revisions.py index 9d2233f282b..c3877dea5cb 100644 --- a/lib/galaxy/webapps/galaxy/api/page_revisions.py +++ b/lib/galaxy/webapps/galaxy/api/page_revisions.py @@ -6,6 +6,7 @@ import logging from galaxy import exceptions from galaxy.model.item_attrs import UsesAnnotations from galaxy.managers.base import get_object +from galaxy.managers.pages import PageManager from galaxy.util.sanitize_html import sanitize_html from galaxy.web import expose_api from galaxy.web.base.controller import ( @@ -19,6 +20,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): """ diff --git a/lib/galaxy/webapps/galaxy/api/pages.py b/lib/galaxy/webapps/galaxy/api/pages.py index de69766d5ef..29ddb522e96 100644 --- a/lib/galaxy/webapps/galaxy/api/pages.py +++ b/lib/galaxy/webapps/galaxy/api/pages.py @@ -10,7 +10,6 @@ from galaxy.managers.pages import ( 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, @@ -82,40 +81,8 @@ 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() - - rval = self.encode_all_ids(trans, page.to_dict(), True) - return rval + page = self.manager.create(trans, payload) + return self.encode_all_ids(trans, page.to_dict(), True) @expose_api def delete(self, trans, id, **kwd): diff --git a/lib/galaxy/webapps/galaxy/controllers/page.py b/lib/galaxy/webapps/galaxy/controllers/page.py index 669bdb24f9f..92f9143e99c 100644 --- a/lib/galaxy/webapps/galaxy/controllers/page.py +++ b/lib/galaxy/webapps/galaxy/controllers/page.py @@ -16,11 +16,15 @@ from sqlalchemy.orm import ( ) from galaxy import ( - managers, + exceptions, model, util, web ) +from galaxy.managers import base +from galaxy.managers.hdas import HDAManager +from galaxy.managers.histories import HistoryManager, HistorySerializer +from galaxy.managers.pages import PageManager from galaxy.model.item_attrs import UsesItemRatings from galaxy.util import unicodify from galaxy.util.sanitize_html import sanitize_html @@ -473,9 +477,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 +546,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") @@ -958,20 +937,20 @@ def _placeholderRenderForSave(trans, item_class, item_id, encode=False): 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) + 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 = managers.hdas.HDAManager(trans.app) + 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 = managers.base.security_check(trans, wf, False, True) + 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 = managers.base.security_check(trans, visualization, False, True) + visualization = base.security_check(trans, visualization, False, True) item_name = visualization.title class_shorthand = PAGE_CLASS_MAPPING[item_class] if encode: From ead233c0c1594d013f149eaf54773db7e29dcb8b Mon Sep 17 00:00:00 2001 From: John Chilton Date: Sat, 25 May 2019 21:32:38 -0400 Subject: [PATCH 05/19] clean-fix-page-api - move page revision creation to manager. --- lib/galaxy/managers/pages.py | 298 +++++++++++++++++- .../webapps/galaxy/api/page_revisions.py | 29 +- lib/galaxy/webapps/galaxy/controllers/page.py | 293 +---------------- scripts/pages_identifier_conversion.py | 4 +- 4 files changed, 308 insertions(+), 316 deletions(-) diff --git a/lib/galaxy/managers/pages.py b/lib/galaxy/managers/pages.py index c8422cc638d..b0b9c798f09 100644 --- a/lib/galaxy/managers/pages.py +++ b/lib/galaxy/managers/pages.py @@ -6,15 +6,51 @@ Pages are markup created and saved by users that can contain Galaxy objects from within Galaxy. """ import logging +import re + +from six.moves.html_entities import name2codepoint +from six.moves.html_parser import HTMLParser from galaxy import exceptions, model -from galaxy.managers import sharable -from galaxy.managers.base import is_valid_slug +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, UsesAnnotations): """ @@ -45,7 +81,7 @@ class PageManager(sharable.SharableModelManager, UsesAnnotations): raise exceptions.ObjectAttributeMissingException("Page name is required") elif not payload.get("slug", None): raise exceptions.ObjectAttributeMissingException("Page id is required") - elif not is_valid_slug(payload["slug"]): + 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") @@ -75,8 +111,33 @@ class PageManager(sharable.SharableModelManager, UsesAnnotations): session.flush() return page - def save_revision(self, trans, page_id, payload): - pass + 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 = 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') + + 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 class PageSerializer(sharable.SharableModelSerializer): @@ -116,3 +177,230 @@ class PageDeserializer(sharable.SharableModelDeserializer): self.deserializers.update({ }) self.deserializable_keyset.update(self.deserializers.keys()) + + +class PageContentProcessor(HTMLParser, object): + """ + Processes page content to produce HTML that is suitable for display. + For now, processor renders embedded objects. + """ + bare_ampersand = re.compile(r"&(?!#\d+;|#x[0-9a-fA-F]+;|\w+;)") + elements_no_end_tag = set([ + 'area', 'base', 'basefont', 'br', 'col', 'command', 'embed', 'frame', + 'hr', 'img', 'input', 'isindex', 'keygen', 'link', 'meta', 'param', + 'source', 'track', 'wbr' + ]) + + def __init__(self, trans, render_embed_html_fn): + HTMLParser.__init__(self) + self.trans = trans + self.ignore_content = False + self.num_open_tags_for_ignore = 0 + self.render_embed_html_fn = render_embed_html_fn + + def reset(self): + self.pieces = [] + HTMLParser.reset(self) + + def _shorttag_replace(self, match): + tag = match.group(1) + if tag in self.elements_no_end_tag: + return '<' + tag + ' />' + else: + return '<' + tag + '>' + + def feed(self, data): + data = re.compile(r'\s]+?)\s*/>', self._shorttag_replace, data) + data = data.replace(''', "'") + data = data.replace('"', '"') + HTMLParser.feed(self, data) + HTMLParser.close(self) + + def handle_starttag(self, tag, attrs): + """ + Called for each start tag + + attrs is a list of (attr, value) tuples, e.g. for
,
+        tag='pre', attrs=[('class', 'screen')]
+        """
+
+        # If ignoring content, just increment tag count and ignore.
+        if self.ignore_content:
+            self.num_open_tags_for_ignore += 1
+            return
+
+        # Not ignoring tag; look for embedded content.
+        embedded_item = False
+        for attribute in attrs:
+            if (attribute[0] == "class") and ("embedded-item" in attribute[1].split(" ")):
+                embedded_item = True
+                break
+        # For embedded content, set ignore flag to ignore current content and add new content for embedded item.
+        if embedded_item:
+            # Set processing attributes to ignore content.
+            self.ignore_content = True
+            self.num_open_tags_for_ignore = 1
+
+            # Insert content for embedded element.
+            for attribute in attrs:
+                name = attribute[0]
+                if name == "id":
+                    # ID has form '-'
+                    item_class, item_id = attribute[1].split("-")
+                    embed_html = self.render_embed_html_fn(self.trans, item_class, item_id)
+                    self.pieces.append(embed_html)
+            return
+
+        # Default behavior: not ignoring and no embedded content.
+        uattrs = []
+        strattrs = ''
+        if attrs:
+            for key, value in attrs:
+                value = value.replace('>', '>').replace('<', '<').replace('"', '"')
+                value = self.bare_ampersand.sub("&", value)
+                uattrs.append((key, value))
+            strattrs = ''.join(' %s="%s"' % (k, v) for k, v in uattrs)
+        if tag in self.elements_no_end_tag:
+            self.pieces.append('<%s%s />' % (tag, strattrs))
+        else:
+            self.pieces.append('<%s%s>' % (tag, strattrs))
+
+    def handle_endtag(self, tag):
+        """
+        Called for each end tag
+
+        E.g. for 
, tag will be 'pre' + """ + # If ignoring content, see if current tag is the end of content to ignore. + if self.ignore_content: + self.num_open_tags_for_ignore -= 1 + if self.num_open_tags_for_ignore == 0: + # Done ignoring content. + self.ignore_content = False + return + + # Default behavior: reconstruct the original end tag. + if tag not in self.elements_no_end_tag: + self.pieces.append("" % tag) + + def handle_charref(self, ref): + # called for each character reference, e.g. for ' ', ref will be '160' + # Reconstruct the original character reference. + ref = ref.lower() + if ref.startswith('x'): + value = int(ref[1:], 16) + else: + value = int(ref) + + if value in _cp1252: + self.pieces.append('&#%s;' % hex(ord(_cp1252[value]))[1:]) + else: + self.pieces.append('&#%s;' % ref) + + def handle_entityref(self, ref): + # called for each entity reference, e.g. for '©', ref will be 'copy' + # Reconstruct the original entity reference. + if ref in name2codepoint or ref == 'apos': + self.pieces.append('&%s;' % ref) + else: + self.pieces.append('&%s' % ref) + + def handle_data(self, text): + """ + Called for each block of plain text + + Called outside of any tag and not containing any character or entity + references. Store the original text verbatim. + """ + if self.ignore_content: + return + self.pieces.append(text) + + def handle_comment(self, text): + # called for each HTML comment, e.g. + # Reconstruct the original comment. + self.pieces.append('' % text) + + def handle_decl(self, text): + # called for the DOCTYPE, if present, e.g. + # + # Reconstruct original DOCTYPE + self.pieces.append('' % text) + + def handle_pi(self, text): + # called for each processing instruction, e.g. + # Reconstruct original processing instruction. + self.pieces.append('' % text) + + def output(self): + '''Return processed HTML as a single string''' + return ''.join(self.pieces) + + +PAGE_MAXRAW = 10**15 + + +def get_page_identifiers(item_id, app): + # Assume if item id is integer and less than 10**15, it's unencoded. + try: + decoded_id = int(item_id) + if decoded_id >= PAGE_MAXRAW: + raise ValueError("Identifier larger than maximum expected raw int, must be already encoded.") + encoded_id = app.security.encode_id(item_id) + except ValueError: + # It's an encoded id. + encoded_id = item_id + decoded_id = app.security.decode_id(item_id) + return (encoded_id, decoded_id) + + +# Utilities for encoding/decoding HTML content. +PLACEHOLDER_TEMPLATE = '''

Embedded Galaxy {class_shorthand} - '{item_name}'

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

''' + +# This is a mapping of the id portion of page contents to the cssclass/shortname. +PAGE_CLASS_MAPPING = { + 'History': 'History', + 'HistoryDatasetAssociation': 'Dataset', + 'StoredWorkflow': 'Workflow', + 'Visualization': 'Visualization' +} + + +def placeholderRenderForEdit(trans, item_class, item_id): + return placeholderRenderForSave(trans, item_class, item_id, encode=True) + + +def placeholderRenderForSave(trans, item_class, item_id, encode=False): + encoded_item_id, decoded_item_id = get_page_identifiers(item_id, trans.app) + item_name = '' + if item_class == 'History': + history = trans.sa_session.query(trans.model.History).get(decoded_item_id) + history = base.security_check(trans, history, False, True) + item_name = history.name + elif item_class == 'HistoryDatasetAssociation': + hda = trans.sa_session.query(trans.model.HistoryDatasetAssociation).get(decoded_item_id) + hda_manager = HDAManager(trans.app) + hda = hda_manager.get_accessible(decoded_item_id, trans.user) + item_name = hda.name + elif item_class == 'StoredWorkflow': + wf = trans.sa_session.query(trans.model.StoredWorkflow).get(decoded_item_id) + wf = base.security_check(trans, wf, False, True) + item_name = wf.name + elif item_class == 'Visualization': + visualization = trans.sa_session.query(trans.model.Visualization).get(decoded_item_id) + visualization = base.security_check(trans, visualization, False, True) + item_name = visualization.title + class_shorthand = PAGE_CLASS_MAPPING[item_class] + if encode: + item_id = encoded_item_id + else: + item_id = decoded_item_id + return PLACEHOLDER_TEMPLATE.format( + item_class=item_class, + class_shorthand=class_shorthand, + class_shorthand_lower=class_shorthand.lower(), + item_id=item_id, + item_name=item_name + ) diff --git a/lib/galaxy/webapps/galaxy/api/page_revisions.py b/lib/galaxy/webapps/galaxy/api/page_revisions.py index c3877dea5cb..5cee22441ca 100644 --- a/lib/galaxy/webapps/galaxy/api/page_revisions.py +++ b/lib/galaxy/webapps/galaxy/api/page_revisions.py @@ -3,11 +3,9 @@ API for updating Galaxy Pages """ import logging -from galaxy import exceptions -from galaxy.model.item_attrs import UsesAnnotations from galaxy.managers.base import get_object from galaxy.managers.pages import PageManager -from galaxy.util.sanitize_html import sanitize_html +from galaxy.model.item_attrs import UsesAnnotations from galaxy.web import expose_api from galaxy.web.base.controller import ( BaseAPIController, @@ -59,26 +57,5 @@ class PageRevisionsController(BaseAPIController, SharableItemSecurityMixin, Uses :returns: Dictionary with 'success' or 'error' element to indicate the result of the request """ page = get_object(trans, page_id, 'Page', check_ownership=True) - - 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 = 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") + page_revision = self.manager.save_new_revision(trans, page, payload) + return self.encode_all_ids(trans, page_revision.to_dict(view="element"), True) diff --git a/lib/galaxy/webapps/galaxy/controllers/page.py b/lib/galaxy/webapps/galaxy/controllers/page.py index 92f9143e99c..92de407430f 100644 --- a/lib/galaxy/webapps/galaxy/controllers/page.py +++ b/lib/galaxy/webapps/galaxy/controllers/page.py @@ -1,9 +1,5 @@ -import re -from json import loads from markupsafe import escape -from six.moves.html_entities import name2codepoint -from six.moves.html_parser import HTMLParser from sqlalchemy import ( and_, desc, @@ -21,10 +17,14 @@ from galaxy import ( util, web ) -from galaxy.managers import base from galaxy.managers.hdas import HDAManager from galaxy.managers.histories import HistoryManager, HistorySerializer -from galaxy.managers.pages import PageManager +from galaxy.managers.pages import ( + get_page_identifiers, + PageContentProcessor, + PageManager, + placeholderRenderForEdit, +) from galaxy.model.item_attrs import UsesItemRatings from galaxy.util import unicodify from galaxy.util.sanitize_html import sanitize_html @@ -44,55 +44,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" @@ -304,166 +255,6 @@ class VisualizationSelectionGrid(ItemSelectionGrid): # Adapted from the _BaseHTMLProcessor class of https://github.com/kurtmckee/feedparser -class _PageContentProcessor(HTMLParser, object): - """ - Processes page content to produce HTML that is suitable for display. - For now, processor renders embedded objects. - """ - bare_ampersand = re.compile(r"&(?!#\d+;|#x[0-9a-fA-F]+;|\w+;)") - elements_no_end_tag = set([ - 'area', 'base', 'basefont', 'br', 'col', 'command', 'embed', 'frame', - 'hr', 'img', 'input', 'isindex', 'keygen', 'link', 'meta', 'param', - 'source', 'track', 'wbr' - ]) - - def __init__(self, trans, render_embed_html_fn): - HTMLParser.__init__(self) - self.trans = trans - self.ignore_content = False - self.num_open_tags_for_ignore = 0 - self.render_embed_html_fn = render_embed_html_fn - - def reset(self): - self.pieces = [] - HTMLParser.reset(self) - - def _shorttag_replace(self, match): - tag = match.group(1) - if tag in self.elements_no_end_tag: - return '<' + tag + ' />' - else: - return '<' + tag + '>' - - def feed(self, data): - data = re.compile(r'\s]+?)\s*/>', self._shorttag_replace, data) - data = data.replace(''', "'") - data = data.replace('"', '"') - HTMLParser.feed(self, data) - HTMLParser.close(self) - - def handle_starttag(self, tag, attrs): - """ - Called for each start tag - - attrs is a list of (attr, value) tuples, e.g. for
,
-        tag='pre', attrs=[('class', 'screen')]
-        """
-
-        # If ignoring content, just increment tag count and ignore.
-        if self.ignore_content:
-            self.num_open_tags_for_ignore += 1
-            return
-
-        # Not ignoring tag; look for embedded content.
-        embedded_item = False
-        for attribute in attrs:
-            if (attribute[0] == "class") and ("embedded-item" in attribute[1].split(" ")):
-                embedded_item = True
-                break
-        # For embedded content, set ignore flag to ignore current content and add new content for embedded item.
-        if embedded_item:
-            # Set processing attributes to ignore content.
-            self.ignore_content = True
-            self.num_open_tags_for_ignore = 1
-
-            # Insert content for embedded element.
-            for attribute in attrs:
-                name = attribute[0]
-                if name == "id":
-                    # ID has form '-'
-                    item_class, item_id = attribute[1].split("-")
-                    embed_html = self.render_embed_html_fn(self.trans, item_class, item_id)
-                    self.pieces.append(embed_html)
-            return
-
-        # Default behavior: not ignoring and no embedded content.
-        uattrs = []
-        strattrs = ''
-        if attrs:
-            for key, value in attrs:
-                value = value.replace('>', '>').replace('<', '<').replace('"', '"')
-                value = self.bare_ampersand.sub("&", value)
-                uattrs.append((key, value))
-            strattrs = ''.join(' %s="%s"' % (k, v) for k, v in uattrs)
-        if tag in self.elements_no_end_tag:
-            self.pieces.append('<%s%s />' % (tag, strattrs))
-        else:
-            self.pieces.append('<%s%s>' % (tag, strattrs))
-
-    def handle_endtag(self, tag):
-        """
-        Called for each end tag
-
-        E.g. for 
, tag will be 'pre' - """ - # If ignoring content, see if current tag is the end of content to ignore. - if self.ignore_content: - self.num_open_tags_for_ignore -= 1 - if self.num_open_tags_for_ignore == 0: - # Done ignoring content. - self.ignore_content = False - return - - # Default behavior: reconstruct the original end tag. - if tag not in self.elements_no_end_tag: - self.pieces.append("" % tag) - - def handle_charref(self, ref): - # called for each character reference, e.g. for ' ', ref will be '160' - # Reconstruct the original character reference. - ref = ref.lower() - if ref.startswith('x'): - value = int(ref[1:], 16) - else: - value = int(ref) - - if value in _cp1252: - self.pieces.append('&#%s;' % hex(ord(_cp1252[value]))[1:]) - else: - self.pieces.append('&#%s;' % ref) - - def handle_entityref(self, ref): - # called for each entity reference, e.g. for '©', ref will be 'copy' - # Reconstruct the original entity reference. - if ref in name2codepoint or ref == 'apos': - self.pieces.append('&%s;' % ref) - else: - self.pieces.append('&%s' % ref) - - def handle_data(self, text): - """ - Called for each block of plain text - - Called outside of any tag and not containing any character or entity - references. Store the original text verbatim. - """ - if self.ignore_content: - return - self.pieces.append(text) - - def handle_comment(self, text): - # called for each HTML comment, e.g. - # Reconstruct the original comment. - self.pieces.append('' % text) - - def handle_decl(self, text): - # called for the DOCTYPE, if present, e.g. - # - # Reconstruct original DOCTYPE - self.pieces.append('' % text) - - def handle_pi(self, text): - # called for each processing instruction, e.g. - # Reconstruct original processing instruction. - self.pieces.append('' % text) - - def output(self): - '''Return processed HTML as a single string''' - return ''.join(self.pieces) - - class PageController(BaseUIController, SharableMixin, UsesStoredWorkflowMixin, UsesVisualizationMixin, UsesItemRatings): @@ -617,7 +408,7 @@ class PageController(BaseUIController, SharableMixin, page = trans.sa_session.query(model.Page).get(id) assert page.user == trans.user content = page.latest_revision.content - processor = _PageContentProcessor(trans, _placeholderRenderForEdit) + processor = PageContentProcessor(trans, placeholderRenderForEdit) processor.feed(content) content = unicodify(processor.output(), 'utf-8') return trans.fill_template("page/editor.mako", page=page, content=content) @@ -668,22 +459,7 @@ class PageController(BaseUIController, SharableMixin, 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 - - trans.sa_session.flush() + self.page_manager.save_new_revision(trans, page, {"content": content}) @web.expose @web.require_login() @@ -708,7 +484,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') @@ -890,7 +666,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) @@ -915,52 +691,3 @@ class PageController(BaseUIController, SharableMixin, elif item_class == model.Page: pass - - -PLACEHOLDER_TEMPLATE = '''

Embedded Galaxy {class_shorthand} - '{item_name}'

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

''' - -# This is a mapping of the id portion of page contents to the cssclass/shortname. -PAGE_CLASS_MAPPING = { - 'History': 'History', - 'HistoryDatasetAssociation': 'Dataset', - 'StoredWorkflow': 'Workflow', - 'Visualization': 'Visualization' -} - - -def _placeholderRenderForEdit(trans, item_class, item_id): - return _placeholderRenderForSave(trans, item_class, item_id, encode=True) - - -def _placeholderRenderForSave(trans, item_class, item_id, encode=False): - encoded_item_id, decoded_item_id = _get_page_identifiers(item_id, trans.app) - item_name = '' - if item_class == 'History': - history = trans.sa_session.query(trans.model.History).get(decoded_item_id) - history = base.security_check(trans, history, False, True) - item_name = history.name - elif item_class == 'HistoryDatasetAssociation': - hda = trans.sa_session.query(trans.model.HistoryDatasetAssociation).get(decoded_item_id) - hda_manager = HDAManager(trans.app) - hda = hda_manager.get_accessible(decoded_item_id, trans.user) - item_name = hda.name - elif item_class == 'StoredWorkflow': - wf = trans.sa_session.query(trans.model.StoredWorkflow).get(decoded_item_id) - wf = base.security_check(trans, wf, False, True) - item_name = wf.name - elif item_class == 'Visualization': - visualization = trans.sa_session.query(trans.model.Visualization).get(decoded_item_id) - visualization = base.security_check(trans, visualization, False, True) - item_name = visualization.title - class_shorthand = PAGE_CLASS_MAPPING[item_class] - if encode: - item_id = encoded_item_id - else: - item_id = decoded_item_id - return PLACEHOLDER_TEMPLATE.format( - item_class=item_class, - class_shorthand=class_shorthand, - class_shorthand_lower=class_shorthand.lower(), - item_id=item_id, - item_name=item_name - ) diff --git a/scripts/pages_identifier_conversion.py b/scripts/pages_identifier_conversion.py index ce914938f21..b9cec401071 100644 --- a/scripts/pages_identifier_conversion.py +++ b/scripts/pages_identifier_conversion.py @@ -9,12 +9,12 @@ sys.path.insert(1, os.path.abspath(os.path.join(os.path.dirname(__file__), os.pa import galaxy import galaxy.app import galaxy.config +from galaxy.managers.pages import PageContentProcessor, placeholderRenderForSave from galaxy.objectstore import build_object_store_from_config from galaxy.security.idencoding import IdEncodingHelper from galaxy.util import unicodify from galaxy.util.bunch import Bunch from galaxy.util.script import app_properties_from_args, populate_config_args -from galaxy.webapps.galaxy.controllers.page import _PageContentProcessor, _placeholderRenderForSave def main(argv): @@ -37,7 +37,7 @@ def main(argv): mock_trans = Bunch(app=Bunch(security=security_helper), model=model, user_is_admin=lambda: True, sa_session=session) for p in pagerevs: try: - processor = _PageContentProcessor(mock_trans, _placeholderRenderForSave) + processor = PageContentProcessor(mock_trans, placeholderRenderForSave) processor.feed(p.content) newcontent = unicodify(processor.output(), 'utf-8') if p.content != newcontent: From ff00b800d34a06983106312cbd6673faf10c4cf9 Mon Sep 17 00:00:00 2001 From: John Chilton Date: Sat, 25 May 2019 22:34:07 -0400 Subject: [PATCH 06/19] clean-fix-page-api - apply placeholderRenderForEdit to API. Was applied only in the controller previously. --- lib/galaxy/managers/pages.py | 8 ++++++++ lib/galaxy/webapps/galaxy/api/page_revisions.py | 8 ++++++-- lib/galaxy/webapps/galaxy/api/pages.py | 6 +++++- lib/galaxy/webapps/galaxy/controllers/page.py | 5 +---- 4 files changed, 20 insertions(+), 7 deletions(-) diff --git a/lib/galaxy/managers/pages.py b/lib/galaxy/managers/pages.py index b0b9c798f09..e940b572b32 100644 --- a/lib/galaxy/managers/pages.py +++ b/lib/galaxy/managers/pages.py @@ -139,6 +139,14 @@ class PageManager(sharable.SharableModelManager, UsesAnnotations): session.flush() return page_revision + 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): """ diff --git a/lib/galaxy/webapps/galaxy/api/page_revisions.py b/lib/galaxy/webapps/galaxy/api/page_revisions.py index 5cee22441ca..f240f6e9155 100644 --- a/lib/galaxy/webapps/galaxy/api/page_revisions.py +++ b/lib/galaxy/webapps/galaxy/api/page_revisions.py @@ -38,7 +38,9 @@ class PageRevisionsController(BaseAPIController, SharableItemSecurityMixin, Uses 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 @@ -58,4 +60,6 @@ class PageRevisionsController(BaseAPIController, SharableItemSecurityMixin, Uses """ page = get_object(trans, page_id, 'Page', check_ownership=True) page_revision = self.manager.save_new_revision(trans, page, payload) - return self.encode_all_ids(trans, page_revision.to_dict(view="element"), True) + rval = self.encode_all_ids(trans, page_revision.to_dict(view="element"), True) + self.manager.rewrite_content_for_export(trans, rval) + return rval diff --git a/lib/galaxy/webapps/galaxy/api/pages.py b/lib/galaxy/webapps/galaxy/api/pages.py index 29ddb522e96..7f04c8a7204 100644 --- a/lib/galaxy/webapps/galaxy/api/pages.py +++ b/lib/galaxy/webapps/galaxy/api/pages.py @@ -82,7 +82,10 @@ class PagesController(BaseAPIController, SharableItemSecurityMixin, UsesAnnotati :returns: Dictionary return of the Page.to_dict call """ page = self.manager.create(trans, payload) - return self.encode_all_ids(trans, page.to_dict(), 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 @expose_api def delete(self, trans, id, **kwd): @@ -118,4 +121,5 @@ class PagesController(BaseAPIController, SharableItemSecurityMixin, UsesAnnotati page = get_object(trans, id, 'Page', check_ownership=False, check_accessible=True) rval = self.encode_all_ids(trans, page.to_dict(), True) rval['content'] = page.latest_revision.content + self.manager.rewrite_content_for_export(trans, rval) return rval diff --git a/lib/galaxy/webapps/galaxy/controllers/page.py b/lib/galaxy/webapps/galaxy/controllers/page.py index 92de407430f..af842df2a36 100644 --- a/lib/galaxy/webapps/galaxy/controllers/page.py +++ b/lib/galaxy/webapps/galaxy/controllers/page.py @@ -23,7 +23,6 @@ from galaxy.managers.pages import ( get_page_identifiers, PageContentProcessor, PageManager, - placeholderRenderForEdit, ) from galaxy.model.item_attrs import UsesItemRatings from galaxy.util import unicodify @@ -408,9 +407,7 @@ class PageController(BaseUIController, SharableMixin, 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') + content = self.page_manager.rewrite_content_for_export(trans, {"content": content})["content"] return trans.fill_template("page/editor.mako", page=page, content=content) @web.expose From 5936478d20914b9b6b8bc571a3ee87cdafd12cc8 Mon Sep 17 00:00:00 2001 From: John Chilton Date: Sat, 25 May 2019 22:50:20 -0400 Subject: [PATCH 07/19] clean-fix-page-api - apply placeholderRenderForSave to initial page save as well. --- lib/galaxy/managers/pages.py | 16 ++++++++++------ lib/galaxy/web/base/controller.py | 1 - 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/lib/galaxy/managers/pages.py b/lib/galaxy/managers/pages.py index e940b572b32..4046b53e990 100644 --- a/lib/galaxy/managers/pages.py +++ b/lib/galaxy/managers/pages.py @@ -87,7 +87,7 @@ class PageManager(sharable.SharableModelManager, UsesAnnotations): raise exceptions.DuplicatedSlugException("Page identifier must be unique") content = payload.get("content", "") - content = sanitize_html(content) + content = self.rewrite_content_for_import(trans, content) # Create the new stored page page = trans.app.model.Page() @@ -122,11 +122,7 @@ class PageManager(sharable.SharableModelManager, UsesAnnotations): else: title = page.title - 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') + content = self.rewrite_content_for_import(trans, content) page_revision = trans.app.model.PageRevision() page_revision.title = title @@ -139,6 +135,14 @@ class PageManager(sharable.SharableModelManager, UsesAnnotations): session.flush() return page_revision + def rewrite_content_for_import(self, trans, 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') + return content + def rewrite_content_for_export(self, trans, as_dict): content = as_dict["content"] processor = PageContentProcessor(trans, placeholderRenderForEdit) diff --git a/lib/galaxy/web/base/controller.py b/lib/galaxy/web/base/controller.py index 98468a54e3e..49e209c8f5e 100644 --- a/lib/galaxy/web/base/controller.py +++ b/lib/galaxy/web/base/controller.py @@ -2,7 +2,6 @@ Contains functionality needed in every web interface """ import logging -import re from six import string_types from sqlalchemy import true From 98a4a8bd005236050623f86545c731ace1936798 Mon Sep 17 00:00:00 2001 From: John Chilton Date: Sun, 26 May 2019 13:48:26 -0400 Subject: [PATCH 08/19] clean-fix-page-api - modern exception handling for embedded ID encoding --- lib/galaxy/managers/pages.py | 17 ++++++++++------ lib/galaxy/webapps/galaxy/api/pages.py | 1 - test/api/test_page_revisions.py | 6 +++--- test/api/test_pages.py | 28 +++++++++++++++++++++----- 4 files changed, 37 insertions(+), 15 deletions(-) diff --git a/lib/galaxy/managers/pages.py b/lib/galaxy/managers/pages.py index 4046b53e990..8d2053acce3 100644 --- a/lib/galaxy/managers/pages.py +++ b/lib/galaxy/managers/pages.py @@ -136,11 +136,16 @@ class PageManager(sharable.SharableModelManager, UsesAnnotations): return page_revision def rewrite_content_for_import(self, trans, 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') + 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): @@ -364,7 +369,7 @@ def get_page_identifiers(item_id, app): except ValueError: # It's an encoded id. encoded_id = item_id - decoded_id = app.security.decode_id(item_id) + decoded_id = base.decode_id(app, item_id) return (encoded_id, decoded_id) diff --git a/lib/galaxy/webapps/galaxy/api/pages.py b/lib/galaxy/webapps/galaxy/api/pages.py index 7f04c8a7204..c86090a29f0 100644 --- a/lib/galaxy/webapps/galaxy/api/pages.py +++ b/lib/galaxy/webapps/galaxy/api/pages.py @@ -3,7 +3,6 @@ API for updating Galaxy Pages """ import logging -from galaxy import exceptions from galaxy.managers.base import get_object from galaxy.managers.pages import ( PageManager, diff --git a/test/api/test_page_revisions.py b/test/api/test_page_revisions.py index 33d29d74fe6..fb89c90d680 100644 --- a/test/api/test_page_revisions.py +++ b/test/api/test_page_revisions.py @@ -28,8 +28,8 @@ class PageRevisionsApiTestCase(BasePageApiTestCase): revisions_json = revisions_response.json() assert len(revisions_json) == 2 # Original revision and new one - def test_404_if_index_unknown_page(self): + def test_malformed_id_if_index_unknown_page(self): revisions_url = "pages/%s/revisions" % self._random_key() revisions_response = self._get(revisions_url) - self._assert_status_code_is(revisions_response, 404) - self._assert_error_code_is(revisions_response, error_codes.USER_OBJECT_NOT_FOUND) + self._assert_status_code_is(revisions_response, 400) + self._assert_error_code_is(revisions_response, error_codes.MALFORMED_ID) diff --git a/test/api/test_pages.py b/test/api/test_pages.py index 6cabf05526a..4126fa0a46e 100644 --- a/test/api/test_pages.py +++ b/test/api/test_pages.py @@ -1,6 +1,7 @@ from requests import delete from base import api # noqa: I100,I202 +from base.populators import DatasetPopulator from galaxy.exceptions import error_codes # noqa: I201 @@ -53,7 +54,7 @@ class PageApiTestCase(BasePageApiTestCase): self._assert_error_code_is(page_response_2, error_codes.USER_SLUG_DUPLICATE) def test_page_requires_name(self): - page_request = self._test_page_payload() + page_request = self._test_page_payload(slug="requires-name") del page_request['title'] page_response = self._post("pages", page_request) self._assert_status_code_is(page_response, 400) @@ -70,10 +71,10 @@ class PageApiTestCase(BasePageApiTestCase): delete_response = delete(self._api_url("pages/%s" % response_json['id'], use_key=True)) self._assert_status_code_is(delete_response, 200) - def test_404_on_delete_unknown_page(self): + def test_400_on_delete_invalid_page_id(self): delete_response = delete(self._api_url("pages/%s" % self._random_key(), use_key=True)) - self._assert_status_code_is(delete_response, 404) - self._assert_error_code_is(delete_response, error_codes.USER_OBJECT_NOT_FOUND) + self._assert_status_code_is(delete_response, 400) + self._assert_error_code_is(delete_response, error_codes.MALFORMED_ID) def test_403_on_delete_unowned_page(self): page_response = self._create_valid_page_as("others_page@bx.psu.edu", "otherspage") @@ -81,6 +82,23 @@ class PageApiTestCase(BasePageApiTestCase): self._assert_status_code_is(delete_response, 403) self._assert_error_code_is(delete_response, error_codes.USER_DOES_NOT_OWN_ITEM) + def test_400_on_invalid_id_encoding(self): + page_request = self._test_page_payload(slug="invalid-id-encding") + page_request["content"] = '''

Page!

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

Page!

''' % valid_id + page_response = self._post("pages", page_request) + self._assert_status_code_is(page_response, 400) + self._assert_error_code_is(page_response, error_codes.USER_REQUEST_INVALID_PARAMETER) + assert "embedded HTML content" in page_response.content + def test_show(self): response_json = self._create_valid_page_with_slug("pagetoshow") show_response = self._get("pages/%s" % response_json['id']) @@ -95,7 +113,7 @@ class PageApiTestCase(BasePageApiTestCase): response_json = self._create_valid_page_as("others_page_show@bx.psu.edu", "otherspageshow") show_response = self._get("pages/%s" % response_json['id']) self._assert_status_code_is(show_response, 403) - self._assert_error_code_is(show_response, error_codes.USER_DOES_NOT_OWN_ITEM) + self._assert_error_code_is(show_response, error_codes.USER_CANNOT_ACCESS_ITEM) def _users_index_has_page_with_id(self, id): index_response = self._get("pages") From 2e3165cb7fcce6f81cea2c4f81737f1625d1db28 Mon Sep 17 00:00:00 2001 From: John Chilton Date: Sun, 26 May 2019 14:34:56 -0400 Subject: [PATCH 09/19] clean-fix-page-api - integration API test case ensure DB contains unencoded IDs --- .../test_page_revision_json_encoding.py | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 test/integration/test_page_revision_json_encoding.py diff --git a/test/integration/test_page_revision_json_encoding.py b/test/integration/test_page_revision_json_encoding.py new file mode 100644 index 00000000000..f509c9b8c10 --- /dev/null +++ b/test/integration/test_page_revision_json_encoding.py @@ -0,0 +1,40 @@ +"""Test pages save JSON with unencoded IDs. + +Verifies the database doesn't get saved with encoded IDs (that would be bad because +the security parameter to encode IDs may be changed by admins). Test case also verifies +exported API values are encoded though. +""" + +from base import api_asserts +from base import integration_util +from base.populators import ( + DatasetPopulator, +) +from galaxy import model # noqa: I101,I201 + + +class PageJsonEncodingIntegrationTestCase(integration_util.IntegrationTestCase): + + def setUp(self): + super(PageJsonEncodingIntegrationTestCase, self).setUp() + self.dataset_populator = DatasetPopulator(self.galaxy_interactor) + + def test_page_encoding(self): + history_id = self.dataset_populator.new_history() + request = dict( + slug="mypage", + title="MY PAGE", + content='''

Page!

''' % history_id, + ) + page_response = self._post("pages", request) + api_asserts.assert_status_code_is_ok(page_response) + sa_session = self._app.model.context + page_revision = sa_session.query(model.PageRevision).all()[0] + assert '''id="History-1"''' in page_revision.content, page_revision.content + assert '''id="History-%s"''' % history_id not in page_revision.content, page_revision.content + + show_page_response = self._get("pages/%s" % page_response.json()["id"]) + api_asserts.assert_status_code_is_ok(show_page_response) + content = show_page_response.json()["content"] + assert '''id="History-1"''' not in content, content + assert '''id="History-%s"''' % history_id in content, content From 7a544aa31b9fe3cc7a7a6b163fdb3da3110339d9 Mon Sep 17 00:00:00 2001 From: John Chilton Date: Fri, 24 May 2019 19:47:18 -0400 Subject: [PATCH 10/19] Page-editor-as-component - initial Selenium test for pages + embedded objects. --- client/galaxy/scripts/galaxy.pages.js | 1 + client/galaxy/scripts/mvc/ui/ui-modal.js | 3 ++ test/galaxy_selenium/navigates_galaxy.py | 17 ++++++ test/galaxy_selenium/navigation.yml | 21 +++++++- test/selenium_tests/framework.py | 10 ++-- test/selenium_tests/test_histories_list.py | 36 ++++--------- test/selenium_tests/test_pages.py | 63 ++++++++++++++++++++++ 7 files changed, 118 insertions(+), 33 deletions(-) create mode 100644 test/selenium_tests/test_pages.py diff --git a/client/galaxy/scripts/galaxy.pages.js b/client/galaxy/scripts/galaxy.pages.js index ecca493eb26..5bd284844d4 100644 --- a/client/galaxy/scripts/galaxy.pages.js +++ b/client/galaxy/scripts/galaxy.pages.js @@ -433,6 +433,7 @@ WYMeditor.editor.prototype.dialog = function(dialogType, dialogFeatures, bodyHtm ) ), closing_events: true, + extra_class: "pages-embed", buttons: { Embed: function() { // Make selected items accessible (importable) ? diff --git a/client/galaxy/scripts/mvc/ui/ui-modal.js b/client/galaxy/scripts/mvc/ui/ui-modal.js index 09c44b8dfc2..25dc0aa7679 100644 --- a/client/galaxy/scripts/mvc/ui/ui-modal.js +++ b/client/galaxy/scripts/mvc/ui/ui-modal.js @@ -81,6 +81,9 @@ export var View = Backbone.View.extend({ // link elements this.$header = this.$(".modal-header"); this.$dialog = this.$(".modal-dialog"); + if (this.options.extra_class) { + this.$dialog.addClass(this.options.extra_class); + } this.$body = this.$(".modal-body"); this.$footer = this.$(".modal-footer"); this.$backdrop = this.$(".modal-backdrop"); diff --git a/test/galaxy_selenium/navigates_galaxy.py b/test/galaxy_selenium/navigates_galaxy.py index 7b9359ae4f7..a38b2a4fd80 100644 --- a/test/galaxy_selenium/navigates_galaxy.py +++ b/test/galaxy_selenium/navigates_galaxy.py @@ -331,6 +331,23 @@ class NavigatesGalaxy(HasDriver): raise self.prepend_timeout_message(e, message) return history_item_selector_state + def click_grid_popup_option(self, item_name, option_label): + item_button = None + grid = self.components.grids.body.wait_for_visible() + for row in grid.find_elements_by_tag_name('tr'): + name_cell = row.find_elements_by_tag_name('td')[1] + if name_cell.text == item_name: + item_button = name_cell + break + + if item_button is None: + raise AssertionError('Failed to find item with name [%s]' % item_name) + + popup_menu_button = item_button.find_element_by_css_selector('.dropdown-toggle') + popup_menu_button.click() + popup_option = self.driver.find_element_by_link_text(option_label) + popup_option.click() + def published_grid_search_for(self, search_term=None): return self._inline_search_for( '#input-free-text-search-filter', diff --git a/test/galaxy_selenium/navigation.yml b/test/galaxy_selenium/navigation.yml index 96b2fdd7e62..eae430b7eed 100644 --- a/test/galaxy_selenium/navigation.yml +++ b/test/galaxy_selenium/navigation.yml @@ -60,6 +60,7 @@ masthead: logout: 'Logout' custom_builds: 'Custom Builds' histories: 'Histories' + pages: 'Pages' # Shared data libraries: 'Data Libraries' @@ -201,6 +202,21 @@ histories: labels: unshare: 'Unshare' +pages: + selectors: + create: '.manage-table-actions .action-button' + submit: '#submit' + editor: + selectors: + wym_iframe: 'div.wym_iframe iframe' + wym_iframe_content: '.text-content' + save: '#save-button' + embed_button: '#embed-galaxy-object' + dataset_selector: '.saved-datasets' + embed_dialog_add_button: '.pages-embed .buttons #button-0' + + labels: + embed_dataset: 'Embed Dataset' login: selectors: @@ -365,9 +381,10 @@ libraries: table: '.dataset_table' table_rows: '.dataset_table table tbody tr' -published_grids: +grids: selectors: - search: '#input-free-text-search-filter' + body: '#grid-table-body' + free_text_search: '#input-free-text-search-filter' gies: jupyter: diff --git a/test/selenium_tests/framework.py b/test/selenium_tests/framework.py index 385e7469132..853c2a04ad1 100644 --- a/test/selenium_tests/framework.py +++ b/test/selenium_tests/framework.py @@ -92,10 +92,12 @@ def managed_history(f): f(self, *args, **kwds) finally: if "GALAXY_TEST_NO_CLEANUP" not in os.environ: - current_history_id = self.current_history_id() - self.dataset_populator.cancel_history_jobs(current_history_id) - self.api_delete("histories/%s" % current_history_id) - + try: + current_history_id = self.current_history_id() + self.dataset_populator.cancel_history_jobs(current_history_id) + self.api_delete("histories/%s" % current_history_id) + except Exception: + print("Faild to cleanup managed history, selenium connection corrupted somehow?") return func_wrapper diff --git a/test/selenium_tests/test_histories_list.py b/test/selenium_tests/test_histories_list.py index c250f4f1877..8f200f2640b 100644 --- a/test/selenium_tests/test_histories_list.py +++ b/test/selenium_tests/test_histories_list.py @@ -19,7 +19,7 @@ class SavedHistoriesTestCase(SharedStateSeleniumTestCase): self._login() self.navigate_to_histories_page() self.screenshot("histories_saved_grid") - self.click_popup_option(self.history2_name, 'Switch') + self.click_grid_popup_option(self.history2_name, 'Switch') self.sleep_for(self.wait_types.UX_RENDER) @retry_assertion_during_transitions @@ -32,7 +32,7 @@ class SavedHistoriesTestCase(SharedStateSeleniumTestCase): def test_history_view(self): self._login() self.navigate_to_histories_page() - self.click_popup_option(self.history2_name, 'View') + self.click_grid_popup_option(self.history2_name, 'View') history_name = self.wait_for_selector('.name.editable-text') self.assertEqual(history_name.text, self.history2_name) @@ -42,7 +42,7 @@ class SavedHistoriesTestCase(SharedStateSeleniumTestCase): self.navigate_to_histories_page() # Publish the history - self.click_popup_option(self.history2_name, 'Share or Publish') + self.click_grid_popup_option(self.history2_name, 'Share or Publish') self.components.histories.sharing.make_accessible_and_publish.wait_for_and_click() self.navigate_to_histories_page() @@ -58,7 +58,7 @@ class SavedHistoriesTestCase(SharedStateSeleniumTestCase): self._login() self.navigate_to_histories_page() - self.click_popup_option('Unnamed history', 'Rename') + self.click_grid_popup_option('Unnamed history', 'Rename') # Rename the history history_name_input = self.wait_for_selector('.ui-form-element input.ui-input') @@ -77,7 +77,7 @@ class SavedHistoriesTestCase(SharedStateSeleniumTestCase): self.navigate_to_histories_page() # Delete the history - self.click_popup_option(self.history2_name, 'Delete') + self.click_grid_popup_option(self.history2_name, 'Delete') self.assert_histories_in_grid([self.history2_name], False) @@ -86,7 +86,7 @@ class SavedHistoriesTestCase(SharedStateSeleniumTestCase): self.sleep_for(self.wait_types.UX_RENDER) # Restore the history - self.click_popup_option(self.history2_name, 'Undelete') + self.click_grid_popup_option(self.history2_name, 'Undelete') self.assert_grid_histories_are([]) self.select_filter('deleted', 'False') @@ -101,7 +101,7 @@ class SavedHistoriesTestCase(SharedStateSeleniumTestCase): self.navigate_to_histories_page() self.assert_histories_in_grid([self.history4_name]) - self.click_popup_option(self.history4_name, 'Delete Permanently') + self.click_grid_popup_option(self.history4_name, 'Delete Permanently') alert = self.driver.switch_to.alert alert.accept() @@ -164,15 +164,14 @@ class SavedHistoriesTestCase(SharedStateSeleniumTestCase): self._login() self.navigate_to_histories_page() - input_selector = '#input-free-text-search-filter' - search_input = self.wait_for_selector(input_selector) + search_input = self.components.grids.free_text_search.wait_for_visible() search_input.send_keys(self.history2_name) self.send_enter(search_input) self.assert_grid_histories_are([self.history2_name]) self.unset_filter('free-text-search', self.history2_name) - search_input = self.wait_for_selector(input_selector) + search_input = self.components.grids.free_text_search.wait_for_visible() search_input.send_keys(self.history4_name) self.send_enter(search_input) @@ -299,23 +298,6 @@ class SavedHistoriesTestCase(SharedStateSeleniumTestCase): (filter_key, filter_value) self.wait_for_and_click_selector(filter_selector) - def click_popup_option(self, history_name, option_label): - history_menu_button = None - grid = self.wait_for_selector('#grid-table-body') - for row in grid.find_elements_by_tag_name('tr'): - name_cell = row.find_elements_by_tag_name('td')[1] - if name_cell.text == history_name: - history_menu_button = name_cell - break - - if history_menu_button is None: - raise AssertionError('Failed to find history with name [%s]' % history_name) - - popup_menu_button = history_menu_button.find_element_by_css_selector('.dropdown-toggle') - popup_menu_button.click() - popup_option = self.driver.find_element_by_link_text(option_label) - popup_option.click() - def get_history_tags_cell(self, history_name): tags_cell = None grid = self.wait_for_selector('#grid-table-body') diff --git a/test/selenium_tests/test_pages.py b/test/selenium_tests/test_pages.py new file mode 100644 index 00000000000..922ed4ce351 --- /dev/null +++ b/test/selenium_tests/test_pages.py @@ -0,0 +1,63 @@ +from .framework import ( + managed_history, + selenium_test, + SeleniumTestCase, +) + + +class PagesTestCase(SeleniumTestCase): + + ensure_registered = True + + @selenium_test + @managed_history + def test_simple_page_creation_edit_and_view(self): + # Upload a file to test embedded object stuff + test_path = self.get_filename("1.fasta") + self.perform_upload(test_path) + self.history_panel_wait_for_hid_ok(1) + self.navigate_to_pages() + self.screenshot("pages_grid") + + self.components.pages.create.wait_for_and_click() + name = self._get_random_name(prefix="page") + slug = self._get_random_name(prefix="pageslug") + self.tool_set_value("title", name) + self.tool_set_value("slug", slug) + self.screenshot("pages_create_form") + + # Sometimes 'submit' button not yet hooked up? + self.sleep_for(self.wait_types.UX_RENDER) + + self.components.pages.submit.wait_for_and_click() + + self.click_grid_popup_option(name, "Edit content") + self.components.pages.editor.wym_iframe.wait_for_visible() + self.screenshot("pages_editor_new") + self.driver.switch_to.frame(0) + try: + self.components.pages.editor.wym_iframe_content.wait_for_and_send_keys("moo\n\n\ncow\n\n") + finally: + self.driver.switch_to.default_content() + + self.components.pages.editor.embed_button.wait_for_and_click() + self.screenshot("pages_editor_embed_menu") + self.components.pages.editor.embed_dataset.wait_for_and_click() + saved_datasets_element = self.components.pages.editor.dataset_selector.wait_for_and_click() + self.screenshot("pages_editor_embed_dataset_dialog") + checkboxes = saved_datasets_element.find_elements_by_css_selector("input[type='checkbox']") + assert len(checkboxes) > 0 + checkboxes[0].click() + self.components.pages.editor.embed_dialog_add_button.wait_for_and_click() + + self.sleep_for(self.wait_types.UX_RENDER) + self.components.pages.editor.save.wait_for_and_click() + self.screenshot("pages_editor_saved") + self.home() + self.navigate_to_pages() + self.click_grid_popup_option(name, "View") + self.screenshot("pages_view_simple") + + def navigate_to_pages(self): + self.click_masthead_user() # Open masthead menu + self.components.masthead.pages.wait_for_and_click() From a71e77c40d0c94e0ab1bde9362850500b2040ece Mon Sep 17 00:00:00 2001 From: John Chilton Date: Tue, 14 May 2019 20:08:32 -0400 Subject: [PATCH 11/19] Page-editor-as-component - cleanup unused embed mako stuff. --- templates/embed_base.mako | 5 ----- templates/webapps/galaxy/dataset/embed.mako | 12 ------------ .../webapps/galaxy/visualization/embed.mako | 3 --- .../galaxy/visualization/embed_in_frame.mako | 4 ---- templates/webapps/galaxy/workflow/embed.mako | 18 ------------------ 5 files changed, 42 deletions(-) diff --git a/templates/embed_base.mako b/templates/embed_base.mako index 1e7c74b2622..a933919f76b 100644 --- a/templates/embed_base.mako +++ b/templates/embed_base.mako @@ -11,7 +11,6 @@ ${self.render_title( item )}
- ${self.render_summary_content( item, item_data )}
@@ -57,7 +56,3 @@ ## Use a hidden var to store the ajax URL for getting an item's content. - -## Methods to override to render summary content. -<%def name="render_summary_content( item, item_data )"> - diff --git a/templates/webapps/galaxy/dataset/embed.mako b/templates/webapps/galaxy/dataset/embed.mako index b31c04e2f27..74948f29527 100644 --- a/templates/webapps/galaxy/dataset/embed.mako +++ b/templates/webapps/galaxy/dataset/embed.mako @@ -1,8 +1,4 @@ <%inherit file="/embed_base.mako"/> -<%! - from galaxy.web.framework.helpers import iff -%> - <%def name="render_item_links( dataset )"> @@ -11,12 +7,4 @@ title="Import dataset" class="icon-button import"> - - - -<%def name="render_summary_content( dataset, data )"> -##
    -##
  • Format : ${dataset.extension} -##
    ${dataset.peek}
    -##
diff --git a/templates/webapps/galaxy/visualization/embed.mako b/templates/webapps/galaxy/visualization/embed.mako index 242d6df7eea..9a6037dcfae 100644 --- a/templates/webapps/galaxy/visualization/embed.mako +++ b/templates/webapps/galaxy/visualization/embed.mako @@ -1,4 +1 @@ <%inherit file="/embed_base.mako"/> - -<%def name="render_summary_content( workflow, steps )"> - diff --git a/templates/webapps/galaxy/visualization/embed_in_frame.mako b/templates/webapps/galaxy/visualization/embed_in_frame.mako index b239bc1ba3a..2bd7d24c5ec 100644 --- a/templates/webapps/galaxy/visualization/embed_in_frame.mako +++ b/templates/webapps/galaxy/visualization/embed_in_frame.mako @@ -24,10 +24,6 @@
${item.annotation}
%endif
- ##
- ## currently, no summary content for visualization - ## could do the title or caption, whatever... - ##
diff --git a/templates/webapps/galaxy/workflow/embed.mako b/templates/webapps/galaxy/workflow/embed.mako index 2297b99c5fc..dee48db4490 100644 --- a/templates/webapps/galaxy/workflow/embed.mako +++ b/templates/webapps/galaxy/workflow/embed.mako @@ -1,24 +1,6 @@ <%inherit file="/embed_base.mako"/> -<%! - from galaxy.web.framework.helpers import iff -%> - <%def name="render_item_links( workflow )"> - ## FIXME: find and set appropriate icon for linking to workflow. - ${parent.render_item_links( workflow )} - -<%def name="render_summary_content( workflow, steps )"> - -##
    -## <% num_steps = len ( steps ) %> -##
  • ${num_steps} step${iff( num_steps != 1, "s", "" )} -##
  • Operations: ... -##
- From ba37ce53ee2c47eb0b0b888e9822758de0f33444 Mon Sep 17 00:00:00 2001 From: John Chilton Date: Wed, 15 May 2019 09:20:41 -0400 Subject: [PATCH 12/19] Page-editor-as-component - refactor embedded mako JS toward reuse outside mako. --- client/galaxy/scripts/bundleEntries.js | 1 + client/galaxy/scripts/mvc/embedded-objects.js | 79 +++++++++++++++++++ templates/embed_base.mako | 2 +- templates/webapps/galaxy/page/display.mako | 73 +---------------- 4 files changed, 82 insertions(+), 73 deletions(-) create mode 100644 client/galaxy/scripts/mvc/embedded-objects.js diff --git a/client/galaxy/scripts/bundleEntries.js b/client/galaxy/scripts/bundleEntries.js index c666d664310..0390749fb83 100644 --- a/client/galaxy/scripts/bundleEntries.js +++ b/client/galaxy/scripts/bundleEntries.js @@ -79,6 +79,7 @@ export function multiHistory(options) { export { default as panels } from "layout/panel"; export { show_in_overlay, hide_modal, show_message, show_modal, Modal } from "layout/modal"; export { make_popupmenu, make_popup_menus } from "ui/popupmenu"; +export { render_embedded_items } from "mvc/embedded-objects"; export { default as async_save_text } from "utils/async-save-text"; // Previously "chart" diff --git a/client/galaxy/scripts/mvc/embedded-objects.js b/client/galaxy/scripts/mvc/embedded-objects.js new file mode 100644 index 00000000000..e89bbeb3e5f --- /dev/null +++ b/client/galaxy/scripts/mvc/embedded-objects.js @@ -0,0 +1,79 @@ +import $ from "jquery"; +import { make_popup_menus } from "ui/popupmenu"; + +export function render_embedded_items() { + $(".embedded-item").each(function() { + var container = $(this); + if (container.hasClass("history")) { + return; + } + //note: we can't do the same override for visualizations + // bc builtins (like trackster) need the handlers/ajax below to work. + // instead: (for registry visualizations) we'll clear the handlers below + // and add new ones (in embed_in_frame.mako) ...ugh. + + // Show embedded item. + var show_embedded_item = function() { + var ajax_url = container.find("input[type=hidden]").val(); + + // Only get item content if it's not already there. + var item_content = $.trim(container.find(".item-content").text()); + if (!item_content) { + $.ajax({ + type: "GET", + url: ajax_url, + error: function() { + alert("Getting item content failed."); + }, + success: function(item_content) { + container.find(".summary-content").hide("fast"); + container.find(".item-content").html(item_content); + container.find(".expanded-content").show("fast"); + container.find(".toggle-expand").hide(); + container.find(".toggle").show(); + + make_popup_menus(); + } + }); + } else { + container.find(".summary-content").hide("fast"); + container.find(".expanded-content").show("fast"); + container.find(".toggle-expand").hide(); + container.find(".toggle").show(); + } + }; + + // Hide embedded item. + var hide_embedded_item = function() { + container.find(".expanded-content").hide("fast"); + container.find(".summary-content").show("fast"); + container.find(".toggle").hide(); + container.find(".toggle-expand").show(); + }; + + // Setup toggle expand. + var toggle_expand = $(this).find(".toggle-expand"); + toggle_expand.click(function() { + show_embedded_item(); + return false; + }); + + // Setup toggle contract. + var toggle_contract = $(this).find(".toggle"); + toggle_contract.click(function() { + hide_embedded_item(); + return false; + }); + + // Setup toggle embed. + var toggle_embed = $(this).find(".toggle-embed"); + toggle_embed.click(function() { + if (container.find(".expanded-content").is(":visible")) { + hide_embedded_item(); + } else { + show_embedded_item(); + } + return false; + }); + }); +} diff --git a/templates/embed_base.mako b/templates/embed_base.mako index a933919f76b..163f293392a 100644 --- a/templates/embed_base.mako +++ b/templates/embed_base.mako @@ -41,7 +41,7 @@ display_href = h.url_for( controller=item_controller, action='display_by_username_and_slug', username=item_user.username, slug=item_slug ) %>
diff --git a/templates/webapps/galaxy/page/display.mako b/templates/webapps/galaxy/page/display.mako index 1ed74810217..cd54a7500ea 100644 --- a/templates/webapps/galaxy/page/display.mako +++ b/templates/webapps/galaxy/page/display.mako @@ -7,80 +7,9 @@ From 6049515cee5e195d3ef36139dfa9899488106e34 Mon Sep 17 00:00:00 2001 From: John Chilton Date: Sat, 25 May 2019 15:07:11 -0400 Subject: [PATCH 13/19] Page-editor-as-component - generate properties client side. --- client/galaxy/scripts/galaxy.pages.js | 17 +++++++++++++++-- templates/webapps/galaxy/page/display.mako | 1 - templates/webapps/galaxy/page/editor.mako | 14 ++------------ 3 files changed, 17 insertions(+), 15 deletions(-) diff --git a/client/galaxy/scripts/galaxy.pages.js b/client/galaxy/scripts/galaxy.pages.js index 5bd284844d4..22336e69129 100644 --- a/client/galaxy/scripts/galaxy.pages.js +++ b/client/galaxy/scripts/galaxy.pages.js @@ -6,6 +6,7 @@ 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"; // Built a generic config container for the properties that // are passed in from python then used (formerly) globally @@ -491,8 +492,20 @@ WYMeditor.editor.prototype.dialog = function(dialogType, dialogFeatures, bodyHtm } }; -export default function pagesEditorOnload(renderedConfigs = {}) { - setPageConfigs(renderedConfigs); +export default function pagesEditorOnload() { + const appRoot = getAppRoot(); + const pageId = $("[name=page_content]").attr("page_id"); + 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/`, + save_url: `${appRoot}page/save` + } + setPageConfigs(pageConfigs); // Generic error handling $(document).ajaxError((e, x) => { diff --git a/templates/webapps/galaxy/page/display.mako b/templates/webapps/galaxy/page/display.mako index cd54a7500ea..5d3d29f613c 100644 --- a/templates/webapps/galaxy/page/display.mako +++ b/templates/webapps/galaxy/page/display.mako @@ -7,7 +7,6 @@ diff --git a/templates/webapps/galaxy/page/editor.mako b/templates/webapps/galaxy/page/editor.mako index 27e2eb0307f..01b07e20d4e 100644 --- a/templates/webapps/galaxy/page/editor.mako +++ b/templates/webapps/galaxy/page/editor.mako @@ -16,18 +16,8 @@ // 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(); }); @@ -55,7 +45,7 @@
- +
From 02832c6fbfb7fa9873492a55e74520392b7c4377 Mon Sep 17 00:00:00 2001 From: John Chilton Date: Sat, 25 May 2019 15:45:11 -0400 Subject: [PATCH 14/19] Page-editor-as-component - fetch content from pages API. --- client/galaxy/scripts/galaxy.pages.js | 28 +++++++++++++++++-- lib/galaxy/webapps/galaxy/controllers/page.py | 4 +-- templates/webapps/galaxy/page/editor.mako | 3 +- 3 files changed, 27 insertions(+), 8 deletions(-) diff --git a/client/galaxy/scripts/galaxy.pages.js b/client/galaxy/scripts/galaxy.pages.js index 22336e69129..947749b1ad0 100644 --- a/client/galaxy/scripts/galaxy.pages.js +++ b/client/galaxy/scripts/galaxy.pages.js @@ -7,6 +7,8 @@ import { make_popupmenu } from "ui/popupmenu"; import { getGalaxyInstance } from "app"; import { buildConfig } from "utils/genericConfig"; import { getAppRoot } from "onload/loadConfig"; +import axios from "axios"; +import { Toast } from "ui/toast"; // Built a generic config container for the properties that // are passed in from python then used (formerly) globally @@ -492,9 +494,8 @@ WYMeditor.editor.prototype.dialog = function(dialogType, dialogFeatures, bodyHtm } }; -export default function pagesEditorOnload() { +function renderEditorWithContent(pageId, content) { const appRoot = getAppRoot(); - const pageId = $("[name=page_content]").attr("page_id"); const pageConfigs = { page_id: pageId, page_list_url: `${appRoot}pages/list`, @@ -514,8 +515,11 @@ export default function pagesEditorOnload() { show_modal("Server error", message, { "Ignore error": hide_modal }); return false; }); + $(".page-editor-content").append( + `` + ) // Create editor - $("[name=page_content]").wymeditor({ + $("[name=page_content]").val(content).wymeditor({ skin: "galaxy", basePath: configs.editor_base_path, iframeBasePath: configs.iframe_base_path, @@ -691,3 +695,21 @@ export default function pagesEditorOnload() { //} }); } + +export default function pagesEditorOnload() { + const pageId = $(".page-editor-content").attr("page_id"); + console.log(pageId); + axios + .get(`${getAppRoot()}api/pages/${pageId}`) + .then(response => { + renderEditorWithContent(pageId, response.data.content); + }) + .catch(e => { + const response = e.response; + if (typeof response.responseJSON !== "undefined") { + Toast.error(response.responseJSON.err_msg); + } else { + Toast.error("An error occurred."); + } + }); +} diff --git a/lib/galaxy/webapps/galaxy/controllers/page.py b/lib/galaxy/webapps/galaxy/controllers/page.py index af842df2a36..c5254697430 100644 --- a/lib/galaxy/webapps/galaxy/controllers/page.py +++ b/lib/galaxy/webapps/galaxy/controllers/page.py @@ -406,9 +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 - content = self.page_manager.rewrite_content_for_export(trans, {"content": content})["content"] - 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") diff --git a/templates/webapps/galaxy/page/editor.mako b/templates/webapps/galaxy/page/editor.mako index 01b07e20d4e..7af1c89127b 100644 --- a/templates/webapps/galaxy/page/editor.mako +++ b/templates/webapps/galaxy/page/editor.mako @@ -44,8 +44,7 @@ -
- +
From 994a95e402eb23e954726c1f80b631f2583dd897 Mon Sep 17 00:00:00 2001 From: John Chilton Date: Sat, 25 May 2019 16:48:38 -0400 Subject: [PATCH 15/19] Page-editor-as-component - git mv to components to save commit history Not an atomic commit but cleaning the git history clean --- .../{galaxy.pages.js => components/PageEditor/PageEditorHtml.vue} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename client/galaxy/scripts/{galaxy.pages.js => components/PageEditor/PageEditorHtml.vue} (100%) diff --git a/client/galaxy/scripts/galaxy.pages.js b/client/galaxy/scripts/components/PageEditor/PageEditorHtml.vue similarity index 100% rename from client/galaxy/scripts/galaxy.pages.js rename to client/galaxy/scripts/components/PageEditor/PageEditorHtml.vue From 80e8ff3a1ed87aebb7576338d9d074225c1b54c6 Mon Sep 17 00:00:00 2001 From: John Chilton Date: Sat, 25 May 2019 16:52:12 -0400 Subject: [PATCH 16/19] Page-editor-as-component - outline VueJS component for HTML editor. Call it PageEditorHtml to support a Markdown version at some point potentially. --- .../components/PageEditor/PageEditorHtml.vue | 153 ++++++++++-------- client/galaxy/scripts/galaxy.pages.js | 31 ++++ templates/webapps/galaxy/page/editor.mako | 24 +-- 3 files changed, 122 insertions(+), 86 deletions(-) create mode 100644 client/galaxy/scripts/galaxy.pages.js diff --git a/client/galaxy/scripts/components/PageEditor/PageEditorHtml.vue b/client/galaxy/scripts/components/PageEditor/PageEditorHtml.vue index 947749b1ad0..17f9ca73274 100644 --- a/client/galaxy/scripts/components/PageEditor/PageEditorHtml.vue +++ b/client/galaxy/scripts/components/PageEditor/PageEditorHtml.vue @@ -1,3 +1,19 @@ + + + + + diff --git a/client/galaxy/scripts/galaxy.pages.js b/client/galaxy/scripts/galaxy.pages.js new file mode 100644 index 00000000000..c7cba88cf44 --- /dev/null +++ b/client/galaxy/scripts/galaxy.pages.js @@ -0,0 +1,31 @@ +import $ from "jquery"; +import axios from "axios"; +import { Toast } from "ui/toast"; +import { getAppRoot } from "onload/loadConfig"; +import PageEditorHtml from "components/PageEditor/PageEditorHtml"; +import Vue from "vue"; + +export default function pagesEditorOnload() { + const pageId = $("#page-editor-content").attr("page_id"); + axios + .get(`${getAppRoot()}api/pages/${pageId}`) + .then(response => { + const pageEditorHtmlInstance = Vue.extend(PageEditorHtml); + new pageEditorHtmlInstance({ + propsData: { + pageId: pageId, + content: response.data.content, + title: response.data.title + }, + 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."); + } + }); +} diff --git a/templates/webapps/galaxy/page/editor.mako b/templates/webapps/galaxy/page/editor.mako index 7af1c89127b..858774b54ef 100644 --- a/templates/webapps/galaxy/page/editor.mako +++ b/templates/webapps/galaxy/page/editor.mako @@ -22,29 +22,9 @@ -<%def name="stylesheets()"> - ${parent.stylesheets()} - ${h.css( "base", "embed_item" )} - - - <%def name="center_panel()"> -
-
- Page Editor: ${page.title | h} - -
-
- -
-
+ + From e96d0b493cd0bbb6a9937e402d3f738c96d85219 Mon Sep 17 00:00:00 2001 From: John Chilton Date: Sat, 25 May 2019 17:55:12 -0400 Subject: [PATCH 17/19] Page-editor-as-component - refactor toward supporting multiple formats. Strong abstraction around "components/PageEditor" directory. --- .../components/PageEditor/PageEditor.vue | 33 +++++++++++++++++++ .../components/PageEditor/PageEditorHtml.vue | 18 ++-------- .../scripts/components/PageEditor/index.js | 1 + client/galaxy/scripts/galaxy.pages.js | 6 ++-- 4 files changed, 40 insertions(+), 18 deletions(-) create mode 100644 client/galaxy/scripts/components/PageEditor/PageEditor.vue create mode 100644 client/galaxy/scripts/components/PageEditor/index.js diff --git a/client/galaxy/scripts/components/PageEditor/PageEditor.vue b/client/galaxy/scripts/components/PageEditor/PageEditor.vue new file mode 100644 index 00000000000..3309c7e7551 --- /dev/null +++ b/client/galaxy/scripts/components/PageEditor/PageEditor.vue @@ -0,0 +1,33 @@ + + + diff --git a/client/galaxy/scripts/components/PageEditor/PageEditorHtml.vue b/client/galaxy/scripts/components/PageEditor/PageEditorHtml.vue index 17f9ca73274..39a07756b88 100644 --- a/client/galaxy/scripts/components/PageEditor/PageEditorHtml.vue +++ b/client/galaxy/scripts/components/PageEditor/PageEditorHtml.vue @@ -1,16 +1,7 @@ diff --git a/client/galaxy/scripts/components/PageEditor/PageEditorHtml.vue b/client/galaxy/scripts/components/PageEditor/PageEditorHtml.vue index 2bb20f69393..bce1497ac72 100644 --- a/client/galaxy/scripts/components/PageEditor/PageEditorHtml.vue +++ b/client/galaxy/scripts/components/PageEditor/PageEditorHtml.vue @@ -14,6 +14,7 @@ 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 @@ -28,6 +29,8 @@ function setPageConfigs(incoming = {}) { var WYMeditor = window.WYMeditor; +var editor = null; + var CONTROLS = { // Item types. ITEM_HISTORY: "item_history", @@ -508,8 +511,7 @@ function renderEditorWithContent(pageId, content) { 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/`, - save_url: `${appRoot}page/save` + iframe_base_path: `${appRoot}static/wymeditor/iframe/galaxy/` }; setPageConfigs(pageConfigs); @@ -568,30 +570,7 @@ function renderEditorWithContent(pageId, content) { ] }); // 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(), - _: "true" - }, - success: function() { - callback(); - } - }); - }; - // Save button - $("#save-button").click(() => { - save(() => { - hide_modal(); - }); - }); + editor = $.wymeditors(0); // // Containers, Galaxy style @@ -678,12 +657,17 @@ export default { }, content: { type: String - }, + } }, created: function() { this.$nextTick(() => { renderEditorWithContent(this.pageId, this.content); }); + }, + methods: { + saveContent: function() { + save(this.pageId, editor.xhtml()); + } } }; diff --git a/client/galaxy/scripts/components/PageEditor/util.js b/client/galaxy/scripts/components/PageEditor/util.js new file mode 100644 index 00000000000..5339f5172f6 --- /dev/null +++ b/client/galaxy/scripts/components/PageEditor/util.js @@ -0,0 +1,24 @@ +import $ from "jquery"; +import _l from "utils/localization"; +import { getAppRoot } from "onload/loadConfig"; +import { show_modal, hide_modal } from "layout/modal"; + +export const save = (pageId, content) => { + console.log(`content is ${content}`); + + show_modal(_l("Saving page"), _l("progress")); + + // Do save. + $.ajax({ + url: `${getAppRoot()}page/save`, + type: "POST", + data: { + id: pageId, + content: content, + _: "true" + }, + success: function() { + hide_modal(); + } + }); +};