Getting started with OMERO.web

Will Moore

Outline

  • OMERO.web setup for developers
  • OMERO.web and Django
  • Relationship between OMERO.web apps
  • Browser - Server communication
  • Where is the OMERO.web JavaScript code?
  • Browser devtools
  • jQuery in OMERO.web
    • jQuery intro
    • Selectors
    • DOM manipulation
    • AJAX
    • Events
    • jsTree, jQuery-UI

OMERO.web setup for developers

OMERO.web framework


OMERO.web documentation

What's in a Django app?

  • urls.py
    url( r'^dataset/(?P<dataset_id>[0-9]+)/$', views.dataset, name="dataset"),
  • views.py
    
    @login_required()
    def dataset(request, dataset_id, conn=None, **kwargs):
        ds = conn.getObject("Dataset", dataset_id)
        return render(request, 'webtest/dataset.html', {'dataset': ds})
                                
  • template.html
    <html><body>
    <script>alert("Hello World")  // JavaScript goes here</script>         
    <h1>{{ dataset.getName }}</h1>
    {% for i in dataset.listChildren %}
        <img src="{% url 'webgateway.views.render_thumbnail' i.id %}" />
        {{ i.getName }}
    {% endfor %}
    </body></html>
See the OMERO.web simple example | Django docs

OMERO.web apps

components/tools/OmeroWeb/omeroweb/

  • webgateway (public): render images, image JSON, full viewer
  • feedback: submitting errors or comments
  • webclient: main web client UI
  • webadmin: manager users & groups
  • api (public): JSON api

Dependencies between apps


components/tools/OmeroWeb/omeroweb/

Browser - Server communication

Browser: JavaScript
Server: Python client of OMERO

Sequence of /webclient/ app

  1. Load containers.html (includes jQuery and lots of other JavaScript)
  2. jQuery loads other data via AJAX and updates page HTML (DOM):
    • jsTree loads Projects, Datasets, Images, Screens & Plates as JSON data
    • jsTree JSON data used to build Dataset thumbnails HTML
    • Selected items trigger right panel loading HTML
    • Plates loaded as JSON data
    • Scripts loaded as JSON data
    • Groups/Users menu loaded as HTML
    • etc.

Loading /webclient/ home page

  • webclient/urls.py
    url(r'^$', views.load_template, {'menu': 'userdata'}, name="webindex"),
  • webclient/views.py
    @login_required()
    @render_response()
    def load_template(request, menu, conn=None, url=None, **kwargs):
        ...
        template = "webclient/data/containers.html"
        ... 
  • webclient/templates/webclient/data/containers.html
    {% extends "webclient/base/base_container.html" %}  
    ...
    <script src="{% static 'webclient/javascript/ome.tree.js'|add:url_suffix %}"></script>
    <script>// More JavaScript... </script>
    ...
    {% block left %}
    <div class="left_panel_content">
    ...
    <div class="dataTree" id="dataTree"></div>
    

Django HTML templates

  • Inheritance
    {% extends "path/to/template.html" %}
    {% block centre_panel %}
    <!-- overwrites parent block -->
    {% endblock %}
  • Composition
    {% includes "path/to/template.html" %}
  • OMERO.web: extensive hierarchy of templates
  • Needs to be simplified (see Trello card)

Where is the JS code in OMERO.web?

Browser devtools (Chrome or Firefox)

  • Inspect DOM (right click on Element -> Inspect)
    Find elements, IDs, classes etc.
  • Edit CSS in the browser
  • Access JavaScript Console
  • Inspect Network requests
    • Requested URL and GET/POST
    • What initialised it?
    • Data sent and received
    • Timing
    • Open GET URLs in new Tab

jQuery

jQuery: run when ready

Need to run jQuery code when page is ready...
// $ == jQuery
$("document").ready(function(){
    // Do stuff when DOM is complete...
});

$(function(){
    // Short-hand to do the same thing...
});
We do this in many different places on the same page
e.g. containers.html, script_launch_head.html, webclient/static/webclient/javascript/ome.tree.js

jQuery: selectors

Uses CSS syntax for selecting "list" of object(s)
// Select by ID
$("#content_details")

// By class name, saving to variable prefixed with $ for jQuery object
var $menu_links = $(".menu_link")

// By element name
$("div")

// By attribute
$("[role='treeitem']")

// combination - spaces separate parent -> child
$("#dataTree li[role='treeitem'] .jstree-anchor span")
  • Need to select ONLY the elements you want, but
  • Longer selectors are more 'fragile' to change than others.
  • IDs are best but don't want them on every element.

jQuery: DOM manipulation

// Set the innerHTML
$("#content_details").html("<h1>Hello World</h1>")

// Load HTML from URL
$("#content_details").load("/webclient/group_user_content/");

// Move new/existing elements
var $hello = $("<h1>Hello World</h1>");
$("#content_details").append($hello)

// HTML generated via Underscore templates (see link below)
var html = iconTmpl(json);
$("#icon_table").html(html);
E.g. updating centre panel with data from jsTree. center_plugin.thumbs.js.html

jQuery: CSS and show/hide

// Single CSS change
$("#content_details").css('background', 'red');

// Multiple changes - use JSON
$("#content_details").css({'background': 'red', 'font-size': '20px'});

// Add/remove Classes can be nicer (from link below)
$this.addClass('ratingFilter_hidden');

// hide / show
$("#content_details").hide();
$("#content_details").show();
center_plugin.thumbs.js.html

jQuery: chaining

functions on jQuery objects usually return
the objects themselves

