miguelgrinberg / turbo-flask

Integration of Hotwire's Turbo library with Flask.
MIT License
301 stars 35 forks source link

Using Turbo-Flask to dynamically update <select> options from a database? #22

Closed ghost closed 2 years ago

ghost commented 2 years ago

Hi, thanks for this library, it's looking like it will solve an issue that I have, I just can't get my head around how right now!

I'm working on a platform where devices can register themselves with a central database using MQTT. Users then select a device for a time-limited session, and assign it to a location via a web interface.

I've got the devices registering perfectly fine with the backend database, and my plan was to use Turbo-Flask to either:

  1. Poll the database every second looking for new devices
  2. Listen to the /register MQTT channel wait for new devices

Turbo-flask would then update a form select dropdown in the UI so that the user can select that device.

I've got the LoadAVG code working fine, I'm just struggling slightly to understand how I update a single form element rather than reloading an entire template!

miguelgrinberg commented 2 years ago

Give your select field an id attribute, then you can send a new version of it with a different list of options as an update.

ghost commented 2 years ago

OK, thanks, so the template would look like

<!-- template/options.html -->
<select id="mySelect">
{% for option in data %}
<option value="{{ option.value }}">{{option.name}}</option>
{% endfor %}

and the call to Turbo-Flask

turbo.push(turbo.replace(render_template('options.html'), 'mySelect'))

?

miguelgrinberg commented 2 years ago

Yeah, I think that should do it. But you need to close your </select>.

ghost commented 2 years ago

OK, I'm getting a lot closer, however I'm now seeing a context error (TypeError: cannot convert dictionary update sequence element #0 to a sequence) when I try and run the code.

I think this is because my turbo-flask call is in a blueprint:

# extensions.py
# Initialise Turbo for websocket connectivity
from turbo_flask import Turbo
turbo = Turbo()
# devices.py
import logging
import sys
import threading
import time
import uuid

from flask import Blueprint, render_template, current_app
from flask_login import login_required, current_user

from . import db
from .models import *
from .extensions import turbo

devices = Blueprint('devices', __name__)

@devices.app_context_processor
def get_devices():
    urd = []
    for d in RegisteredDevices.query.filter(RegisteredDevices.device_is_registered == False).all():
        urd.append(d)
    return urd

@devices.before_app_first_request
def before_first_request():
    threading.Thread(target=device_updates, args=(current_app._get_current_object(),)).start()

def device_updates(app):
    with app.app_context():
        while True:
            print("Publishing...")
            time.sleep(5)
            turbo.push(turbo.replace(render_template('session_devices.html'), 'unregistered_devices'))
# __init__.py
import os 
import socket

from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from flask_login import LoginManager
from .extensions import turbo

# init SQLAlchemy so we can use it later in our models
db = SQLAlchemy()

def create_app():
    app = Flask(__name__)

    app.config['SERVER_NAME'] = os.getenv("APP_SERVER_NAME")
    app.config['SECRET_KEY'] = os.getenv("APP_SECRET_KEY")
    app.config['SQLALCHEMY_DATABASE_URI'] = os.getenv("APP_DB_URI")

    db.init_app(app)
    turbo.init_app(app)

    login_manager = LoginManager()
    login_manager.login_view = 'auth.login'
    login_manager.init_app(app)

    from .models import User

    @login_manager.user_loader
    def load_user(user_id):
        # since the user_id is just the primary key of our user table, use it in the query for the user
        return User.query.get(int(user_id))

    # blueprint for auth routes in our app
    from .auth import auth as auth_blueprint
    app.register_blueprint(auth_blueprint)

    # blueprint for non-auth parts of app
    from .main import main as main_blueprint
    app.register_blueprint(main_blueprint)

    # blueprint for session parts of app
    from .devices import devices as devices_blueprint
    app.register_blueprint(devices_blueprint)

    return app

When I visit any URI, I get the following error on the screen and in the logs:

127.0.0.1 - - [19/Jan/2022 07:20:54] "GET /devices HTTP/1.1" 500 -
Traceback (most recent call last):
  File "/home/ff/.local/share/virtualenvs/device_test/lib/python3.9/site-packages/flask/app.py", line 2091, in __call__
    return self.wsgi_app(environ, start_response)
  File "/home/ff/.local/share/virtualenvs/device_test/lib/python3.9/site-packages/flask/app.py", line 2076, in wsgi_app
    response = self.handle_exception(e)
  File "/home/ff/.local/share/virtualenvs/device_test/lib/python3.9/site-packages/flask/app.py", line 2073, in wsgi_app
    response = self.full_dispatch_request()
  File "/home/ff/.local/share/virtualenvs/device_test/lib/python3.9/site-packages/flask/app.py", line 1518, in full_dispatch_request
    rv = self.handle_user_exception(e)
  File "/home/ff/.local/share/virtualenvs/device_test/lib/python3.9/site-packages/flask/app.py", line 1516, in full_dispatch_request
    rv = self.dispatch_request()
  File "/home/ff/.local/share/virtualenvs/device_test/lib/python3.9/site-packages/flask/app.py", line 1502, in dispatch_request
    return self.ensure_sync(self.view_functions[rule.endpoint])(**req.view_args)
  File "/home/ff/.local/share/virtualenvs/device_test/lib/python3.9/site-packages/flask_login/utils.py", line 272, in decorated_view
    return func(*args, **kwargs)
  File "/home/ff/device_test/devices.py", line 32, in index
    return render_template(
  File "/home/ff/.local/share/virtualenvs/device_test/lib/python3.9/site-packages/flask/templating.py", line 146, in render_template
    ctx.app.update_template_context(context)
  File "/home/ff/.local/share/virtualenvs/device_test/lib/python3.9/site-packages/flask/app.py", line 756, in update_template_context
    context.update(func())
TypeError: cannot convert dictionary update sequence element #0 to a sequence
127.0.0.1 - - [19/Jan/2022 07:20:54] "GET /devices?__debugger__=yes&cmd=resource&f=style.css HTTP/1.1" 200 -
127.0.0.1 - - [19/Jan/2022 07:20:54] "GET /devices?__debugger__=yes&cmd=resource&f=debugger.js HTTP/1.1" 200 -
127.0.0.1 - - [19/Jan/2022 07:20:54] "GET /devices?__debugger__=yes&cmd=resource&f=console.png HTTP/1.1" 200 -
127.0.0.1 - - [19/Jan/2022 07:20:54] "GET /devices?__debugger__=yes&cmd=resource&f=console.png HTTP/1.1" 200 -
Exception in thread Thread-4:
Traceback (most recent call last):
  File "/usr/lib/python3.9/threading.py", line 973, in _bootstrap_inner
    self.run()
  File "/usr/lib/python3.9/threading.py", line 910, in run
    self._target(*self._args, **self._kwargs)
  File "/home/ff/device_test/devices.py", line 70, in device_updates
    turbo.push(turbo.replace(render_template('session_devices.html'), 'unregistered_devices'))
  File "/home/ff/.local/share/virtualenvs/device_test/lib/python3.9/site-packages/flask/templating.py", line 146, in render_template
    ctx.app.update_template_context(context)
  File "/home/ff/.local/share/virtualenvs/device_test/lib/python3.9/site-packages/flask/app.py", line 756, in update_template_context
    context.update(func())
TypeError: cannot convert dictionary update sequence element #0 to a sequence

It works fine until I try to push the DB query results to the turbo front-end, do I need to re-marshall the device data into an different dict?

miguelgrinberg commented 2 years ago

Your template context function appears to be returning something that is not a dict.

ghost commented 2 years ago

OK, thanks, I'll close this off for now and debug the template issue! :)