I'm still relatively inexperienced in Flask, though I've been using for a while now.
Also, the HTML & CSS templates for the webpages can be found here:
Recently, as part of a project, I needed to update a webpage dynamically when a remote trigger was activated.
Let's say there are two webpages: Webpage A & Webpage B. Both A & B are running at the same time, and have been set some buttons within forms. These buttons are assigned values and, when clicked, these values are sent to the Flask app & stored in some database.
Depending on the selected button, the text on the opposite webpage should dynamically update to display the value of the button. That is, if "Btn 1" on Webpage A was clicked, it should dynamically update the \
element in Webpage B to say "Btn 1".
So far, the webpages were able interact in such a way using a database, by storing the latest button values and feeding them to each webpage. However, the values were not displayed on the opposite webpage unless it was refreshed manually. Quite a few sources suggested using Sockets as a way to update webpages in real-time.
From what I could understanding, however, that may be a bit excessive for tasks like this. Thus, Turbo-Flask was recommended as a solution, as it seemed to sidestep the process of manually setting up a secondary Socket server.
2. The Code:
I don't know a lot about client and server side interactions, or the turbo.js resource. Using the documentation, though, I implemented the following solution:
from flask import Flask, request, render_template
from turbo_flask import Turbo
import os
def create_app():
app = Flask(__name__)
app.config.from_mapping(
SECRET_KEY=os.urandom(16),
)
turbo = Turbo(app)
@app.route("/A", methods=["GET", "POST"])
def A():
if request.method == "POST":
value = request.form["option"]
if turbo.can_stream():
turbo.stream(
turbo.update(value, "bTextB"),
)
print("Pushed!")
else:
print("Can't stream updates")
return render_template("TempA.html", bText="Bread Text")
@app.route("/B", methods=["GET", "POST"])
def B():
if request.method == "POST":
value = request.form["option"]
if turbo.can_stream():
turbo.stream(
turbo.update(value, "bTextA"),
)
print("Pushed!")
else:
print("Can't Stream updates!")
return render_template("TempB.html", bText="Bread Text")
return app
3. The Error(s):
When running the following code, however, the text elements stayed the same. Instead, the following error popped up:
"GET /turbo-stream HTTP/1.1" 400 -
The console did output a "Pushed!" message, however, meaning the turbo could stream and attempted to do so. When inspecting the Firefox console, the console reported the following errors:
Firefox can’t establish a connection to the server at ws://127.0.0.1:5000/turbo-stream. (Error in /A)
Error: Form responses must redirect to another location. (Error in turbo.js)
4. The Question(s):
This leads me to the following questions:
Is Turbo-Flask the best Flask extension for tasks like this? Should I be considering other extensions, such as Sockets?
Based on the current implementation, would the code actually work? Is it possible to add such inter-webpage functionality in future, or would it be too niche?
What could I do to fix my current code?
I apologize in advance if I've missed any details or have added to much.
First of all, the Firefox issue is I think a known problem that has been fixed a while ago in Werkzeug, but hasn't been released yet. You may want to install the main branch of Werkzeug to see if the problem goes away.
Now on to your questions:
I can't really tell you it is the "best" extension. There are really too many ways to do this, and which one is the best depends on your personal preferences and experience. It is an adequate solution, that much I can tell you.
I think your main (maybe only) problem is that you are not returning the streams. When you call turbo.stream() to generate an update stream, you have to return that as the response. You are generating the stream, then discarding it, and then returning the original page template, which invalidates all the work Turbo-Flask is doing.
After installing the Werkzeug library via GitHub (as you suggested), the Firefox error seemed to be resolved!
However, returning the streams as is didn't seem to update the other webpage. I'm supposing that the Streams' scopes are restricted to the assigned App Route, so it can't update the second webpage dynamically.
Instead, I've tried using a database to track button presses from each webpage. Using the database values, the application compares the values stored into the \
with the DB value. If the value is different, it changes the \
element to the new value, and the server keeps track of the new element value.
Below are the updates to the previous code, using the same HTML pages but now introduces an SQLite Database.
# Set a global 'dict' variable to track the values of div elements
session = {'bTextA': None,
'bTextB': None}
def create_app():
...
from .DB import init_app, get_db
init_app(app)
turbo = Turbo(app)
@app.route("/A", methods=["GET", "POST"])
def A():
global session
db = get_db()
bTextA = db.execute("SELECT OptnVal FROM Options WHERE toTable = 'A' ORDER BY OptionID DESC").fetchone()
print(bTextA['OptnVal'])
if session['bTextA'] != bTextA['OptnVal']:
if turbo.can_stream():
session['bTextA'] = bTextA['OptnVal']
print("Pushed!")
return turbo.stream(
turbo.update(bTextA['OptnVal'], "bTextA"),
)
else:
print("Can't stream updates")
if request.method == "POST":
value = request.form["option"]
db.execute("INSERT INTO Options(OptnVal, toTable) VALUES(?, 'B')", (value,))
db.commit()
return render_template("TempA.html", bText="Bread Text")
@app.route("/B", methods=["GET", "POST"])
def B():
global session
db = get_db()
bTextB = db.execute("SELECT OptnVal FROM Options WHERE toTable = 'B' ORDER BY OptionID DESC").fetchone()
print(bTextB['OptnVal'])
if session['bTextB'] != bTextB['OptnVal']:
session['bTextB'] = bTextB['OptnVal']
if turbo.can_stream():
print("Pushed!")
return turbo.stream(
turbo.update(bTextB['OptnVal'], "bTextB"),
)
else:
print("Can't stream updates!")
if request.method == "POST":
value = request.form["option"]
db.execute("INSERT INTO Options (OptnVal, toTable) VALUES(?, 'A')", (value,))
db.commit()
return render_template("TempB.html", bText="Bread Text")
return app
Note: There isn't much to the SQLite Database Schema. There are three fields:
OptionID: The Primary Key that Auto-Increments whenever it receives a new button press.
OptnVal: The value of the button press.
toTable: This field tracks which webpage's element must be updated.
I'm aware this is a quite rushed functionality implementation, based on the number of bugs present:
The \
still don't refresh dynamically, defeating the whole purpose of the implementation.
The \
don't update unless another action occurs on the webpage, causing the webpage to update.
If the webpages were refreshed manually, the database & updating system breaks down for a while.
When setting the original "bText" values to the recorded session button values, the system breaks down significantly.
I hope to understand why does the application break in the ways it does, and if there are ways to resolving these issues.
PS. Is it possible to add some form of multi-webpage modification functionality to Turbo-Flask in some way, so that it works independently of a database? Something along of the lines of: turbo.update( content, target, template [defaults to the scoped route])
I understand if this is a niche subject, or if it's too much of an effort to implement such a functionality.
Also, I know could use a meta refresh HTML tag, but that would refresh the entire page. So...
This may sound bad, but I think you are overcomplicating this. Let's go back to the original question. Unfortunately since there is no code I can run, I can only make suggestions that I think will work. I now realize that I was close on my suggestion to return the turbo.stream() response, but really what you need is slightly different.
There are two operations in the Turbo Stream module, turbo.stream() and turbo.push(). The stream operation sends a streamed response to the originating client only, which I now realize is not what you need. The push operation sends an update to one, a set or all connected clients. So basically what you need is to push an update, which will trigger all connected clients to update. Something like this:
@app.route("/A", methods=["GET", "POST"])
def A():
if request.method == "POST":
value = request.form["option"]
if turbo.can_push():
turbo.push(
turbo.update(value, "bTextB"),
)
print("Pushed!")
else:
print("Can't push updates")
return render_template("TempA.html", bText="Bread Text")
Once again, I'm writing this by memory, so it may need some polishing to make it into a fully working solution.
It works now!!!
Once again, thank you for the quick response, Miguel! The webpages interact with each other as expected, updating itself when a button from the opposite webpage is clicked. Below is the final code:
from flask import Flask, request, render_template
from turbo_flask import Turbo
import os
def create_app():
# Set up Flask application & Configure it
app = Flask(__name__)
app.config.from_mapping(
SECRET_KEY=os.urandom(16),
DATABASE="TestApp/TestConnect.db"
)
# Initialise the Turbo object within the Flask app
turbo = Turbo(app)
# Execute the following code, whenever the "/A" (Webpage A) route is called from the domain
@app.route("/A", methods=["GET", "POST"])
def A():
# Store the value of the POSTed button input, whenever any button is pressed
if request.method == "POST":
value = request.form["option"]
# Push an update to all HTML templates,
# that updates all <div> element with an id="bTextB" to received button input
if turbo.can_push():
print("Pushed!")
turbo.push(
turbo.update(value, "bTextB"),
)
# Report if there are any issues with pushing using turbo
else:
print("Can't push updates to B!")
# Return the original webpage template for Webpage A, with some default text
return render_template("TempA.html", bText="Bread Text")
# Execute the following code, whenever the "/A" (Webpage A) route is called from the domain
@app.route("/B", methods=["GET", "POST"])
def B():
# Store the value of the POSTed button input, whenever any button is pressed
if request.method == "POST":
value = request.form["option"]
# Push an update to all HTML templates,
# that updates all <div> element with an id="bTextA" to received button input
if turbo.can_push():
print("Pushed!")
turbo.push(
turbo.update(value, "bTextA"),
)
# Report if there are any issues with pushing using turbo
else:
print("Can't push updates to A!")
# Return the original webpage template for Webpage B, with some default text
return render_template("TempB.html", bText="Butter Text")
return app
A minor downside to this implementation, however, is the case when one of the webpages is refreshed. In that situation, the webpage resets to the default text outlined in the code, instead of preserving the previous button value. This is because the routes of both Webpages A & B are set to render a template with static text in the beginning. Since the webpages rerun the application script whenever they are refreshed, the Webpages reset themselves accordingly.
A simple(ish) fix to this, however, is to use a database to track button presses. That way, instead of using some default text for the original web template value, the text can be preset to the latest button press before the refresh. The final code - if a database similar to the one previously outlined was used - would then look like the following:
from flask import Flask, request, render_template
from turbo_flask import Turbo
import os
def create_app():
# Set up Flask application & Configure it
app = Flask(__name__)
app.config.from_mapping(
SECRET_KEY=os.urandom(16),
DATABASE="TestApp/TestConnect.db"
)
# Import the DB module
# (see Flask Documentation for how to set up this module: https://flask.palletsprojects.com/en/2.0.x/tutorial/database/)
from .DB import init_app, get_db
init_app(app)
# Initialise the Turbo object within the Flask app
turbo = Turbo(app)
# Execute the following code, whenever the "/A" (Webpage A) route is called from the domain
@app.route("/A", methods=["GET", "POST"])
def A():
# Access the database, and record the latest button value to be displayed on the webpage
db = get_db()
bTextA = db.execute("SELECT OptnVal FROM Options WHERE toTable = 'A' ORDER BY OptionID DESC").fetchone()
# Store the value of the POSTed button input, whenever any button is pressed
if request.method == "POST":
value = request.form["option"]
# Push an update to all HTML templates,
# that updates all <div> element with an id="bTextB" to received button input
if turbo.can_push():
# Track the button value and the intended Webpage destination
db.execute("INSERT INTO Options(OptnVal, toTable) VALUES(?, 'B')", (value,))
db.commit()
print("Pushed!")
turbo.push(
turbo.update(value, "bTextB"),
)
# Report if there are any issues with pushing using turbo
else:
print("Can't push updates to B!")
# Return the original webpage template for Webpage A, with the latest recorded value from the database
return render_template("TempA.html", bText=bTextA['OptnVal'])
# Execute the following code, whenever the "/A" (Webpage A) route is called from the domain
@app.route("/B", methods=["GET", "POST"])
def B():
# Access the database, and record the latest button value to be displayed on the webpage
db = get_db()
bTextB = db.execute("SELECT OptnVal FROM Options WHERE toTable = 'B' ORDER BY OptionID DESC").fetchone()
# Store the value of the POSTed button input, whenever any button is pressed
if request.method == "POST":
value = request.form["option"]
# Push an update to all HTML templates,
# that updates all <div> element with an id="bTextA" to received button input
if turbo.can_push():
# Access the database, and record the latest button value to be displayed on the webpage
db.execute("INSERT INTO Options (OptnVal, toTable) VALUES(?, 'A')", (value,))
db.commit()
print("Pushed!")
turbo.push(
turbo.update(value, "bTextA"),
)
# Report if there are any issues with pushing using turbo
else:
print("Can't push updates to A!")
# Return the original webpage template for Webpage B, with the latest recorded value from the database
return render_template("TempB.html", bText=bTextB['OptnVal'])
return app
Although there may be more efficient methods to solve this issue, this solution required the least effort to implement, as it was set up during my previous response.
Final Questions:
In what instances would turbo.stream() be preferred over the turbo.push() method?
Using the .push() method updated the second webpage dynamically, without calling a full page refresh. Would this method have affected any other webpages, so long as those webpages contains elements that had the same "target id" as the one specified in the method?
The stream() method is used to return an optimized response to a POST request. Because it is an HTTP response, it goes only to the client that made the originating request. The best use case for this is when a form is submitted, you can return validation errors via stream().
The push() method accepts a second argument that is optional, used to indicate who should receive the update. When this argument is omitted, all connected clients get it. If you have other clients that also use Turbo-Flask and happen to have an element with the same ID you are passing here, then they will also update. If you assign IDs to your users (see documentation) you can then pass the second argument, which can be the ID of a single client to target, or a list with all the IDs you want to get the push.
@SlodeSoft first of all, you seem to have an error in your template. Can you fix that to eliminate that as a possible contributor to the problem?
If turbo.can_push() returns false it means that in that request, the client cannot handle turbo stream updates. I think you may need to familiarize yourself with the turbo client-side documentation to understand why your usage is invalid. Turbo stream updates are accepted only as a response of a form POST, I don't see any POST requests in your log.
For reference / future readers. I got the example to work on Azure App Service Linux (Python 3.9)
Using the following line in the portal under Configuration >> General Settings >> Startup Command:
gunicorn --bind=0.0.0.0 --workers=1 --threads=100 app:app
And the somewhat modified code for app.py:
from flask import Flask, request, render_template
from turbo_flask import Turbo
import os
app = Flask(__name__)
turbo = Turbo(app)
app.secret_key = os.getenv("APP_SECRET_KEY")
# Execute the following code, whenever the "/A" (Webpage A) route is called from the domain
@app.route("/A", methods=["GET", "POST"])
def A():
# Store the value of the POSTed button input, whenever any button is pressed
if request.method == "POST":
value = request.form["option"]
# Push an update to all HTML templates,
# that updates all <div> element with an id="bTextB" to received button input
if turbo.can_push():
print("Pushed!")
turbo.push(
turbo.update(value, "bTextB"),
)
# Report if there are any issues with pushing using turbo
else:
print("Can't push updates to B!")
# Return the original webpage template for Webpage A, with some default text
return render_template("TempA.html", bText="Bread Text")
# Execute the following code, whenever the "/A" (Webpage A) route is called from the domain
@app.route("/B", methods=["GET", "POST"])
def B():
# Store the value of the POSTed button input, whenever any button is pressed
if request.method == "POST":
value = request.form["option"]
# Push an update to all HTML templates,
# that updates all <div> element with an id="bTextA" to received button input
if turbo.can_push():
print("Pushed!")
turbo.push(
turbo.update(value, "bTextA"),
)
# Report if there are any issues with pushing using turbo
else:
print("Can't push updates to A!")
# Return the original webpage template for Webpage B, with some default text
return render_template("TempB.html", bText="Butter Text")
if __name__ == "__main__":
app.run()
The secret key is set in the Portal under Configuration >> Application Settings
0. Note:
Hello,
I'm still relatively inexperienced in Flask, though I've been using for a while now. Also, the HTML & CSS templates for the webpages can be found here:
TempA.txt TempB.txt
1. The Premise:
Recently, as part of a project, I needed to update a webpage dynamically when a remote trigger was activated.
Let's say there are two webpages: Webpage A & Webpage B. Both A & B are running at the same time, and have been set some buttons within forms. These buttons are assigned values and, when clicked, these values are sent to the Flask app & stored in some database.
Depending on the selected button, the text on the opposite webpage should dynamically update to display the value of the button. That is, if "Btn 1" on Webpage A was clicked, it should dynamically update the \
So far, the webpages were able interact in such a way using a database, by storing the latest button values and feeding them to each webpage. However, the values were not displayed on the opposite webpage unless it was refreshed manually. Quite a few sources suggested using Sockets as a way to update webpages in real-time. From what I could understanding, however, that may be a bit excessive for tasks like this. Thus, Turbo-Flask was recommended as a solution, as it seemed to sidestep the process of manually setting up a secondary Socket server.
2. The Code:
I don't know a lot about client and server side interactions, or the turbo.js resource. Using the documentation, though, I implemented the following solution:
3. The Error(s):
When running the following code, however, the text elements stayed the same. Instead, the following error popped up:
"GET /turbo-stream HTTP/1.1" 400 -
The console did output a "Pushed!" message, however, meaning the turbo could stream and attempted to do so. When inspecting the Firefox console, the console reported the following errors:
4. The Question(s):
This leads me to the following questions:
I apologize in advance if I've missed any details or have added to much.
First of all, the Firefox issue is I think a known problem that has been fixed a while ago in Werkzeug, but hasn't been released yet. You may want to install the main branch of Werkzeug to see if the problem goes away.
Now on to your questions:
I can't really tell you it is the "best" extension. There are really too many ways to do this, and which one is the best depends on your personal preferences and experience. It is an adequate solution, that much I can tell you.
I think your main (maybe only) problem is that you are not returning the streams. When you call
turbo.stream()
to generate an update stream, you have to return that as the response. You are generating the stream, then discarding it, and then returning the original page template, which invalidates all the work Turbo-Flask is doing.Progress Update:
Thank you for the quick response, Miguel!
After installing the Werkzeug library via GitHub (as you suggested), the Firefox error seemed to be resolved! However, returning the streams as is didn't seem to update the other webpage. I'm supposing that the Streams' scopes are restricted to the assigned App Route, so it can't update the second webpage dynamically.
Instead, I've tried using a database to track button presses from each webpage. Using the database values, the application compares the values stored into the \
Below are the updates to the previous code, using the same HTML pages but now introduces an SQLite Database.
Note: There isn't much to the SQLite Database Schema. There are three fields:
I'm aware this is a quite rushed functionality implementation, based on the number of bugs present:
I hope to understand why does the application break in the ways it does, and if there are ways to resolving these issues.
For anyone wanting to recreate the .DB Blueprint, use the code outlined in the Flask Documentation's Tutorial for Accessing Databases: https://flask.palletsprojects.com/en/2.0.x/tutorial/database/
PS. Is it possible to add some form of multi-webpage modification functionality to Turbo-Flask in some way, so that it works independently of a database? Something along of the lines of: turbo.update( content, target, template [defaults to the scoped route]) I understand if this is a niche subject, or if it's too much of an effort to implement such a functionality.
Also, I know could use a meta refresh HTML tag, but that would refresh the entire page. So...
This may sound bad, but I think you are overcomplicating this. Let's go back to the original question. Unfortunately since there is no code I can run, I can only make suggestions that I think will work. I now realize that I was close on my suggestion to return the
turbo.stream()
response, but really what you need is slightly different.There are two operations in the Turbo Stream module,
turbo.stream()
andturbo.push()
. The stream operation sends a streamed response to the originating client only, which I now realize is not what you need. The push operation sends an update to one, a set or all connected clients. So basically what you need is to push an update, which will trigger all connected clients to update. Something like this:Once again, I'm writing this by memory, so it may need some polishing to make it into a fully working solution.
Progress Update:
It works now!!! Once again, thank you for the quick response, Miguel! The webpages interact with each other as expected, updating itself when a button from the opposite webpage is clicked. Below is the final code:
A minor downside to this implementation, however, is the case when one of the webpages is refreshed. In that situation, the webpage resets to the default text outlined in the code, instead of preserving the previous button value. This is because the routes of both Webpages A & B are set to render a template with static text in the beginning. Since the webpages rerun the application script whenever they are refreshed, the Webpages reset themselves accordingly.
A simple(ish) fix to this, however, is to use a database to track button presses. That way, instead of using some default text for the original web template value, the text can be preset to the latest button press before the refresh. The final code - if a database similar to the one previously outlined was used - would then look like the following:
Although there may be more efficient methods to solve this issue, this solution required the least effort to implement, as it was set up during my previous response.
Final Questions:
turbo.stream()
be preferred over theturbo.push()
method?.push()
method updated the second webpage dynamically, without calling a full page refresh. Would this method have affected any other webpages, so long as those webpages contains elements that had the same "target id" as the one specified in the method?Glad to hear this is working. Congrats!
Answers to your questions:
stream()
method is used to return an optimized response to aPOST
request. Because it is an HTTP response, it goes only to the client that made the originating request. The best use case for this is when a form is submitted, you can return validation errors viastream()
.push()
method accepts a second argument that is optional, used to indicate who should receive the update. When this argument is omitted, all connected clients get it. If you have other clients that also use Turbo-Flask and happen to have an element with the same ID you are passing here, then they will also update. If you assign IDs to your users (see documentation) you can then pass the second argument, which can be the ID of a single client to target, or a list with all the IDs you want to get the push.Hi,
Same uage, i think.
i've got a HTML page with return(render_template(...))
Result HTML :
My goal was to get a dynamic update without refresh page/frame, about usage and current transfer on DISK !
On DEBUG mode :
print('Turbo pousse pas ! :( ') # Translate = Turbo doesn't push ! :(
What's wrong ?
Thanks in advance !
@SlodeSoft first of all, you seem to have an error in your template. Can you fix that to eliminate that as a possible contributor to the problem?
If
turbo.can_push()
returns false it means that in that request, the client cannot handle turbo stream updates. I think you may need to familiarize yourself with the turbo client-side documentation to understand why your usage is invalid. Turbo stream updates are accepted only as a response of a form POST, I don't see any POST requests in your log.For reference / future readers. I got the example to work on Azure App Service Linux (Python 3.9)
Using the following line in the portal under Configuration >> General Settings >> Startup Command:
gunicorn --bind=0.0.0.0 --workers=1 --threads=100 app:app
And the somewhat modified code for app.py:
The secret key is set in the Portal under Configuration >> Application Settings