about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--wqflask/wqflask/metadata_edits.py2
-rw-r--r--wqflask/wqflask/oauth2/client.py14
-rw-r--r--wqflask/wqflask/oauth2/data.py165
-rw-r--r--wqflask/wqflask/oauth2/groups.py8
-rw-r--r--wqflask/wqflask/oauth2/request_utils.py8
-rw-r--r--wqflask/wqflask/static/new/javascript/auth/search.js169
-rw-r--r--wqflask/wqflask/static/new/javascript/auth/search_genotypes.js94
-rw-r--r--wqflask/wqflask/static/new/javascript/auth/search_mrna.js96
-rw-r--r--wqflask/wqflask/static/new/javascript/auth/search_phenotypes.js160
-rw-r--r--wqflask/wqflask/templates/correlation_page.html2
-rw-r--r--wqflask/wqflask/templates/generif.html102
-rw-r--r--wqflask/wqflask/templates/oauth2/data-list-genotype.html124
-rw-r--r--wqflask/wqflask/templates/oauth2/data-list-mrna.html168
-rw-r--r--wqflask/wqflask/templates/oauth2/data-list-phenotype.html209
-rw-r--r--wqflask/wqflask/templates/oauth2/group.html6
-rw-r--r--wqflask/wqflask/templates/show_trait_details.html3
-rw-r--r--wqflask/wqflask/views.py16
17 files changed, 1064 insertions, 282 deletions
diff --git a/wqflask/wqflask/metadata_edits.py b/wqflask/wqflask/metadata_edits.py
index 28606c8b..64fc3f19 100644
--- a/wqflask/wqflask/metadata_edits.py
+++ b/wqflask/wqflask/metadata_edits.py
@@ -360,7 +360,7 @@ View the diffs <a href='{url}' target='_blank'>here</a>", "success")
                     json_data=json.dumps(diff_data),
                 ),
             )
-        conn.commit()
+            conn.commit()
         flash(f"Diff-data: \n{diff_data}\nhas been uploaded", "success")
     return redirect(
         f"/datasets/{dataset_id}/traits/{name}"
diff --git a/wqflask/wqflask/oauth2/client.py b/wqflask/wqflask/oauth2/client.py
index 999bbfc8..1d3e07ae 100644
--- a/wqflask/wqflask/oauth2/client.py
+++ b/wqflask/wqflask/oauth2/client.py
@@ -1,4 +1,5 @@
 """Common oauth2 client utilities."""
+from typing import Optional
 from urllib.parse import urljoin
 
 from flask import session, current_app as app
@@ -15,7 +16,7 @@ def oauth2_client():
         scope=SCOPE, token_endpoint_auth_method="client_secret_post",
         token=session.get("oauth2_token"))
 
-def oauth2_get(uri_path: str, data: dict = {}) -> Either:
+def oauth2_get(uri_path: str, data: dict = {}, **kwargs) -> Either:
     token = session.get("oauth2_token")
     config = app.config
     client = OAuth2Session(
@@ -23,19 +24,24 @@ def oauth2_get(uri_path: str, data: dict = {}) -> Either:
         token=token, scope=SCOPE)
     resp = client.get(
         urljoin(config["GN_SERVER_URL"], uri_path),
-        data=data)
+        data=data,
+        **kwargs)
     if resp.status_code == 200:
         return Right(resp.json())
 
     return Left(resp)
 
-def oauth2_post(uri_path: str, data: dict) -> Either:
+def oauth2_post(
+        uri_path: str, data: Optional[dict] = None, json: Optional[dict] = None,
+        **kwargs) -> Either:
     token = session.get("oauth2_token")
     config = app.config
     client = OAuth2Session(
         config["OAUTH2_CLIENT_ID"], config["OAUTH2_CLIENT_SECRET"],
         token=token, scope=SCOPE)
-    resp = client.post(urljoin(config["GN_SERVER_URL"], uri_path), data=data)
+    resp = client.post(
+        urljoin(config["GN_SERVER_URL"], uri_path), data=data, json=json,
+        **kwargs)
     if resp.status_code == 200:
         return Right(resp.json())
 
diff --git a/wqflask/wqflask/oauth2/data.py b/wqflask/wqflask/oauth2/data.py
index 1dcacb7e..5026a6d1 100644
--- a/wqflask/wqflask/oauth2/data.py
+++ b/wqflask/wqflask/oauth2/data.py
@@ -1,10 +1,16 @@
 """Handle linking data to groups."""
+import sys
+import json
+import uuid
+from datetime import datetime
 from urllib.parse import urljoin
 
+from redis import Redis
 from flask import (
-    flash, request, url_for, redirect, Response, Blueprint, render_template,
-    current_app as app)
+    flash, request, jsonify, url_for, redirect, Response, Blueprint,
+    render_template, current_app as app)
 
+from jobs import jobs
 from .request_utils import process_error
 from .client import oauth2_get, oauth2_post
 
@@ -19,40 +25,104 @@ def __render_template__(templatepath, **kwargs):
         templatepath, **kwargs, user_privileges=user_privileges)
 
 def __search_mrna__(query, template, **kwargs):
-    return __render_template__(template, **kwargs)
+    species_name = kwargs["species_name"]
+    search_uri = urljoin(app.config["GN_SERVER_URL"], "oauth2/data/search")
+    datasets = oauth2_get(
+        "oauth2/data/search",
+        json = {
+            "query": query,
+            "dataset_type": "mrna",
+            "species_name": species_name,
+            "selected": __selected_datasets__()
+        }).either(
+            lambda err: {"datasets_error": process_error(err)},
+            lambda datasets: {"datasets": datasets})
+    return __render_template__(template, search_uri=search_uri, **datasets, **kwargs)
+
+def __selected_datasets__():
+    if bool(request.json):
+        return request.json.get(
+            "selected",
+            request.args.get("selected",
+                             request.form.get("selected", [])))
+    return request.args.get("selected",
+                            request.form.get("selected", []))
 
 def __search_genotypes__(query, template, **kwargs):
     species_name = kwargs["species_name"]
