renamed toast.js to jobs.js and moved Job related calls to jobs.py form files.py AND get job.py to allow job_mgr msgs to go to F/E via a POST of /checkforjobs (picked up in templates/base.html). move files also calls new CheckForJobs() to pick up when move job finishes without needing a page reload

This commit is contained in:
2023-01-06 17:37:15 +11:00
parent e1c0622be6
commit 1ba9bf4312
8 changed files with 180 additions and 95 deletions

21
TODO
View File

@@ -1,4 +1,18 @@
### GENERAL
* get all status messages to use toasts AND get func to also increase/descrease the job counter as appropriate)
- [DONE] all (success/creation) status messages use toasts
-- any non-sucess still stays a top of the page as an alert (for now?)
-- [DONE] make a helper func for setting toast body and use it in base.html && file_support.js
-- [DONE] make a helper func for setting 'Active Jobs' text/badge and call it when document ready (rather/both than start of base.html)
-- [DONE] trigger a timeout to play back pa_job_mgr message to FE and reset 'Active Jobs' text/badge until it hits 0
-- [DONE] (re)start this code if any new jobs is created (on move only for now - in the long run all job creation should consider this POST/json model)
-- [TODO] fix base.html to only call toast() and only in document.ready()
-- [TODO] this means handling danger, and persistent / no time-out toast()s
-- [DONE] warning works now (colors, etc.)
-- [TODO] have not thought about danger at all (the StatusMsg() code should work) - but missing Clear*() and persistence
* [TODO] delete files should behave like /move_files (stay on same page, job start with toast(), etc.)
* [TODO] ALL job creation should follow above pattern
* change the rotation code to use that jpeg util to reduce/remove compression loss?
* ignore face should ignore ALL matching faces (re: Declan)
@@ -13,13 +27,6 @@
response.headers["Content-Type"] = "application/json"
return response
* get all status messages to use toasts AND get func to also increase/descrease the job counter as appropriate)
- [DONE] all (success/creation) status messages use toasts
-- any non-sucess still stays a top of the page as an alert (always?)
-- [DONE] make a helper func for setting toast body and use it in base.html && file_support.js
-- [TODO] make a helper func for setting 'Active Jobs' text/badge and call it when document ready (rather/both than start of base.html)
-- should allow all JM Messages, or other messages to be an active Ajax poll until a job has completed
* should allow context menu from View thumbs (particularly useful on search) to show other files around this one by date (maybe that folder or something?)
* could get better AI optim, by keeping track of just new files since scan (even if we did this from the DB),

View File

@@ -141,38 +141,6 @@ class FileType(db.Model):
def __repr__(self):
return f"<id: {self.id}, name={self.name}>"
################################################################################
# Class describing PA_JobManager_Message and in the DB (via sqlalchemy)
# the job manager can send a message back to the front end (this code) via the
# DB. has to be about a specific job_id and is success/danger, etc. (alert)
# and a message
################################################################################
class PA_JobManager_Message(db.Model):
__tablename__ = "pa_job_manager_fe_message"
id = db.Column(db.Integer, db.Sequence('pa_job_manager_fe_message_id_seq'), primary_key=True )
job_id = db.Column(db.Integer, db.ForeignKey('job.id') )
alert = db.Column(db.String)
message = db.Column(db.String)
job = db.relationship ("Job" )
def __repr__(self):
return f"<id: {self.id}, job_id: {self.job_id}, alert: {self.alert}, message: {self.message}, job: {self.job}"
################################################################################
# GetJM_Message: used in html to display any message for this front-end
################################################################################
def GetJM_Message():
msg=PA_JobManager_Message.query.first()
return msg
################################################################################
# ClearJM_Message: used in html to clear any message just displayed
################################################################################
def ClearJM_Message(id):
PA_JobManager_Message.query.filter(PA_JobManager_Message.id==id).delete()
db.session.commit()
return
################################################################################
# util function to just update the current/first/last positions needed for
# viewing / using pa_user_state DB table

View File

