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__* *__pycache__*
.venv* .venv*
.env .env*
mongo_data/* 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 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 install --upgrade pip
RUN pip --default-timeout=1000 install -r requirements.txt RUN pip --default-timeout=1000 install -r requirements.txt

View File

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

View File

@ -1,4 +1,4 @@
FROM pool_base_image as flask FROM pool_base_image AS flask
COPY requirements.txt . 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 Flask-WTF
bootstrap-flask bootstrap-flask
waitress 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 queried record
""" """
query = { "test_user" : f"{test_user}", "date" : f"{date}"} 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: if record:
self.existing_record = record self.existing_record = record
return True return True

View File

@ -1,3 +1,4 @@
from bokeh.core.enums import SpatialUnitsType
import mongo.build_db as pool_database import mongo.build_db as pool_database
import mongo.query_db as pool_database_query 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_wtf import FlaskForm, CSRFProtect
from flask_bootstrap import Bootstrap5 from flask_bootstrap import Bootstrap5
from wtforms import StringField, SubmitField, DateField, IntegerField, PasswordField, DecimalField, RadioField, TextAreaField 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 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 = Flask(__name__)
app.secret_key = 'testsecret' #this value will change app.secret_key = 'testsecret' #this value will change
@ -14,6 +20,21 @@ app.secret_key = 'testsecret' #this value will change
bootstrap = Bootstrap5(app) bootstrap = Bootstrap5(app)
csrf = CSRFProtect(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): class userForm(FlaskForm):
username = StringField("User Name?", validators=[DataRequired()]) username = StringField("User Name?", validators=[DataRequired()])
@ -24,15 +45,15 @@ class dataForm(FlaskForm):
test_user = RadioField("Tester:", test_user = RadioField("Tester:",
choices=[("Isabella"), ("Heather"), ("Ariah")]) choices=[("Isabella"), ("Heather"), ("Ariah")])
Date = DateField("Date:") Date = DateField("Date:")
free_chlorine = IntegerField("Free Chlorine:") free_chlorine = IntegerField("Free Chlorine:", validators=[Optional()])
total_chlorine = IntegerField("Total Chlorine:") total_chlorine = IntegerField("Total Chlorine:", validators=[Optional()])
alkalinity = DecimalField("Alkalinity:") alkalinity = DecimalField("Alkalinity:", validators=[Optional()])
PH = DecimalField("PH:") PH = DecimalField("PH:", validators=[Optional()])
hardness = IntegerField("Hardness") hardness = IntegerField("Hardness", validators=[Optional()])
stabiliser = IntegerField("CYA - Stabliser") stabiliser = IntegerField("CYA - Stabliser", validators=[Optional()])
salt = IntegerField("Salt:") salt = IntegerField("Salt:", validators=[Optional()])
temp = DecimalField("Water Temperature") temp = DecimalField("Water Temperature", validators=[Optional()])
comment = TextAreaField("Any Comments?") comment = TextAreaField("Any Comments?", validators=[Optional()])
submit = SubmitField("Write it, Write it REAAAAAAL GOOOD") submit = SubmitField("Write it, Write it REAAAAAAL GOOOD")
@app.route("/", methods=["GET","POST"]) @app.route("/", methods=["GET","POST"])
@ -54,7 +75,7 @@ def index():
def updater(): def updater():
if 'logged_in' not in session: if 'logged_in' not in session:
return redirect("/") return redirect("/")
create_graphs()
query_db = pool_database_query.pool_query() query_db = pool_database_query.pool_query()
query = query_db.get_top(10, "ph") query = query_db.get_top(10, "ph")
form = dataForm() form = dataForm()
@ -87,9 +108,9 @@ def updater():
new_record["salt"], new_record["comment"]) 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: 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"]) @app.route("/update_db", methods=["POST"])
def pool_data_update(): def pool_data_update():
@ -117,4 +138,3 @@ def user_detail(id):
if __name__ == '__main__': if __name__ == '__main__':
#app.run(host='0.0.0.0') #app.run(host='0.0.0.0')
serve(app, host='0.0.0.0', port=5000, url_scheme='https') 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"> <html lang="en">
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1" />
<title> <title>{% block title %} Let there Be Pool Data {% endblock %}</title>
{% block title %}
Let there Be Pool Data
{% endblock %}
</title>
{{ bootstrap.load_css() }} {{ bootstrap.load_css() }}
<style> <style>
body { background: #e8f1f9; } body {
background: #e8f1f9;
}
</style> </style>
</head> </head>
<body> <body>
<!-- this is a base template using Bootstrap-Flask <!-- this is a base template using Bootstrap-Flask
https://bootstrap-flask.readthedocs.io/ --> https://bootstrap-flask.readthedocs.io/ -->
{% block content %} {% block content %} {% endblock %}
{% endblock %}
<!-- you can delete the next line if you're not using any Bootstrap JS -->
<!-- you can delete the next line if you're not using any Bootstrap JS --> {{ bootstrap.load_js() }}
{{ bootstrap.load_js() }} </body>
</body>
</html> </html>

View File

@ -5,7 +5,6 @@
Data Input Data Input
{% endblock %} {% endblock %}
{% block content %} {% block content %}
<!-- <!--
TIPS about using Bootstrap-Flask: TIPS about using Bootstrap-Flask:
https://github.com/helloflask/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"> <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> <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="row">
<div class="col-md-6"> <div class="col-md-6">
<table border=0> <table border=0>
@ -24,7 +27,7 @@
<td> <td>
{{ render_form(form) }} {{ render_form(form) }}
</td> </td>
<td> <!-- <td>
<div class="container"> <div class="container">
<div class="col-md-10 col-lg-8 mx-lg-auto mx-md-auto"> <div class="col-md-10 col-lg-8 mx-lg-auto mx-md-auto">
@ -42,11 +45,20 @@
</table> </table>
</div> </div>
</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> </td>
</tr> </tr>
</table>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div>
{% endblock %} {% endblock %}

View File

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