added persistent and cant_close to PA_JobManager_FE_Message, used them from pa_job_manager to set status messages with persistence/close buttons appropriately for items like fix_dups/stale_jobs. When "fixing" now, the persistent Status message stays, but its now positioned approx. below the navbar on the right and is ok. Started on changing status to a more sensible naming conventions (away from alert to level) - more work to complete this

This commit is contained in:
2023-01-10 17:45:02 +11:00
parent 56c2d586b6
commit 0784861331
8 changed files with 46 additions and 64 deletions

15
TODO
View File

@@ -1,17 +1,7 @@
### GENERAL ### GENERAL
* get all status messages to use toasts AND get func to also increase/descrease the job counter as appropriate) * 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 - [DONE] all (success/creation) status messages use toasts
-- [DONE] make a helper func for setting toast body and use it in base.html && file_support.js -- [TODO] ensure all pa_job_mgr jobs are sending messages back to the FE -- SHOULD??? I just hook FinishJob???
-- [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
-- [DONE] make js StatusMsg() take a 'data' (json response) and process that
-- [DONE] make it include message from the API endpoint doing the work, not in the js code...
-- [TODO] fix base.html to only call toast() and only in document.ready()
-- [DONE] this means handling danger, and persistent / no time-out toast()s
-- [DONE] warning works now (colors, etc.)
-- [DONE] clean up has to be a POST back to server to clear DB (jscript/async has no other way)
-- [TODO] go through the crazy persistent status msgs for checkdups, etc. and make sure they all work
* should be using jsonify to return real json to my API calls, e.g: * should be using jsonify to return real json to my API calls, e.g:
use make_response( jsonify (... ) ) use make_response( jsonify (... ) )
@@ -19,7 +9,8 @@
all GETs stay as is for now (as they expect a html reply, not data, and then html is base.html+ and it handles the status) all GETs stay as is for now (as they expect a html reply, not data, and then html is base.html+ and it handles the status)
-- ONLY time this will fail is multiple status messages (which can occur I believe if a Wake of job mgr and then a job is created in DB, the success will only be shown) -- ONLY time this will fail is multiple status messages (which can occur I believe if a Wake of job mgr and then a job is created in DB, the success will only be shown)
-- [TODO] simple fix will be to get 'hidden' jobs (wake job_mgr and maybe? metadata) to post failure events to the JobMgr_FE DB queue -- [TODO] simple fix will be to get 'hidden' jobs (wake job_mgr and maybe? metadata) to post failure events to the JobMgr_FE DB queue
-- [TODO] when ALL converted, replace 'alert="..."' with 'status="..."' -- [TODO] -- change class Status to class UILog
-- [TODO] -- change alert to level
* delete files should behave like /move_files (stay on same page) as well as the status messages above * delete files should behave like /move_files (stay on same page) as well as the status messages above

View File

@@ -593,7 +593,7 @@ def move_files():
return make_response( jsonify( return make_response( jsonify(
job_id=job.id, job_id=job.id,
message=f"Created&nbsp;<a class='link-light' href=/job/{job.id}>Job #{job.id}</a>&nbsp;to move selected file(s)", message=f"Created&nbsp;<a class='link-light' href=/job/{job.id}>Job #{job.id}</a>&nbsp;to move selected file(s)",
status="success", alert="success" ) ) level="success", alert="success", persistent=False, cant_close=False ) )
@login_required @login_required
@app.route("/viewlist", methods=["POST"]) @app.route("/viewlist", methods=["POST"])

View File

@@ -3,11 +3,14 @@ var next_toast_id=1
function NewToast(data) function NewToast(data)
{ {
console.log(data)
// make new div, include data.alert as background colour, and data.message as toast body // make new div, include data.alert as background colour, and data.message as toast body
d_id='st' + String(next_toast_id) d_id='st' + String(next_toast_id)
div='<div id="' + d_id + '"' div='<div id="' + d_id + '"'
if( data.persistent === true ) if( data.persistent === true )
div+=' data-bs-autohide="false"' div+=' data-bs-autohide="false"'
if( data.job_id !== undefined )
div+=' job_id=' + String(data.job_id)
div +=' class="toast hide align-items-center border-0' div +=' class="toast hide align-items-center border-0'
if( data.alert == "success" || data.alert == "danger" ) if( data.alert == "success" || data.alert == "danger" )
div += ' text-white' div += ' text-white'
@@ -42,6 +45,7 @@ function NewToast(data)
// can reuse any that are hidden, OR, create a new one by appending as needed (so we can have 2+ toasts on screen) // 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) function StatusMsg(st)
{ {
console.log('StatusMsg' + st )
el=NewToast(st) el=NewToast(st)
$('#' + el ).toast("show") $('#' + el ).toast("show")
// if there is a job_id, then clear the message for it or it will be picked up again on reload // if there is a job_id, then clear the message for it or it will be picked up again on reload
@@ -49,7 +53,11 @@ function StatusMsg(st)
// now, we will do this to get a first pass working // now, we will do this to get a first pass working
if( st.job_id !== undefined ) if( st.job_id !== undefined )
{ {
$.ajax( { type: 'POST', url: '/clearmsgforjob/'+st.job_id, success: function(data) { } } ) console.log( 'set hidden.bs.toast handler for: ' + st.job_id )
$('#' + el).on( 'hidden.bs.toast',
function() {
$.ajax( { type: 'POST', url: '/clearmsgforjob/'+st.job_id, success: function(data) { console.log('cleared job id' )} } )
} )
} }
} }

9
job.py
View File

@@ -74,6 +74,8 @@ class PA_JobManager_Message(db.Model):
job_id = db.Column(db.Integer, db.ForeignKey('job.id') ) job_id = db.Column(db.Integer, db.ForeignKey('job.id') )
alert = db.Column(db.String) alert = db.Column(db.String)
message = db.Column(db.String) message = db.Column(db.String)
persistent = db.Column(db.Boolean)
cant_close = db.Column(db.Boolean)
job = db.relationship ("Job" ) job = db.relationship ("Job" )
def __repr__(self): def __repr__(self):
return f"<id: {self.id}, job_id: {self.job_id}, alert: {self.alert}, message: {self.message}, job: {self.job}" return f"<id: {self.id}, job_id: {self.job_id}, alert: {self.alert}, message: {self.message}, job: {self.job}"
@@ -90,6 +92,8 @@ def GetJM_Message():
# ClearJM_Message: used in html to clear any message just displayed # ClearJM_Message: used in html to clear any message just displayed
################################################################################ ################################################################################
def ClearJM_Message(id): def ClearJM_Message(id):
print(f"DDP: DID NOT clear JM message: {id}" )
return
PA_JobManager_Message.query.filter(PA_JobManager_Message.id==id).delete() PA_JobManager_Message.query.filter(PA_JobManager_Message.id==id).delete()
db.session.commit() db.session.commit()
return return
@@ -307,12 +311,11 @@ def joblog_search():
@login_required @login_required
def CheckForJobs(): def CheckForJobs():
num=GetNumActiveJobs() num=GetNumActiveJobs()
print( f"called: /checkforjobs -- num={num}" )
sts=[] sts=[]
print("CheckForJobs called" )
for msg in PA_JobManager_Message.query.all(): 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>: ' 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, 'job_id': msg.job_id } ) sts.append( { 'message': u+msg.message, 'alert': msg.alert, 'job_id': msg.job_id, 'persistent': msg.persistent, 'cant_close': msg.cant_close } )
return make_response( jsonify( num_active_jobs=num, sts=sts ) ) return make_response( jsonify( num_active_jobs=num, sts=sts ) )
############################################################################### ###############################################################################

View File

@@ -503,6 +503,8 @@ class PA_JobManager_FE_Message(Base):
job_id = Column(Integer, ForeignKey('job.id') ) job_id = Column(Integer, ForeignKey('job.id') )
alert = Column(String) alert = Column(String)
message = Column(String) message = Column(String)
persistent = Column(Boolean)
cant_close = Column(Boolean)
def __repr__(self): def __repr__(self):
return "<id: {}, job_id: {}, alert: {}, message: {}".format(self.id, self.job_id, self.alert, self.message) return "<id: {}, job_id: {}, alert: {}, message: {}".format(self.id, self.job_id, self.alert, self.message)
@@ -561,8 +563,8 @@ def NewJob(name, num_files=0, wait_for=None, jex=None, parent_job=None ):
# MessageToFE(): sends a specific alert/messasge for a given job via the DB to # MessageToFE(): sends a specific alert/messasge for a given job via the DB to
# the front end # the front end
############################################################################## ##############################################################################
def MessageToFE( job_id, alert, message ): def MessageToFE( job_id, alert, message, persistent, cant_close ):
msg = PA_JobManager_FE_Message( job_id=job_id, alert=alert, message=message) msg = PA_JobManager_FE_Message( job_id=job_id, alert=alert, message=message, persistent=persistent, cant_close=cant_close)
session.add(msg) session.add(msg)
session.commit() session.commit()
return msg.id return msg.id
@@ -904,7 +906,7 @@ def HandleJobs(first_run=False):
job.pa_job_state = 'Stale' job.pa_job_state = 'Stale'
session.add(job) session.add(job)
AddLogForJob( job, "ERROR: Job has been marked stale as it did not complete" ) AddLogForJob( job, "ERROR: Job has been marked stale as it did not complete" )
MessageToFE( job.id, "danger", f'Stale job, click&nbsp; <a href="javascript:document.body.innerHTML+=\'<form id=_fm method=GET action=/stale_jobs></form>\'; document.getElementById(\'_fm\').submit();">here</a>&nbsp;to restart or cancel' ) MessageToFE( job.id, "danger", f'Stale job, click&nbsp; <a href="javascript:document.body.innerHTML+=\'<form id=_fm method=GET action=/stale_jobs></form>\'; document.getElementById(\'_fm\').submit();">here</a>&nbsp;to restart or cancel', True, False )
session.commit() session.commit()
continue continue
if job.pa_job_state == 'New': if job.pa_job_state == 'New':
@@ -930,7 +932,7 @@ def HandleJobs(first_run=False):
# threading.Thread(target=RunJob, args=(job,)).start() # threading.Thread(target=RunJob, args=(job,)).start()
except Exception as e: except Exception as e:
try: try:
MessageToFE( job.id, "danger", "Failed with: {} (try job log for details)".format(e) ) MessageToFE( job.id, "danger", "Failed with: {} (try job log for details)".format(e), True, False )
except Exception as e2: except Exception as e2:
print("ERROR: Failed to let front-end know, but back-end Failed to run job (id: {}, name: {} -- orig exep was: {}, this exception was: {})".format( job.id, job.name, e, e2) ) print("ERROR: Failed to let front-end know, but back-end Failed to run job (id: {}, name: {} -- orig exep was: {}, this exception was: {})".format( job.id, job.name, e, e2) )
print("INFO: PA job manager is waiting for a job") print("INFO: PA job manager is waiting for a job")
@@ -954,7 +956,7 @@ def JobScanNow(job):
JobProgressState( job, "In Progress" ) JobProgressState( job, "In Progress" )
ProcessImportDirs(job) ProcessImportDirs(job)
FinishJob( job, "Completed (scan for new files)" ) FinishJob( job, "Completed (scan for new files)" )
MessageToFE( job.id, "success", "Completed (scan for new files)" ) MessageToFE( job.id, "success", "Completed (scan for new files)", False, False )
return return
############################################################################## ##############################################################################
@@ -964,7 +966,7 @@ def JobScanStorageDir(job):
JobProgressState( job, "In Progress" ) JobProgressState( job, "In Progress" )
ProcessStorageDirs(job) ProcessStorageDirs(job)
FinishJob( job, "Completed (scan for new files)" ) FinishJob( job, "Completed (scan for new files)" )
MessageToFE( job.id, "success", "Completed (scan for new files)" ) MessageToFE( job.id, "success", "Completed (scan for new files)", False, False )
return return
@@ -1078,7 +1080,7 @@ def JobForceScan(job):
ProcessImportDirs(job) ProcessImportDirs(job)
ProcessStorageDirs(job) ProcessStorageDirs(job)
FinishJob(job, "Completed (forced remove and recreation of all file data)") FinishJob(job, "Completed (forced remove and recreation of all file data)")
MessageToFE( job.id, "success", "Completed (forced remove and recreation of all file data)" ) MessageToFE( job.id, "success", "Completed (forced remove and recreation of all file data)", False, False )
return return
############################################################################## ##############################################################################
@@ -1319,6 +1321,10 @@ def MoveFileToRecycleBin(job,del_me):
bin_path=session.query(Path).join(PathType).filter(PathType.name=='Bin').first() bin_path=session.query(Path).join(PathType).filter(PathType.name=='Bin').first()
parent_dir=session.query(Dir).join(PathDirLink).filter(PathDirLink.path_id==bin_path.id).first() parent_dir=session.query(Dir).join(PathDirLink).filter(PathDirLink.path_id==bin_path.id).first()
# check/delete if we already have a deleted file of this name/number (only # happened in testing, but jic)
# 99.9% of the time, this will just delete nothing
session.query(DelFile).filter(DelFile.file_eid==del_me.id).delete()
# if we ever need to restore, lets remember this file's original path # if we ever need to restore, lets remember this file's original path
# (use a string in case the dir/path is ever deleted from FS (and then DB) and we need to recreate) # (use a string in case the dir/path is ever deleted from FS (and then DB) and we need to recreate)
del_file_details = DelFile( file_eid=del_me.id, orig_path_prefix=del_me.in_dir.in_path.path_prefix ) del_file_details = DelFile( file_eid=del_me.id, orig_path_prefix=del_me.in_dir.in_path.path_prefix )
@@ -2036,7 +2042,7 @@ def JobCheckForDups(job):
for row in res: for row in res:
if row.count > 0: if row.count > 0:
AddLogForJob(job, f"Found duplicates, Creating Status message in front-end for attention") AddLogForJob(job, f"Found duplicates, Creating Status message in front-end for attention")
MessageToFE( job.id, "danger", f'Found duplicate(s), click&nbsp; <a href="javascript:document.body.innerHTML+=\'<form id=_fm method=POST action=/fix_dups></form>\'; document.getElementById(\'_fm\').submit();">here</a>&nbsp;to finalise import by removing duplicates' ) MessageToFE( job.id, "danger", f'Found duplicate(s), click&nbsp; <a href="javascript:document.body.innerHTML+=\'<form id=_fm method=POST action=/fix_dups></form>\'; document.getElementById(\'_fm\').submit();">here</a>&nbsp;to finalise import by removing duplicates', True, True )
else: else:
FinishJob(job, f"No duplicates found") FinishJob(job, f"No duplicates found")
FinishJob(job, f"Finished looking for duplicates") FinishJob(job, f"Finished looking for duplicates")
@@ -2107,7 +2113,8 @@ def JobRemoveDups(job):
# Need to put another checkdups job in now to force / validate we have no dups # Need to put another checkdups job in now to force / validate we have no dups
next_job=NewJob( "checkdups" ) next_job=NewJob( "checkdups" )
AddLogForJob(job, "adding <a href='/job/{}'>job id={} {}</a> to confirm there are no more duplicates".format( next_job.id, next_job.id, next_job.name ) ) AddLogForJob(job, f"adding <a href='/job/{next_job.id}'>job id={next_job.id} {next_job.name}</a> to confirm there are no more duplicates" )
MessageToFE( job.id, "success", f"Finished Job#{job.id} removing duplicate files", False, False )
return return
#################################################################################################################################### ####################################################################################################################################
@@ -2137,7 +2144,7 @@ def JobMoveFiles(job):
move_me=session.query(Entry).get(jex.value) move_me=session.query(Entry).get(jex.value)
MoveEntriesToOtherFolder( job, move_me, dst_storage_path, f"{prefix}{suffix}" ) MoveEntriesToOtherFolder( job, move_me, dst_storage_path, f"{prefix}{suffix}" )
next_job=NewJob( "checkdups" ) next_job=NewJob( "checkdups" )
MessageToFE( job.id, "success", "Completed (move of selected files)" ) MessageToFE( job.id, "success", "Completed (move of selected files)", False, False )
FinishJob(job, f"Finished move selected file(s)") FinishJob(job, f"Finished move selected file(s)")
return return
@@ -2152,7 +2159,7 @@ def JobDeleteFiles(job):
del_me=session.query(Entry).join(File).filter(Entry.id==jex.value).first() del_me=session.query(Entry).join(File).filter(Entry.id==jex.value).first()
MoveFileToRecycleBin(job,del_me) MoveFileToRecycleBin(job,del_me)
next_job=NewJob( "checkdups" ) next_job=NewJob( "checkdups" )
MessageToFE( job.id, "success", "Completed (delete of selected files)" ) MessageToFE( job.id, "success", "Completed (delete of selected files)", False, False )
FinishJob(job, f"Finished deleting selected file(s)") FinishJob(job, f"Finished deleting selected file(s)")
return return
@@ -2167,7 +2174,7 @@ def JobRestoreFiles(job):
restore_me=session.query(Entry).join(File).filter(Entry.id==jex.value).first() restore_me=session.query(Entry).join(File).filter(Entry.id==jex.value).first()
RestoreFile(job,restore_me) RestoreFile(job,restore_me)
next_job=NewJob( "checkdups" ) next_job=NewJob( "checkdups" )
MessageToFE( job.id, "success", "Completed (restore of selected files)" ) MessageToFE( job.id, "success", "Completed (restore of selected files)", False, False )
FinishJob(job, f"Finished restoring selected file(s)") FinishJob(job, f"Finished restoring selected file(s)")
return return

View File

@@ -5,18 +5,18 @@ from shared import PA
# including when duplicates are found on import. These status messages are exposed into # including when duplicates are found on import. These status messages are exposed into
# the html files, and they show once for success/green, and sticky for danger/red # the html files, and they show once for success/green, and sticky for danger/red
class Status(PA): class Status(PA):
alert="success" level="success"
message="" message=""
def GetAlert(self): def GetAlert(self):
return self.alert return self.level
def GetMessage(self): def GetMessage(self):
return self.message return self.message
def SetMessage(self, msg, al="success"): def SetMessage(self, msg, l="success"):
self.message=msg self.message=msg
self.alert=al self.level=l
return return
def AppendMessage(self, msg): def AppendMessage(self, msg):
@@ -24,7 +24,7 @@ class Status(PA):
return return
def ClearStatus(self): def ClearStatus(self):
self.alert="success" self.level="success"
self.message="" self.message=""
return "" return ""

View File

@@ -160,7 +160,7 @@ create table JOBEXTRA ( ID integer, JOB_ID integer, NAME varchar(32), VALUE varc
create table JOBLOG ( ID integer, JOB_ID integer, LOG_DATE timestamptz, LOG varchar, create table JOBLOG ( ID integer, JOB_ID integer, LOG_DATE timestamptz, LOG varchar,
constraint PK_JL_ID primary key(ID), constraint FK_JL_JOB_ID foreign key(JOB_ID) references JOB(ID) ); constraint PK_JL_ID primary key(ID), constraint FK_JL_JOB_ID foreign key(JOB_ID) references JOB(ID) );
create table PA_JOB_MANAGER_FE_MESSAGE ( ID integer, JOB_ID integer, ALERT varchar(16), MESSAGE varchar(1024), create table PA_JOB_MANAGER_FE_MESSAGE ( ID integer, JOB_ID integer, ALERT varchar(16), MESSAGE varchar(1024), PERSISTENT boolean, CANT_CLOSE boolean,
constraint PA_JOB_MANAGER_FE_ACKS_ID primary key(ID), constraint PA_JOB_MANAGER_FE_ACKS_ID primary key(ID),
constraint FK_PA_JOB_MANAGER_FE_MESSAGE_JOB_ID foreign key(JOB_ID) references JOB(ID) ); constraint FK_PA_JOB_MANAGER_FE_MESSAGE_JOB_ID foreign key(JOB_ID) references JOB(ID) );

View File

@@ -128,40 +128,14 @@
</div class="collapse navbar-collapse"> </div class="collapse navbar-collapse">
</div class="container-fluid"> </div class="container-fluid">
</nav> </nav>
{% endif %} {# not InDBox #}
{% 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.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>
</button>
{% endif %}
{% if msg.job_id %}
<a href="{{url_for('joblog', id=msg.job_id)}}">Job #{{msg.job_id}}</a>:
{% endif %}
{{msg.message|safe}}
</form>
</div>
{# if a JM is a danger message, allow code to remove the status, it should be 'permanent' until addressed -- e.g. you have duplicates, fix them #}
{% if msg.alert != "danger" %}
{% set dont_print=ClearJM_Message(msg.id) %}
{% endif %}
{% endif %}
{% endif %}
{% endif %}
{% endif %}
{% block main_content %} {% block main_content %}
{% endblock main_content %} {% endblock main_content %}
{% if not InDBox %} {% if not InDBox %}
{%block script_content %}{% endblock script_content %} {%block script_content %}{% endblock script_content %}
<div id="status_container" class="position-fixed top-1 end-0 p-1" style="z-index: 11"> <div id="status_container" class="position-fixed top-0 end-0 p-1 my-5" "z-index: 11">
<div id="st1" class="toast hide align-items-center text-white bg-success border-0" role="alert" aria-live="assertive" aria-atomic="true"> <div id="st1" class="toast hide align-items-center text-white bg-success border-0" role="alert" aria-live="assertive" aria-atomic="true">
<div class="d-flex"> <div class="d-flex">
<div class="toast-body"> <div class="toast-body">
@@ -177,7 +151,6 @@
msg = "{{ GetMessage()|safe }}" msg = "{{ GetMessage()|safe }}"
msg=msg.replace('href=', 'class=link-light href=') msg=msg.replace('href=', 'class=link-light href=')
st=Object; st.message=msg; st.alert='{{GetAlert()}}'; StatusMsg(st) st=Object; st.message=msg; st.alert='{{GetAlert()}}'; StatusMsg(st)
alert( '{{GetAlert()}}' )
<!-- call ClearStatus: strictly not needed as we are near finished rendering, and any new page will lose it from memory (better to be explicit) --> <!-- call ClearStatus: strictly not needed as we are near finished rendering, and any new page will lose it from memory (better to be explicit) -->
{{ ClearStatus() }} {{ ClearStatus() }}
{% endif %} {% endif %}