@@ -64,7 +64,7 @@ function MoveSubmit()
// reorder the images via ecnt again, so highlighting can work
document.mf_id=0; $('.figure').each( function() { $(this).attr('ecnt', document.mf_id ); document.mf_id++ } )
$('#dbox').modal('hide')
$.ajax({ type: 'POST', data: $('#mv_fm').serialize(), url: '/move_files', success: function(data){ st=Object; st.message="Created&nbsp;<a class='link-light' href=/job/" + data.job_id + ">Job #" + data.job_id + "</a>&nbsp;to move selected file(s)"; st.alert="success"; StatusMsg(st); return false; } })
$.ajax({ type: 'POST', data: $('#mv_fm').serialize(), url: '/move_files', success: function(data){ st=Object; st.message="Created&nbsp;<a class='link-light' href=/job/" + data.job_id + ">Job #" + data.job_id + "</a>&nbsp;to move selected file(s)"; st.alert="success"; StatusMsg(st); CheckForJobs(); return false; } })
}
// show the DBox for a move file, includes all thumbnails of selected files to move

103
internal/js/jobs.js Normal file
View File

@@ -0,0 +1,103 @@
// create a bs toast in the status_container
// can reuse any that are hidden, OR, create a new one by appending as needed (so we can have 2+ toasts on screen)
function StatusMsg(st)
{
console.log( 'StatusMsg() -> ' + st.alert + ': ' + st.message )
// look for any '.hide' and '.toast'
if( $('.toast.hide').length !== 0 )
{
// as there may be more than 1 and as bs toast deals with ordering on screen, so just grab first()
tid=$('.toast.hide').first().attr('id')
// reset body, and the text-* and bg-* class for success, danger, etc.
$('#'+tid ).find( '.toast-body').html(st.message)
$('#'+tid ).removeClass( function( index, className ) { return (className.match( /(^|\s)(bg-|text-)\S+/g) || []).join(' ') } )
$('#'+tid ).addClass( 'bg-' + st.alert )
// get rid of white on button (if its there)
$('#'+tid ).find( 'button' ).removeClass('btn-close-white')
if( st.alert == "success" || st.alert == "danger" )
{
$('#'+tid ).addClass( 'text-white' )
$('#'+tid ).find( 'button' ).addClass('btn-close-white')
}
// show the popup (by default it fades)
$('#'+tid ).toast("show")
}
else
{
// find the id of the 'last' toast (either there are none, or they are all visible [note: we are in the else already])
tmp=$('.toast').last().attr('id')
// if none, there are no toasts at all, so make '#1'
if( tmp== '' )
tid=1
else
{
// skip 'st' at front of DOM id, and then increment to get id for new one
tid=tmp.substr(2)
tid++
}
// make new div, include st.alert as background colour, and st.message as toast body
div='<div id="st' + tid + '" class="toast hide align-items-center border-0'
if( st.alert == "success" || st.alert == "danger" )
div += ' text-white'
div += ' bg-' + st.alert
div += `" role="alert" aria-live="assertive" aria-atomic="true">
<div class="d-flex">
<div class="toast-body">
`
div += st.message
div += `
</div>
<button type="button" class="btn-close me-2 m-auto
`
if( st.alert == "success" || st.alert == "danger" )
div =+ ' btn-close-white'
div += `
" data-bs-dismiss="toast" aria-label="Close"></button>
</div>
</div>
`
// can be appended straight after st1 as the .toast("show") deals with display ordering
$('#st1').append(div)
// show the popup (by default it fades)
$('#st' + tid ).toast("show")
}
}
// this will make the active jobs badge red with a > 0 value, or navbar colours
// if 0 (effectively hiding it, but leaving the same width)
function SetActiveJobsBadge(num_jobs)
{
if( num_jobs > 0 )
$('#num_active_jobs').removeClass("invisible")
else
$('#num_active_jobs').addClass("invisible")
$('#num_active_jobs').html(num_jobs)
}
// this function is called either when we load and PA page and active jobs > 0
// OR, when a new job is created in the F/E and therefore active jobs > 0
// it keeps looking to see if the pa_job_manager has sent a response (via the DB)
// if so, it handles the response(s) with toast()s and updates the active job
// badge in the navbar. If all jobs are complete it stops calling itself
// after a 1 second timeout
function CheckForJobs()
{
$.ajax(
{
type: 'POST', url: '/checkforjobs',
success: function(data) {
data.sts.forEach(
function(el)
{
StatusMsg(el)
}
)
SetActiveJobsBadge(data.num_active_jobs)
if( data.num_active_jobs > 0 )
{
setTimeout( function() { CheckForJobCompletion() }, 1000 );
}
},
} )
return false;
}

