Skip to content

Commit

Permalink
Merge pull request #62 from clemente-lab/multi-user-support
Browse files Browse the repository at this point in the history
  • Loading branch information
alxkp committed Jun 25, 2021
2 parents afdcf87 + 6e40855 commit b2cfea2
Show file tree
Hide file tree
Showing 9 changed files with 96 additions and 30 deletions.
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ Make sure that no conda environments containing `tesla-ver` in the name exist, a

---

## Notes

The values for time must be integers.

## Users

Currently, the Tesla-ver server is not running, but may be available as a web app in future iterations
Expand Down
File renamed without changes.
File renamed without changes.
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,21 @@
import pandas as pd
import pyarrow as pa

from flask import session

from ast import literal_eval
from plotly.graph_objects import Scatter
from dash.dependencies import Input, Output, State
from dash.exceptions import PreventUpdate

from tesla_ver.bubble_chart.bubble_chart_layout import LAYOUT
from tesla_ver.charting.charting_layout import LAYOUT
from tesla_ver.redis_manager import redis_manager


def generate_bubble_chart(server):
def generate_charting(server):

# TODO Use metadata in charting/graphing system, reenable feature flag when metadata is used for charting/graphing

app = dash.Dash(__name__, server=server, url_base_pathname="/bubblechart.html/")

app.layout = LAYOUT
Expand Down Expand Up @@ -64,16 +69,27 @@ def extract_mdata(df, x_column_name):

context = pa.default_serialization_context()

if redis_manager.redis.exists("data_numeric"):
df = context.deserialize(redis_manager.redis.get("data_numeric"))
redis_manager.redis.flushdb()
# Gets session UUID to get user specific data
session_uuid = session.get('uuid')

if redis_manager.redis.exists(session_uuid + "_numeric_data"):

# User specific key for data stored in redis
numeric_data_key = session_uuid + "_numeric_data"

server.logger.debug("reading data from redis key: " + numeric_data_key)

df = context.deserialize(redis_manager.redis.get(session_uuid + "_numeric_data"))

# Clear user specific data after read
redis_manager.redis.delete(numeric_data_key)
else:
# Because of the need to return data matching all the different areas, displaying an error message
# to the end user would require either another callback to chain with, which would complicate the code and
# likely add a small bit of latency, which is this is left as a console-based error message.
raise PreventUpdate("Data could not be loaded from redis")

server.logger.debug("redis db flushed")
server.logger.debug("redis data for UUID " + session_uuid + " flushed")

