Compare commits

..

7 Commits

Author SHA1 Message Date
56771308a6 updated BUGs in general to remove older / fixed BUGs relating to the confusion of current/eids, etc.
update amendments in tables.sql to include job_id in entry_ammendment
added amend.py to move amendment-related code into its own file when we create a job (NewJob)
  and that job matches an amendmentType (via job_name or job_name:amt <- where amt relates to how we do a transform_image), then
  we enter a new EntryAmendment pa_job_mgr knows when a Transform job ends, and removes relevant EntryAmendment
files*.js use EntryAmendment data to render thumbnails with relevant AmendmentType
if a normal page load (like /files_ip), and there is an EntryAmendment, mark up the thumb, run the check jobs to look for completion of the job,
  removeal of the EntryAmendment and update the entry based on 'transformed' image

OVERALL: this is a functioning version that uses EntryAmendments and can handle loading a new page with outstanding amendments
  and 'deals' with it.  This is a good base, but does not cater for remove_files or move_files
2025-10-20 19:31:57 +11:00
905910ecf0 updated BUGs in general to remove older / fixed BUGs relating to the confusion of current/eids, etc.
update amendments in tables.sql to include job_id in entry_ammendment
added amend.py to move amendment-related code into its own file
when we create a job (NewJob) and that job matches an amendmentType (via job_name or job_name:amt <- where amt relates to how we do a transform_image), then
  we enter a new EntryAmendment
pa_job_mgr knows when a Transform job ends, and removes relevant EntryAmendment
files*.js use EntryAmendment data to render thumbnails with relevant AmendmentType and
  if a normal page load (like /files_ip), and there is an EntryAmendment, mark
  up the thumb, run  the check jobs to look for completion of the job, removeal
  of the EntryAmendment and update the entry based on 'transformed' image
OVERALL: this is a functioning version that uses EntryAmendments and can handle
loading a new page with outstanding amendments and 'deals' with it.  This is a
good base, but does not cater for remove_files or move_files
2025-10-20 19:23:52 +11:00
a38c54812c use amendments to render throbber/grayscale 2025-10-20 19:23:12 +11:00
dc6b831481 remove overkill use of make_response 2025-10-20 19:16:25 +11:00
8969cd452e for now, store away amendment types into dom for client-side additions - still working on right place for the additions 2025-10-19 11:29:52 +11:00
d65f3b32d3 pass amendmentTypes to client in query_data, make query_data single func to remove duplicate code, reference new amend.py for class defintions 2025-10-19 11:29:10 +11:00
0b0035d1d2 adding job_name into amendment type, and updating inserts to match 2025-10-19 11:24:54 +11:00
11 changed files with 337 additions and 187 deletions

19
BUGs
View File

@@ -1,15 +1,7 @@
### Next: 141 ### Next: 143
BUG-142: after transforming, the face data is still in the old spots, really should delete it / make it recalc
BUG-141: can currently try to flip a video (in a highlighted group)
BUG-140: When db is restarted underneath PA, it crashes job mgr... It should just accept timeouts, and keep trying to reconnect every 2? mins BUG-140: When db is restarted underneath PA, it crashes job mgr... It should just accept timeouts, and keep trying to reconnect every 2? mins
BUG-139: using any large entry list and going next a few times, ends say 4 pages of 50 into 4000 matches (entries from DB < 50)...
- confirmed this is when person has 2 or more refimgs:
- on page "2", we get 49 pulled back in the ORM instead of the 50 expected -- b/c I use that to indicate we must be at the end of the list if not 50 found
-- really, need to fix once and for all the eids / re-running query.
do GetEntries as we do now, once done however, get all entry ids. Stick those into the DB with a unique query-id and datestamp
new func to get all details needed for entries in an eid list (of 1-page) - show this page of entries
use current, full eidlist and to work our start/end of list (next/prev), disabling.
then client can keep current page of data, if you hit next/prev, use DB unique query id / full list and page of eids, and give full data for new page of entries
Implications though, are if a search is invalidated (maybe delete / move a photo), need to remove them from the list on the DB too OR let user know/decide to fix/wait.
BUG-100: I managed to get 2 photos matching mich in the NOT_WORKING photo (probably dif refimgs but same p.tag?) BUG-100: I managed to get 2 photos matching mich in the NOT_WORKING photo (probably dif refimgs but same p.tag?)
= /photos/2012/20120414-damien/IMG_8467.JPG = /photos/2012/20120414-damien/IMG_8467.JPG
@@ -31,11 +23,6 @@ BUG-125: when an image is highlighted, then post the contextmenu on a different
There is a chance we need to change the document on click to a mouse down (or whatever the context menu There is a chance we need to change the document on click to a mouse down (or whatever the context menu
uses for default), rather than just fix the highlight uses for default), rather than just fix the highlight
BUG-130: moving files and then trying to go next page and it got confused...
BUG-132: right arrow to go to next photo in viewer ALSO scrolls to the right, needs a return somewhere in the jscript BUG-132: right arrow to go to next photo in viewer ALSO scrolls to the right, needs a return somewhere in the jscript
BUG-133: when rebuilding pa[dev], the first run fails to have symlinks to the right paths for Import/Storage, etc. a simple restart fixes - so potentially the intial copy or some other race condition?
BUG-134: when moving set of photos on page, then move another set of photos on page, the first set reappears. Could really delete them from the dom? BUG-134: when moving set of photos on page, then move another set of photos on page, the first set reappears. Could really delete them from the dom?
BUG-135: failed to rotate: 2006/20061215-ITS-xmas-KP/DSC00582.JPG - not sure why && not repeatable, so its not the image, timing/race condition maybe?
BUG-137: after moving/refiling photos, the next shift-click is out of order (reload fixes it) BUG-137: after moving/refiling photos, the next shift-click is out of order (reload fixes it)
BUG-138: Placeholder for all the ways we can get the front-end confused:
---> JUST fix all these BUGs (relating to confused/lost state) by revisiting the overally complex way I remember state and my position in a list (probably FAR easier, to make an initial sql just save all eids, and then not try to recreate that list ever again and not care how I got into the list). Can attach a "running server-side sequence number", and if old sequence, and the original eid list results in a failure, then just pop up that the saved list is no longer valid, and ask user to re-do their search/list..."

26
TODO
View File

@@ -1,18 +1,18 @@
### major fix - go to everywhere I call GetEntries(), and redo the logic totally... * new viewing model (get ids of query on first load, then paginate only inside that known list)
* client side: - BUT, when we finish a delete, what do I do with pageList / entryList???
* instead of removing deleted images from DOM, we should gray them out and put a big Del (red circle with line?) though it as overlay. - start by showing them as deleted (via amend)
[DONE] * Create another table of entry_ammendments - note the deletions, rotations, flips of specific eids - then reproduce that on the client side visually as needed - then on success, remove the ids from the *List arrays in js -- but do
[DONE] - at least grayed-out, to indicate a pending action is not complete. this via repagination, invalidate page cache fully, then getPage(currentPage)
- When job that flips, rotates, deletes completes then create an entry_amendment in the DB. (e.g. assume 1, 2, 3 ... 40 in eList). delete 23, 24,
- Also hand fudge the jscript amendments for each job / next get_entry_by_id (if needed will also set amendments as needed) then reset lists to remove 23 and 24, pageList would then
- When job finishes, remove amendment from DB get reset to page with: 21,22,25,26 ... 30, 31
- when job finishes, remove amendment from document.amendments
need to rework all the throbber stuff, I think it is probably better not to have a div I never use with the throbber in it, just add when I need it... ? get rid of style and just use class -- think this should work, so change in
like in code for amendments. Also get rid of style and just use class templates/files.html for throbber, etc. and dont set style as much in view_support.js
### GENERAL ### GENERAL
* jobs for AI should show path name * jobs for AI should show path name
* rm dups job should show progress bar * rm dups job should show progress bar
* in viewer, there is no move button (maybe add one?) * in viewer, there is no move button (maybe add one?)
* think I killed pa_job_manager without passing an eid to a transform job, shouldn't crash * think I killed pa_job_manager without passing an eid to a transform job, shouldn't crash
- SHOULD JUST get AI to help clean-up and write defensive code here... - SHOULD JUST get AI to help clean-up and write defensive code here...