+    search_uri = urljoin(app.config["GN_SERVER_URL"], "oauth2/data/search")
     datasets = oauth2_get(
         "oauth2/data/search",
-        data = {
+        json = {
             "query": query,
             "dataset_type": "genotype",
-            "species_name": species_name
+            "species_name": species_name,
+            "selected": __selected_datasets__()
         }).either(
             lambda err: {"datasets_error": process_error(err)},
             lambda datasets: {"datasets": datasets})
-    return __render_template__(template, **datasets, **kwargs)
+    return __render_template__(template, search_uri=search_uri, **datasets, **kwargs)
 
 def __search_phenotypes__(query, template, **kwargs):
-    per_page = int(request.args.get("per_page", 500))
-    species_name = kwargs["species_name"]
-    search_uri = (f"search/?type=phenotype&per_page={per_page}&query="
-                  f"species:{species_name}") + (
-                      f" AND ({query})" if bool(query) else "")
-    traits = oauth2_get(search_uri).either(
-             lambda err: {"traits_error": process_error(err)},
-             lambda trts: {"traits": tuple({
-                 "index": idx, **trait
-             } for idx, trait in enumerate(trts, start=1))})
-
+    page = int(request.args.get("page", 1))
+    per_page = int(request.args.get("per_page", 50))
     selected_traits = request.form.getlist("selected_traits")
+    def __search_error__(error):
+        raise Exception(error)
+    def __search_success__(search_results):
+        job_id = uuid.UUID(search_results["job_id"])
+        return __render_template__(
+            template, traits=[], per_page=per_page, query=query,
+            selected_traits=selected_traits, search_results=search_results,
+            search_endpoint=urljoin(
+                app.config["GN_SERVER_URL"], "oauth2/data/search"),
+            results_endpoint=urljoin(
+                app.config["GN_SERVER_URL"],
+                f"oauth2/data/search/phenotype/{job_id}"),
+            **kwargs)
+    return oauth2_get("oauth2/data/search", json={
+        "dataset_type": "phenotype",
+        "species_name": kwargs["species_name"],
+        "per_page": per_page,
+        "page": page,
+        "gn3_server_uri": app.config["GN_SERVER_URL"]
+    }).either(
+        lambda err: __search_error__(process_error(err)),
+        __search_success__)
+
+@data.route("/genotype/search", methods=["POST"])
+def json_search_genotypes() -> Response:
+    def __handle_error__(err):
+        error = process_error(err)
+        return jsonify(error), error["status_code"]
+    
+    return oauth2_get(
+        "oauth2/data/search",
+        json = {
+            "query": request.json["query"],
+            "dataset_type": "genotype",
+            "species_name": request.json["species_name"],
+            "selected": __selected_datasets__()
+        }).either(
+            __handle_error__,
+            lambda datasets: jsonify(datasets))
 
-    return __render_template__(
-        template, **traits, per_page=per_page, query=query,
-        selected_traits=selected_traits,
-        search_endpoint=urljoin(app.config["GN_SERVER_URL"], "search/"),
-        **kwargs)
+@data.route("/mrna/search", methods=["POST"])
+def json_search_mrna() -> Response:
+    def __handle_error__(err):
+        error = process_error(err)
+        return jsonify(error), error["status_code"]
+
+    return oauth2_get(
+        "oauth2/data/search",
+        json = {
+            "query": request.json["query"],
+            "dataset_type": "mrna",
+            "species_name": request.json["species_name"],
+            "selected": __selected_datasets__()
+        }).either(
+            __handle_error__,
+            lambda datasets: jsonify(datasets))
 
 @data.route("/<string:species_name>/<string:dataset_type>/list",
             methods=["GET", "POST"])
@@ -154,3 +224,54 @@ def link_data():
     except AssertionError as aserr:
         flash("You must provide all the expected data.", "alert-danger")
         return redirect(url_for("oauth2.data.list_data"))
+
+@data.route("/link/genotype", methods=["POST"])
+def link_genotype_data():
+    """Link genotype data to a group."""
+    form = request.form
+    link_source_url = redirect(url_for("oauth2.data.list_data"))
+    if bool(form.get("species_name")):
+        link_source_url = redirect(url_for(
+            "oauth2.data.list_data_by_species_and_dataset",
+            species_name=form["species_name"], dataset_type="genotype"))
+
+    def __link_error__(err):
+        flash(f"{err['error']}: {err['error_description']}", "alert-danger")
+        return link_source_url
+
+    def __link_success__(success):
+        flash(success["description"], "alert-success")
+        return link_source_url
+
+    return oauth2_post("oauth2/data/link/genotype", json={
+        "species_name": form.get("species_name"),
+        "group_id": form.get("group_id"),
+        "selected": tuple(json.loads(dataset) for dataset
+                                   in form.getlist("selected"))
+    }).either(lambda err: __link_error__(process_error(err)), __link_success__)
+
+
+@data.route("/link/mrna", methods=["POST"])
+def link_mrna_data():
+    """Link mrna data to a group."""
+    form = request.form
+    link_source_url = redirect(url_for("oauth2.data.list_data"))
+    if bool(form.get("species_name")):
+        link_source_url = redirect(url_for(
+            "oauth2.data.list_data_by_species_and_dataset",
+            species_name=form["species_name"], dataset_type="mrna"))
+
+    def __link_error__(err):
+        flash(f"{err['error']}: {err['error_description']}", "alert-danger")
+        return link_source_url
+
+    def __link_success__(success):
+        flash(success["description"], "alert-success")
+        return link_source_url
+
+    return oauth2_post("oauth2/data/link/mrna", json={
+        "species_name": form.get("species_name"),
+        "group_id": form.get("group_id"),
+        "selected": tuple(json.loads(dataset) for dataset
+                                   in form.getlist("selected"))
+    }).either(lambda err: __link_error__(process_error(err)), __link_success__)
diff --git a/wqflask/wqflask/oauth2/groups.py b/wqflask/wqflask/oauth2/groups.py
index 551c0640..3bc5fb6d 100644
--- a/wqflask/wqflask/oauth2/groups.py
+++ b/wqflask/wqflask/oauth2/groups.py
@@ -9,7 +9,7 @@ from flask import (
 from .checks import require_oauth2
 from .client import oauth2_get, oauth2_post
 from .request_utils import (
-    user_details, handle_error, request_error, process_error, handle_success,
+    user_details, handle_error, process_error, handle_success,
     raise_unimplemented)
 
 groups = Blueprint("group", __name__)
@@ -32,8 +32,12 @@ def user_group():
                 user_error=process_error(error)),
             partial(__get_join_requests__, group))
 
+    def __group_error__(err):
+        return render_template(
+            "oauth2/group.html", group_error=process_error(err))
+
     return oauth2_get("oauth2/user/group").either(
-        request_error, __success__)
+        __group_error__, __success__)
 
 @groups.route("/create", methods=["POST"])
 @require_oauth2
diff --git a/wqflask/wqflask/oauth2/request_utils.py b/wqflask/wqflask/oauth2/request_utils.py
index ed523614..fae3a347 100644
--- a/wqflask/wqflask/oauth2/request_utils.py
+++ b/wqflask/wqflask/oauth2/request_utils.py
@@ -20,12 +20,14 @@ def process_error(error: Response,
                                 "server.")
                   ) -> dict:
     if error.status_code == 404:
+        msg = error.json()["error_description"] if hasattr(error, "json") else message
         return {
             "error": "NotFoundError",
-            "error_message": message,
-            "error_description": message
+            "error_message": msg,
+            "error_description": msg,
+            "status_code": error.status_code
         }
-    return error.json()
+    return {**error.json(), "status_code": error.status_code}
 
 def request_error(response):
     app.logger.error(f"{response}: {response.url} [{response.status_code}]")
