mirror of
https://github.com/GSA/notifications-admin.git
synced 2026-09-11 10:28:41 -04:00
install socket io and getting an example up
This commit is contained in:
@@ -20,6 +20,7 @@ from flask import (
|
|||||||
)
|
)
|
||||||
from flask.globals import request_ctx
|
from flask.globals import request_ctx
|
||||||
from flask_login import LoginManager, current_user
|
from flask_login import LoginManager, current_user
|
||||||
|
from flask_socketio import SocketIO
|
||||||
from flask_talisman import Talisman
|
from flask_talisman import Talisman
|
||||||
from flask_wtf import CSRFProtect
|
from flask_wtf import CSRFProtect
|
||||||
from flask_wtf.csrf import CSRFError
|
from flask_wtf.csrf import CSRFError
|
||||||
@@ -121,6 +122,7 @@ from notifications_utils.url_safe_token import generate_token
|
|||||||
login_manager = LoginManager()
|
login_manager = LoginManager()
|
||||||
csrf = CSRFProtect()
|
csrf = CSRFProtect()
|
||||||
talisman = Talisman()
|
talisman = Talisman()
|
||||||
|
socketio = SocketIO()
|
||||||
|
|
||||||
# The current service attached to the request stack.
|
# The current service attached to the request stack.
|
||||||
current_service = LocalProxy(partial(getattr, request_ctx, "service"))
|
current_service = LocalProxy(partial(getattr, request_ctx, "service"))
|
||||||
@@ -217,6 +219,7 @@ def create_app(application):
|
|||||||
|
|
||||||
init_govuk_frontend(application)
|
init_govuk_frontend(application)
|
||||||
init_jinja(application)
|
init_jinja(application)
|
||||||
|
socketio.init_app(application, cors_allowed_origins=['http://localhost:6012'])
|
||||||
|
|
||||||
for client in (
|
for client in (
|
||||||
csrf,
|
csrf,
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
const socket = io();
|
||||||
|
const jobId = document.querySelector("[data-job-id]").dataset.jobId;
|
||||||
|
|
||||||
|
socket.on('connect', () => {
|
||||||
|
console.log('Connected to server');
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
// Join a room
|
||||||
|
socket.emit("join", { room: `job-${jobId}` });
|
||||||
|
console.log("🧠 Joined room:", `job-${jobId}`);
|
||||||
|
|
||||||
|
// Listen for job updates
|
||||||
|
socket.on("job_update", function (data) {
|
||||||
|
const el = document.querySelector(`[data-key="${data.key}"]`);
|
||||||
|
if (el) {
|
||||||
|
el.textContent = "✅ Real-time update success!";
|
||||||
|
el.classList.add("text-success"); // Add a class instead
|
||||||
|
}
|
||||||
|
});
|
||||||
+38
-1
@@ -21,6 +21,7 @@ from app import (
|
|||||||
format_datetime_table,
|
format_datetime_table,
|
||||||
notification_api_client,
|
notification_api_client,
|
||||||
service_api_client,
|
service_api_client,
|
||||||
|
socketio,
|
||||||
)
|
)
|
||||||
from app.formatters import get_time_left, message_count_noun
|
from app.formatters import get_time_left, message_count_noun
|
||||||
from app.main import main
|
from app.main import main
|
||||||
@@ -28,6 +29,7 @@ from app.main.forms import SearchNotificationsForm
|
|||||||
from app.models.job import Job
|
from app.models.job import Job
|
||||||
from app.utils import parse_filter_args, set_status_filters
|
from app.utils import parse_filter_args, set_status_filters
|
||||||
from app.utils.csv import generate_notifications_csv
|
from app.utils.csv import generate_notifications_csv
|
||||||
|
from flask_socketio import emit, SocketIO, send, join_room
|
||||||
from app.utils.pagination import (
|
from app.utils.pagination import (
|
||||||
generate_next_dict,
|
generate_next_dict,
|
||||||
generate_previous_dict,
|
generate_previous_dict,
|
||||||
@@ -57,6 +59,7 @@ def view_job(service_id, job_id):
|
|||||||
|
|
||||||
filter_args = parse_filter_args(request.args)
|
filter_args = parse_filter_args(request.args)
|
||||||
filter_args["status"] = set_status_filters(filter_args)
|
filter_args["status"] = set_status_filters(filter_args)
|
||||||
|
|
||||||
return render_template(
|
return render_template(
|
||||||
"views/jobs/job.html",
|
"views/jobs/job.html",
|
||||||
job=job,
|
job=job,
|
||||||
@@ -103,9 +106,11 @@ def view_job_csv(service_id, job_id):
|
|||||||
@user_has_permissions("send_messages")
|
@user_has_permissions("send_messages")
|
||||||
def cancel_job(service_id, job_id):
|
def cancel_job(service_id, job_id):
|
||||||
Job.from_id(job_id, service_id=service_id).cancel()
|
Job.from_id(job_id, service_id=service_id).cancel()
|
||||||
|
|
||||||
return redirect(url_for("main.service_dashboard", service_id=service_id))
|
return redirect(url_for("main.service_dashboard", service_id=service_id))
|
||||||
|
|
||||||
|
# //this is the resource that gets updated by being passed to ajax via updates_url to data-resource
|
||||||
|
# and everything that is within get_job_partials is the html that gets updated
|
||||||
@main.route("/services/<uuid:service_id>/jobs/<uuid:job_id>.json")
|
@main.route("/services/<uuid:service_id>/jobs/<uuid:job_id>.json")
|
||||||
@user_has_permissions()
|
@user_has_permissions()
|
||||||
def view_job_updates(service_id, job_id):
|
def view_job_updates(service_id, job_id):
|
||||||
@@ -113,6 +118,38 @@ def view_job_updates(service_id, job_id):
|
|||||||
|
|
||||||
return jsonify(**get_job_partials(job))
|
return jsonify(**get_job_partials(job))
|
||||||
|
|
||||||
|
# def emit_job_update(service_id, job, job_id):
|
||||||
|
# print("⚡️ Emitting job update for:", job_id)
|
||||||
|
# socketio.emit(
|
||||||
|
# "job_update",
|
||||||
|
# {"key": "status", "html": render_template("partials/jobs/status.html", job=job)},
|
||||||
|
# room=f"job-{job_id}"
|
||||||
|
# )
|
||||||
|
|
||||||
|
@socketio.on("join")
|
||||||
|
def on_join(data):
|
||||||
|
room = data["room"]
|
||||||
|
join_room(room)
|
||||||
|
print(f"Client joined room {room}")
|
||||||
|
|
||||||
|
@main.route("/services/<uuid:service_id>/jobs/<uuid:job_id>/test-socket-update")
|
||||||
|
def test_socket_update(service_id,job_id):
|
||||||
|
print(f"⚡️ Emitting job update for: {job_id}")
|
||||||
|
# Replace with a real job ID and service ID for testing
|
||||||
|
job = Job.from_id(job_id, service_id)
|
||||||
|
service_id=service_id
|
||||||
|
job_id = job_id
|
||||||
|
|
||||||
|
# Call your emit function
|
||||||
|
socketio.emit(
|
||||||
|
"job_update",
|
||||||
|
{
|
||||||
|
"key": "status",
|
||||||
|
"html": "<div class='text-success'>✅ Real-time update success!</div>"
|
||||||
|
},
|
||||||
|
room=f"job-{job_id}" # match the room name used in frontend
|
||||||
|
)
|
||||||
|
return "Update sent!"
|
||||||
|
|
||||||
@main.route("/services/<uuid:service_id>/notifications", methods=["GET", "POST"])
|
@main.route("/services/<uuid:service_id>/notifications", methods=["GET", "POST"])
|
||||||
@main.route(
|
@main.route(
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ from zoneinfo import ZoneInfo
|
|||||||
from app.extensions import redis_client
|
from app.extensions import redis_client
|
||||||
from app.notify_client import NotifyAdminAPIClient, _attach_current_user, cache
|
from app.notify_client import NotifyAdminAPIClient, _attach_current_user, cache
|
||||||
from app.utils.csv import get_user_preferred_timezone
|
from app.utils.csv import get_user_preferred_timezone
|
||||||
|
# from app.main.views.jobs import emit_job_update
|
||||||
|
|
||||||
class JobApiClient(NotifyAdminAPIClient):
|
class JobApiClient(NotifyAdminAPIClient):
|
||||||
JOB_STATUSES = {
|
JOB_STATUSES = {
|
||||||
|
|||||||
@@ -12,3 +12,17 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endmacro %}
|
{% endmacro %}
|
||||||
|
|
||||||
|
{% macro socket_block(partials, job_id, key, finished=False, form='') %}
|
||||||
|
{% if not finished %}
|
||||||
|
<div
|
||||||
|
data-module="live-content"
|
||||||
|
data-key="{{ key }}"
|
||||||
|
data-room="job-{{ job_id }}"
|
||||||
|
>
|
||||||
|
{% endif %}
|
||||||
|
{{ partials[key]|safe }}
|
||||||
|
{% if not finished %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endmacro %}
|
||||||
|
|||||||
@@ -3,12 +3,21 @@
|
|||||||
{% from "components/page-footer.html" import page_footer %}
|
{% from "components/page-footer.html" import page_footer %}
|
||||||
{% from "components/page-header.html" import page_header %}
|
{% from "components/page-header.html" import page_header %}
|
||||||
{% from "components/components/back-link/macro.njk" import usaBackLink %}
|
{% from "components/components/back-link/macro.njk" import usaBackLink %}
|
||||||
|
{% from 'components/ajax-block.html' import socket_block %}
|
||||||
{% block service_page_title %}
|
{% block service_page_title %}
|
||||||
{{ "Message status" }}
|
{{ "Message status" }}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block maincolumn_content %}
|
{% block maincolumn_content %}
|
||||||
|
{{ socket_block(partials, job_id, "status") }}
|
||||||
|
<div id="status" data-key="status" data-job-id="{{ job.id }}">
|
||||||
|
⏳ Waiting for updates...
|
||||||
|
</div>
|
||||||
|
<!-- <div data-key="status">
|
||||||
|
{{ partials["status"]|safe }}
|
||||||
|
</div> -->
|
||||||
|
<!-- {{emit_job_update}}
|
||||||
|
{{job.id}} -->
|
||||||
|
|
||||||
{{ page_header("Message status") }}
|
{{ page_header("Message status") }}
|
||||||
{% if not job.finished_processing %}
|
{% if not job.finished_processing %}
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ const javascripts = () => {
|
|||||||
paths.npm + 'textarea-caret/index.js',
|
paths.npm + 'textarea-caret/index.js',
|
||||||
paths.npm + 'cbor-js/cbor.js',
|
paths.npm + 'cbor-js/cbor.js',
|
||||||
paths.npm + 'd3/dist/d3.min.js',
|
paths.npm + 'd3/dist/d3.min.js',
|
||||||
|
paths.npm + 'socket.io-client/dist/socket.io.min.js',
|
||||||
])
|
])
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -83,6 +84,8 @@ const javascripts = () => {
|
|||||||
paths.src + 'javascripts/activityChart.js',
|
paths.src + 'javascripts/activityChart.js',
|
||||||
paths.src + 'javascripts/sidenav.js',
|
paths.src + 'javascripts/sidenav.js',
|
||||||
paths.src + 'javascripts/validation.js',
|
paths.src + 'javascripts/validation.js',
|
||||||
|
paths.src + 'javascripts/socketio.js',
|
||||||
|
|
||||||
])
|
])
|
||||||
.pipe(plugins.prettyerror())
|
.pipe(plugins.prettyerror())
|
||||||
.pipe(
|
.pipe(
|
||||||
|
|||||||
Generated
+87
-3
@@ -25,6 +25,7 @@
|
|||||||
"python": "^0.0.4",
|
"python": "^0.0.4",
|
||||||
"query-command-supported": "1.0.0",
|
"query-command-supported": "1.0.0",
|
||||||
"sass-embedded": "^1.86.0",
|
"sass-embedded": "^1.86.0",
|
||||||
|
"socket.io-client": "^4.8.1",
|
||||||
"textarea-caret": "3.1.0",
|
"textarea-caret": "3.1.0",
|
||||||
"timeago": "1.6.7",
|
"timeago": "1.6.7",
|
||||||
"vinyl-buffer": "^1.0.1",
|
"vinyl-buffer": "^1.0.1",
|
||||||
@@ -2637,6 +2638,12 @@
|
|||||||
"@sinonjs/commons": "^3.0.0"
|
"@sinonjs/commons": "^3.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@socket.io/component-emitter": {
|
||||||
|
"version": "3.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz",
|
||||||
|
"integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@tootallnate/once": {
|
"node_modules/@tootallnate/once": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz",
|
||||||
@@ -5308,7 +5315,6 @@
|
|||||||
"version": "4.3.7",
|
"version": "4.3.7",
|
||||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
|
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
|
||||||
"integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
|
"integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"ms": "^2.1.3"
|
"ms": "^2.1.3"
|
||||||
},
|
},
|
||||||
@@ -5691,6 +5697,49 @@
|
|||||||
"once": "^1.4.0"
|
"once": "^1.4.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/engine.io-client": {
|
||||||
|
"version": "6.6.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.3.tgz",
|
||||||
|
"integrity": "sha512-T0iLjnyNWahNyv/lcjS2y4oE358tVS/SYQNxYXGAJ9/GLgH4VCvOQ/mhTjqU88mLZCQgiG8RIegFHYCdVC+j5w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@socket.io/component-emitter": "~3.1.0",
|
||||||
|
"debug": "~4.3.1",
|
||||||
|
"engine.io-parser": "~5.2.1",
|
||||||
|
"ws": "~8.17.1",
|
||||||
|
"xmlhttprequest-ssl": "~2.1.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/engine.io-client/node_modules/ws": {
|
||||||
|
"version": "8.17.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz",
|
||||||
|
"integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"bufferutil": "^4.0.1",
|
||||||
|
"utf-8-validate": ">=5.0.2"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"bufferutil": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"utf-8-validate": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/engine.io-parser": {
|
||||||
|
"version": "5.2.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz",
|
||||||
|
"integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/entities": {
|
"node_modules/entities": {
|
||||||
"version": "4.5.0",
|
"version": "4.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
|
"resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
|
||||||
@@ -9943,8 +9992,7 @@
|
|||||||
"node_modules/ms": {
|
"node_modules/ms": {
|
||||||
"version": "2.1.3",
|
"version": "2.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
|
||||||
"dev": true
|
|
||||||
},
|
},
|
||||||
"node_modules/mute-stdout": {
|
"node_modules/mute-stdout": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
@@ -12292,6 +12340,34 @@
|
|||||||
"npm": ">= 3.0.0"
|
"npm": ">= 3.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/socket.io-client": {
|
||||||
|
"version": "4.8.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.8.1.tgz",
|
||||||
|
"integrity": "sha512-hJVXfu3E28NmzGk8o1sHhN3om52tRvwYeidbj7xKy2eIIse5IoKX3USlS6Tqt3BHAtflLIkCQBkzVrEEfWUyYQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@socket.io/component-emitter": "~3.1.0",
|
||||||
|
"debug": "~4.3.2",
|
||||||
|
"engine.io-client": "~6.6.1",
|
||||||
|
"socket.io-parser": "~4.2.4"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/socket.io-parser": {
|
||||||
|
"version": "4.2.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz",
|
||||||
|
"integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@socket.io/component-emitter": "~3.1.0",
|
||||||
|
"debug": "~4.3.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/socks": {
|
"node_modules/socks": {
|
||||||
"version": "2.8.3",
|
"version": "2.8.3",
|
||||||
"resolved": "https://registry.npmjs.org/socks/-/socks-2.8.3.tgz",
|
"resolved": "https://registry.npmjs.org/socks/-/socks-2.8.3.tgz",
|
||||||
@@ -13667,6 +13743,14 @@
|
|||||||
"integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==",
|
"integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/xmlhttprequest-ssl": {
|
||||||
|
"version": "2.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.2.tgz",
|
||||||
|
"integrity": "sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/xtend": {
|
"node_modules/xtend": {
|
||||||
"version": "2.1.2",
|
"version": "2.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/xtend/-/xtend-2.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/xtend/-/xtend-2.1.2.tgz",
|
||||||
|
|||||||
@@ -41,6 +41,7 @@
|
|||||||
"python": "^0.0.4",
|
"python": "^0.0.4",
|
||||||
"query-command-supported": "1.0.0",
|
"query-command-supported": "1.0.0",
|
||||||
"sass-embedded": "^1.86.0",
|
"sass-embedded": "^1.86.0",
|
||||||
|
"socket.io-client": "^4.8.1",
|
||||||
"textarea-caret": "3.1.0",
|
"textarea-caret": "3.1.0",
|
||||||
"timeago": "1.6.7",
|
"timeago": "1.6.7",
|
||||||
"vinyl-buffer": "^1.0.1",
|
"vinyl-buffer": "^1.0.1",
|
||||||
|
|||||||
Reference in New Issue
Block a user