diff --git a/README.md b/README.md index 93e93e3..cfe6b10 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/src/tesla_ver/bubble_chart/__init__.py b/src/tesla_ver/charting/__init__.py similarity index 100% rename from src/tesla_ver/bubble_chart/__init__.py rename to src/tesla_ver/charting/__init__.py diff --git a/src/tesla_ver/bubble_chart/assets/bWLwgP.css b/src/tesla_ver/charting/assets/bWLwgP.css similarity index 100% rename from src/tesla_ver/bubble_chart/assets/bWLwgP.css rename to src/tesla_ver/charting/assets/bWLwgP.css diff --git a/src/tesla_ver/bubble_chart/assets/layout.css b/src/tesla_ver/charting/assets/layout.css similarity index 100% rename from src/tesla_ver/bubble_chart/assets/layout.css rename to src/tesla_ver/charting/assets/layout.css diff --git a/src/tesla_ver/bubble_chart/assets/materialize.css b/src/tesla_ver/charting/assets/materialize.css similarity index 100% rename from src/tesla_ver/bubble_chart/assets/materialize.css rename to src/tesla_ver/charting/assets/materialize.css diff --git a/src/tesla_ver/bubble_chart/bubble_chart.py b/src/tesla_ver/charting/charting.py similarity index 88% rename from src/tesla_ver/bubble_chart/bubble_chart.py rename to src/tesla_ver/charting/charting.py index 4c032a1..66227e7 100644 --- a/src/tesla_ver/bubble_chart/bubble_chart.py +++ b/src/tesla_ver/charting/charting.py @@ -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 @@ -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) @@ -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") @@ -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")], diff --git a/src/tesla_ver/bubble_chart/bubble_chart_layout.py b/src/tesla_ver/charting/charting_layout.py similarity index 100% rename from src/tesla_ver/bubble_chart/bubble_chart_layout.py rename to src/tesla_ver/charting/charting_layout.py diff --git a/src/tesla_ver/data_uploading/data_uploading.py b/src/tesla_ver/data_uploading/data_uploading.py index 39e8538..2f55381 100644 --- a/src/tesla_ver/data_uploading/data_uploading.py +++ b/src/tesla_ver/data_uploading/data_uploading.py @@ -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 @@ -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 diff --git a/src/wsgi.py b/src/wsgi.py index 13734e3..24f82e9 100644 --- a/src/wsgi.py +++ b/src/wsgi.py @@ -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__": @@ -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__":