diff options
Diffstat (limited to 'gn2/wqflask/collect.py')
-rw-r--r-- | gn2/wqflask/collect.py | 396 |
1 files changed, 396 insertions, 0 deletions
diff --git a/gn2/wqflask/collect.py b/gn2/wqflask/collect.py new file mode 100644 index 00000000..21168908 --- /dev/null +++ b/gn2/wqflask/collect.py @@ -0,0 +1,396 @@ +import os +import uuid +import hashlib +import datetime +import simplejson as json +from urllib.parse import urljoin + +from flask import g +from flask import render_template +from flask import url_for +from flask import request +from flask import redirect +from flask import flash +from flask import current_app + +from gn2.wqflask import app +from gn2.utility import hmac +from gn2.utility.formatting import numify +from gn2.utility.tools import GN_SERVER_URL, TEMPDIR +from gn2.utility.redis_tools import get_redis_conn + +from gn2.base.trait import create_trait +from gn2.base.trait import retrieve_trait_info +from gn2.base.trait import jsonable +from gn2.base.data_set import create_dataset + +from gn2.wqflask.oauth2 import client +from gn2.wqflask.oauth2 import session +from gn2.wqflask.oauth2.session import session_info +from gn2.wqflask.oauth2.checks import user_logged_in +from gn2.wqflask.oauth2.request_utils import ( + process_error, with_flash_error, with_flash_success) +from gn2.wqflask.oauth2.client import ( + oauth2_get, oauth2_post, no_token_get, no_token_post) + + +Redis = get_redis_conn() + + +def process_traits(unprocessed_traits): + if isinstance(unprocessed_traits, bytes): + unprocessed_traits = unprocessed_traits.decode('utf-8').split(",") + else: # It's a string + unprocessed_traits = unprocessed_traits.split(",") + traits = set() + for trait in unprocessed_traits: + data, _separator, the_hmac = trait.rpartition(':') + data = data.strip() + if g.user_session.logged_in: + assert the_hmac == hmac.hmac_creation(data), "Data tampering?" + traits.add(str(data)) + + return tuple(traits) + + +def report_change(len_before, len_now): + new_length = len_now - len_before + if new_length: + flash("We've added {} to your collection.".format( + numify(new_length, 'new trait', 'new traits'))) + + +@app.route("/collections/store_trait_list", methods=('POST',)) +def store_traits_list(): + params = request.form + + traits = params['traits'] + hash = params['hash'] + + Redis.set(hash, traits) + + return hash + + +@app.route("/collections/add", methods=["POST"]) +def collections_add(): + anon_id = session_info()["anon_id"] + traits = request.args.get("traits", request.form.get("traits")) + the_hash = request.args.get("hash", request.form.get("hash")) + collections = g.user_session.user_collections + collections = oauth2_get("auth/user/collections/list").either( + lambda _err: tuple(), lambda colls: tuple(colls)) + no_token_get( + f"auth/user/collections/{anon_id}/list").either( + lambda _err: tuple(), lambda colls: tuple(colls)) + + def __create_new_coll_error__(error): + err = process_error(error) + flash(f"{err['error']}:{err['error_description']}", "alert-danger") + return redirect("/") + + if len(collections) < 1: + new_coll = client.post( + "auth/user/collections/new", + json={ + "anon_id": str(anon_id), + "name": "Your Default Collection", + "traits": [] + }).either(__create_new_coll_error__, lambda coll: coll) + collections = (new_coll,) + + if bool(traits): + return render_template("collections/add.html", + traits=traits, + collections=collections) + else: + return render_template("collections/add.html", + hash=the_hash, + collections=collections) + +def __compute_traits__(params): + if "hash" in params: + unprocessed_traits = Redis.get(params['hash']) or "" + Redis.delete(params['hash']) + else: + unprocessed_traits = params['traits'] + return process_traits(unprocessed_traits) + +@app.route("/collections/new", methods=["POST"]) +def collections_new(): + params = request.form + anon_id = session_info()["anon_id"] + + if "sign_in" in params: + return redirect(url_for('login')) + if "create_new" in params: + collection_name = ( + params.get("new_collection", "").strip() or + datetime.datetime.utcnow().strftime('Collection_%b_%d_%H:%M')) + request_data = { + "uri_path": "auth/user/collections/new", + "json": { + "name": collection_name, + "anon_id": str(anon_id), + "traits": __compute_traits__(params), + "hash": params.get("hash", False) + }} + if user_logged_in(): + resp = oauth2_post(**request_data) + else: + resp = no_token_post(**request_data) + #return create_new(collection_name) + def __error__(err): + error = process_error(err) + flash(f"{error['error']}: {error['error_description']}", + "alert-danger") + return redirect("/") + def __view_collection__(collection): + return redirect(url_for("view_collection", uc_id=collection["id"])) + return resp.either(__error__, __view_collection__) + elif "add_to_existing" in params: + traits = process_traits(params["traits"]) + coll_id, *_coll_name = tuple( + part.strip() for part in params["existing_collection"].split(":")) + collection_id = uuid.UUID(coll_id) + resp = redirect(url_for('view_collection', uc_id=collection_id)) + return client.post( + f"auth/user/collections/{collection_id}/traits/add", + json={ + "anon_id": str(anon_id), + "traits": traits + }).either( + with_flash_error(resp), with_flash_success(resp)) + else: + # CauseAnError + pass + + +def create_new(collection_name): + params = request.args + if "hash" in params: + unprocessed_traits = Redis.get(params['hash']) + Redis.delete(params['hash']) + else: + unprocessed_traits = params['traits'] + + traits = process_traits(unprocessed_traits) + + uc_id = g.user_session.add_collection(collection_name, traits) + + return redirect(url_for('view_collection', uc_id=uc_id)) + + +@app.route("/collections/list") +def list_collections(): + params = request.args + anon_id = session.session_info()["anon_id"] + anon_collections = no_token_get( + f"auth/user/collections/{anon_id}/list").either( + lambda err: {"anon_collections_error": process_error(err)}, + lambda colls: {"anon_collections": colls}) + + user_collections = {"collections": []} + if user_logged_in(): + user_collections = oauth2_get("auth/user/collections/list").either( + lambda err: {"user_collections_error": process_error(err)}, + lambda colls: {"collections": colls}) + + return render_template("collections/list.html", + params=params, + **user_collections, + **anon_collections) + +@app.route("/collections/handle_anonymous", methods=["POST"]) +def handle_anonymous_collections(): + """Handle any anonymous collection on logging in.""" + choice = request.form.get("anon_choice") + if choice not in ("import", "delete"): + flash("Invalid choice!", "alert-danger") + return redirect("/") + def __impdel_error__(err): + error = process_error(err) + flash(f"{error['error']}: {error['error_description']}", + "alert-danger") + return redirect("/") + def __impdel_success__(msg): + flash(f"Success: {msg['message']}", "alert-success") + return redirect("/") + return oauth2_post( + f"auth/user/collections/anonymous/{choice}", + json={ + "anon_id": str(session_info()["anon_id"]) + }).either(__impdel_error__, __impdel_success__) + +@app.route("/collections/remove", methods=('POST',)) +def remove_traits(): + params = request.form + uc_id = params['uc_id'] + traits_to_remove = process_traits(params['trait_list']) + resp = redirect(url_for("view_collection", uc_id=uc_id)) + return client.post( + f"auth/user/collections/{uc_id}/traits/remove", + json = { + "anon_id": str(session_info()["anon_id"]), + "traits": traits_to_remove + }).either(with_flash_error(resp), with_flash_success(resp)) + + +@app.route("/collections/delete", methods=('POST',)) +def delete_collection(): + def __error__(err): + error = process_error(err) + flash(f"{error['error']}: {error['error_description']}", + "alert-danger") + return redirect(url_for('list_collections')) + + def __success__(msg): + flash(msg["message"], "alert-success") + return redirect(url_for('list_collections')) + + uc_ids = [item for item in request.form.get("uc_id", "").split(":") + if bool(item)] + if len(uc_ids) > 0: + return (oauth2_post if user_logged_in() else no_token_post)( + "auth/user/collections/delete", + json = { + "anon_id": str(session_info()["anon_id"]), + "collection_ids": uc_ids + }).either( + __error__, __success__) + + flash("Nothing to delete.", "alert-info") + return redirect(url_for('list_collections')) + + +def trait_info_str(trait): + """Provide a string representation for given trait""" + def __trait_desc(trt): + if trait.dataset.type == "Geno": + return f"Marker: {trt.name}" + if hasattr(trt, "description_display"): + return trt.description_display + else: + return "N/A" + + def __symbol(trt): + return (trt.symbol or trt.abbreviation or "N/A")[:20] + + def __lrs(trt): + if trait.dataset.type == "Geno": + return 0 + else: + if trait.LRS_score_repr != "N/A": + return ( + f"{float(trait.LRS_score_repr):0.3f}" if float(trait.LRS_score_repr) > 0 + else f"{trait.LRS_score_repr}") + else: + return "N/A" + + def __lrs_location(trt): + if hasattr(trt, "LRS_location_repr"): + return trt.LRS_location_repr + else: + return "N/A" + + def __location(trt): + if hasattr(trt, "location_repr"): + return trt.location_repr + return None + + def __mean(trt): + if trait.mean: + return trt.mean + else: + return 0 + + return "{}|||{}|||{}|||{}|||{}|||{:0.3f}|||{}|||{}".format( + trait.name, trait.dataset.name, __trait_desc(trait), __symbol(trait), + __location(trait), __mean(trait), __lrs(trait), __lrs_location(trait)) + +@app.route("/collections/import", methods=('POST',)) +def import_collection(): + import_file = request.files['import_file'] + if import_file.filename != '': + file_path = os.path.join(TEMPDIR, import_file.filename) + import_file.save(file_path) + collection_csv = open(file_path, "r") + traits = [row.strip() for row in collection_csv if row[0] != "#"] + os.remove(file_path) + + return json.dumps(traits) + else: + return render_template( + "collections/list.html") + +@app.route("/collections/view") +def view_collection(): + params = request.args + + uc_id = params['uc_id'] + request_data = { + "uri_path": f"auth/user/collections/{uc_id}/view", + "json": {"anon_id": str(session_info()["anon_id"])} + } + if user_logged_in(): + coll = oauth2_post(**request_data) + else: + coll = no_token_post(**request_data) + + def __view__(uc): + traits = uc["members"] + + trait_obs = [] + json_version = [] + + for atrait in traits: + if ':' not in atrait: + continue + name, dataset_name = atrait.split(':') + if dataset_name == "Temp": + group = name.split("_")[2] + dataset = create_dataset( + dataset_name, dataset_type="Temp", group_name=group) + trait_ob = create_trait(name=name, dataset=dataset) + else: + dataset = create_dataset(dataset_name) + trait_ob = create_trait(name=name, dataset=dataset) + trait_ob = retrieve_trait_info( + trait_ob, dataset, get_qtl_info=True) + trait_obs.append(trait_ob) + + trait_json = jsonable(trait_ob) + trait_json['trait_info_str'] = trait_info_str(trait_ob) + + json_version.append(trait_json) + + collection_info = dict( + trait_obs=trait_obs, + uc=uc, + heatmap_data_url=urljoin(GN_SERVER_URL, "heatmaps/clustered")) + + if "json" in params: + return json.dumps(json_version) + else: + return render_template( + "collections/view.html", + traits_json=json_version, + trait_info_str=trait_info_str, + **collection_info) + + def __error__(err): + error = process_error(err) + flash(f"{error['error']}: {error['error_description']}", "alert-danger") + return redirect(url_for("list_collections")) + + return coll.either(__error__, __view__) + +@app.route("/collections/change_name", methods=('POST',)) +def change_collection_name(): + collection_id = request.form['collection_id'] + resp = redirect(url_for("view_collection", uc_id=collection_id)) + return client.post( + f"auth/user/collections/{collection_id}/rename", + json={ + "anon_id": str(session_info()["anon_id"]), + "new_name": request.form["new_collection_name"] + }).either(with_flash_error(resp), with_flash_success(resp)) |