View File

@@ -1,40 +0,0 @@
// create a bs toast in the status_container
function StatusMsg(st)
{
// look for first '.hide' in #status_container (there may be more than 1)
if( $('.toast.hide').length !== 0 )
{
tid=$('.toast.hide').first().attr('id')
$('#'+tid ).find( '.toast-body').html(st.message)
$('#'+tid ).removeClass( function( index, className ) { return (className.match( /(^|\s)bg-\S+/g) || []).join(' ') } )
$('#'+tid ).addClass( 'bg-' + st.alert )
$('#'+tid ).toast("show")
}
else
{
tmp=$('.toast').last().attr('id')
if( tmp== '' )
tid=1
else
{
// skip 'st'
tid=tmp.substr(2)
tid++
}
div='<div id="st' + tid + '" class="toast hide align-items-center text-white border-0 bg-' + st.alert
div += `" role="alert" aria-live="assertive" aria-atomic="true">
<div class="d-flex">
<div class="toast-body">
`
div += st.message
div += `
</div>
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast" aria-label="Close"></button>
</div>
</div>
`
// can be appended straight after st1 as the .toast("show") deals with display ordering
$('#st1').append(div)
$('#st' + tid ).toast("show")
}
}

52
job.py
View File

@@ -1,16 +1,15 @@
from wtforms import SubmitField, StringField, FloatField, HiddenField, validators, Form
from flask_wtf import FlaskForm
from flask import request, render_template, redirect
from flask import request, render_template, redirect, make_response, jsonify, url_for
from settings import Settings
from main import db, app, ma
from sqlalchemy import Sequence, func
from sqlalchemy.exc import SQLAlchemyError
from status import st, Status
from datetime import datetime, timedelta
from flask_login import login_required, current_user
import pytz
import socket
from shared import PA_JOB_MANAGER_HOST, PA_JOB_MANAGER_PORT, NEWEST_LOG_LIMIT, OLDEST_LOG_LIMIT
from shared import PA, PA_JOB_MANAGER_HOST, PA_JOB_MANAGER_PORT, NEWEST_LOG_LIMIT, OLDEST_LOG_LIMIT
from flask_login import login_required, current_user
from sqlalchemy.dialects.postgresql import INTERVAL
from sqlalchemy.sql.functions import concat
@@ -63,6 +62,38 @@ class Job(db.Model):
def __repr__(self):
return "<id: {}, start_time: {}, last_update: {}, name: {}, state: {}, num_files: {}, current_file_num: {}, current_file: {}, pa_job_state: {}, wait_for: {}, extra: {}, logs: {}>".format(self.id, self.start_time, self.last_update, self.name, self.state, self.num_files, self.current_file_num, self.current_file, self.pa_job_state, self.wait_for, self.extra, self.logs)
################################################################################
# Class describing PA_JobManager_Message and in the DB (via sqlalchemy)
# the job manager can send a message back to the front end (this code) via the
# DB. has to be about a specific job_id and is success/danger, etc. (alert)
# and a message
################################################################################
class PA_JobManager_Message(db.Model):
__tablename__ = "pa_job_manager_fe_message"
id = db.Column(db.Integer, db.Sequence('pa_job_manager_fe_message_id_seq'), primary_key=True )
job_id = db.Column(db.Integer, db.ForeignKey('job.id') )
alert = db.Column(db.String)
message = db.Column(db.String)
job = db.relationship ("Job" )
def __repr__(self):
return f"<id: {self.id}, job_id: {self.job_id}, alert: {self.alert}, message: {self.message}, job: {self.job}"
################################################################################
# GetJM_Message: used in html to display any message for this front-end
################################################################################
def GetJM_Message():
msg=PA_JobManager_Message.query.first()
return msg
################################################################################
# ClearJM_Message: used in html to clear any message just displayed
################################################################################
def ClearJM_Message(id):
print("ClearJM_Message called")
PA_JobManager_Message.query.filter(PA_JobManager_Message.id==id).delete()
db.session.commit()
return
################################################################################
# Used in main html to show a red badge of # jobs to draw attention there are
@@ -269,6 +300,21 @@ def joblog_search():
return ret
###############################################################################
# / -> POST -> looks for pa_job_manager status to F/E jobs and sends json of
# them back to F/E (called form internal/js/jobs.js:CheckForJobs()
################################################################################
@app.route("/checkforjobs", methods=["POST"])
@login_required
def CheckForJobs():
num=GetNumActiveJobs()
sts=[]
print("CheckForJobs called" )
for msg in PA_JobManager_Message.query.all():
print("there is a PA_J_MGR status message" )
u='<a class="link-light" href="' + url_for('joblog', id=msg.job_id) + '">Job # ' + str(msg.job_id) + '</a>: '
sts.append( { 'message': u+msg.message, 'alert': msg.alert } )
return make_response( jsonify( num_active_jobs=num, sts=sts ) )
###############################################################################
# This func creates a new filter in jinja2 to format the time from the db in a

