Warning
This document is for an in-development version of Galaxy. You can alternatively view this page in the latest release if it exists or view the top of the latest release's documentation.
Source code for galaxy.webapps.galaxy.services.visualizations
import json
import logging
from typing import (
cast,
Optional,
Tuple,
Union,
)
from galaxy import exceptions
from galaxy.managers.base import (
is_valid_slug,
security_check,
)
from galaxy.managers.context import ProvidesUserContext
from galaxy.managers.sharable import (
slug_exists,
SlugBuilder,
)
from galaxy.managers.visualizations import (
VisualizationManager,
VisualizationSerializer,
)
from galaxy.model import (
Visualization,
VisualizationRevision,
)
from galaxy.model.base import transaction
from galaxy.model.item_attrs import (
add_item_annotation,
get_item_annotation_str,
)
from galaxy.schema.fields import DecodedDatabaseIdField
from galaxy.schema.visualization import (
VisualizationCreatePayload,
VisualizationCreateResponse,
VisualizationIndexQueryPayload,
VisualizationRevisionResponse,
VisualizationShowResponse,
VisualizationSummaryList,
VisualizationUpdatePayload,
VisualizationUpdateResponse,
)
from galaxy.security.idencoding import IdEncodingHelper
from galaxy.structured_app import StructuredApp
from galaxy.util.hash_util import md5_hash_str
from galaxy.visualization.plugins.plugin import VisualizationPlugin
from galaxy.visualization.plugins.registry import VisualizationsRegistry
from galaxy.web import url_for
from galaxy.webapps.galaxy.services.base import ServiceBase
from galaxy.webapps.galaxy.services.notifications import NotificationService
from galaxy.webapps.galaxy.services.sharable import ShareableService
log = logging.getLogger(__name__)
[docs]class VisualizationsService(ServiceBase):
"""Common interface/service logic for interactions with visualizations in the context of the API.
Provides the logic of the actions invoked by API controllers and uses type definitions
and pydantic models to declare its parameters and return types.
"""
[docs] def __init__(
self,
security: IdEncodingHelper,
manager: VisualizationManager,
serializer: VisualizationSerializer,
notification_service: NotificationService,
):
super().__init__(security)
self.manager = manager
self.serializer = serializer
self.shareable_service = ShareableService(self.manager, self.serializer, notification_service)
# TODO: add the rest of the API actions here and call them directly from the API controller
[docs] def index(
self,
trans: ProvidesUserContext,
payload: VisualizationIndexQueryPayload,
include_total_count: bool = False,
) -> Tuple[VisualizationSummaryList, int]:
"""Return a list of Visualizations viewable by the user
:rtype: list
:returns: dictionaries containing Visualization details
"""
if not trans.user_is_admin:
user_id = trans.user and trans.user.id
if payload.user_id and payload.user_id != user_id:
raise exceptions.AdminRequiredException("Only admins can index the visualizations of others")
entries, total_matches = self.manager.index_query(trans, payload, include_total_count)
return (
VisualizationSummaryList(root=[entry.to_dict() for entry in entries]),
total_matches,
)
[docs] def show(
self,
trans: ProvidesUserContext,
visualization_id: DecodedDatabaseIdField,
) -> VisualizationShowResponse:
"""Return a dictionary containing the Visualization's details
:rtype: dictionary
:returns: Visualization details
"""
# TODO: revisions should be a contents/nested controller like viz/xxx/r/xxx)?
# the important thing is the config
# TODO:?? /api/visualizations/registry -> json of registry.listings?
visualization = self._get_visualization(trans, visualization_id, check_ownership=False, check_accessible=True)
dictionary = {
"model_class": "Visualization",
"id": visualization.id,
"title": visualization.title,
"type": visualization.type,
"user_id": visualization.user.id,
"dbkey": visualization.dbkey,
"slug": visualization.slug,
# to_dict only the latest revision (allow older to be fetched elsewhere)
"latest_revision": (
self._get_visualization_revision(visualization.latest_revision)
if visualization.latest_revision
else None
),
# need to encode ids in revisions as well
# NOTE: does not encode ids inside the configs
"revisions": [r.id for r in visualization.revisions],
}
# replace with trans.url_builder if possible
dictionary["url"] = url_for(
controller="visualization",
action="display_by_username_and_slug",
username=visualization.user.username,
slug=visualization.slug,
)
dictionary["username"] = visualization.user.username
dictionary["email_hash"] = md5_hash_str(visualization.user.email)
dictionary["tags"] = visualization.make_tag_string_list()
dictionary["annotation"] = get_item_annotation_str(trans.sa_session, trans.user, visualization)
app = cast(StructuredApp, trans.app)
if app.visualizations_registry:
visualizations_registry = cast(VisualizationsRegistry, app.visualizations_registry)
visualization_plugin = cast(VisualizationPlugin, visualizations_registry.get_plugin(dictionary["type"]))
dictionary["plugin"] = visualization_plugin.to_dict()
return VisualizationShowResponse(**dictionary)
[docs] def create(
self,
trans: ProvidesUserContext,
import_id: Optional[DecodedDatabaseIdField],
payload: VisualizationCreatePayload,
) -> VisualizationCreateResponse:
"""Returns a dictionary of the created visualization
:rtype: dictionary
:returns: dictionary containing Visualization details
"""
if import_id:
visualization = self._import_visualization(trans, import_id)
else:
type = payload.type
title = payload.title
slug = payload.slug
dbkey = payload.dbkey
annotation = payload.annotation
config = payload.config
# generate defaults - this will err if given a weird key?
visualization = self._create_visualization(trans, type, title, dbkey, slug, annotation)
# Create and save first visualization revision
revision = VisualizationRevision(visualization=visualization, title=title, config=config, dbkey=dbkey)
visualization.latest_revision = revision
session = trans.sa_session
session.add(revision)
with transaction(session):
session.commit()
return VisualizationCreateResponse(id=str(visualization.id))
[docs] def update(
self,
trans: ProvidesUserContext,
visualization_id: DecodedDatabaseIdField,
payload: VisualizationUpdatePayload,
) -> Optional[VisualizationUpdateResponse]:
"""
Update a visualization
:rtype: dictionary
:returns: dictionary containing Visualization details
"""
rval = None
# there's a differentiation here between updating the visualization and creating a new revision
# that needs to be handled clearly here or alternately, using a different controller
# like e.g. PUT /api/visualizations/{visualization_id}/r/{revision_id}
# TODO: consider allowing direct alteration of revisions title (without a new revision)
# only create a new revsion on a different config
# only update owned visualizations
visualization = self._get_visualization(trans, visualization_id, check_ownership=True)
latest_revision = cast(VisualizationRevision, visualization.latest_revision)
title = payload.title or latest_revision.title
dbkey = payload.dbkey or latest_revision.dbkey
deleted = payload.deleted or visualization.deleted
config = payload.config or latest_revision.config
latest_config = latest_revision.config
if (
(title != latest_revision.title)
or (dbkey != latest_revision.dbkey)
or (json.dumps(config) != json.dumps(latest_config))
):
revision = self._add_visualization_revision(trans, visualization, config, title, dbkey)
rval = {"id": str(visualization_id), "revision": str(revision.id)}
# allow updating vis title
visualization.title = title
visualization.deleted = deleted
with transaction(trans.sa_session):
trans.sa_session.commit()
return VisualizationUpdateResponse(**rval) if rval else None
def _get_visualization(
self,
trans: ProvidesUserContext,
visualization_id: DecodedDatabaseIdField,
check_ownership=True,
check_accessible=False,
) -> Visualization:
"""
Get a Visualization from the database by id, verifying ownership.
"""
visualization = trans.sa_session.get(Visualization, visualization_id)
if not visualization:
raise exceptions.ObjectNotFound("Visualization not found")
else:
return security_check(trans, visualization, check_ownership, check_accessible)
def _get_visualization_revision(
self,
revision: VisualizationRevision,
) -> VisualizationRevisionResponse:
"""
Return a set of detailed attributes for a visualization in dictionary form.
NOTE: that encoding ids isn't done here should happen at the caller level.
"""
revision_dict = {
"model_class": "VisualizationRevision",
"id": revision.id,
"visualization_id": revision.visualization.id,
"title": revision.title,
"dbkey": revision.dbkey,
"config": revision.config,
}
return VisualizationRevisionResponse(**revision_dict)
def _add_visualization_revision(
self,
trans: ProvidesUserContext,
visualization: Visualization,
config: Optional[Union[dict, bytes]],
title: Optional[str],
dbkey: Optional[str],
) -> VisualizationRevision:
"""
Adds a new `VisualizationRevision` to the given `visualization` with
the given parameters and set its parent visualization's `latest_revision`
to the new revision.
"""
# precondition: only add new revision on owned vis's
# TODO:?? should we default title, dbkey, config? to which: visualization or latest_revision?
revision = VisualizationRevision(visualization=visualization, title=title, dbkey=dbkey, config=config)
visualization.latest_revision = revision
# TODO:?? does this automatically add revision to visualzation.revisions?
trans.sa_session.add(revision)
with transaction(trans.sa_session):
trans.sa_session.commit()
return revision
def _create_visualization(
self,
trans: ProvidesUserContext,
type: str,
title: Optional[str] = "Untitled Visualization",
dbkey: Optional[str] = None,
slug: Optional[str] = None,
annotation: Optional[str] = None,
) -> Visualization:
"""Create visualization but not first revision. Returns Visualization object."""
user = trans.get_user()
# Error checking.
if slug:
slug_err = ""
if not is_valid_slug(slug):
slug_err = (
"visualization identifier must consist of only lowercase letters, numbers, and the '-' character"
)
elif slug_exists(trans.sa_session, Visualization, user, slug, ignore_deleted=True):
slug_err = "visualization identifier must be unique"
if slug_err:
# TODO: handle this error structure better
raise exceptions.RequestParameterMissingException(slug_err)
# Create visualization
visualization = Visualization(user=user, title=title, dbkey=dbkey, type=type)
if slug:
visualization.slug = slug
else:
slug_builder = SlugBuilder()
slug_builder.create_item_slug(trans.sa_session, visualization)
if annotation:
# TODO: if this is to stay in the mixin, UsesAnnotations should be added to the superclasses
# right now this is depending on the classes that include this mixin to have UsesAnnotations
add_item_annotation(trans.sa_session, trans.user, visualization, annotation)
session = trans.sa_session
session.add(visualization)
with transaction(session):
session.commit()
return visualization
def _import_visualization(
self,
trans: ProvidesUserContext,
visualization_id: DecodedDatabaseIdField,
) -> Visualization:
"""
Copy the visualization with the given id and associate the copy
with the given user (defaults to trans.user).
Raises `ItemAccessibilityException` if `user` is not passed and
the current user is anonymous, and if the visualization is not `importable`.
Raises `ItemDeletionException` if the visualization has been deleted.
"""
# default to trans.user, error if anon
if not trans.user:
raise exceptions.ItemAccessibilityException("You must be logged in to import Galaxy visualizations")
user = trans.user
# check accessibility
visualization = self._get_visualization(trans, visualization_id, check_ownership=False)
if not visualization.importable:
raise exceptions.ItemAccessibilityException(
"The owner of this visualization has disabled imports via this link."
)
if visualization.deleted:
raise exceptions.ItemDeletionException("You can't import this visualization because it has been deleted.")
# copy vis and alter title
# TODO: need to handle custom db keys.
imported_visualization = visualization.copy(user=user, title=f"imported: {visualization.title}")
trans.sa_session.add(imported_visualization)
with transaction(trans.sa_session):
trans.sa_session.commit()
return imported_visualization