64
amend.py Normal file
View File

@@ -0,0 +1,64 @@
from sqlalchemy import select
from flask import request, jsonify
from flask_login import login_required
from shared import PA
from main import db, app
################################################################################
# Amendments are used to define types of changes being made to an entry (e.g.
# rotate, flip) should contain relatively transient content (e.g. we might be
# processing a long-running job now, and then add a rotate, the rotate wont
# finish for minutes, so these classes allow the UI to handle that gracefully
################################################################################
################################################################################
# Class describing AmendmentType in the DB (via sqlalchemy)
################################################################################
class AmendmentType(PA,db.Model):
__tablename__ = "amendment_type"
id = db.Column(db.Integer, db.Sequence('file_type_id_seq'), primary_key=True )
job_name = db.Column(db.String, nullable=False )
which = db.Column(db.String, nullable=False )
what = db.Column(db.String, nullable=False )
colour = db.Column(db.String, nullable=False )
################################################################################
# Class describing which Entry has a pending Amendment in the DB (via sqlalchemy)
################################################################################
class EntryAmendment(PA,db.Model):
__tablename__ = "entry_amendment"
eid = db.Column(db.Integer, db.ForeignKey("entry.id"), primary_key=True )
job_id = db.Column(db.Integer, db.ForeignKey("job.id"), primary_key=True )
amend_type = db.Column(db.Integer, db.ForeignKey("amendment_type.id"))
type = db.relationship("AmendmentType", backref="entry_amendment")
################################################################################
# check if this job is something we need to log an EntryAmendment for, based on
# job name and potentially amt in extras, to find the type of amendment
################################################################################
def inAmendmentTypes(job):
if not hasattr(job, 'extra' ) or not job.extra:
return None
amt=None
for jex in job.extra:
if jex.name == "amt":
amt=jex.value
# FIXME: should just cache this once per build, only would change with code updates
for at in getAmendments():
# for transform_image, amt=flip*, 90/180/270 - so amt will be set, use it, otherwise just use job.name
if (amt and f"{job.name}:{amt}" == at.job_name) or (at.job_name == job.name):
return at.id
return None
################################################################################
# Class describing which Entry has a pending Amendment in the DB (via sqlalchemy)
################################################################################
def getAmendments():
# get Amend types (get EAT data once - used in inAmendmentTypes()
stmt=select(AmendmentType)
eat=db.session.execute(stmt).scalars().all()
return eat

View File