diff --git a/wqflask/wqflask/static/new/javascript/auth/search.js b/wqflask/wqflask/static/new/javascript/auth/search.js
new file mode 100644
index 00000000..4e79bfd4
--- /dev/null
+++ b/wqflask/wqflask/static/new/javascript/auth/search.js
@@ -0,0 +1,169 @@
+class InvalidCSSIDSelector extends Error {
+    constructor(message) {
+	super(message);
+	this.name = "InvalidCSSIDSelector";
+    }
+}
+
+class InvalidDataAttributeName extends Error {
+    constructor(message) {
+	super(message);
+	this.name = "InvalidDataAttributeName";
+    }
+}
+
+/**
+ * CSSIDSelector: A CSS ID Selector
+ * @param {String} A CSS selector of the form '#...'
+ */
+class CSSIDSelector {
+    constructor(selector) {
+	if(!selector.startsWith("#")) {
+	    throw new InvalidCSSIDSelector(
+		"Expected the CSS selector to begin with a `#` character.");
+	}
+	let id_str = selector.slice(1, selector.length);
+	if(document.getElementById(id_str) == null) {
+	    throw new InvalidCSSIDSelector(
+		"Element with ID '" + id_str + "' does not exist.");
+	}
+	this.selector = selector;
+    }
+}
+
+/**
+ * TableDataSource: A type to represent a table's data source
+ * @param {String} A CSS selector for an ID
+ * @param {String} A `data-*` attribute name
+ */
+class TableDataSource {
+    constructor(table_id, data_attribute_name, checkbox_creation_function) {
+	this.table_id = new CSSIDSelector(table_id);
+	let data = document.querySelector(
+	    table_id).getAttribute(data_attribute_name);
+	if(data == null) {
+	    throw new InvalidDataAttributeName(
+		"data-* attribute '" + data_attribute_name + "' does not exist " +
+		    "for table with ID '" + table_id.slice(1, table_id.length) +
+		    "'.");
+	} else {
+	    this.data_attribute_name = data_attribute_name;
+	}
+	this.checkbox_creation_function = checkbox_creation_function;
+    }
+}
+
+/**
+ * Render the table
+ * @param {String} The selector for the table's ID
+ * @param {String} The name of the data-* attribute holding the table's data
+ * @param {Function} The function to call to generate the appropriate checkbox
+ */
+function render_table(table_data_source) {
+    table_id = table_data_source.table_id.selector;
+    data_attr_name = table_data_source.data_attribute_name;
+    $(table_id + " tbody tr").remove();
+    table_data = JSON.parse($(table_id).attr(data_attr_name));
+    if(table_data.length < 1) {
+	row = $("<tr>")
+	cell = $('<td colspan="100%" align="center">');
+	cell.append(
+	    $('<span class="glyphicon glyphicon-info-sign text-info">'));
+	cell.append("&nbsp;");
+	cell.append("No genotype datasets remaining.");
+	row.append(cell);
+	$(table_id + " tbody").append(row);
+    }
+    table_data.forEach(function(dataset) {
+	row = $("<tr>")
+	row.append(table_data_source.checkbox_creation_function(dataset));
+	row.append(table_cell(dataset.InbredSetName));
+	row.append(table_cell(dataset.dataset_name));
+	row.append(table_cell(dataset.dataset_fullname));
+	row.append(table_cell(dataset.dataset_shortname));
+	$(table_id + " tbody").append(row);
+    });
+}
+
+function remove_from_table_data(dataset, table_data_source) {
+    let table_id = table_data_source.table_id.selector;
+    let data_attr_name = table_data_source.data_attribute_name;
+    without_dataset = JSON.parse($(table_id).attr(data_attr_name)).filter(
+	function(dst) {
+	    return !(dst.SpeciesId == dataset.SpeciesId &&
+		     dst.InbredSetId == dataset.InbredSetId &&
+		     dst.GenoFreezeId == dataset.GenoFreezeId);
+	});
+    $(table_id).attr(data_attr_name, JSON.stringify(without_dataset));
+}
+
+function add_to_table_data(dataset, table_data_source) {
+    let table_id = table_data_source.table_id.selector;
+    let data_attr_name = table_data_source.data_attribute_name;
+    table_data = JSON.parse($(table_id).attr(data_attr_name));
+    if(!in_array(dataset, table_data)) {
+	table_data.push(dataset);
+    }
+    $(table_id).attr(data_attr_name, JSON.stringify(Array.from(table_data)));
+}
+
+/**
+ * Switch the dataset from search table to selection table and vice versa
+ * @param {Object} A genotype dataset
+ * @param {TableDataSource} The table to switch the dataset from
+ * @param {TableDataSource} The table to switch the dataset to
+ */
+function select_deselect_dataset(dataset, source, destination) {
+    dest_selector = destination.table_id.selector
+    dest_data = JSON.parse(
+	$(dest_selector).attr(destination.data_attribute_name));
+    add_to_table_data(dataset, destination); // Add to destination table
+    remove_from_table_data(dataset, source); // Remove from source table
+    /***** BEGIN: Re-render tables *****/
+    render_table(destination);
+    render_table(source);
+    /***** END: Re-render tables *****/
+}
+
+function debounce(func, delay=500) {
+    var timeout;
+    return function search(event) {
+	clearTimeout(timeout);
+	timeout = setTimeout(func, delay);
+    };
+}
+
+/**
+ * Build a checkbox
+ * @param {Dataset Object} A JSON.stringify-able object
+ * @param {String} The name to assign the checkbox
+ */
+function build_checkbox(data_object, checkbox_name, checkbox_aux_classes="", checked=false) {
+    cell = $("<td>");
+    check = $(
+	'<input type="checkbox" class="checkbox" ' +
+	    'name="' + checkbox_name + '">');
+    check.val(JSON.stringify(data_object));
+    check.prop("checked", checked);
+    auxilliary_classes = checkbox_aux_classes.trim();
+    if(Boolean(auxilliary_classes)) {
+	check.attr("class",
+		   check.attr("class") + " " + auxilliary_classes.trim());
+    }
+    cell.append(check);
+    return cell;
+}
+
+function link_checkbox(dataset) {
+    return build_checkbox(dataset, "selected", "checkbox-selected", true);
+}
+
+function search_checkbox(dataset) {
+    return build_checkbox(dataset, "search_datasets", "checkbox-search");
+}
+
+function table_cell(value) {
+    cell = $("<td>");
+    cell.html(value);
+    return cell;
+}
diff --git a/wqflask/wqflask/static/new/javascript/auth/search_genotypes.js b/wqflask/wqflask/static/new/javascript/auth/search_genotypes.js
new file mode 100644
index 00000000..40f88121
--- /dev/null
+++ b/wqflask/wqflask/static/new/javascript/auth/search_genotypes.js
@@ -0,0 +1,94 @@
+/**
+ * Check whether `dataset` is in array of `datasets`.
+ * @param {GenotypeDataset} A genotype dataset.
+ * @param {Array} An array of genotype datasets.
+ */
+function in_array(dataset, datasets) {
+    found = datasets.filter(function(dst) {
+	return (dst.SpeciesId == dataset.SpeciesId &&
+		dst.InbredSetId == dataset.InbredSetId &&
+		dst.GenoFreezeId == dataset.GenoFreezeId);
+    });
+    return found.length > 0;
+}
+
+function toggle_link_button() {
+    num_groups = $("#frm-link-genotypes select option").length - 1;
+    num_selected = JSON.parse(
+	$("#tbl-link-genotypes").attr("data-selected-datasets")).length;
+    if(num_groups > 0 && num_selected > 0) {
+	$("#frm-link-genotypes input[type='submit']").prop("disabled", false);
+    } else {
+	$("#frm-link-genotypes input[type='submit']").prop("disabled", true);
+    }
+}
+
+function search_genotypes() {
+    query = document.getElementById("txt-query").value;
+    selected = JSON.parse(document.getElementById(
+	"tbl-link-genotypes").getAttribute("data-selected-datasets"));
+    species_name = document.getElementById("txt-species-name").value
+    search_endpoint = "/oauth2/data/genotype/search"
+    search_table = new TableDataSource(
+	"#tbl-genotypes", "data-datasets", search_checkbox);
+    $.ajax(
+	search_endpoint,
+	{
+	    "method": "POST",
+	    "contentType": "application/json; charset=utf-8",
+	    "dataType": "json",
+	    "data": JSON.stringify({
+		"query": query,
+		"selected": selected,
+		"dataset_type": "genotype",
+		"species_name": species_name}),
+	    "error": function(jqXHR, textStatus, errorThrown) {
+		data = jqXHR.responseJSON
+		elt = document.getElementById("search-error").setAttribute(
+		    "style", "display: block;");
+		document.getElementById("search-error-text").innerHTML = (
+		    data.error + " (" + data.status_code + "): " +
+			data.error_description);
+		document.getElementById("tbl-genotypes").setAttribute(
+		    "data-datasets", JSON.stringify([]));
+		render_table(search_table);
+	    },
+	    "success": function(data, textStatus, jqXHR) {
+		document.getElementById("search-error").setAttribute(
+		    "style", "display: none;");
+		document.getElementById("tbl-genotypes").setAttribute(
+		    "data-datasets", JSON.stringify(data));
+		render_table(search_table);
+	    }
+	});
+}
+
+$(document).ready(function() {
+    let search_table = new TableDataSource(
+	"#tbl-genotypes", "data-datasets", search_checkbox);
+    let link_table = new TableDataSource(
+	"#tbl-link-genotypes", "data-selected-datasets", link_checkbox);
+
+    $("#frm-search-traits").submit(function(event) {
+	event.preventDefault();
+	return false;
+    });
+
+    $("#txt-query").keyup(debounce(search_genotypes));
+
+    $("#tbl-genotypes").on("change", ".checkbox-search", function(event) {
+        if(this.checked) {
+	    select_deselect_dataset(
+		JSON.parse(this.value), search_table, link_table);
+	    toggle_link_button();
+        }
+    });
+
+    $("#tbl-link-genotypes").on("change", ".checkbox-selected", function(event) {
+	if(!this.checked) {
+	    select_deselect_dataset(
+		JSON.parse(this.value), link_table, search_table);
+	    toggle_link_button();
+	}
+    });
+});
diff --git a/wqflask/wqflask/static/new/javascript/auth/search_mrna.js b/wqflask/wqflask/static/new/javascript/auth/search_mrna.js
new file mode 100644
index 00000000..e754ae76
--- /dev/null
+++ b/wqflask/wqflask/static/new/javascript/auth/search_mrna.js
@@ -0,0 +1,96 @@
+/**
+ * Check whether `dataset` is in array of `datasets`.
+ * @param {mRNADataset} A mrna dataset.
+ * @param {Array} An array of mrna datasets.
+ */
+function in_array(dataset, datasets) {
+    found = datasets.filter(function(dst) {
+	return (dst.SpeciesId == dataset.SpeciesId &&
+		dst.InbredSetId == dataset.InbredSetId &&
+		dst.ProbeFreezeId == dataset.ProbeFreezeId &&
+		dst.ProbeSetFreezeId == dataset.ProbeSetFreezeId);
+    });
+    return found.length > 0;
+}
+
+function toggle_link_button() {
+    num_groups = $("#frm-link select option").length - 1;
+    num_selected = JSON.parse(
+	$("#tbl-link").attr("data-datasets")).length;
+    if(num_groups > 0 && num_selected > 0) {
+	$("#frm-link input[type='submit']").prop("disabled", false);
+    } else {
+	$("#frm-link input[type='submit']").prop("disabled", true);
+    }
+}
+
+function search_mrna() {
+    query = document.getElementById("txt-query").value;
+    selected = JSON.parse(document.getElementById(
+	"tbl-link").getAttribute("data-datasets"));
+    species_name = document.getElementById("txt-species-name").value
+    search_endpoint = "/oauth2/data/mrna/search"
+    search_table = new TableDataSource(
+	"#tbl-search", "data-datasets", search_checkbox);
+    $.ajax(
+	search_endpoint,
+	{
+	    "method": "POST",
+	    "contentType": "application/json; charset=utf-8",
+	    "dataType": "json",
+	    "data": JSON.stringify({
+		"query": query,
+		"selected": selected,
+		"dataset_type": "mrna",
+		"species_name": species_name}),
+	    "error": function(jqXHR, textStatus, errorThrown) {
+		error_data = jqXHR.responseJSON
+		console.debug("ERROR_DATA:", error_data);
+		elt = document.getElementById("search-error").setAttribute(
+		    "style", "display: block;");
+		document.getElementById("search-error-text").innerHTML = (
+		    error_data.error + " (" + error_data.status_code + "): " +
+			error_data.error_description);
+		document.getElementById("tbl-search").setAttribute(
+		    "data-datasets", JSON.stringify([]));
+		render_table(search_table);
+	    },
+	    "success": function(data, textStatus, jqXHR) {
+		document.getElementById("search-error").setAttribute(
+		    "style", "display: none;");
+		document.getElementById("tbl-search").setAttribute(
+		    "data-datasets", JSON.stringify(data));
+		render_table(search_table);
+	    }
+	});
+}
+
+$(document).ready(function() {
+    let search_table = new TableDataSource(
+	"#tbl-search", "data-datasets", search_checkbox);
+    let link_table = new TableDataSource(
+	"#tbl-link", "data-datasets", link_checkbox);
+
+    $("#frm-search").submit(function(event) {
+	event.preventDefault();
+	return false;
+    });
+
+    $("#txt-query").keyup(debounce(search_mrna));
+
+    $("#tbl-search").on("change", ".checkbox-search", function(event) {
+        if(this.checked) {
+	    select_deselect_dataset(
+		JSON.parse(this.value), search_table, link_table);
+	    toggle_link_button();
+        }
+    });
+
+    $("#tbl-link").on("change", ".checkbox-selected", function(event) {
+	if(!this.checked) {
+	    select_deselect_dataset(
+		JSON.parse(this.value), link_table, search_table);
+	    toggle_link_button();
+	}
+    });
+});
diff --git a/wqflask/wqflask/static/new/javascript/auth/search_phenotypes.js b/wqflask/wqflask/static/new/javascript/auth/search_phenotypes.js
new file mode 100644
index 00000000..61e71771
--- /dev/null
+++ b/wqflask/wqflask/static/new/javascript/auth/search_phenotypes.js
@@ -0,0 +1,160 @@
+/**
+ * Global variables: Bad idea - figure out how to pass them down a call stack.
+ */
+search_table = new TableDataSource(
+    "#tbl-phenotypes", "data-traits", (trait) => {
+	return build_checkbox(trait, "search_traits", "checkbox-search");
+    });
+link_table = new TableDataSource(
+    "#tbl-link-phenotypes", "data-traits", (trait) => {
+	return build_checkbox(
+	    trait, "selected", "checkbox-selected", checked=true);
+    });
+
+/**
+ * Toggle the state for the "Link Traits" button
+ */
+function toggle_link_button() {
+    num_groups = $("#frm-link-phenotypes select option").length - 1;
+    num_selected = JSON.parse(
+	$("#tbl-link-phenotypes").attr("data-traits")).length;
+    if(num_groups > 0 && num_selected > 0) {
+	$("#frm-link-phenotypes input[type='submit']").prop("disabled", false);
+    } else {
+	$("#frm-link-phenotypes input[type='submit']").prop("disabled", true);
+    }
+}
+
+/**
+ * Default error function: print out debug messages
+ */
+function default_error_fn(jqXHR, textStatus, errorThrown) {
+    console.debug("XHR:", jqXHR);
+    console.debug("STATUS:", textStatus);
+    console.debug("ERROR:", errorThrown);
+}
+
+function render_pheno_table(table_data_source) {
+    table_id = table_data_source.table_id.selector;
+    data_attr_name = table_data_source.data_attribute_name;
+    $(table_id + " tbody tr").remove();
+    table_data = JSON.parse($(table_id).attr(data_attr_name));
+    if(table_data.length < 1) {
+	row = $("<tr>")
+	cell = $('<td colspan="100%" align="center">');
+	cell.append(
+	    $('<span class="glyphicon glyphicon-info-sign text-info">'));
+	cell.append("&nbsp;");
+	cell.append("No phenotype traits to select from.");
+	row.append(cell);
+	$(table_id + " tbody").append(row);
+    }
+    table_data.forEach(function(trait) {
+	row = $("<tr>")
+	row.append(table_data_source.checkbox_creation_function(trait));
+	row.append(table_cell(trait.name));
+	row.append(table_cell(trait.group));
+	row.append(table_cell(trait.dataset));
+	row.append(table_cell(trait.dataset_fullname));
+	row.append(table_cell(trait.description));
+	row.append(table_cell(trait.authors.join(", ")));
+	row.append(table_cell(
+	    '<a href="' + trait.pubmed_link +
+		'" title="Pubmed link for trait ' + trait.name + '.">' +
+		trait.year + "</a>"));
+	row.append(table_cell("Chr:" + trait.geno_chr + "@" + trait.geno_mb));
+	row.append(table_cell(trait.lrs));
+	row.append(table_cell(trait.additive));
+	$(table_id + " tbody").append(row);
+    });
+}
+
+function display_search_results(data, textStatus, jqXHR) {
+    if(data.status == "queued" || data.status == "started") {
+	setTimeout(() => {
+	    fetch_search_results(data.job_id, display_search_results);
+	}, 250);
+	return;
+    }
+    if(data.status == "completed") {
+	$("#tbl-phenotypes").attr(
+	    "data-traits", JSON.stringify(data.search_results));
+	render_pheno_table(search_table);
+    }
+    $("#txt-search").prop("disabled", false);
+}
+
+/**
+ * Fetch the search results
+ * @param {UUID}: The job id to fetch data for
+ */
+function fetch_search_results(job_id, success, error=default_error_fn) {
+    endpoint = $("#frm-search-traits").attr("data-search-results-endpoint");
+    $("#txt-search").prop("disabled", true);
+    $.ajax(
+	endpoint,
+	{
+	    "method": "GET",
+	    "contentType": "application/json; charset=utf-8",
+	    "dataType": "json",
+	    "error": error,
+	    "success": success
+	}
+    );
+}
+
+function search_phenotypes() {
+    query = document.getElementById("txt-query").value;
+    selected = JSON.parse(document.getElementById(
+	"tbl-link-phenotypes").getAttribute("data-traitss"));
+    species_name = document.getElementById("txt-species-name").value
+    per_page = document.getElementById("txt-per-page").value
+    search_table = new TableDataSource(
+	"#tbl-phenotypes", "data-traits", search_checkbox);
+    $.ajax(
+	"/oauth2/data/search",
+	{
+	    "method": "GET",
+	    "contentType": "application/json; charset=utf-8",
+	    "dataType": "json",
+	    "data": JSON.stringify({
+		"query": query,
+		"species_name": species_name,
+		"dataset_type": "phenotype",
+		"per_page": per_page
+	    }),
+	    "error": default_error_fn,
+	    "success": (data, textStatus, jqXHR) => {
+		fetch_search_results(data.job_id);
+	    }
+	});
+}
+
+$(document).ready(function() {
+    $("#frm-search-traits").submit(event => {
+	event.preventDefault();
+	return false;
+    });
+
+    $("#txt-query").keyup(debounce(search_phenotypes));
+
+    $("#tbl-phenotypes").on("change", ".checkbox-selected", function(event) {
+	if(this.checked) {
+	    select_deselect(JSON.parse(this.value), search_table, link_table);
+	    toggle_link_button();
+	}
+    });
+
+    $("#tbl-link-phenotypes").on("change", ".checkbox-search", function(event) {
+	if(!this.checked) {
+	    select_deselect(JSON.parse(this.value), search_table, link_table);
+	    toggle_link_button();
+	}
+    });
+
+    setTimeout(() => {
+	fetch_search_results(
+	    $("#tbl-phenotypes").attr("data-initial-job-id"),
+	    display_search_results);
+    }, 500);
+});
diff --git a/wqflask/wqflask/templates/correlation_page.html b/wqflask/wqflask/templates/correlation_page.html
index 29fdc5f1..a85ac19b 100644
--- a/wqflask/wqflask/templates/correlation_page.html
+++ b/wqflask/wqflask/templates/correlation_page.html
@@ -411,7 +411,7 @@
               'data': null,
               'width': "80px",
               'render': function(data) {
-                if (data.pubmed_id != "N/A"){
+                if (data.pubmed_link != "N/A"){
                   return '<a href="' + data.pubmed_link + '">' + data.pubmed_text + '</a>'
                 } else {
                   return data.pubmed_text
diff --git a/wqflask/wqflask/templates/generif.html b/wqflask/wqflask/templates/generif.html
new file mode 100644
index 00000000..d8229ac1
--- /dev/null
+++ b/wqflask/wqflask/templates/generif.html
@@ -0,0 +1,102 @@
+{% extends "base.html" %}
+
+{% block title %}
+GeneWiki Entry for {{ symbol }}
+{% endblock %}
+
+{% block css %}
+<style>
+
+ .badge {
+     vertical-align: top;
+     background-color: #336699;
+ }
+
+ .list-group {
+     counter-reset: gnentries;
+ }
+
+ summary::before {
+     counter-increment: gnentries;
+     content: counter(gnentries) "." " ";
+ }
+
+ summary:hover {
+     cursor: zoom-in;
+ }
+</style>
+
+{% endblock %}
+{% block content %}
+
+
+<div class="container">
+    <h1 class="page-header">GeneWiki For {{ symbol }}</h1>
+    <p class="well"><strong>GeneWiki</strong> enables you to enrich the annotation of genes and transcripts.</p>
+
+    <h3>
+	<strong>GeneNetwork</strong>
+	<span class="badge">
+	    {{ entries.gn_entries|length if entries.gn_entries[0] else 0 }}
+	</span>:
+    </h3>
+    {% if entries.gn_entries[0] %}
+    <ul class="list-group">
+	{% for entry in entries.gn_entries %}
+	<li class="list-group-item">
+	    <details>
+		<summary>
+		    {{ entry["entry"]["value"] }}
+		    {% if entry.get("weburl") %}
+		    glyphicon glyphicon-open
+		    <sup><small><a href="{{ entry.weburl.value }}" target="_blank"><span class="glyphicon glyphicon-globe" aria-hidden="true"></span> web</a></small></sup>
+		    {% endif %}
+		</summary>
+		<dl class="dl-horizontal">
+		    <dt>Author:</dt>
+		    <dd>{{ entry["author"]["value"] }}</dd>
+
+		    {% if entry.get("geneCategory") %}
+		    <dt>Category:</dt>
+		    <dd>{{ entry["geneCategory"]["value"]}}</dd>
+		    {% endif %}
+
+		    <dt>Add Time:</dt>
+		    <dd>{{ entry["created"]["value"]}}</dd>
+		</dl>
+	    </details>
+	</li>
+	{% endfor %}
+    </ul>
+
+    {% else %}
+
+    <p class="well"><u>There are no GeneNetwork entries for <b>{{ symbol }}.</b></u></p>
+
+    {% endif %}
+
+    <h3>
+	<strong>GeneRIF from NCBI</strong>
+	<span class="badge">
+	    {{ entries.ncbi_entries|length if entries.ncbi_entries[0] else 0 }}
+	</span>:
+    </h3>
+    {% if entries.ncbi_entries[0] %}
+	<ol>
+	{% for entry in entries.ncbi_entries %}
+	<li>
+	    {{ entry.entry.value }}
+	    (<a href="{{ entry['generif']['value'] }}" target="_blank">{{ entry["speciesBinomialName"]["value"] }}</a>)
+            {% if entry.PubMedId.value != "" %}
+	    {% set pmids = entry.PubMedId.value.split(",") %}
+	    (PubMed: {% for id in pmids %} <a href="http://rdf.ncbi.nlm.nih.gov/pubmed/{{ id }}" target="_blank">{{ id }}</a>{% endfor %})
+	    <sup><small><em>{{ entry.createdOn.value }}</em></small></sup>
+	    {% endif %}
+	</li>
+	{% endfor %}
+    </ol>
+    {% else %}
+    <p class="well"><u>There are no NCBI entries for <b>{{ symbol }}.</b></u></p>
+    {% endif %}
+</div>
+{% endblock %}
diff --git a/wqflask/wqflask/templates/oauth2/data-list-genotype.html b/wqflask/wqflask/templates/oauth2/data-list-genotype.html
index 3ef810ec..c780a583 100644
--- a/wqflask/wqflask/templates/oauth2/data-list-genotype.html
+++ b/wqflask/wqflask/templates/oauth2/data-list-genotype.html
@@ -23,8 +23,11 @@
   </div>
 
   <div class="row">
-    <form id="frm-link-genotypes">
-      <legend>Link Genotype Traits to Group</legend>
+    <form id="frm-link-genotypes" method="POST"
+	  action="{{url_for('oauth2.data.link_genotype_data')}}">
+      <legend>Link Genotype Datasets to Group</legend>
+
+      <input type="hidden" name="species_name" value="{{species_name}}" />
 
       <div class="form-group">
 	<label for="select-group">Group</label>
@@ -54,8 +57,8 @@
 	  {%for dataset in selected_datasets%}
 	  <tr>
 	    <td>
-	      <input type="checkbox" class="checkbox checkbox-link"
-		     name="selected_datasets"
+	      <input type="checkbox" class="checkbox checkbox-selected"
+		     name="selected"
 		     value='{{dataset | tojson}}' />
 	    </td>
 	    <td>{{dataset.dataset_name}}</td>
@@ -88,12 +91,12 @@
 
   <div class="row">
     <span id="search-messages" class="alert-danger" style="display:none"></span>
-    <form id="frm-search-traits"
-	  action="#"
+    <form id="frm-search"
+	  action="{{search_uri}}"
 	  method="POST">
       <legend>Search: Genotype</legend>
       <input type="hidden" value="{{species_name}}" name="species"
-	     id="txt-species" />
+	     id="txt-species-name" />
       <input type="hidden" value="{{dataset_type}}" name="dataset_type"
 	     id="txt-dataset-type"  />
       <input type="hidden" value="{{per_page}}" name="per_page"
@@ -108,6 +111,11 @@
   </div>
 
   <div class="row">
+    <div id="search-error" class="text-danger" style="display: none;">
+      <span class="glyphicon glyphicon-exclamation-sign"></span>
+      &nbsp
+      <span id="search-error-text"></span>
+    </div>
     <table id="tbl-genotypes"
 	   class="table-hover table-striped cell-border dataTable no-footer"
 	   data-datasets='{{datasets | list | tojson}}'>
@@ -151,102 +159,8 @@
 {%endblock%}
 
 {%block js%}
-<script language="javascript" type="text/javascript">
-  function link_checkbox(dataset) {
-      cell = $("<td>");
-      check = $('<input type="checkbox" class="checkbox checkbox-selected" ' +
-		'name="selected_datasets" checked="checked">');
-      check.val(JSON.stringify(dataset));
-      cell.append(check);
-      return cell;
-  }
-
-  function table_cell(value) {
-      cell = $("<td>");
-      cell.html(value);
-      return cell;
-  }
-
-  function render_table(table_id, data_attr_name) {
-      $(table_id + " tbody tr").remove();
-      table_data = JSON.parse($(table_id).attr(data_attr_name));
-      if(table_data.length < 1) {
-	  row = $("<tr>")
-	  cell = $('<td colspan="100%" align="center">');
-	  cell.append(
-	      $('<span class="glyphicon glyphicon-info-sign text-info">'));
-	  cell.append("&nbsp;");
-	  cell.append("No genotype datasets remaining.");
-	  row.append(cell);
-	  $(table_id + " tbody").append(row);
-      }
-      table_data.forEach(function(dataset) {
-	  row = $("<tr>")
-	  row.append(link_checkbox(dataset));
-	  row.append(table_cell(dataset.InbredSetName));
-	  row.append(table_cell(dataset.dataset_name));
-	  row.append(table_cell(dataset.dataset_fullname));
-	  row.append(table_cell(dataset.dataset_shortname));
-	  $(table_id + " tbody").append(row);
-      });
-  }
-
-  function in_array(dataset, datasets) {
-      found = datasets.filter(function(dst) {
-	  return (dst.SpeciesId == dataset.SpeciesId &&
-		  dst.InbredSetId == dataset.InbredSetId &&
-		  dst.GenoFreezeId == dataset.GenoFreezeId);
-      });
-      return found.length > 0;
-  }
-
-  function remove_from_table_data(dataset, table_id, data_attr_name) {
-      without_dataset = JSON.parse($(table_id).attr(data_attr_name)).filter(
-	  function(dst) {
-	      return !(dst.SpeciesId == dataset.SpeciesId &&
-		       dst.InbredSetId == dataset.InbredSetId &&
-		       dst.GenoFreezeId == dataset.GenoFreezeId);
-	  });
-      $(table_id).attr(data_attr_name, JSON.stringify(without_dataset));
-  }
-
-  function toggle_link_button() {
-      num_groups = $("#frm-link-genotypes select option").length - 1;
-      num_selected = JSON.parse(
-	  $("#tbl-link-genotypes").attr("data-selected-datasets")).length;
-      if(num_groups > 0 && num_selected > 0) {
-	  $("#frm-link-genotypes input[type='submit']").prop("disabled", false);
-      } else {
-	  $("#frm-link-genotypes input[type='submit']").prop("disabled", true);
-      }
-  }
-
-  $(document).ready(function() {
-      $("#frm-search-traits").submit(function(event) {
-	  event.preventDefault();
-	  return false;
-      });
-      /* $("#txt-query").keyup(debounced_search()); */
-      $(".checkbox-search").change(function(event) {
-          if(this.checked) {
-              selected = JSON.parse(
-		  $("#tbl-link-genotypes").attr("data-selected-datasets"));
-	      this_item = JSON.parse(this.value);
-	      if(!in_array(this_item, selected)) {
-		  selected.push(this_item);
-	      }
-	      $("#tbl-link-genotypes").attr(
-		  "data-selected-datasets",
-		  JSON.stringify(Array.from(selected)));
-	      /* Remove from source table */
-	      remove_from_table_data(
-		  this_item, "#tbl-genotypes", "data-datasets");
-	      /* Re-render tables */
-	      render_table("#tbl-link-genotypes", "data-selected-datasets");
-	      render_table("#tbl-genotypes", "data-datasets");
-	      toggle_link_button();
-          }
-      });
-  });
-</script>
+<script src="/static/new/javascript/auth/search.js"
+	language="javascript" type="text/javascript"></script>
+<script src="/static/new/javascript/auth/search_genotypes.js"
+	language="javascript" type="text/javascript"></script>
 {%endblock%}
diff --git a/wqflask/wqflask/templates/oauth2/data-list-mrna.html b/wqflask/wqflask/templates/oauth2/data-list-mrna.html
new file mode 100644
index 00000000..0e163235
--- /dev/null
+++ b/wqflask/wqflask/templates/oauth2/data-list-mrna.html
@@ -0,0 +1,168 @@
+{%extends "base.html"%}
+{%from "oauth2/profile_nav.html" import profile_nav%}
+{%from "oauth2/display_error.html" import display_error%}
+
+{%block title%}Link Data: Genotype{%endblock%}
+
+{%block css%}
+<link rel="stylesheet" type="text/css"
+      href="/css/DataTables/css/jquery.dataTables.css" />
+<link rel="stylesheet" type="text/css"
+      href="https://code.jquery.com/ui/1.12.1/themes/base/jquery-ui.css" />
+<link rel="stylesheet" type="text/css" href="/static/new/css/show_trait.css" />
+{%endblock%}
+
+{%block content%}
+<div class="container" style="width: 98%;">
+  {{profile_nav("data", user_privileges)}}
+
+  {{flash_me()}}
+
+  <div class="row">
+    <noscript>This page needs javascript to work correctly</noscript>
+  </div>
+
+  <div class="row">
+    <form id="frm-link" method="POST"
+	  action="{{url_for('oauth2.data.link_mrna_data')}}">
+      <legend>Link mRNA Assay Datasets to Group</legend>
+
+      <input type="hidden" name="species_name" value="{{species_name}}" />
+
+      <div class="form-group">
+	<label for="select-group">Group</label>
+	<select id="select-group" name="group_id" required="required"
+		class="form-control">
+	  <option value="">Select group</option>
+	  {%for group in groups%}
+	  <option value="{{group.group_id}}">{{group.group_name}}</option>
+	  {%endfor%}
+	</select>
+      </div>
+
+      <div class="form-group">
+      <table id="tbl-link"
+	     class="table-hover table-striped cell-border dataTable no-footer"
+	     data-datasets='{{selected_datasets | list | tojson}}'>
+	<thead>
+	  <tr>
+	    <th>Deselect</th>
+	    <th>Group</th>
+	    <th>Dataset Name</th>
+	    <th>Dataset FullName</th>
+	    <th>Dataset ShortName</th>
+	  </tr>
+	</thead>
+	<tbody>
+	  {%for dataset in selected_datasets%}
+	  <tr>
+	    <td>
+	      <input type="checkbox" class="checkbox checkbox-selected"
+		     name="selected"
+		     value='{{dataset | tojson}}' />
+	    </td>
+	    <td>{{dataset.dataset_name}}</td>
+	    <td>{{dataset.dataset_fullname}}</td>
+	    <td>{{dataset.dataset_shortname}}</td>
+	  </tr>
+	  {%else%}
+	  <tr>
+	    <td colspan="100%" align="center">
+	      <span class="glyphicon glyphicon-info-sign text-info"></span>
+	      &nbsp
+	      No datasets selected for linking.
+	    </td>
+	  </tr>
+	  {%endfor%}
+	</tbody>
+      </table>
+      </div>
+
+      <div class="form-group text-center">
+	<input type="submit" value="Link Selected"
+	       class="btn btn-primary"
+	       style="border-top: 0.3em;"
+	       {%if groups | length <= 0 or selected_datasets | length <= 0%}
+	       disabled="disabled"
+	       {%endif%} />
+      </div>
+    </form>
+  </div>
+
+  <div class="row">
+    <span id="search-messages" class="alert-danger" style="display:none"></span>
+    <form id="frm-search"
+	  action="{{search_uri}}"
+	  method="POST">
+      <legend>Search: mRNA Assay</legend>
+      <input type="hidden" value="{{species_name}}" name="species"
+	     id="txt-species-name" />
+      <input type="hidden" value="{{dataset_type}}" name="dataset_type"
+	     id="txt-dataset-type"  />
+      <input type="hidden" value="{{per_page}}" name="per_page"
+	     id="txt-per-page"  />
+
+      <div class="form-group">
+	<label for="txt-query">Dataset Search String</label>
+	<input type="text" id="txt-query" name="query" class="form-control"
+	       value="{{query}}"/>
+      </div>
+    </form>
+  </div>
+
+  <div class="row">
+    <div id="search-error" class="text-danger" style="display: none;">
+      <span class="glyphicon glyphicon-exclamation-sign"></span>
+      &nbsp
+      <span id="search-error-text"></span>
+    </div>
+    <table id="tbl-search"
+	   class="table-hover table-striped cell-border dataTable no-footer"
+	   data-datasets='{{datasets | list | tojson}}'>
+      <thead>
+	<tr>
+	  <th>Select</th>
+	  <th>Group</th>
+	  <th>Study Name</th>
+	  <th>Dataset Name</th>
+	  <th>Dataset FullName</th>
+	  <th>Dataset ShortName</th>
+	</tr>
+      </thead>
+      <tbody>
+	{%for dataset in datasets:%}
+	<tr>
+	  <td>
+	    <input type="checkbox" class="checkbox checkbox-search"
+		   name="search_datasets"
+		   value='{{dataset | tojson}}' />
+	  </td>
+	  <td>{{dataset.InbredSetName}}</td>
+	  <td>{{dataset.StudyName}}</td>
+	  <td>{{dataset.dataset_name}}</td>
+	  <td>{{dataset.dataset_fullname}}</td>
+	  <td>{{dataset.dataset_shortname}}</td>
+	</tr>
+        {%else%}
+	<tr>
+	  <td colspan="100%" align="center">
+	      <span class="glyphicon glyphicon-info-sign text-info"></span>
+	      &nbsp
+	      No datasets available for selection.
+	  </td>
+	</tr>
+	{%endfor%}
+      </tbody>
+    </table>
+  </div>
+
+
+</div>
+{%endblock%}
+
+{%block js%}
+<script src="/static/new/javascript/auth/search.js"
+	language="javascript" type="text/javascript"></script>
+<script src="/static/new/javascript/auth/search_mrna.js"
+	language="javascript" type="text/javascript"></script>
+{%endblock%}
diff --git a/wqflask/wqflask/templates/oauth2/data-list-phenotype.html b/wqflask/wqflask/templates/oauth2/data-list-phenotype.html
index 53c6ce8c..6afabdf8 100644
--- a/wqflask/wqflask/templates/oauth2/data-list-phenotype.html
+++ b/wqflask/wqflask/templates/oauth2/data-list-phenotype.html
@@ -40,7 +40,9 @@
 	</select>
       </div>
 
-      <table id="tbl-link-phenotypes" class="table-hover table-striped cell-border">
+      <table id="tbl-link-phenotypes"
+	     class="table-hover table-striped cell-border dataTable no-footer"
+	     data-traits="[]">
 	<tbody>
 	  <tr>
 	    <td colspan="100%" align="center" style="text-align: center;">
@@ -53,12 +55,14 @@
 	  </tr>
       </table>
 
-      {%if groups | length > 0%}
-      <input type="submit" value="Link Selected" class="btn btn-primary" />
-      {%else%}
-      <input type="submit" value="No group to link to" class="btn btn-warning"
-	     disabled="disabled" />
-      {%endif%}
+      <div class="form-group text-center">
+	<input type="submit" value="Link Selected"
+	       class="btn btn-primary"
+	       style="border-top: 0.3em;"
+	       {%if groups | length <= 0 or traits | length <= 0%}
+	       disabled="disabled"
+	       {%endif%} />
+      </div>
     </form>
   </div>
 
@@ -66,7 +70,8 @@
     <span id="search-messages" class="alert-danger" style="display:none"></span>
     <form id="frm-search-traits"
 	  action="#"
-	  method="POST">
+	  method="POST"
+	  data-search-results-endpoint="{{results_endpoint}}">
       {%if dataset_type == "mrna"%}
       <legend>mRNA: Search</legend>
       {%else%}
@@ -75,7 +80,7 @@
       </legend>
       {%endif%}
       <input type="hidden" value="{{species_name}}" name="species"
-	     id="txt-species" />
+	     id="txt-species-name" />
       <input type="hidden" value="{{dataset_type}}" name="dataset_type"
 	     id="txt-dataset-type"  />
       <input type="hidden" value="{{per_page}}" name="per_page"
@@ -84,23 +89,68 @@
       <div class="form-group">
 	<label for="txt-query">Search</label>
 	<div class="input-group">
-	  <span class="input-group-addon">species:{{species_name}} AND (</span>
+	  <span class="input-group-addon">species:{{species_name}} AND </span>
 	  <input type="text" id="txt-query" name="query" class="form-control"
 		 value="{{query}}"/>
-	  <span class="input-group-addon">)</span>
 	</div>
       </div>
     </form>
   </div>
 
   <div class="row">
-    <table id="tbl-phenotypes" class="table-hover table-striped cell-border">
+    <table id="tbl-phenotypes"
+	   class="table-hover table-striped cell-border dataTable no-footer"
+	   data-traits="{{traits | tojson}}"
+	   data-initial-job-id={{search_results.job_id}}
+	   data-initial-search-res={{search_results | tojson}}>
+      <thead>
+	<tr>
+	  <th>Select</th>
+	  <th>Name</th>
+	  <th>Group</th>
+	  <th>Dataset</th>
+	  <th>Dataset Fullname</th>
+	  <th>Description</th>
+	  <th>Authors</th>
+	  <th>Year</th>
+	  <th>Location</th>
+	  <th>LRS</th>
+	  <th>Additive</th>
+	</tr>
+      </thead>
       <tbody>
+	{%for trait in traits%}
+	<tr>
+	  <th>
+	    <input type="checkbox" class="checkbox checkbox-search"
+		   name="search_traits" value="{{trait | tojson}}" />
+	  </th>
+	  <th>{{trait.name}}</th>
+	  <th>{{trait.group}}</th>
+	  <th>{{trait.dataset}}</th>
+	  <th>{{trait.dataset_fullname}}</th>
+	  <th>{{trait.description}}</th>
+	  <th>{{trait.authors | join(" ")}}</th>
+	  <th>
+	    <a href="{{trait.pubmed_linj}}" title="Pubmed link for trait">
+	      {{trait.year}}
+	    </a>
+	  </th>
+	  <th>CHR{{trait.geno_chr}}@{{trait.geno_mb}}</th>
+	  <th>{{trait.lrs}}</th>
+	  <th>{{trait.additive}}</th>
+	</tr>
+	{%else%}
 	<tr>
-	  <td colspan="100%" align="center">
-	    <br><b><font size="15">Loading...</font></b><br>
+	  <td colspan="100%" align="center" style="text-align: center;">
+	    <br/><b><font size="4">
+		<span class="glyphicon glyphicon-info-sign text-info"></span>
+		&nbsp;
+		There are no phenotype traits to select from.
+	    </font></b><br />
 	  </td>
 	</tr>
+	{%endfor%}
     </table>
   </div>
 
@@ -110,134 +160,7 @@
 
 {%block js%}
 <script language="javascript" type="text/javascript"
-	src="/js/DataTables/js/jquery.dataTables.min.js"></script>
-
-<script language="javascript" type="text/javascript"
-	src="/js/DataTablesExtensions/plugins/sorting/natural.js"></script>
-
-<script language="javascript" type="text/javascript"
-	src="/js/DataTablesExtensions/colReorder/js/dataTables.colReorder.js">
-</script>
-
-<script language="javascript" type="text/javascript"
-	src="/js/DataTablesExtensions/colResize/dataTables.colResize.js">
-</script>
-
+	src="/static/new/javascript/auth/search.js"></script>
 <script language="javascript" type="text/javascript"
-	src="/static/new/javascript/create_datatable.js"></script>
-
-<script language="javascript" type="text/javascript">
-  function init_table(table_id, traits) {
-      create_table(
-	  tableId=table_id, tableData=traits,
-	  columnDefs=[
-	      {"data": null, "render": function(data) {
-		  return (
-		      '<input type="checkbox" ' +
-			  (table_id == 'tbltbl-phenotypes' ?
-			   'name="pheno_traits" ': 'name="selected_traits" ') +
-			  'class="checkbox" value="' +
-			  data.name + ':' + data.dataset +
-			  '" />');
-	      }},
-	      {"title": "Index", "data": "index"},
-	      {"title": "Dataset", "data": "dataset_fullname"},
-	      {"title": "Group", "data": "group"},
-	      {"title": "Record", "data": null, "render": function(data) {
-		  return (
-		      '<a target="_blank" href="/show_trait?trait_id=' +
-			  data.name + '&dataset=' + data.dataset +
-			  '">' + data.name + "</a>");
-	      }},
-	      {"title": "Description", "data": "description"},
-	      {"title": "Authors", "data": "authors"},
-	      {"title": "Year", "data": null, render: function(data) {
-		  return (
-		      '<a target="_blank" href="' + data.pubmed_link +
-			  '" >' + data.year + '</a>');
-	      }},
-	      {"title": "LRS", "data": null, "render": function(data) {
-		  return data.lrs ? data.lrs.toFixed(4) : "N/A";
-	      }},
-	      {"title": "Peak Location", "data": null, "render": function(data) {
-		  return 'Chr' + data.geno_chr + ': ' + data.geno_mb;
-	      }},
-	      {"title": "Additive Effects", "data": null, "render": function(data) {
-		  return data.additive ? data.additive.toFixed(4) : "N/A";
-	      }}],
-	  customSettings = {
-	      "scrollY": "40vh",
-	      "language": {
-		  "emptyTable": "No traits to display!",
-		  "info": "Showing _START_ to _END_ of _TOTAL_ entries",
-		  "infoEmpty": "No entries to show",
-		  "loadingRecords": "Loading entries ..."
-	      }
-	  });
-  }
-
-  function add_index(item, index) {
-      return {"index": index, ...item};
-  }
-
-  function do_search() {
-      /*vent.preventDefault();*/
-      user_query = $("#txt-query").val();
-      dataset_type = $("#txt-dataset-type").val();
-      per_page = $("#txt-per-page").val();
-      species = $("#txt-species").val();
-      query = "species:" + species;
-      if (user_query.length > 2) {
-	  query = query + " AND (" + user_query + ")";
-      }
-      $("#search-messages").html("");
-      $("#search-messages").attr("style", "display:none;");
-      $.ajax(
-	  "{{search_endpoint}}",
-	  {
-	      "method": "GET",
-	      "data": {
-		  "type": dataset_type,
-		  "per_page": per_page,
-		  "query": query
-	      },
-	      "error": function(jqXHR, textStatus, errorThrown) {
-		  msg_elt = $("#search-messages")
-		  console.debug(jqXHR)
-		  $("#search-messages").html(
-		      "<strong>" + textStatus + "</strong>: Search for '" +
-		      user_query + "' failed! Try a different search.");
-		  $("#search-messages").attr("style", "display: block;");
-	      },
-	      "success": function (data, textStatus, jqXHR) {
-		  init_table("tbl-phenotypes", data.map(add_index));
-	      }
-	  });
-  }
-
-  function debounced_search() {
-      var timeout;
-      return function search(event) {
-	  clearTimeout(timeout);
-	  timeout = setTimeout(do_search, 500);
-      };
-  }
-
-  function sanitised_table_data(data) {
-      if(data && data.length > 0) {
-	  return data
-      }
-      return null
-  }
-
-  $(document).ready(function() {
-      $("#frm-search-traits").submit(function(event) {
-	  event.preventDefault();
-	  return false;
-      });
-      $("#txt-query").keyup(debounced_search())
-      init_table("tbl-phenotypes", {{traits | list | tojson}});
-      init_table("tbl-link-phenotypes", {{selected_traits | list | tojson}});
-  });
-</script>
+	src="/static/new/javascript/auth/search_phenotypes.js"></script>
 {%endblock%}
diff --git a/wqflask/wqflask/templates/oauth2/group.html b/wqflask/wqflask/templates/oauth2/group.html
index a6a3e6c3..a2380df7 100644
--- a/wqflask/wqflask/templates/oauth2/group.html
+++ b/wqflask/wqflask/templates/oauth2/group.html
@@ -8,6 +8,11 @@
 
   {{flash_me()}}
 
+  {%if group_error is defined%}
+  <div class="row" style="text-align:center;line-height:5em;">
+    {{display_error("Group", group_error)}}
+  </div>
+  {%else%}
   <div class="container-fluid">
     <div class="row">
       {%if group_join_requests_error is defined %}
@@ -103,6 +108,7 @@
     </table>
 
   </div>
+  {%endif%}
 
 </div>
 {%endblock%}
diff --git a/wqflask/wqflask/templates/show_trait_details.html b/wqflask/wqflask/templates/show_trait_details.html
index 2a94bfe1..83ab1482 100644
--- a/wqflask/wqflask/templates/show_trait_details.html
+++ b/wqflask/wqflask/templates/show_trait_details.html
@@ -224,7 +224,8 @@
         <button type="button" class="btn btn-default" title="Check probe locations at UCSC" onclick="window.open('{{ UCSC_BLAT_URL }}', '_blank')">Verify</button>
         {% endif %}
         {% if this_trait.symbol != None %}
-        <button type="button" class="btn btn-default" title="Write or review comments about this gene" onclick="window.open('http://gn1.genenetwork.org/webqtl/main.py?FormID=geneWiki&symbol={{ this_trait.symbol }}', '_blank')">GeneWiki</button>
+	<button type="button" class="btn btn-default" title="Write or review comments about this gene" onclick="window.open('http://gn1.genenetwork.org/webqtl/main.py?FormID=geneWiki&symbol={{ this_trait.symbol }}', '_blank')">(GN1) GeneWiki</button>
+        <button type="button" class="btn btn-default" title="Write or review comments about this gene" onclick="window.open('{{ url_for('display_generif_page', symbol=this_trait.symbol) }}', '_blank')">(GN2) GeneWiki</button>
         {% if dataset.group.species == "mouse" or dataset.group.species == "rat" %}
         <button type="button" class="btn btn-default" title="View SNPs and Indels" onclick="window.open('/snp_browser?first_run=true&species={{ dataset.group.species }}&gene_name={{ this_trait.symbol }}&limit_strains=on', '_blank')">SNPs</button>
         {% endif %}
diff --git a/wqflask/wqflask/views.py b/wqflask/wqflask/views.py
index 69576cc4..24426539 100644
--- a/wqflask/wqflask/views.py
+++ b/wqflask/wqflask/views.py
@@ -1048,3 +1048,19 @@ def display_diffs_users():
                        files)
     return render_template("display_files_user.html",
                            files=files)
+
+
+@app.route("/genewiki/<symbol>")
+def display_generif_page(symbol):
+    """Fetch GeneRIF metadata from GN3 and display it"""
+    entries = requests.get(
+        urljoin(
+            GN3_LOCAL_URL,
+            f"/api/metadata/genewiki/{symbol}"
+        )
+    ).json()
+    return render_template(
+        "generif.html",
+        symbol=symbol,
+        entries=entries
+    )