// All these methods called on the same <h1> element
$("<h1>Hello World</h1>")
    .appendTo("#content_details")
    .css('background', 'red')
    .hide()
    .fadeIn()
                    

jQuery: iterating

Call a function on each element in collection

// Filter thumbnails by rating:
$("#dataIcons li.row").each(function() {
    var $this = $(this),
        iid = $this.attr("data-id");
    if (rating === 0 || rdata[iid]) {
        $this.removeClass('ratingFilter_hidden');
    } else {
        $this.addClass('ratingFilter_hidden');
    }
});
                    
center_plugin.thumbs.js.html

jQuery: AJAX

GET or POST data to a URL
// Get rating annotations for filtering:
var query = "image=" + iids.join("&image=");
$.getJSON("{% url 'api_annotations' %}?type=rating&" + query, function(data){
...
});

// POST to create new Container
var ajax_data = {
    "name" : new_container_name,
    "folder_type" : cont_type,
    "description" : new_container_desc,
}
$.ajax({
    url: url,
    data: ajax_data,
    dataType: "json",
    type: "POST",
    success: function(data){
        // handle success
    }
});
                    
center_plugin.thumbs.js.html , containers.html

jQuery: Events

Listen for and trigger events
// double-click handler on image. N.B. event bound to parent container
$("#content_details").on("dblclick", "li.row", function(event) {
...});

// Listen for custom events. right_plugin.general.js.html
$("body").on("selection_change.ome", function(event) {
    // clear contents of right panel, then update
    $("#metadata_general").empty();
    update_metadata_general_tab();
});

// Trigger events. ome.webclient.actions.js
OME.handle_tree_selection = function(data, event) {
    ...
    $("body").trigger("selection_change.ome", data);
}
                    
center_plugin.thumbs.js.html , right_plugin.general.js.html , ome.webclient.actions.js

jsTree

Managing hierarchies
$("#dataTree")
    .on('changed.jstree', function (e, data) {
        OME.tree_selection_changed(data, e);
    })
    .on('move_node.jstree', function(e, data) {
        // object links saved to server
    })
    // initialise jstree plugin...
    .jstree({
        'core': {
            // called when jsTree wants children data for a node
            'data': function(node, callback) {
                // set url and payload based on node
                // N.B. very simplified example...
                if (node.type === 'project') {
                    url = WEBCLIENT.URLS.api_datasets;
                }
                var payload = {'id': node.data.obj.id};

                $.ajax({url: url, data: payload,
                    success: function (data, textStatus, jqXHR) {
                        callback.call(this, data);
                    }
                });
            }
        }
    });
/webclient/static/webclient/javascript/ome.tree.js

jQuery-UI

UI widgets:

// tabs
$("#annotation_tabs").tabs();

// sliders
$("#thumb_size_slider").slider({
    max: 200,
    min: 30,
    value: iconSize,
    slide: function(event, ui) {
        iconSize = ui.value;
        setIconSize();
    }
});
                    

Underscore templates

Used to generate HTML from 'JSON'
<script id="icon_thumbnails_template" type="text/template">
    <h1> <%= title %> </h1>
    <% _.each(images, function(img) { %>
        <li> <%= img.name %> </li>
    <% }) %>
</script>
var t = $("#icon_thumbnails_template").html();
var compiled = _.template(t);
var jsonData = {
    title: "Dataset Name",
    images: [
        {name: "image.tiff"}, {name: "test.dv"}
    ]
};
var html = compiled(jsonData);

Full example

Right panel - show file paths for image
// when right panel loads...
$(document).ready(function() {

    var $toolbar_info_panel = $("#toolbar_info_panel"),
        $panel_title = $("#toolbar_info_panel .panel_title"),
        $panel_div = $("#toolbar_info_panel .panel_div");

    var original_file_paths_url = "{% url 'original_file_paths' manager.image.id %}";

    $("#show_fs_files_btn").click(function(){

        // If we're already showing Image file info, toggle hide
        if ($toolbar_info_panel.is(":visible") && 
                $panel_title.text().split("Image file").length > 1) {
            $toolbar_info_panel.hide();
            return;
        }
        $("#toolbar_info_panel").show();
        $panel_title.html("Loading...");
        $panel_div.empty();
        if (original_file_paths_url) {
            $.getJSON(original_file_paths_url,
                function(data) {
                    var repo = data.repo,
                        client = data.client,
                        html = "";
                    $panel_title.html(repo.length + " Image file" + (repo.length>1 ? "s:" : ":"));

                    if (importTransfer) {
                        html += "<p>Imported with <strong>--transfer="+ importTransfer;
                        html += "</strong></p><hr/>";
                    }

                    html += "<p>Imported from:</p>";
                    html += "<p class='pathlist'>" + client.slice(0,2).join("<br>") + "<br>";
                    if (client.length > 2) {
                        html += "<a class='show_more' href='#'> Show more...</a>";
                        html += "<span style='display:none'>" + client.slice(2).join("<br>");
                        html += "</span>";
                    }
                    html += "</p><hr/>";

                    html += "<p>Paths on server:</p>";
                    html += "<p class='pathlist'>" + repo.slice(0,2).join("<br>") + "<br>";
                    if (repo.length > 2) {
                        html += "<a class='show_more' href='#'> Show more...</a>";
                        html += "<span style='display:none'>" + repo.slice(2).join("<br>");
                        html += "</span>";
                    }
                    html += "</p>";
                    $panel_div.append(html);
                });
        }
    });

});
                    
templates/webclient/annotations/metadata_general.html