@@ -1,5 +1,5 @@
from flask_wtf import FlaskForm from flask_wtf import FlaskForm
from flask import request, render_template, redirect, send_from_directory, url_for, jsonify, make_response from flask import request, render_template, redirect, send_from_directory, url_for, jsonify
from marshmallow import Schema, fields from marshmallow import Schema, fields
from main import db, app, ma from main import db, app, ma
from sqlalchemy import Sequence, text, select, union, or_ from sqlalchemy import Sequence, text, select, union, or_
@@ -22,6 +22,7 @@ import pytz
import html import html
from flask_login import login_required, current_user from flask_login import login_required, current_user
from types import SimpleNamespace from types import SimpleNamespace
from amend import EntryAmendment, AmendmentType
# Local Class imports # Local Class imports
################################################################################ ################################################################################
@@ -33,6 +34,7 @@ from person import Refimg, Person, PersonRefimgLink
from settings import Settings, SettingsIPath, SettingsSPath, SettingsRBPath from settings import Settings, SettingsIPath, SettingsSPath, SettingsRBPath
from shared import SymlinkName, ICON, PA from shared import SymlinkName, ICON, PA
from dups import Duplicates from dups import Duplicates
from amend import getAmendments
from face import Face, FaceFileLink, FaceRefimgLink, FaceOverrideType, FaceNoMatchOverride, FaceForceMatchOverride from face import Face, FaceFileLink, FaceRefimgLink, FaceOverrideType, FaceNoMatchOverride, FaceForceMatchOverride
# pylint: disable=no-member # pylint: disable=no-member
@@ -126,20 +128,6 @@ class FileType(PA,db.Model):
id = db.Column(db.Integer, db.Sequence('file_type_id_seq'), primary_key=True ) id = db.Column(db.Integer, db.Sequence('file_type_id_seq'), primary_key=True )
name = db.Column(db.String, unique=True, nullable=False ) name = db.Column(db.String, unique=True, nullable=False )
class AmendmentType(PA,db.Model):
__tablename__ = "amendment_type"
id = db.Column(db.Integer, db.Sequence('file_type_id_seq'), primary_key=True )
which = db.Column(db.String, nullable=False )
what = db.Column(db.String, nullable=False )
colour = db.Column(db.String, nullable=False )
class EntryAmendment(PA,db.Model):
__tablename__ = "entry_amendment"
eid = db.Column(db.Integer, db.ForeignKey("entry.id"), primary_key=True )
amend_type = db.Column(db.Integer, db.ForeignKey("amendment_type.id"))
type = db.relationship("AmendmentType", backref="entry_amendment")
################################################################################ ################################################################################
# this is how we order all queries based on value of 'noo' - used with # this is how we order all queries based on value of 'noo' - used with
# access *order_map.get(OPT.noo) # access *order_map.get(OPT.noo)
@@ -254,6 +242,7 @@ class EntryAmendmentSchema(ma.SQLAlchemyAutoSchema):
model = EntryAmendment model = EntryAmendment
load_instance = True load_instance = True
eid = ma.auto_field() eid = ma.auto_field()
job_id = ma.auto_field()
type = ma.Nested(AmendmentTypeSchema) type = ma.Nested(AmendmentTypeSchema)
################################################################################ ################################################################################
@@ -282,6 +271,8 @@ entries_schema = EntrySchema(many=True)
FOT_Schema = FaceOverrideTypeSchema(many=True) FOT_Schema = FaceOverrideTypeSchema(many=True)
path_Schema = PathSchema(many=True) path_Schema = PathSchema(many=True)
person_Schema = PersonSchema(many=True) person_Schema = PersonSchema(many=True)
et_schema = AmendmentTypeSchema(many=True)
ea_schema = EntryAmendmentSchema(many=True)
################################################################################ ################################################################################
# /get_entries_by_ids -> route where we supply list of entry ids (for next/prev # /get_entries_by_ids -> route where we supply list of entry ids (for next/prev
@@ -319,9 +310,7 @@ def process_ids():
# get any pending entry amendments # get any pending entry amendments
stmt = select(EntryAmendment).join(AmendmentType) stmt = select(EntryAmendment).join(AmendmentType)
ea = db.session.execute(stmt).unique().scalars().all() ea = db.session.execute(stmt).unique().scalars().all()
ea_schema = EntryAmendmentSchema(many=True)
ea_data=ea_schema.dump(ea) ea_data=ea_schema.dump(ea)
print( ea_data )
return jsonify(entries=entries_schema.dump(sorted_data), amend=ea_data) return jsonify(entries=entries_schema.dump(sorted_data), amend=ea_data)
@@ -379,17 +368,21 @@ def getPeople():
people=db.session.execute(stmt).scalars().all() people=db.session.execute(stmt).scalars().all()
return person_Schema.dump(people) return person_Schema.dump(people)
def initQueryData():
################################################################################
# Get all relevant Entry.ids based on search_term passed in and OPT visuals
################################################################################
def GetSearchQueryData(OPT):
query_data={} query_data={}
query_data['entry_list']=None query_data['entry_list']=None
query_data['root_eid']=0 query_data['root_eid']=0
query_data['NMO'] = getFOT() query_data['NMO'] = getFOT()
query_data['move_paths'] = getMoveDetails() query_data['move_paths'] = getMoveDetails()
query_data['people'] = getPeople() query_data['people'] = getPeople()
query_data['amendTypes'] = et_schema.dump( getAmendments() )
return query_data
################################################################################
# Get all relevant Entry.ids based on search_term passed in and OPT visuals
################################################################################
def GetSearchQueryData(OPT):
query_data=initQueryData()
search_term = OPT.search_term search_term = OPT.search_term
# turn * wildcard into sql wildcard of % # turn * wildcard into sql wildcard of %
@@ -426,11 +419,7 @@ def GetSearchQueryData(OPT):
# Get all relevant Entry.ids based on files_ip/files_sp/files_rbp and OPT visuals # Get all relevant Entry.ids based on files_ip/files_sp/files_rbp and OPT visuals
################################################################################# #################################################################################
def GetQueryData( OPT ): def GetQueryData( OPT ):
query_data={} query_data=initQueryData()
query_data['entry_list']=None
query_data['NMO'] = getFOT()
query_data['move_paths'] = getMoveDetails()
query_data['people'] = getPeople()
# always get the top of the (OPT.prefix) Path's eid and keep it for OPT.folders toggling/use # always get the top of the (OPT.prefix) Path's eid and keep it for OPT.folders toggling/use
dir_stmt=( dir_stmt=(
@@ -481,7 +470,7 @@ def change_file_opts():
query_data = GetSearchQueryData( OPT ) query_data = GetSearchQueryData( OPT )
else: else:
query_data = GetQueryData( OPT ) query_data = GetQueryData( OPT )
return make_response( jsonify( query_data=query_data ) ) return jsonify( query_data=query_data )
################################################################################ ################################################################################
@@ -667,7 +656,7 @@ def move_files():
jex.append( JobExtra( name=f"{el}", value=str(request.form[el]) ) ) jex.append( JobExtra( name=f"{el}", value=str(request.form[el]) ) )
job=NewJob( name="move_files", num_files=0, wait_for=None, jex=jex, desc="to move selected file(s)" ) job=NewJob( name="move_files", num_files=0, wait_for=None, jex=jex, desc="to move selected file(s)" )
# data is not used, but send response to trigger CheckForJobs() # data is not used, but send response to trigger CheckForJobs()
return make_response( jsonify( job_id=job.id ) ) return jsonify( job_id=job.id )
@login_required @login_required
@app.route("/view/", methods=["POST"]) @app.route("/view/", methods=["POST"])
@@ -709,7 +698,7 @@ def transform():
jex.append( JobExtra( name=f"{el}", value=str(request.form[el]) ) ) jex.append( JobExtra( name=f"{el}", value=str(request.form[el]) ) )
job=NewJob( name="transform_image", num_files=0, wait_for=None, jex=jex, desc="to transform selected file(s)" ) job=NewJob( name="transform_image", num_files=0, wait_for=None, jex=jex, desc="to transform selected file(s)" )
return make_response( jsonify( job_id=job.id ) ) return jsonify( job_id=job.id )
################################################################################ ################################################################################
# /check_transform_job -> URL that is called repeatedly by front-end waiting for the # /check_transform_job -> URL that is called repeatedly by front-end waiting for the
@@ -721,13 +710,16 @@ def transform():
@login_required @login_required
def check_transform_job(): def check_transform_job():
job_id = request.form['job_id'] job_id = request.form['job_id']
job = Job.query.get(job_id) stmt=select(Job).where(Job.id==job_id)
job=db.session.execute(stmt).scalars().one_or_none()
j=jsonify( finished=False ) j=jsonify( finished=False )
if job.pa_job_state == 'Completed': if job.pa_job_state == 'Completed':
id=[jex.value for jex in job.extra if jex.name == "id"][0] id=[jex.value for jex in job.extra if jex.name == "id"][0]
e=Entry.query.join(File).filter(Entry.id==id).first() stmt=select(Entry).where(Entry.id==id)
j=jsonify( finished=True, thumbnail=e.file_details.thumbnail ) ent=db.session.execute(stmt).scalars().all()
return make_response( j ) ent_data=entries_schema.dump(ent)
j=jsonify( finished=True, entry=ent_data[0] )
return j
################################################################################ ################################################################################
# /include -> return contents on /include and does not need a login, so we # /include -> return contents on /include and does not need a login, so we
@@ -773,7 +765,7 @@ def get_existing_paths(dt):
except: except:
# this is not a date, so we cant work out possible dirs, just # this is not a date, so we cant work out possible dirs, just
# return an empty set # return an empty set
return make_response( '[]' ) return jsonify( '[]' )
new_dt=new_dtime.strftime('%Y%m%d') new_dt=new_dtime.strftime('%Y%m%d')
# find dirs named with this date # find dirs named with this date
dirs_arr+=Dir.query.filter(Dir.rel_path.ilike('%'+new_dt+'%')).all(); dirs_arr+=Dir.query.filter(Dir.rel_path.ilike('%'+new_dt+'%')).all();
@@ -787,8 +779,8 @@ def get_existing_paths(dt):
ret='[ ' ret='[ '
first_dir=1 first_dir=1
for dir in dirs: for dir in dirs:
# this can occur if there is a file with this date name in the top-levle of the path, its legit, but only really happens in DEV # this can occur if there is a file with this date name in the top-level of the path, its legit, but only really happens in DEV
# regardless, it cant be used for a existpath button in the F/E, ignore it # regardless, it cant be used for a existing path button in the F/E, ignore it
if dir.rel_path == '': if dir.rel_path == '':
continue continue
if not first_dir: if not first_dir:
@@ -806,7 +798,7 @@ def get_existing_paths(dt):
ret+= ' } ' ret+= ' } '
first_dir=0 first_dir=0
ret+= ' ]' ret+= ' ]'
return make_response( ret ) return jsonify ( ret )
# quick helper func to return timestamps of jscript files # quick helper func to return timestamps of jscript files
# we use this as a quick/hacky way of versioning them # we use this as a quick/hacky way of versioning them

View File

@@ -94,7 +94,7 @@ function MoveOrDelCleanUpUI()
// remove the images being moved (so UI immediately 'sees' the move) // remove the images being moved (so UI immediately 'sees' the move)
$("[name^=eid-]").each( function() { $('#'+$(this).attr('value')).remove() } ) $("[name^=eid-]").each( function() { $('#'+$(this).attr('value')).remove() } )
// reorder the images via ecnt again, so future highlighting can work // reorder the images via ecnt again, so future highlighting can work
document.mf_id=0; $('.figure').each( function() { $(this).attr('ecnt', document.mf_id ); document.mf_id++ } ) // document.mf_id=0; $('.figure').each( function() { $(this).attr('ecnt', document.mf_id ); document.mf_id++ } )
$('#dbox').modal('hide') $('#dbox').modal('hide')
} }
@@ -354,34 +354,27 @@ function NoSel() {
return true return true
} }
// quick wrapper to add a single <figure> to the #figures div
function addFigure( obj )
{
html=createFigureHtml( obj )
$('#figures').append( html )
}
/** /**
* Renders a group header or entry based on the object and options. * Renders a group header or entry based on the object and options.
* obj - The object containing file/directory details. * obj - The object containing file/directory details.
* last - Tracks the last printed group (e.g., { printed: null }).
* ecnt - Entry counter (e.g., { val: 0 }).
* returns {string} - Generated HTML string. * returns {string} - Generated HTML string.
*/ */
function addFigure( obj, last, ecnt ) function createFigureHtml( obj )
{ {
let html = ""; // if am is null, no amendment for this obj, otherwise we have one
var am=null
for (const tmp of document.amendments)
if( tmp.eid == obj.id )
am=tmp
// Grouping logic let html = "";
if (OPT.grouping === "Day") {
if (last.printed !== obj.file_details.day) {
html += `<div class="row ps-3"><h6>Day: ${obj.file_details.day} of ${obj.file_details.month}/${obj.file_details.year}</h6></div>`;
last.printed = obj.file_details.day;
}
} else if (OPT.grouping === "Week") {
if (last.printed !== obj.file_details.woy) {
html += `<div class="row ps-3"><h6>Week #: ${obj.file_details.woy} of ${obj.file_details.year}</h6></div>`;
last.printed = obj.file_details.woy;
}
} else if (OPT.grouping === "Month") {
if (last.printed !== obj.file_details.month) {
html += `<div class="row ps-3"><h6>Month: ${obj.file_details.month} of ${obj.file_details.year}</h6></div>`;
last.printed = obj.file_details.month;
}
}
// Image/Video/Unknown entry // Image/Video/Unknown entry
if (obj.type.name === "Image" || obj.type.name === "Video" || obj.type.name === "Unknown") { if (obj.type.name === "Image" || obj.type.name === "Video" || obj.type.name === "Unknown") {
@@ -395,13 +388,27 @@ function addFigure( obj, last, ecnt )
const prettyDate = `${obj.file_details.day}/${obj.file_details.month}/${obj.file_details.year}`; const prettyDate = `${obj.file_details.day}/${obj.file_details.month}/${obj.file_details.year}`;
const type = obj.type.name; const type = obj.type.name;
// if amendment for this obj, do not add entry class - prevents highlighting
if( am ) {
ent=""
gs="style='filter: grayscale(100%);'"
am_html ='<img class="position-absolute top-50 start-50 translate-middle" height="60" src="/internal/white-circle.png">'
am_html +='<img class="position-absolute top-50 start-50 translate-middle" height="64" src="/internal/throbber.gif">'
if( am.type.which == 'icon' )
am_html+=`<svg class="position-absolute top-50 start-50 translate-middle" height="32" style="color:${am.type.colour}" fill="${am.type.colour}"><use xlink:href="/internal/icons.svg#${am.type.what}"></use></svg>`
else
am_html+=`<img class="position-absolute top-50 start-50 translate-middle" src="/internal/${am.type.what}?v={{js_vers['r270']}}" height="32">`
} else {
ent="entry"
gs=""
am_html=""
}
html += ` html += `
<figure id="${obj.id}" ecnt="${ecnt}" class="col col-auto g-0 figure entry m-1" <figure id="${obj.id}" class="col col-auto g-0 figure ${ent} m-1"
path_type="${pathType}" size="${size}" hash="${hash}" in_dir="${inDir}" path_type="${pathType}" size="${size}" hash="${hash}" in_dir="${inDir}"
fname="${fname}" yr="${yr}" date="${date}" pretty_date="${prettyDate}" type="${type}"> fname="${fname}" yr="${yr}" date="${date}" pretty_date="${prettyDate}" type="${type}">
${renderMedia(obj)} ${renderMedia(obj,gs,am_html)}
</figure> </figure>`;
`;
} }
// Directory entry // Directory entry
else if (obj.type.name === "Directory" && OPT.folders) { else if (obj.type.name === "Directory" && OPT.folders) {
@@ -410,7 +417,7 @@ function addFigure( obj, last, ecnt )
: obj.dir_details.in_path.path_prefix; : obj.dir_details.in_path.path_prefix;
html += ` html += `
<figure class="col col-auto g-0 dir entry m-1" id="${obj.id}" ecnt="${ecnt}" dir="${dirname}" type="Directory"> <figure class="col col-auto g-0 dir entry m-1" id="${obj.id}" dir="${dirname}" type="Directory">
<svg class="svg" width="${OPT.size - 22}" height="${OPT.size - 22}" fill="currentColor"> <svg class="svg" width="${OPT.size - 22}" height="${OPT.size - 22}" fill="currentColor">
<use xlink:href="/internal/icons.svg#Directory"></use> <use xlink:href="/internal/icons.svg#Directory"></use>
</svg> </svg>
@@ -419,66 +426,42 @@ function addFigure( obj, last, ecnt )
`; `;
html += `<script>f=$('#${obj.id}'); w=f.find('svg').width(); f.find('figcaption').width(w);</script>`; html += `<script>f=$('#${obj.id}'); w=f.find('svg').width(); f.find('figcaption').width(w);</script>`;
} }
// moved the bindings to here as we need to reset them if we recreate this Figure (after a transform job)
$('#figures').append( html ) html += `<script>
if( "${obj.type.name}" === "Directory" ) {
// check if there is a pending amendment for this entry, if so mark it up $("#${obj.id}").click( function(e) { document.back_id=this.id; getDirEntries(this.id,false) } )
// (e.g. its being deleted, rotated, etc) - details in the am obj } else {
for (const am of document.amendments) $('#${obj.id}').click( function(e) { DoSel(e, this ); SetButtonState(); return false; });
{ $('#${obj.id}').dblclick( function(e) { startViewing( $(this).attr('id') ) } )
if( am.eid == obj.id )
{
$('#'+obj.id).find('img.thumb').attr('style', 'filter: grayscale(100%);' )
$('#'+obj.id).removeClass('entry')
html='<img class="position-absolute top-50 start-50 translate-middle" height="60" src="/internal/white-circle.png">'
html+='<img class="position-absolute top-50 start-50 translate-middle" height="64" src="/internal/throbber.gif">'
if( am.type.which == 'icon' )
html+=`<svg class="position-absolute top-50 start-50 translate-middle" height="32" style="color:${am.type.colour}" fill="${am.type.colour}"><use xlink:href="/internal/icons.svg#${am.type.what}"></use></svg>`
else
html+=`<img class="position-absolute top-50 start-50 translate-middle" src="/internal/${am.type.what}?v={{js_vers['r270']}}" height="32">`
$('#'+obj.id).find('a').append(html)
} }
} </script>`
return return html
} }
// Helper function to render media (image/video/unknown) // Helper function to render media (image/video/unknown)
function renderMedia(obj) { function renderMedia(obj,gs,am_html) {
const isImageOrUnknown = obj.type.name === "Image" || obj.type.name === "Unknown"; const isImageOrUnknown = obj.type.name === "Image" || obj.type.name === "Unknown";
const isVideo = obj.type.name === "Video"; const isVideo = obj.type.name === "Video";
const path = `${obj.in_dir.in_path.path_prefix}/${obj.in_dir.rel_path}/${obj.name}`; const path = `${obj.in_dir.in_path.path_prefix}/${obj.in_dir.rel_path}/${obj.name}`;
const thumb = obj.file_details.thumbnail const thumb = obj.file_details.thumbnail
? `<a href="${path}"><img alt="${obj.name}" class="thumb" height="${OPT.size}" src="data:image/jpeg;base64,${obj.file_details.thumbnail}"></a>` ? `<a href="${path}"><img alt="${obj.name}" ${gs} class="thumb" height="${OPT.size}" src="data:image/jpeg;base64,${obj.file_details.thumbnail}"></a>`
: `<a href="${path}"><svg width="${OPT.size}" height="${OPT.size}" fill="white"><use xlink:href="/internal/icons.svg#unknown_ftype"/></svg></a>`; : `<a href="${path}"><svg width="${OPT.size}" height="${OPT.size}" fill="white"><use xlink:href="/internal/icons.svg#unknown_ftype"/></svg></a>`;
let mediaHtml = `<div style="position:relative; width:100%">${thumb}`; let mediaHtml = `<div style="position:relative; width:100%">${thumb}${am_html}`;
if (isImageOrUnknown) { if (isVideo) {
if (OPT.search_term) {
mediaHtml += `
<div style="position:absolute; bottom: 0px; left: 2px;">
<svg width="16" height="16" fill="white"><use xlink:href="/internal/icons.svg#${getLocationIcon(obj)}"/></svg>
</div>
`;
}
mediaHtml += `
<div id="s${obj.id}" style="display:none; position:absolute; top: 50%; left:50%; transform:translate(-50%, -50%);">
<img height="64px" src="/internal/throbber.gif">
</div>
`;
} else if (isVideo) {
mediaHtml += ` mediaHtml += `
<div style="position:absolute; top: 0px; left: 2px;"> <div style="position:absolute; top: 0px; left: 2px;">
<svg width="16" height="16" fill="white"><use xlink:href="/internal/icons.svg#film"/></svg> <svg width="16" height="16" fill="white"><use xlink:href="/internal/icons.svg#film"/></svg>
</div> </div>
`; `;
if (OPT.search_term) { }
if (OPT.search_term) {
mediaHtml += ` mediaHtml += `
<div style="position:absolute; bottom: 0px; left: 2px;"> <div style="position:absolute; bottom: 0px; left: 2px;">
<svg width="16" height="16" fill="white"><use xlink:href="/internal/icons.svg#${getLocationIcon(obj)}"/></svg> <svg width="16" height="16" fill="white"><use xlink:href="/internal/icons.svg#${getLocationIcon(obj)}"/></svg>
</div> </div>
`; `;
}
} }
mediaHtml += `</div>`; mediaHtml += `</div>`;
@@ -527,7 +510,6 @@ function drawPageOfFigures()
{ {
$('#figures').empty() $('#figures').empty()
var last = { printed: null } var last = { printed: null }
var ecnt=0
// something is up, let the user know // something is up, let the user know
if( document.alert ) if( document.alert )
@@ -557,30 +539,41 @@ function drawPageOfFigures()
// with clas "back" this gets a different click handler which flags server to return data by 'going back/up' in dir tree // with clas "back" this gets a different click handler which flags server to return data by 'going back/up' in dir tree
// we give the server the id of the first item on the page so it can work out how to go back // we give the server the id of the first item on the page so it can work out how to go back
html=`<div class="col col-auto g-0 m-1"> html=`<div class="col col-auto g-0 m-1">
<figure id="${back_id}" ecnt="0" class="${cl} entry m-1" type="Directory"> <figure id="${back_id}" class="${cl} entry m-1" type="Directory">
<svg class="svg" width="${OPT.size-22}" height="${OPT.size-22}"> <svg class="svg" width="${OPT.size-22}" height="${OPT.size-22}">
<use xlink:href="internal/icons.svg#folder_back${gray}"/> <use xlink:href="internal/icons.svg#folder_back${gray}"/>
</svg> </svg>
<figcaption class="figure-caption text-center">${back}</figcaption> <figcaption class="figure-caption text-center">${back}</figcaption>
</figure> </figure>
</div>` </div>`
ecnt++
$('#figures').append(html) $('#figures').append(html)
} }
for (const obj of document.entries) { for (const obj of document.entries) {
addFigure( obj, last, ecnt ) // Grouping logic
ecnt++ if (OPT.grouping === "Day") {
if (last.printed !== obj.file_details.day) {
$('#figures').append(`<div class="row ps-3"><h6>Day: ${obj.file_details.day} of ${obj.file_details.month}/${obj.file_details.year}</h6></div>` );
last.printed = obj.file_details.day;
}
} else if (OPT.grouping === "Week") {
if (last.printed !== obj.file_details.woy) {
$('#figures').append(`<div class="row ps-3"><h6>Week #: ${obj.file_details.woy} of ${obj.file_details.year}</h6></div>` );
last.printed = obj.file_details.woy;
}
} else if (OPT.grouping === "Month") {
if (last.printed !== obj.file_details.month) {
$('#figures').append(`<div class="row ps-3"><h6>Month: ${obj.file_details.month} of ${obj.file_details.year}</h6></div>` );
last.printed = obj.file_details.month;
}
}
addFigure( obj )
} }
$(".back").click( function(e) { getDirEntries(this.id,true) } )
if( document.entries.length == 0 ) if( document.entries.length == 0 )
if( OPT.search_term ) if( OPT.search_term )
$('#figures').append( `<span class="alert alert-danger p-2 col-auto"> No matches for: '${OPT.search_term}'</span>` ) $('#figures').append( `<span class="alert alert-danger p-2 col-auto"> No matches for: '${OPT.search_term}'</span>` )
else if( OPT.root_eid == 0 ) else if( OPT.root_eid == 0 )
$('#figures').append( `<span class="alert alert-danger p-2 col-auto d-flex align-items-center">No files in Path!</span>` ) $('#figures').append( `<span class="alert alert-danger p-2 col-auto d-flex align-items-center">No files in Path!</span>` )
$('.figure').click( function(e) { DoSel(e, this ); SetButtonState(); return false; });
$('.figure').dblclick( function(e) { startViewing( $(this).attr('id') ) } )
// for dir, getDirEntries 2nd param is back (or "up" a dir)
$(".dir").click( function(e) { document.back_id=this.id; getDirEntries(this.id,false) } )
$(".back").click( function(e) { getDirEntries(this.id,true) } )
} }
// emtpy out file_list_div, and repopulate it with new page of content // emtpy out file_list_div, and repopulate it with new page of content
@@ -669,7 +662,14 @@ function getPage(pageNumber, successCallback, viewingIdx=0)
type: 'POST', url: '/get_entries_by_ids', type: 'POST', url: '/get_entries_by_ids',
data: JSON.stringify(data), contentType: 'application/json', data: JSON.stringify(data), contentType: 'application/json',
dataType: 'json', dataType: 'json',
success: function(res) { document.amendments=res.amend; getEntriesByIdSuccessHandler( res.entries, pageNumber, successCallback, viewingIdx ) }, success: function(res) {
document.amendments=res.amend;
// this is only called when we are viewing a page in files/list view, so check for job(s) ending...
for (const tmp of document.amendments) {
CheckTransformJob(tmp.eid,tmp.job_id,handleTransformFiles)
}
getEntriesByIdSuccessHandler( res.entries, pageNumber, successCallback, viewingIdx )
},
error: function(xhr, status, error) { console.error("Error:", error); } }); error: function(xhr, status, error) { console.error("Error:", error); } });
return return
} }