mdata = extract_mdata(df, "Year")
df.rename(columns={"Year": "X"}, inplace=True)
Expand Down Expand Up @@ -169,11 +185,15 @@ def update_figure(time_value, y_column_name, x_column_name, json_data, marks, md
if time_value == None:
time_value = int(mdata.get("time_min"))

if time_value == int(mdata.get("time_max")):
time_value

# Loads dataframe at specific time value by getting the time as a key from a dictionary,
# then evaluates it to turn it into a python dictionary, and then loads it as a dataframe
try:
df_by_time = pd.DataFrame.from_dict(literal_eval(json.loads(json_data).get(str(time_value))))
except ValueError:
server.logger.debug(f"❌ unable to load dataframe at time value: {str(time_value)}")
pass

server.logger.debug("✅ dataframe filtered by time")
Expand Down Expand Up @@ -228,12 +248,15 @@ def play_pause_switch(n_clicks):
return [play_status, play_bool]

@app.callback(
Output("time-slider", "value"), [Input("play-interval", "n_intervals")], [State("time-slider", "value")]
[Output("time-slider", "value"), Output("play-interval", "disabled")], [Input("play-interval", "n_intervals")], [State("time-slider", "value"), State("df-mdata", "data")]
)
def play_increment(n_intervals, time_value):
def play_increment(n_intervals, time_value, mdata):
if time_value is None:
raise PreventUpdate
return str(int(time_value) + 1)
if int(time_value) == int(mdata.get("time_max")):
server.logger.debug(f'Max time value reached, returning max value')
return [str(int(time_value)), True]
return [str(int(time_value) + 1), False]

@app.callback(
[Output("left-line-plot-graph", "figure"), Output("right-line-plot-graph", "figure")],
Expand Down
File renamed without changes.
29 changes: 21 additions & 8 deletions src/tesla_ver/data_uploading/data_uploading.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import numpy as np
import pyarrow as pa

from flask import session

from dash.dependencies import Input, Output, State
from dash.exceptions import PreventUpdate
Expand Down Expand Up @@ -126,20 +127,32 @@ def push_to_redis(button_clicks, data_dict, selected_columns, selected_rows):

serialization_context = pa.default_serialization_context()


# Gets session UUID to write user specific data
session_uuid = session.get('uuid')

redis_manager.redis.set(
"data_numeric",
session_uuid + "_numeric_data",
serialization_context.serialize(df[sorted(set(["Year", "Subject", *selected_columns]))])
.to_buffer()
.to_pybytes(),
)
server.logger.debug("redis numeric data set")
server.logger.debug("redis numeric data set at key: " + session_uuid + "_numeric_data")

# Metadata storage feature flag
# TODO Use metadata in charting/graphing system, reenable feature flag when metadata is used for charting/graphing

store_mdata = False

if store_mdata:
# This may be an empty dataframe (checking is needed once the mdata starts getting used)
redis_manager.redis.set(
session.get('uuid') + "_metadata",
serialization_context.serialize(df[["Year", "Subject", *mdata_cols]]).to_buffer().to_pybytes(),
)

server.logger.debug("redis metadata set at key: " + session_uuid + "_metadata")

# This may be an empty dataframe (checking is needed once the mdata starts getting used)
redis_manager.redis.set(
"data_mdata",
serialization_context.serialize(df[["Year", "Subject", *mdata_cols]]).to_buffer().to_pybytes(),
)
server.logger.debug("redis mdata set")
return {"visibility": "visible"}

return app
52 changes: 39 additions & 13 deletions src/wsgi.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,18 @@
import flask
import logging
from flask import Flask, render_template, redirect, session
from werkzeug.debug import DebuggedApplication
from datetime import datetime
from tesla_ver.bubble_chart.bubble_chart import generate_bubble_chart
from os import urandom
from uuid import uuid4
from functools import wraps
from tesla_ver.charting.charting import generate_charting
from tesla_ver.data_uploading.data_uploading import generate_data_uploading


server = flask.Flask(__name__, static_folder="homepage/build/static", template_folder="homepage/build/")
server = Flask(__name__, static_folder="homepage/build/static", template_folder="homepage/build/")

# set a new random secret key for sessions on each app launch
server.secret_key = urandom(24)

# If application is being run through gunicorn, pass logging through to gunicorn
if __name__ != "__main__":
Expand All @@ -25,29 +31,49 @@
server.logger.debug("✅ Data Uploading Screen created and connected")

# Creates the bubble chart and connects it to the flask server
generate_bubble_chart(server=server)
generate_charting(server=server)

server.logger.debug("✅ Bubble Chart Screen created and connected")



# Wrapper to check if UUID is initialized, and initialize it if not
def check_uuid_initialized(redirect_func):
@wraps(redirect_func)
def wrapped(*args, **kwargs):
uuid = session.get('uuid')
if uuid:
return redirect_func(*args, **kwargs)
else:
session['uuid'] = str(uuid4().hex)
return redirect_func(*args, **kwargs)
return wrapped



@server.route("/")
@check_uuid_initialized
def index():
"""Renders the landing page."""
server.logger.debug("rendering homepage")
return flask.render_template("index.html")
server.logger.debug("User with UUID:" + session.get('uuid') + "connecting to homepage")
return render_template("index.html")

@server.route("/datauploading.html")
@check_uuid_initialized
def render_data_uploading():
server.logger.debug("redirecting to data uploader")
server.logger.debug("User with UUID:" + session.get('uuid') + "connecting to data uploading")
return redirect("/datauploading.html")


@server.route("/bubblechart.html")
def render_bubble_chart():
@check_uuid_initialized
def render_charting_page():
"""Redirects to the Dash Bubble chart."""
server.logger.debug("redirecting to bubblechart")
return flask.redirect("/bubblechart.html")


@server.route("/datauploading.html")
def render_data_uploading():
server.logger.debug("redirecting to data uploader")
return flask.redirect("/datauploading.html")
server.logger.debug("User with UUID:" + session.get('uuid') + "connecting to charting")
return redirect("/bubblechart.html")


if __name__ == "__main__":
Expand Down

0 comments on commit b2cfea2

Please sign in to comment.