View File

@@ -57,9 +57,9 @@ Compress(app)
################################# Now, import separated class files ###################################
from ai import aistats
from files import Entry, GetJM_Message, ClearJM_Message
from files import Entry
from person import Person
from job import Job, GetNumActiveJobs
from job import Job, GetNumActiveJobs, GetJM_Message, ClearJM_Message
from settings import Settings
from user import PAUser

View File

@@ -17,7 +17,7 @@
<script src="{{ url_for( 'internal', filename='upstream/bootstrap-5.0.2-dist/js/bootstrap.bundle.min.js')}}"></script>
<script src="{{ url_for( 'internal', filename='upstream/jquery.contextMenu.min.js')}}"></script>
<script src="{{ url_for( 'internal', filename='upstream/jquery.ui.position.min.js')}}"></script>
<script src="{{ url_for( 'internal', filename='js/toast.js')}}"></script>
<script src="{{ url_for( 'internal', filename='js/jobs.js')}}"></script>
<link rel="shortcut icon" href="{{ url_for('internal', filename='favicon.ico') }}">
@@ -106,12 +106,7 @@
</div class="nav-item dropdown">
<div class="d-flex nav-link flex-grow-1 justify-content-center p-0">
<a href="{{url_for('jobs')}}"}}<span class="navbar-text">Active Jobs:
{% set num_active_jobs = GetNumActiveJobs() %}
{% if num_active_jobs > 0 %}
<span id="num_active_jobs" class="badge bg-danger text-white"}}>{{num_active_jobs}}</span>
{% else %}
<span class="badge">0</span>
{% endif %}
<span id="num_active_jobs" class="badge badge-pill bg-danger invisible">0</></span>
</a>
</div class="nav-item">
@@ -136,10 +131,11 @@
{% if GetJM_Message() != None %}
{% set msg=GetJM_Message() %}
<!-- if we are fixing things dont put up alert -->
{% if request.endpoint != "fix_dups" and request.endpoint != "rm_dups" and request.endpoint != "stale_jobs" %}
{% if msg.alert != "success" %}
<div class="py-2 mx-1 alert alert-{{msg.alert}}">
{% if msg.alert != "success" and msg.job.name != "checkdups" %}
{% if msg.job.name != "checkdups" %}
<form id="_dismiss" action="{{url_for('clear_jm_msg', id=msg.id)}}" method="POST">
<button type="button" class="close btn border-secondary me-3" aria-label="Close" onClick="$('#_dismiss').submit()">
<span aria-hidden="true">&times;</span>
@@ -176,12 +172,16 @@
</div>
<script>
$(document).ready(function() {
<!-- f/e messages are resident in memory of the page being rendered, just process now -->
{% if GetMessage()|length %}
msg = "{{ GetMessage()|safe }}"
msg=msg.replace('href=', 'class=link-light href=')
st=Object; st.message=msg; st.alert="success"; StatusMsg(st)
{{ ClearStatus() }}
st=Object; st.message=msg; st.alert='{{GetAlert()}}'; StatusMsg(st)
{% endif %}
CheckForJobs()
/*
<!-- this this is totally useless as can only live for this 1 page render anyway -->
{{ ClearStatus() }}
{% if GetJM_Message() != None and GetJM_Message().alert == "success" %}
msg="{{GetJM_Message().message}}"
msg=msg.replace('href=', 'class=link-light href=')
@@ -189,6 +189,7 @@
StatusMsg(st)
{% set dont_print=ClearJM_Message(GetJM_Message().id) %}
{% endif %}
*/
} )
</script>
</body>