View File

@@ -1,11 +1,31 @@
// This function will remove the matching amendment for this entry (id)
// can only have 1 ammendment per image, its grayed out for other changes
function removeAmendment( id )
{
document.amendments=document.amendments.filter(obj => obj.eid !== id)
}
// POST to a check URL, that will tell us if the transformation has completed,
// if not, try again in 1 second... If it has finished then reset the thumbnail
// to full colour, put it back to being an entry and reset the thumbnail to the
// newly created one that was sent back in the response to the POST
function handleTransformFiles(data,id,job_id) function handleTransformFiles(data,id,job_id)
{ {
if( data.finished ) if( data.finished )
{ {
$('#s'+id).hide() id=parseInt(id)
$('#'+id).find('img.thumb').attr('style', 'filter: color(100%);' ); idx = entryList.indexOf(id)
$('#'+id).addClass('entry') // replace data for this entry now its been transformed
$('#'+id).find('.thumb').attr('src', 'data:image/jpeg;base64,'+data.thumbnail) document.entries[idx]=data.entry
// update cache too
// document.page[getPageNumberForId(id)][howFarIntoPageCache(id)]=data.entry
// FIXME: for now just invalidate whole cache
document.page.length=0
removeAmendment( id )
// redraw into figure html in dom
last={ 'printed': 'not required' }
html = createFigureHtml( data.entry, last, 9999 )
$('#'+id).replaceWith( html )
return false; return false;
} }
else else
@@ -15,17 +35,15 @@ function handleTransformFiles(data,id,job_id)
} }
// POST to a check URL, that will tell us if the transformation has completed, // POST to a check URL, that will tell us if the transformation has completed,
// if not, try again in 1 second... If it has finished then reset the thumbnail // if not, try again in 1 second... If it has finished then reset the image
// to full colour, put it back to being an entry and reset the thumbnail to the // to full colour
// newly created one that was sent back in the response to the POST
function handleTransformViewing(data,id,job_id) function handleTransformViewing(data,id,job_id)
{ {
if( data.finished ) if( data.finished )
{ {
// stop throbber, remove grayscale & then force reload with timestamped version of im.src // stop throbber, remove grayscale & then force reload with timestamped version of im.src
grayscale=0
throbber=0
im.src=im.src + '?t=' + new Date().getTime(); im.src=im.src + '?t=' + new Date().getTime();
removeAmendment( id )
return false; return false;
} }
else else
@@ -41,7 +59,18 @@ function handleTransformViewing(data,id,job_id)
function CheckTransformJob(id,job_id,successCallback) function CheckTransformJob(id,job_id,successCallback)
{ {
CheckForJobs() CheckForJobs()
$.ajax( { type: 'POST', data: '&job_id='+job_id, url: '/check_transform_job', success: function(res) { successCallback(res,id,job_id); } } ) $.ajax( { type: 'POST', data: '&job_id='+job_id, url: '/check_transform_job',
success: function(res) { successCallback(res,id,job_id); } } )
}
// function to add data for document.amendment based on id and amt
// used when we transform several images in files_*, or single image in viewer
function addTransformAmendment(id,amt)
{
am={}
am.eid=parseInt(id)
am.type = document.amendTypes.filter(obj => obj.job_name === 'transform_image:'+amt )[0]
document.amendments.push(am)
} }
// for each highlighted image, POST the transform with amt (90, 180, 270, // for each highlighted image, POST the transform with amt (90, 180, 270,
@@ -55,16 +84,28 @@ function Transform(amt)
if( document.viewing ) if( document.viewing )
{ {
post_data = '&amt='+amt+'&id='+document.viewing.id post_data = '&amt='+amt+'&id='+document.viewing.id
// send /transform for this image, grayscale the thumbmail, add color spinning wheel overlay, and start checking for job end // POST /transform for image, grayscale the image, add throbber, & start checking for end of job
$.ajax({ type: 'POST', data: post_data, url: '/transform', success: function(data) { grayscale=1; throbber=1; DrawImg(); CheckTransformJob(document.viewing.id,data.job_id,handleTransformViewing); return false; } }) $.ajax({ type: 'POST', data: post_data, url: '/transform', success: function(data) {
addTransformAmendment(document.viewing.id, amt)
DrawImg();
CheckTransformJob(document.viewing.id,data.job_id,handleTransformViewing);
return false;
} })
} }
else else
{ {
$('.highlight').each(function( id, e ) { $('.highlight').each(function( cnt, e ) {
post_data = '&amt='+amt+'&id='+e.id post_data = '&amt='+amt+'&id='+e.id
// send /transform for this image, grayscale the thumbmail, add color spinning wheel overlay, and start checking for job end // POST /transform for image, grayscale the thumbnail, add throbber, & start checking for end of job
$.ajax({ type: 'POST', data: post_data, url: '/transform', success: function(data){ $('#'+e.id).find('img.thumb').attr('style', 'filter: grayscale(100%);' ); $('#'+e.id).removeClass('entry'); $('#s'+e.id).show(); CheckTransformJob(e.id,data.job_id,handleTransformFiles); return false; } }) $.ajax({ type: 'POST', data: post_data, url: '/transform', success: function(data){
addTransformAmendment(e.id, amt)
last={ 'printed': 'not required' }
idx = pageList.indexOf(parseInt(e.id))
html = createFigureHtml( document.entries[idx], last, 9999 )
$('#'+e.id).replaceWith( html )
CheckTransformJob(e.id,data.job_id,handleTransformFiles);
return false;
} })
} ) } )
} }
} }

