Merge pull request 'charts' (#2) from charts into master

Reviewed-on: #2
This commit is contained in:
armistace 2024-07-23 15:08:02 +10:00
commit e3b80c96b8
16 changed files with 273 additions and 54 deletions

2
.gitignore vendored
View File

@ -1,4 +1,4 @@
*__pycache__*
.venv*
.env
.env*
mongo_data/*

View File

@ -1,4 +1,4 @@
FROM python:3.10 as pool_base_image
FROM python:3.10 AS pool_base_image
WORKDIR /pool_data
@ -9,4 +9,3 @@ RUN apt-get update -y && apt-get upgrade -y && apt-get install -y libsasl2-dev p
RUN pip install --upgrade pip
RUN pip --default-timeout=1000 install -r requirements.txt

View File

@ -8,7 +8,7 @@ services:
context: .
dockerfile: flask.Dockerfile
volumes:
- ./src/flask:/pool_data_web/src/flask
- ./src/flask:/pool_data/src/flask
ports:
- "80:80"
- "5000:5000"

View File

@ -1,4 +1,4 @@
FROM pool_base_image as flask
FROM pool_base_image AS flask
COPY requirements.txt .

3
pyproject.toml Normal file
View File

@ -0,0 +1,3 @@
[tool.pyright]
venvPath = "."
venv = ".venv"

View File

@ -6,3 +6,4 @@ click
Flask-WTF
bootstrap-flask
waitress
bokeh

61
src/flask/chart_test.html Normal file
View File

@ -0,0 +1,61 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Bokeh Plot</title>
<style>
html, body {
box-sizing: border-box;
display: flow-root;
height: 100%;
margin: 0;
padding: 0;
}
</style>
<script type="text/javascript" src="https://cdn.bokeh.org/bokeh/release/bokeh-3.5.0.min.js"></script>
<script type="text/javascript">
Bokeh.set_log_level("info");
</script>
</head>
<body>
<div id="bb5c0e1d-dc95-4d96-9cbc-a29ed2eade5c" data-root-id="p1001" style="display: contents;"></div>
<script type="application/json" id="af8187ed-c6d3-40c3-84a3-bbe58799b466">
{"00bec54e-f52e-4b7f-b2b6-8e6e4eb73c82":{"version":"3.5.0","title":"Bokeh Application","roots":[{"type":"object","name":"Figure","id":"p1001","attributes":{"height":250,"x_range":{"type":"object","name":"FactorRange","id":"p1011","attributes":{"factors":["2024-03-27","2024-03-25","2024-03-15","2024-03-13"]}},"y_range":{"type":"object","name":"DataRange1d","id":"p1003","attributes":{"start":6}},"x_scale":{"type":"object","name":"CategoricalScale","id":"p1012"},"y_scale":{"type":"object","name":"LinearScale","id":"p1013"},"title":{"type":"object","name":"Title","id":"p1004","attributes":{"text":"Pool data"}},"renderers":[{"type":"object","name":"GlyphRenderer","id":"p1030","attributes":{"data_source":{"type":"object","name":"ColumnDataSource","id":"p1024","attributes":{"selected":{"type":"object","name":"Selection","id":"p1025","attributes":{"indices":[],"line_indices":[]}},"selection_policy":{"type":"object","name":"UnionRenderers","id":"p1026"},"data":{"type":"map","entries":[["x",["2024-03-27","2024-03-25","2024-03-15","2024-03-13"]],["y",[7.25,7.5,7.6,7.0]]]}}},"view":{"type":"object","name":"CDSView","id":"p1031","attributes":{"filter":{"type":"object","name":"AllIndices","id":"p1032"}}},"glyph":{"type":"object","name":"Line","id":"p1027","attributes":{"x":{"type":"field","field":"x"},"y":{"type":"field","field":"y"},"line_color":"#1f77b4"}},"nonselection_glyph":{"type":"object","name":"Line","id":"p1028","attributes":{"x":{"type":"field","field":"x"},"y":{"type":"field","field":"y"},"line_color":"#1f77b4","line_alpha":0.1}},"muted_glyph":{"type":"object","name":"Line","id":"p1029","attributes":{"x":{"type":"field","field":"x"},"y":{"type":"field","field":"y"},"line_color":"#1f77b4","line_alpha":0.2}}}}],"toolbar":{"type":"object","name":"Toolbar","id":"p1010"},"toolbar_location":null,"left":[{"type":"object","name":"LinearAxis","id":"p1019","attributes":{"ticker":{"type":"object","name":"BasicTicker","id":"p1020","attributes":{"mantissas":[1,2,5]}},"formatter":{"type":"object","name":"BasicTickFormatter","id":"p1021"},"major_label_policy":{"type":"object","name":"AllLabels","id":"p1022"}}}],"below":[{"type":"object","name":"CategoricalAxis","id":"p1014","attributes":{"ticker":{"type":"object","name":"CategoricalTicker","id":"p1015"},"formatter":{"type":"object","name":"CategoricalTickFormatter","id":"p1016"},"major_label_policy":{"type":"object","name":"AllLabels","id":"p1017"}}}],"center":[{"type":"object","name":"Grid","id":"p1018","attributes":{"axis":{"id":"p1014"},"grid_line_color":null}},{"type":"object","name":"Grid","id":"p1023","attributes":{"dimension":1,"axis":{"id":"p1019"}}}]}}]}}
</script>
<script type="text/javascript">
(function() {
const fn = function() {
Bokeh.safely(function() {
(function(root) {
function embed_document(root) {
const docs_json = document.getElementById('af8187ed-c6d3-40c3-84a3-bbe58799b466').textContent;
const render_items = [{"docid":"00bec54e-f52e-4b7f-b2b6-8e6e4eb73c82","roots":{"p1001":"bb5c0e1d-dc95-4d96-9cbc-a29ed2eade5c"},"root_ids":["p1001"]}];
root.Bokeh.embed.embed_items(docs_json, render_items);
}
if (root.Bokeh !== undefined) {
embed_document(root);
} else {
let attempts = 0;
const timer = setInterval(function(root) {
if (root.Bokeh !== undefined) {
clearInterval(timer);
embed_document(root);
} else {
attempts++;
if (attempts > 100) {
clearInterval(timer);
console.log("Bokeh: ERROR: Unable to run BokehJS code because BokehJS library is missing");
}
}
}, 10, root)
}
})(window);
});
};
if (document.readyState != "loading") fn();
else document.addEventListener("DOMContentLoaded", fn);
})();
</script>
</body>
</html>

17
src/flask/chart_test.py Normal file
View File

@ -0,0 +1,17 @@
from bokeh.models.layouts import HBox
from bokeh.plotting import column
from charts import PoolCharts
from bokeh.io import output_file, show
from bokeh.layouts import row
output_file("static/data_plot.html")
chart = PoolCharts.PoolCharts()
ph = chart.line_chart("Pool PH", "ph", 50)
total_chlorine = chart.line_chart("Pool Total Chlorine", "total_chlorine", 50)
free_chlorine = chart.line_chart("Pool Free Chlorine", "free_chlorine", 50)
show(column(ph, total_chlorine, free_chlorine))

View File

@ -0,0 +1,49 @@
import sys
from bokeh.models.widgets import DataCube
from bokeh.plotting import figure
from mongo.get_conn import db_conn
class PoolCharts():
def __init__(self):
self.db = db_conn()
def line_chart(self, title, field, limit):
data = self.db.pool_db
data = data.find({}).sort("date", -1).limit(limit)
dates = []
data_list = []
date_count = {}
for record in data:
if field in record and record[field] != "None":
if record["date"] not in dates:
print("new date record")
dates.append(record["date"])
data_list.append(float(record[field]))
print (dates)
print (data_list)
else:
if record["date"] in date_count:
date_count[record["date"]] += 1
else:
date_count[record["date"]] = 2
print(len(data_list))
current_data = data_list[len(data_list) - 1]
data_list[len(data_list) - 1] = (float(record[field]) + current_data) / date_count[record["date"]]
print(dates)
print(data_list)
p = figure(x_range=dates, height=250, title=title,
toolbar_location=None, tools="")
p.line(x=dates, y=data_list)
p.xgrid.grid_line_color = None
if not data_list:
p.y_range.start = 0
else:
p.y_range.start = round(min(data_list) - 1)
return p

View File

View File

@ -23,7 +23,7 @@ class pool_query:
queried record
"""
query = { "test_user" : f"{test_user}", "date" : f"{date}"}
record = self.db.real_db.find_one(query)
record = self.db.pool_db.find_one(query)
if record:
self.existing_record = record
return True

View File

@ -1,3 +1,4 @@
from bokeh.core.enums import SpatialUnitsType
import mongo.build_db as pool_database
import mongo.query_db as pool_database_query
@ -5,8 +6,13 @@ from flask import Flask, render_template, request, jsonify, redirect, session
from flask_wtf import FlaskForm, CSRFProtect
from flask_bootstrap import Bootstrap5
from wtforms import StringField, SubmitField, DateField, IntegerField, PasswordField, DecimalField, RadioField, TextAreaField
from wtforms.validators import DataRequired, Length
from wtforms.validators import DataRequired, Length, Optional
from waitress import serve
from bokeh.models.layouts import HBox
from bokeh.plotting import column
from charts import PoolCharts
from bokeh.io import output_file, show
from bokeh.layouts import row
app = Flask(__name__)
app.secret_key = 'testsecret' #this value will change
@ -14,6 +20,21 @@ app.secret_key = 'testsecret' #this value will change
bootstrap = Bootstrap5(app)
csrf = CSRFProtect(app)
# used to configure the bokeh plot for graphs
output_file("/pool_data/src/flask/static/data_plot.html")
def create_graphs():
chart = PoolCharts.PoolCharts()
ph = chart.line_chart("Pool PH", "ph", 30)
total_chlorine = chart.line_chart("Pool Total Chlorine", "total_chlorine", 30)
free_chlorine = chart.line_chart("Pool Free Chlorine", "free_chlorine", 30)
alkalinity = chart.line_chart("Alkalinity", "alkalinity", 30)
salt = chart.line_chart("Salt", "salt", 30)
temp = chart.line_chart("Temperature", "temp", 30)
hardness = chart.line_chart("Hardness", "hardness", 30)
stabiliser = chart.line_chart("Stabiliser", "stabiliser", 30)
show(row(column(ph, total_chlorine, free_chlorine, temp), column(salt, alkalinity, hardness, stabiliser)))
class userForm(FlaskForm):
username = StringField("User Name?", validators=[DataRequired()])
@ -21,20 +42,20 @@ class userForm(FlaskForm):
submit = SubmitField("Letsa GO!")
class dataForm(FlaskForm):
test_user = RadioField("Tester:",
test_user = RadioField("Tester:",
choices=[("Isabella"), ("Heather"), ("Ariah")])
Date = DateField("Date:")
free_chlorine = IntegerField("Free Chlorine:")
total_chlorine = IntegerField("Total Chlorine:")
alkalinity = DecimalField("Alkalinity:")
PH = DecimalField("PH:")
hardness = IntegerField("Hardness")
stabiliser = IntegerField("CYA - Stabliser")
salt = IntegerField("Salt:")
temp = DecimalField("Water Temperature")
comment = TextAreaField("Any Comments?")
free_chlorine = IntegerField("Free Chlorine:", validators=[Optional()])
total_chlorine = IntegerField("Total Chlorine:", validators=[Optional()])
alkalinity = DecimalField("Alkalinity:", validators=[Optional()])
PH = DecimalField("PH:", validators=[Optional()])
hardness = IntegerField("Hardness", validators=[Optional()])
stabiliser = IntegerField("CYA - Stabliser", validators=[Optional()])
salt = IntegerField("Salt:", validators=[Optional()])
temp = DecimalField("Water Temperature", validators=[Optional()])
comment = TextAreaField("Any Comments?", validators=[Optional()])
submit = SubmitField("Write it, Write it REAAAAAAL GOOOD")
@app.route("/", methods=["GET","POST"])
def index():
form = userForm()
@ -54,7 +75,7 @@ def index():
def updater():
if 'logged_in' not in session:
return redirect("/")
create_graphs()
query_db = pool_database_query.pool_query()
query = query_db.get_top(10, "ph")
form = dataForm()
@ -81,15 +102,15 @@ def updater():
if database.existing_record[field] != new_record[field]:
database.update_re_record(database.existing_record["_id"], field, new_record[field])
else:
database.create_re_record(new_record["ph"], new_record["total_chlorine"], new_record["free_chlorine"],
database.create_re_record(new_record["ph"], new_record["total_chlorine"], new_record["free_chlorine"],
new_record["alkalinity"], new_record["date"], new_record["test_user"],
new_record["temp"], new_record["hardness"], new_record["stabiliser"],
new_record["temp"], new_record["hardness"], new_record["stabiliser"],
new_record["salt"], new_record["comment"])
return render_template("updater.html", list=query, form=form)
return render_template("updater.html", list=query, form=form, success=True, updater_name = new_record["test_user"])
else:
return render_template("updater.html", list=query, form=form)
return render_template("updater.html", list=query, form=form, sucess=False)
@app.route("/update_db", methods=["POST"])
def pool_data_update():
@ -102,9 +123,9 @@ def pool_data_update():
if database.existing_record[field] != new_record[field]:
database.update_re_record(database.existing_record["_id"], field, new_record[field])
else:
database.create_re_record(new_record["ph"], new_record["total_chlorine"], new_record["free_chlorine"],
database.create_re_record(new_record["ph"], new_record["total_chlorine"], new_record["free_chlorine"],
new_record["alkalinity"], new_record["date"], new_record["test_user"],
new_record["temp"], new_record["hardness"], new_record["stabiliser"],
new_record["temp"], new_record["hardness"], new_record["stabiliser"],
new_record["salt"], new_record["comment"])
@ -117,4 +138,3 @@ def user_detail(id):
if __name__ == '__main__':
#app.run(host='0.0.0.0')
serve(app, host='0.0.0.0', port=5000, url_scheme='https')

File diff suppressed because one or more lines are too long

View File

@ -1,30 +1,26 @@
<!DOCTYPE html>
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>
{% block title %}
Let there Be Pool Data
{% endblock %}
</title>
<title>{% block title %} Let there Be Pool Data {% endblock %}</title>
{{ bootstrap.load_css() }}
{{ bootstrap.load_css() }}
<style>
body { background: #e8f1f9; }
</style>
</head>
<body>
<!-- this is a base template using Bootstrap-Flask
<style>
body {
background: #e8f1f9;
}
</style>
</head>
<body>
<!-- this is a base template using Bootstrap-Flask
https://bootstrap-flask.readthedocs.io/ -->
{% block content %}
{% endblock %}
{% block content %} {% endblock %}
<!-- you can delete the next line if you're not using any Bootstrap JS -->
{{ bootstrap.load_js() }}
</body>
<!-- you can delete the next line if you're not using any Bootstrap JS -->
{{ bootstrap.load_js() }}
</body>
</html>

View File

@ -5,7 +5,6 @@
Data Input
{% endblock %}
{% block content %}
<!--
TIPS about using Bootstrap-Flask:
https://github.com/helloflask/bootstrap-flask
@ -17,6 +16,10 @@
<div class="col-md-10 col-lg-8 mx-lg-auto mx-md-auto">
<h1 class="pt-5 pb-2">Input Data you want to store</h1>
{% if success %}
<h3 class="pt-3 pb02">Thank you {{ updater_name | safe }}, Data Updated Successfully</h1>
{% endif %}
<div class="row">
<div class="col-md-6">
<table border=0>
@ -24,10 +27,10 @@
<td>
{{ render_form(form) }}
</td>
<td>
<!-- <td>
<div class="container">
<div class="col-md-10 col-lg-8 mx-lg-auto mx-md-auto">
<table border = 0>
{% for row in list %}
<table border=0>
@ -40,13 +43,22 @@
</table>
{% endfor %}
</table>
</div>
</div>
</td> -->
<td>
<div class="container">
<div class= "col-xs-12 col-sm-12 col-md-12">
<iframe style="width: 100vw;height: 80vh;position: relative;" src="static/data_plot.html" frameborder="0" allowfullscreen>
</iframe>
</div>
</div>
</td>
</tr>
</div>
</table>
</div>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@ -1,4 +1,4 @@
# /bin/bash
ou /bin/bash
source .venv/bin/activate
source .env