Initial commit: Location Management Flask app
This commit is contained in:
1
app/routes/__init__.py
Normal file
1
app/routes/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""Routes package."""
|
||||
87
app/routes/admin.py
Normal file
87
app/routes/admin.py
Normal file
@@ -0,0 +1,87 @@
|
||||
"""Admin routes – user management (admin only)."""
|
||||
from flask import Blueprint, render_template, redirect, url_for, flash, request, abort
|
||||
from flask_login import login_required, current_user
|
||||
from werkzeug.security import generate_password_hash
|
||||
|
||||
from app import db
|
||||
from app.models.user import User
|
||||
|
||||
admin_bp = Blueprint("admin", __name__)
|
||||
|
||||
|
||||
def _require_admin():
|
||||
if not current_user.is_authenticated or not current_user.is_admin():
|
||||
abort(403)
|
||||
|
||||
|
||||
@admin_bp.route("/users")
|
||||
@login_required
|
||||
def list_users():
|
||||
_require_admin()
|
||||
users = User.query.order_by(User.username).all()
|
||||
return render_template("admin/users.html", users=users)
|
||||
|
||||
|
||||
@admin_bp.route("/users/add", methods=["GET", "POST"])
|
||||
@login_required
|
||||
def add_user():
|
||||
_require_admin()
|
||||
if request.method == "POST":
|
||||
username = request.form.get("username", "").strip()
|
||||
password = request.form.get("password", "")
|
||||
role = request.form.get("role", "user")
|
||||
|
||||
if not username or not password:
|
||||
flash("Username and password are required.", "danger")
|
||||
return render_template("admin/user_form.html", user=None)
|
||||
|
||||
if User.query.filter_by(username=username).first():
|
||||
flash("Username already exists.", "danger")
|
||||
return render_template("admin/user_form.html", user=None)
|
||||
|
||||
user = User(
|
||||
username=username,
|
||||
password_hash=generate_password_hash(password),
|
||||
role=role,
|
||||
is_active=True,
|
||||
)
|
||||
db.session.add(user)
|
||||
db.session.commit()
|
||||
flash(f"User '{username}' created.", "success")
|
||||
return redirect(url_for("admin.list_users"))
|
||||
|
||||
return render_template("admin/user_form.html", user=None)
|
||||
|
||||
|
||||
@admin_bp.route("/users/<int:user_id>/edit", methods=["GET", "POST"])
|
||||
@login_required
|
||||
def edit_user(user_id: int):
|
||||
_require_admin()
|
||||
user = db.get_or_404(User, user_id)
|
||||
|
||||
if request.method == "POST":
|
||||
user.username = request.form.get("username", user.username).strip()
|
||||
user.role = request.form.get("role", user.role)
|
||||
user.is_active = "is_active" in request.form
|
||||
new_password = request.form.get("password", "").strip()
|
||||
if new_password:
|
||||
user.password_hash = generate_password_hash(new_password)
|
||||
db.session.commit()
|
||||
flash("User updated.", "success")
|
||||
return redirect(url_for("admin.list_users"))
|
||||
|
||||
return render_template("admin/user_form.html", user=user)
|
||||
|
||||
|
||||
@admin_bp.route("/users/<int:user_id>/delete", methods=["POST"])
|
||||
@login_required
|
||||
def delete_user(user_id: int):
|
||||
_require_admin()
|
||||
if user_id == current_user.id:
|
||||
flash("You cannot delete your own account.", "danger")
|
||||
return redirect(url_for("admin.list_users"))
|
||||
user = db.get_or_404(User, user_id)
|
||||
db.session.delete(user)
|
||||
db.session.commit()
|
||||
flash(f"User '{user.username}' deleted.", "warning")
|
||||
return redirect(url_for("admin.list_users"))
|
||||
81
app/routes/api.py
Normal file
81
app/routes/api.py
Normal file
@@ -0,0 +1,81 @@
|
||||
"""REST API – board webhook receiver and JSON relay control."""
|
||||
from datetime import datetime
|
||||
from flask import Blueprint, request, jsonify, abort
|
||||
|
||||
from app import db, socketio
|
||||
from app.models.board import Board
|
||||
from app.services import workflow_engine
|
||||
|
||||
api_bp = Blueprint("api", __name__)
|
||||
|
||||
|
||||
# ── webhook endpoint (boards POST input events here) ──────────────────────────
|
||||
|
||||
@api_bp.route("/webhook/<int:board_id>", methods=["POST"])
|
||||
def webhook(board_id: int):
|
||||
board = db.session.get(Board, board_id)
|
||||
if board is None:
|
||||
abort(404)
|
||||
|
||||
data = request.get_json(silent=True) or {}
|
||||
input_num = data.get("input")
|
||||
state = data.get("state")
|
||||
|
||||
if input_num is None or state is None:
|
||||
return jsonify({"error": "missing input or state"}), 400
|
||||
|
||||
# Update cached input state
|
||||
states = board.input_states
|
||||
states[f"input_{input_num}"] = bool(state)
|
||||
board.input_states = states
|
||||
board.is_online = True
|
||||
board.last_seen = datetime.utcnow()
|
||||
db.session.commit()
|
||||
|
||||
# Let the workflow engine decide what to do
|
||||
workflow_engine.process_input_event(board_id, int(input_num), bool(state))
|
||||
|
||||
# Push live update to all connected clients immediately
|
||||
socketio.emit("board_update", {
|
||||
"board_id": board_id,
|
||||
"is_online": True,
|
||||
"input_states": board.input_states,
|
||||
"relay_states": board.relay_states,
|
||||
})
|
||||
|
||||
return jsonify({"status": "ok"})
|
||||
|
||||
|
||||
# ── JSON relay status ─────────────────────────────────────────────────────────
|
||||
|
||||
@api_bp.route("/boards/<int:board_id>/relays")
|
||||
def relay_states(board_id: int):
|
||||
board = db.get_or_404(Board, board_id)
|
||||
return jsonify({
|
||||
"board_id": board.id,
|
||||
"name": board.name,
|
||||
"is_online": board.is_online,
|
||||
"relay_states": board.relay_states,
|
||||
"input_states": board.input_states,
|
||||
})
|
||||
|
||||
|
||||
# ── JSON board list ───────────────────────────────────────────────────────────
|
||||
|
||||
@api_bp.route("/boards")
|
||||
def board_list():
|
||||
boards = Board.query.order_by(Board.name).all()
|
||||
return jsonify([
|
||||
{
|
||||
"id": b.id,
|
||||
"name": b.name,
|
||||
"board_type": b.board_type,
|
||||
"host": b.host,
|
||||
"port": b.port,
|
||||
"is_online": b.is_online,
|
||||
"last_seen": b.last_seen.isoformat() if b.last_seen else None,
|
||||
"relay_states": b.relay_states,
|
||||
"input_states": b.input_states,
|
||||
}
|
||||
for b in boards
|
||||
])
|
||||
38
app/routes/auth.py
Normal file
38
app/routes/auth.py
Normal file
@@ -0,0 +1,38 @@
|
||||
"""Authentication routes."""
|
||||
from flask import Blueprint, render_template, redirect, url_for, flash, request
|
||||
from flask_login import login_user, logout_user, login_required, current_user
|
||||
from werkzeug.security import check_password_hash
|
||||
|
||||
from app.models.user import User
|
||||
|
||||
auth_bp = Blueprint("auth", __name__)
|
||||
|
||||
|
||||
@auth_bp.route("/login", methods=["GET", "POST"])
|
||||
def login():
|
||||
if current_user.is_authenticated:
|
||||
return redirect(url_for("dashboard.index"))
|
||||
|
||||
if request.method == "POST":
|
||||
username = request.form.get("username", "").strip()
|
||||
password = request.form.get("password", "")
|
||||
remember = bool(request.form.get("remember"))
|
||||
|
||||
user = User.query.filter_by(username=username).first()
|
||||
if user and user.is_active and check_password_hash(user.password_hash, password):
|
||||
login_user(user, remember=remember)
|
||||
next_page = request.args.get("next")
|
||||
flash(f"Welcome back, {user.username}!", "success")
|
||||
return redirect(next_page or url_for("dashboard.index"))
|
||||
|
||||
flash("Invalid username or password.", "danger")
|
||||
|
||||
return render_template("auth/login.html")
|
||||
|
||||
|
||||
@auth_bp.route("/logout")
|
||||
@login_required
|
||||
def logout():
|
||||
logout_user()
|
||||
flash("You have been logged out.", "info")
|
||||
return redirect(url_for("auth.login"))
|
||||
258
app/routes/boards.py
Normal file
258
app/routes/boards.py
Normal file
@@ -0,0 +1,258 @@
|
||||
"""Board management routes."""
|
||||
import json
|
||||
from flask import Blueprint, render_template, redirect, url_for, flash, request, abort, jsonify
|
||||
from flask_login import login_required, current_user
|
||||
|
||||
from app import db
|
||||
from app.models.board import Board
|
||||
from app.services.board_service import poll_board, register_webhook, set_relay
|
||||
from app.drivers.registry import registry
|
||||
from flask import current_app
|
||||
|
||||
boards_bp = Blueprint("boards", __name__)
|
||||
|
||||
|
||||
def _board_types():
|
||||
"""Dynamic list read from the driver registry — auto-updates when drivers are added."""
|
||||
return registry.choices()
|
||||
|
||||
|
||||
# ── list ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
@boards_bp.route("/")
|
||||
@login_required
|
||||
def list_boards():
|
||||
boards = Board.query.order_by(Board.name).all()
|
||||
return render_template("boards/list.html", boards=boards)
|
||||
|
||||
|
||||
# ── detail / quick controls ───────────────────────────────────────────────────
|
||||
|
||||
@boards_bp.route("/<int:board_id>")
|
||||
@login_required
|
||||
def board_detail(board_id: int):
|
||||
board = db.get_or_404(Board, board_id)
|
||||
# Refresh states from device
|
||||
poll_board(current_app._get_current_object(), board_id)
|
||||
board = db.session.get(Board, board_id)
|
||||
return render_template("boards/detail.html", board=board)
|
||||
|
||||
|
||||
# ── add ───────────────────────────────────────────────────────────────────────
|
||||
|
||||
@boards_bp.route("/add", methods=["GET", "POST"])
|
||||
@login_required
|
||||
def add_board():
|
||||
if not current_user.is_admin():
|
||||
abort(403)
|
||||
|
||||
if request.method == "POST":
|
||||
name = request.form.get("name", "").strip()
|
||||
board_type = request.form.get("board_type", "olimex_esp32_c6_evb")
|
||||
host = request.form.get("host", "").strip()
|
||||
port = int(request.form.get("port", 80))
|
||||
num_relays = int(request.form.get("num_relays", 4))
|
||||
num_inputs = int(request.form.get("num_inputs", 4))
|
||||
|
||||
if not name or not host:
|
||||
flash("Name and host are required.", "danger")
|
||||
return render_template("boards/add.html", board_types=_board_types(), drivers=registry.all())
|
||||
|
||||
board = Board(
|
||||
name=name,
|
||||
board_type=board_type,
|
||||
host=host,
|
||||
port=port,
|
||||
num_relays=num_relays,
|
||||
num_inputs=num_inputs,
|
||||
)
|
||||
db.session.add(board)
|
||||
db.session.commit()
|
||||
|
||||
# Try to register webhook immediately
|
||||
server_url = current_app.config.get("SERVER_BASE_URL", "http://localhost:5000")
|
||||
register_webhook(board, server_url)
|
||||
|
||||
flash(f"Board '{name}' added successfully.", "success")
|
||||
return redirect(url_for("boards.board_detail", board_id=board.id))
|
||||
|
||||
return render_template("boards/add.html", board_types=_board_types(), drivers=registry.all())
|
||||
|
||||
|
||||
# ── edit ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
@boards_bp.route("/<int:board_id>/edit", methods=["GET", "POST"])
|
||||
@login_required
|
||||
def edit_board(board_id: int):
|
||||
if not current_user.is_admin():
|
||||
abort(403)
|
||||
board = db.get_or_404(Board, board_id)
|
||||
|
||||
if request.method == "POST":
|
||||
board.name = request.form.get("name", board.name).strip()
|
||||
board.host = request.form.get("host", board.host).strip()
|
||||
board.port = int(request.form.get("port", board.port))
|
||||
board.board_type = request.form.get("board_type", board.board_type)
|
||||
board.num_relays = int(request.form.get("num_relays", board.num_relays))
|
||||
board.num_inputs = int(request.form.get("num_inputs", board.num_inputs))
|
||||
|
||||
# Update labels
|
||||
labels = {}
|
||||
for n in range(1, board.num_relays + 1):
|
||||
lbl = request.form.get(f"relay_{n}_label", "").strip()
|
||||
if lbl:
|
||||
labels[f"relay_{n}"] = lbl
|
||||
for n in range(1, board.num_inputs + 1):
|
||||
lbl = request.form.get(f"input_{n}_label", "").strip()
|
||||
if lbl:
|
||||
labels[f"input_{n}"] = lbl
|
||||
board.labels = labels
|
||||
|
||||
db.session.commit()
|
||||
flash("Board updated.", "success")
|
||||
return redirect(url_for("boards.board_detail", board_id=board.id))
|
||||
|
||||
return render_template("boards/edit.html", board=board, board_types=_board_types())
|
||||
|
||||
|
||||
# ── delete ────────────────────────────────────────────────────────────────────
|
||||
|
||||
@boards_bp.route("/<int:board_id>/delete", methods=["POST"])
|
||||
@login_required
|
||||
def delete_board(board_id: int):
|
||||
if not current_user.is_admin():
|
||||
abort(403)
|
||||
board = db.get_or_404(Board, board_id)
|
||||
db.session.delete(board)
|
||||
db.session.commit()
|
||||
flash(f"Board '{board.name}' deleted.", "warning")
|
||||
return redirect(url_for("boards.list_boards"))
|
||||
|
||||
|
||||
# ── quick relay toggle ───────────────────────────────────────────────────────
|
||||
|
||||
@boards_bp.route("/<int:board_id>/relay/<int:relay_num>/toggle", methods=["POST"])
|
||||
@login_required
|
||||
def toggle_relay_view(board_id: int, relay_num: int):
|
||||
from app import socketio
|
||||
board = db.get_or_404(Board, board_id)
|
||||
|
||||
# Always flip the local cached state first (optimistic update)
|
||||
states = board.relay_states
|
||||
current = states.get(f"relay_{relay_num}", False)
|
||||
new_state = not current
|
||||
states[f"relay_{relay_num}"] = new_state
|
||||
board.relay_states = states
|
||||
db.session.commit()
|
||||
|
||||
# Best-effort: send the command to the physical board using set_relay
|
||||
# (uses /relay/on or /relay/off — same endpoints as the detail page ON/OFF buttons)
|
||||
hw_ok = set_relay(board, relay_num, new_state)
|
||||
|
||||
# Push live update to all clients
|
||||
socketio.emit("board_update", {
|
||||
"board_id": board_id,
|
||||
"is_online": board.is_online,
|
||||
"relay_states": board.relay_states,
|
||||
"input_states": board.input_states,
|
||||
})
|
||||
|
||||
label = board.get_relay_label(relay_num)
|
||||
status_text = "ON" if new_state else "OFF"
|
||||
hw_warning = not hw_ok # True when board was unreachable
|
||||
|
||||
# JSON response for AJAX callers (dashboard)
|
||||
if request.accept_mimetypes.accept_json and not request.accept_mimetypes.accept_html:
|
||||
return jsonify({
|
||||
"relay": relay_num,
|
||||
"state": new_state,
|
||||
"label": label,
|
||||
"hw_ok": not hw_warning,
|
||||
})
|
||||
|
||||
# HTML response for form-submit callers (detail page)
|
||||
if hw_warning:
|
||||
flash(f"{label}: {status_text} (board unreachable — local state updated)", "warning")
|
||||
else:
|
||||
flash(f"{label}: {status_text}", "info")
|
||||
return redirect(url_for("boards.board_detail", board_id=board_id))
|
||||
|
||||
|
||||
# ── quick relay set ───────────────────────────────────────────────────────────
|
||||
|
||||
@boards_bp.route("/<int:board_id>/relay/<int:relay_num>/set", methods=["POST"])
|
||||
@login_required
|
||||
def set_relay_view(board_id: int, relay_num: int):
|
||||
from app import socketio
|
||||
board = db.get_or_404(Board, board_id)
|
||||
state = request.form.get("state", "off") == "on"
|
||||
|
||||
# Always update local state
|
||||
states = board.relay_states
|
||||
states[f"relay_{relay_num}"] = state
|
||||
board.relay_states = states
|
||||
db.session.commit()
|
||||
|
||||
# Best-effort send to hardware
|
||||
hw_ok = set_relay(board, relay_num, state)
|
||||
|
||||
socketio.emit("board_update", {
|
||||
"board_id": board_id,
|
||||
"is_online": board.is_online,
|
||||
"relay_states": board.relay_states,
|
||||
"input_states": board.input_states,
|
||||
})
|
||||
|
||||
label = board.get_relay_label(relay_num)
|
||||
status_text = "ON" if state else "OFF"
|
||||
if not hw_ok:
|
||||
flash(f"{label}: {status_text} (board unreachable — local state updated)", "warning")
|
||||
else:
|
||||
flash(f"{label}: {status_text}", "info")
|
||||
return redirect(url_for("boards.board_detail", board_id=board_id))
|
||||
|
||||
|
||||
# ── edit entity configuration ─────────────────────────────────────────────────
|
||||
|
||||
@boards_bp.route("/<int:board_id>/entities", methods=["GET", "POST"])
|
||||
@login_required
|
||||
def edit_entities(board_id: int):
|
||||
if not current_user.is_admin():
|
||||
abort(403)
|
||||
board = db.get_or_404(Board, board_id)
|
||||
|
||||
if request.method == "POST":
|
||||
entities = {}
|
||||
for n in range(1, board.num_relays + 1):
|
||||
entities[f"relay_{n}"] = {
|
||||
"type": request.form.get(f"relay_{n}_type", "switch"),
|
||||
"name": request.form.get(f"relay_{n}_name", "").strip()[:20],
|
||||
"icon": request.form.get(f"relay_{n}_icon", "").strip(),
|
||||
}
|
||||
for n in range(1, board.num_inputs + 1):
|
||||
entities[f"input_{n}"] = {
|
||||
"type": request.form.get(f"input_{n}_type", "generic"),
|
||||
"name": request.form.get(f"input_{n}_name", "").strip()[:20],
|
||||
"icon": request.form.get(f"input_{n}_icon", "").strip(),
|
||||
}
|
||||
board.entities = entities
|
||||
db.session.commit()
|
||||
flash("Entity configuration saved.", "success")
|
||||
return redirect(url_for("boards.board_detail", board_id=board_id))
|
||||
|
||||
from app.models.entity_types import RELAY_ENTITY_TYPES, INPUT_ENTITY_TYPES, ICON_PALETTE
|
||||
return render_template(
|
||||
"boards/edit_entities.html",
|
||||
board=board,
|
||||
relay_types=RELAY_ENTITY_TYPES,
|
||||
input_types=INPUT_ENTITY_TYPES,
|
||||
icon_palette=ICON_PALETTE,
|
||||
)
|
||||
|
||||
|
||||
# ── edit labels (legacy — redirects to edit_entities) ─────────────────────────
|
||||
|
||||
@boards_bp.route("/<int:board_id>/labels", methods=["GET", "POST"])
|
||||
@login_required
|
||||
def edit_labels(board_id: int):
|
||||
return redirect(url_for("boards.edit_entities", board_id=board_id))
|
||||
20
app/routes/dashboard.py
Normal file
20
app/routes/dashboard.py
Normal file
@@ -0,0 +1,20 @@
|
||||
"""Dashboard route."""
|
||||
from flask import Blueprint, render_template
|
||||
from flask_login import login_required
|
||||
|
||||
from app.models.board import Board
|
||||
from app.models.workflow import Workflow
|
||||
|
||||
dashboard_bp = Blueprint("dashboard", __name__)
|
||||
|
||||
|
||||
@dashboard_bp.route("/")
|
||||
@login_required
|
||||
def index():
|
||||
boards = Board.query.order_by(Board.name).all()
|
||||
workflows = Workflow.query.filter_by(is_enabled=True).count()
|
||||
return render_template(
|
||||
"dashboard/index.html",
|
||||
boards=boards,
|
||||
active_workflows=workflows,
|
||||
)
|
||||
98
app/routes/workflows.py
Normal file
98
app/routes/workflows.py
Normal file
@@ -0,0 +1,98 @@
|
||||
"""Workflow management routes."""
|
||||
from flask import Blueprint, render_template, redirect, url_for, flash, request, abort
|
||||
from flask_login import login_required, current_user
|
||||
|
||||
from app import db
|
||||
from app.models.board import Board
|
||||
from app.models.workflow import Workflow
|
||||
|
||||
workflows_bp = Blueprint("workflows", __name__)
|
||||
|
||||
EVENTS = [("press", "Press (rising edge)"), ("release", "Release (falling)"), ("both", "Both")]
|
||||
ACTIONS = [("on", "Turn ON"), ("off", "Turn OFF"), ("toggle", "Toggle")]
|
||||
|
||||
|
||||
@workflows_bp.route("/")
|
||||
@login_required
|
||||
def list_workflows():
|
||||
workflows = Workflow.query.order_by(Workflow.name).all()
|
||||
return render_template("workflows/list.html", workflows=workflows)
|
||||
|
||||
|
||||
@workflows_bp.route("/add", methods=["GET", "POST"])
|
||||
@login_required
|
||||
def add_workflow():
|
||||
if not current_user.is_admin():
|
||||
abort(403)
|
||||
boards = Board.query.order_by(Board.name).all()
|
||||
|
||||
if request.method == "POST":
|
||||
wf = Workflow(
|
||||
name=request.form.get("name", "").strip(),
|
||||
trigger_board_id=int(request.form["trigger_board_id"]),
|
||||
trigger_input=int(request.form["trigger_input"]),
|
||||
trigger_event=request.form.get("trigger_event", "press"),
|
||||
action_board_id=int(request.form["action_board_id"]),
|
||||
action_relay=int(request.form["action_relay"]),
|
||||
action_type=request.form.get("action_type", "toggle"),
|
||||
is_enabled=True,
|
||||
)
|
||||
if not wf.name:
|
||||
flash("Name is required.", "danger")
|
||||
return render_template("workflows/edit.html", wf=None, boards=boards,
|
||||
events=EVENTS, actions=ACTIONS)
|
||||
db.session.add(wf)
|
||||
db.session.commit()
|
||||
flash(f"Workflow '{wf.name}' created.", "success")
|
||||
return redirect(url_for("workflows.list_workflows"))
|
||||
|
||||
return render_template("workflows/edit.html", wf=None, boards=boards,
|
||||
events=EVENTS, actions=ACTIONS)
|
||||
|
||||
|
||||
@workflows_bp.route("/<int:wf_id>/edit", methods=["GET", "POST"])
|
||||
@login_required
|
||||
def edit_workflow(wf_id: int):
|
||||
if not current_user.is_admin():
|
||||
abort(403)
|
||||
wf = db.get_or_404(Workflow, wf_id)
|
||||
boards = Board.query.order_by(Board.name).all()
|
||||
|
||||
if request.method == "POST":
|
||||
wf.name = request.form.get("name", wf.name).strip()
|
||||
wf.trigger_board_id = int(request.form.get("trigger_board_id", wf.trigger_board_id))
|
||||
wf.trigger_input = int(request.form.get("trigger_input", wf.trigger_input))
|
||||
wf.trigger_event = request.form.get("trigger_event", wf.trigger_event)
|
||||
wf.action_board_id = int(request.form.get("action_board_id", wf.action_board_id))
|
||||
wf.action_relay = int(request.form.get("action_relay", wf.action_relay))
|
||||
wf.action_type = request.form.get("action_type", wf.action_type)
|
||||
wf.is_enabled = "is_enabled" in request.form
|
||||
db.session.commit()
|
||||
flash("Workflow updated.", "success")
|
||||
return redirect(url_for("workflows.list_workflows"))
|
||||
|
||||
return render_template("workflows/edit.html", wf=wf, boards=boards,
|
||||
events=EVENTS, actions=ACTIONS)
|
||||
|
||||
|
||||
@workflows_bp.route("/<int:wf_id>/delete", methods=["POST"])
|
||||
@login_required
|
||||
def delete_workflow(wf_id: int):
|
||||
if not current_user.is_admin():
|
||||
abort(403)
|
||||
wf = db.get_or_404(Workflow, wf_id)
|
||||
db.session.delete(wf)
|
||||
db.session.commit()
|
||||
flash(f"Workflow '{wf.name}' deleted.", "warning")
|
||||
return redirect(url_for("workflows.list_workflows"))
|
||||
|
||||
|
||||
@workflows_bp.route("/<int:wf_id>/toggle", methods=["POST"])
|
||||
@login_required
|
||||
def toggle_workflow(wf_id: int):
|
||||
wf = db.get_or_404(Workflow, wf_id)
|
||||
wf.is_enabled = not wf.is_enabled
|
||||
db.session.commit()
|
||||
state = "enabled" if wf.is_enabled else "disabled"
|
||||
flash(f"Workflow '{wf.name}' {state}.", "info")
|
||||
return redirect(url_for("workflows.list_workflows"))
|
||||
Reference in New Issue
Block a user