View File

@@ -62,6 +62,11 @@ function DrawImg()
if( im.width == 0 ) if( im.width == 0 )
return return
// find any matching ammendment
am=document.amendments.filter(obj => obj.eid === document.viewing.id)
if( am.length )
am=am[0]
canvas.width=NewWidth(im) canvas.width=NewWidth(im)
canvas.height=NewHeight(im) canvas.height=NewHeight(im)
@@ -69,14 +74,29 @@ function DrawImg()
$('#img-cap').width(canvas.width) $('#img-cap').width(canvas.width)
// actually draw the pixel images to the canvas at the right size // actually draw the pixel images to the canvas at the right size
if( grayscale ) if (!Array.isArray(am))
context.filter='grayscale(1)' context.filter='grayscale(1)'
context.drawImage(im, 0, 0, canvas.width, canvas.height ) context.drawImage(im, 0, 0, canvas.width, canvas.height )
// -50 is a straight up hack, no idea why this works, but its good enough for me // -50 is a straight up hack, no idea why this works, but its good enough for me
if( throbber ) if (!Array.isArray(am))
$('#throbber').attr('style', 'display:show; position:absolute; left:'+canvas.width/2+'px; top:'+(canvas.height/2-50)+'px' ) {
else const style = 'position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%);';
$('#throbber').hide(); $('#throbber').attr('style', style + ' height: 96px;');
$('#white-circle').attr('style', style + ' height: 72px;');
if(am.type.which == 'img' )
$('#inside-img').attr('style', style + ' height: 64px;').attr('src', '/internal/'+am.type.what );
else
{
$('#inside-icon').attr('style', `${style} color:${am.type.colour}; height: 64px;`)
$('#inside-icon').attr('fill', am.type.colour )
$('#inside-icon use').attr('xlink:href', `/internal/icons.svg#${am.type.what}`);
}
} else {
$('#throbber').hide()
$('#white-circle').hide()
$('#inside-img').hide()
$('#inside-icon').hide()
}
// show (or not) the whole figcaption with fname in it - based on state of fname_toggle // show (or not) the whole figcaption with fname in it - based on state of fname_toggle
if( $('#fname_toggle').prop('checked' ) ) if( $('#fname_toggle').prop('checked' ) )
@@ -678,13 +698,11 @@ function addViewerKeyHandler() {
{ {
case "Left": // IE/Edge specific value case "Left": // IE/Edge specific value
case "ArrowLeft": case "ArrowLeft":
if( $('#la').prop('disabled') == false ) $('#la').click()
$('#la').click()
break; break;
case "Right": // IE/Edge specific value case "Right": // IE/Edge specific value
case "ArrowRight": case "ArrowRight":
if( $('#ra').prop('disabled') == false ) $('#ra').click()
$('#ra').click()
break; break;
case "d": case "d":
$('#distance').click() $('#distance').click()
@@ -724,7 +742,6 @@ function nextImageInViewer()
getNextEntry() getNextEntry()
setDisabledForViewingNextPrevBttons() setDisabledForViewingNextPrevBttons()
ViewImageOrVideo() ViewImageOrVideo()
} }
// wrapper func to start the viewer - needed as we have a dbl-click & View file // wrapper func to start the viewer - needed as we have a dbl-click & View file

20
job.py
View File

@@ -9,6 +9,7 @@ from datetime import datetime, timedelta
import pytz import pytz
import socket import socket
from shared import PA, 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 amend import EntryAmendment, inAmendmentTypes
from flask_login import login_required, current_user from flask_login import login_required, current_user
from sqlalchemy.dialects.postgresql import INTERVAL from sqlalchemy.dialects.postgresql import INTERVAL
from sqlalchemy.sql.functions import concat from sqlalchemy.sql.functions import concat
@@ -114,8 +115,25 @@ def NewJob(name, num_files="0", wait_for=None, jex=None, desc="No description pr
db.session.add(job) db.session.add(job)
db.session.commit() db.session.commit()
SetFELog( message=f'Created <a class="link-light" href="/job/{job.id}">Job #{job.id}</a> to {desc}', level="success" )
# if this job changes an eid we store that in DB and client shows until it finishes the job
at_id = inAmendmentTypes(job)
if at_id:
if job.name == 'transform_image':
id=[jex.value for jex in job.extra if jex.name == "id"][0]
ea=EntryAmendment( eid=id, job_id=job.id, amend_type=at_id )
print( f"just added an EA for eid={id}, j={job.id}" )
db.session.add(ea)
elif job.name == 'delete_files':
for j in jex:
if 'eid-' in j.name:
ea=EntryAmendment( eid=j.value, amend_type=at_id )
db.session.add(ea)
# need to return this to the f/e somehow
# this is for removes, really need to think about this more
#job.amendment=ea
SetFELog( message=f'Created <a class="link-light" href="/job/{job.id}">Job #{job.id}</a> to {desc}', level="success" )
WakePAJobManager(job.id) WakePAJobManager(job.id)
return job return job

View File

@@ -1,4 +1,3 @@
# #
# This file controls the 'external' job control manager, that (periodically # # This file controls the 'external' job control manager, that (periodically #
# looks / somehow is pushed an event?) picks up new jobs, and processes them. # looks / somehow is pushed an event?) picks up new jobs, and processes them.
@@ -15,7 +14,7 @@
### SQLALCHEMY IMPORTS ### ### SQLALCHEMY IMPORTS ###
from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy import Column, Integer, String, Sequence, Float, ForeignKey, DateTime, LargeBinary, Boolean, func, text from sqlalchemy import Column, Integer, String, Sequence, Float, ForeignKey, DateTime, LargeBinary, Boolean, func, text, select
from sqlalchemy.exc import SQLAlchemyError from sqlalchemy.exc import SQLAlchemyError
from sqlalchemy.orm import relationship from sqlalchemy.orm import relationship
from sqlalchemy import create_engine from sqlalchemy import create_engine
@@ -23,7 +22,7 @@ from sqlalchemy.orm import sessionmaker
from sqlalchemy.orm import scoped_session from sqlalchemy.orm import scoped_session
### LOCAL FILE IMPORTS ### ### LOCAL FILE IMPORTS ###
from shared import DB_URL, PA_JOB_MANAGER_HOST, PA_JOB_MANAGER_PORT, THUMBSIZE, SymlinkName, GenThumb, SECS_IN_A_DAY, PA_EXIF_ROTATER from shared import DB_URL, PA_JOB_MANAGER_HOST, PA_JOB_MANAGER_PORT, THUMBSIZE, SymlinkName, GenThumb, SECS_IN_A_DAY, PA_EXIF_ROTATER, PA
from datetime import datetime, timedelta, date from datetime import datetime, timedelta, date
### PYTHON LIB IMPORTS ### ### PYTHON LIB IMPORTS ###
@@ -46,6 +45,8 @@ import re
import sys import sys
import ffmpeg import ffmpeg
import subprocess import subprocess
# FIXME: remove this
import time
# global debug setting # global debug setting
@@ -512,6 +513,15 @@ class PA_JobManager_FE_Message(Base):
def __repr__(self): def __repr__(self):
return "<id: {}, job_id: {}, level: {}, message: {}".format(self.id, self.job_id, self.level, self.message) return "<id: {}, job_id: {}, level: {}, message: {}".format(self.id, self.job_id, self.level, self.message)
################################################################################
# Class describing which Entry has a pending Amendment in the DB (via sqlalchemy)
################################################################################
class EntryAmendment(PA,Base):
__tablename__ = "entry_amendment"
eid = Column(Integer, ForeignKey("entry.id"), primary_key=True )
job_id = Column(Integer, ForeignKey("job.id"), primary_key=True )
# don't over think this, we just use eid to delete this entry anyway
amend_type = Column(Integer)
############################################################################## ##############################################################################
# PAprint(): convenience function to prepend a timestamp to a printed string # PAprint(): convenience function to prepend a timestamp to a printed string
@@ -876,6 +886,7 @@ def RunJob(job):
elif job.name == "run_ai_on_path": elif job.name == "run_ai_on_path":
JobRunAIOnPath(job) JobRunAIOnPath(job)
elif job.name == "transform_image": elif job.name == "transform_image":
#time.sleep(10)
JobTransformImage(job) JobTransformImage(job)
elif job.name == "clean_bin": elif job.name == "clean_bin":
JobCleanBin(job) JobCleanBin(job)
@@ -1865,7 +1876,6 @@ def JobRunAIOn(job):
#################################################################################################################################### ####################################################################################################################################
# JobTransformImage(): transform an image by the amount requested (can also flip horizontal or vertical) # JobTransformImage(): transform an image by the amount requested (can also flip horizontal or vertical)
# TODO: should be JobTransformImage() ;)
#################################################################################################################################### ####################################################################################################################################
def JobTransformImage(job): def JobTransformImage(job):
JobProgressState( job, "In Progress" ) JobProgressState( job, "In Progress" )
@@ -1897,6 +1907,15 @@ def JobTransformImage(job):
e.file_details.hash = md5( job, e ) e.file_details.hash = md5( job, e )
PAprint( f"JobTransformImage DONE thumb: job={job.id}, id={id}, amt={amt}" ) PAprint( f"JobTransformImage DONE thumb: job={job.id}, id={id}, amt={amt}" )
session.add(e) session.add(e)
# now remove the matching amendment for the transform job
stmt=select(EntryAmendment).where(EntryAmendment.eid==id)
ea=session.execute(stmt).scalars().one_or_none()
if ea:
session.delete(ea)
else:
AddLogForJob( job, f"ERROR: failed to remove entry amendment in DB for this transformation? (eid={id})" )
PAprint( f"ERROR: failed to remove entry amendment in DB for this transformation? (eid={id}, job={job} )" )
FinishJob(job, "Finished Processesing image rotation/flip") FinishJob(job, "Finished Processesing image rotation/flip")
return return
@@ -2707,7 +2726,6 @@ def ScheduledJobs():
created_jobs=True created_jobs=True
return created_jobs return created_jobs
#################################################################################################################################### ####################################################################################################################################
# MAIN - start with validation, then grab any jobs in the DB to process, then # MAIN - start with validation, then grab any jobs in the DB to process, then
# go into waiting on a socket to be woken up (and then if woken, back into HandleJobs() # go into waiting on a socket to be woken up (and then if woken, back into HandleJobs()

View File

@@ -168,18 +168,21 @@ CREATE TABLE pa_job_manager_fe_message ( id INTEGER, job_id INTEGER, level VARCH
CONSTRAINT pk_pa_job_manager_fe_acks_id PRIMARY KEY(id), CONSTRAINT pk_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) );
CREATE TABLE amendment_type ( id INTEGER, which VARCHAR(8), what VARCHAR(32), colour VARCHAR(32), CREATE TABLE amendment_type ( id INTEGER, job_name VARCHAR(64), which VARCHAR(8), what VARCHAR(32), colour VARCHAR(32),
CONSTRAINT pk_amendment_type_id PRIMARY KEY(id) ); CONSTRAINT pk_amendment_type_id PRIMARY KEY(id) );
INSERT INTO amendment_type ( id, which, what, colour ) VALUES ( 1, 'icon', 'trash', 'red' ); INSERT INTO amendment_type ( id, job_name, which, what, colour ) VALUES ( 1, 'delete_files', 'icon', 'trash', 'var(--bs-danger)' );
INSERT INTO amendment_type ( id, which, what, colour ) VALUES ( 2, 'img', 'rot90.png', '#009EFF' ); INSERT INTO amendment_type ( id, job_name, which, what, colour ) VALUES ( 2, 'restore_files', 'icon', 'trash', 'var(--bs-success)' );
INSERT INTO amendment_type ( id, which, what, colour ) VALUES ( 3, 'img', 'rot180.png', '#009EFF' ); INSERT INTO amendment_type ( id, job_name, which, what, colour ) VALUES ( 3, 'transform_image:90', 'img', 'rot90.png', '#009EFF' );
INSERT INTO amendment_type ( id, which, what, colour ) VALUES ( 4, 'img', 'rot270.png', '#009EFF' ); INSERT INTO amendment_type ( id, job_name, which, what, colour ) VALUES ( 4, 'transform_image:180', 'img', 'rot180.png', '#009EFF' );
INSERT INTO amendment_type ( id, which, what, colour ) VALUES ( 5, 'icon', 'flip_h', '#009EFF' ); INSERT INTO amendment_type ( id, job_name, which, what, colour ) VALUES ( 5, 'transform_image:270', 'img', 'rot270.png', '#009EFF' );
INSERT INTO amendment_type ( id, which, what, colour ) VALUES ( 6, 'icon', 'flip_v', '#009EFF' ); INSERT INTO amendment_type ( id, job_name, which, what, colour ) VALUES ( 6, 'transform_image:fliph', 'icon', 'flip_h', '#009EFF' );
INSERT INTO amendment_type ( id, job_name, which, what, colour ) VALUES ( 7, 'transform_image:flipv', 'icon', 'flip_v', '#009EFF' );
INSERT INTO amendment_type ( id, job_name, which, what, colour ) VALUES ( 8, 'move_files', 'icon', 'folder_plus', 'var(--bs-primary)' );
CREATE TABLE entry_amendment ( eid INTEGER, amend_type INTEGER, CREATE TABLE entry_amendment ( amend_type INTEGER, eid INTEGER, job_id INTEGER,
CONSTRAINT pk_entry_amendment_eid_name PRIMARY KEY(eid,amend_type), CONSTRAINT pk_entry_amendment_eid_job_id PRIMARY KEY(eid,job_id),
CONSTRAINT fk_entry_amendment_amendment_type FOREIGN KEY(amend_type) REFERENCES amendment_type(id) ); CONSTRAINT fk_entry_amendment_amendment_type FOREIGN KEY(amend_type) REFERENCES amendment_type(id),
CONSTRAINT fk_entry_amendment_job_id FOREIGN KEY(job_id) REFERENCES job(id) );
-- default data for types of paths -- default data for types of paths
INSERT INTO path_type VALUES ( (SELECT NEXTVAL('path_type_id_seq')), 'Import' ); INSERT INTO path_type VALUES ( (SELECT NEXTVAL('path_type_id_seq')), 'Import' );

View File

@@ -137,9 +137,15 @@
<use xlink:href="{{url_for('internal', filename='icons.svg')}}?v={{js_vers['ic']}}#prev"/></svg> <use xlink:href="{{url_for('internal', filename='icons.svg')}}?v={{js_vers['ic']}}#prev"/></svg>
</button> </button>
</div> </div>
<figure class="col col-auto border border-info rounded m-0 p-1" id="figure"> <figure style="position: relative;" class="col col-auto border border-info rounded m-0 p-1" id="figure">
<canvas id="canvas"></canvas> <canvas id="canvas"></canvas>
<!-- next 4 are placeholders and called on during amendments only in viewer code -->
<img id="throbber" src="{{url_for('internal', filename='throbber.gif')}}?v={{js_vers[th]}}" style="display:none;"> <img id="throbber" src="{{url_for('internal', filename='throbber.gif')}}?v={{js_vers[th]}}" style="display:none;">
<img id="white-circle" src="{{url_for('internal', filename='white-circle.png')}}?v={{js_vers[th]}}" style="display:none;">
<img id="inside-img" style="display:none;">
<svg id="inside-icon" style="display:none;" fill="currentColor">
<use xlink:href="{{url_for('internal', filename='icons.svg')}}?v={{js_vers['ic']}}#flip_v">
</use></svg>
<script> <script>
var im=new Image(); var im=new Image();
im.onload=DrawImg im.onload=DrawImg
@@ -251,6 +257,10 @@
// this implies no content in the Path at all // this implies no content in the Path at all
OPT.root_eid = {{ query_data.root_eid }}; OPT.root_eid = {{ query_data.root_eid }};
// amendment types are stable per code release, store them once and use as
// needed when we amend entrys in Transforms, removes, etc.
document.amendTypes = {{ query_data.amendTypes|tojson }};
// get items out of query_data into convenience javascript vars... // get items out of query_data into convenience javascript vars...
var move_paths = {{ query_data.move_paths|tojson }}; var move_paths = {{ query_data.move_paths|tojson }};
var NMO={{query_data.NMO|tojson}} var NMO={{query_data.NMO|tojson}}