removed __repr__ from classes in files.py, and added in sqlalchemy class and marshmallow schemas for entry amendments, then load amendments on get_entry_by_id - so any page load (first or next/prev) will see amendments, we then display them into the files list and now add a white circle inside the throbber and overlay that with approrpiate icon/image - all of which is taken from amendment type and eid. tables.sql also updated to create the amendment data, tweaked icons.svg to remove hardcoded-colours for flip_[vh]
This commit is contained in:
14
TODO
14
TODO
@@ -1,10 +1,14 @@
|
|||||||
### major fix - go to everywhere I call GetEntries(), and redo the logic totally...
|
### major fix - go to everywhere I call GetEntries(), and redo the logic totally...
|
||||||
* client side:
|
* client side:
|
||||||
* for real chance to stop confusion, 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.
|
* 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.
|
||||||
* Create another table of entry_ammendments - note the deletions, rotations, flips of specific eids - then reproduce that on the client side visually as needed
|
[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
|
||||||
- at least grayed-out, to indicate a pending action is not complete.
|
[DONE] - at least grayed-out, to indicate a pending action is not complete.
|
||||||
- When job that flips, rotates, deletes completes then lets update the query details (e.g. remove eids, or remove the ammendments)
|
- When job that flips, rotates, deletes completes then create an entry_amendment in the DB.
|
||||||
- this actually is quite an improvement, if someone is deleting 2 as per above, I will see that as a pending change in my unrelated query, ditto flips, etc.
|
- Also hand fudge the jscript amendments for each job / next get_entry_by_id (if needed will also set amendments as needed)
|
||||||
|
- When job finishes, remove amendment from DB
|
||||||
|
- 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...
|
||||||
|
like in code for amendments. Also get rid of style and just use class
|
||||||
|
|
||||||
### GENERAL
|
### GENERAL
|
||||||
* jobs for AI should show path name
|
* jobs for AI should show path name
|
||||||
|
|||||||
72
files.py
72
files.py
@@ -31,7 +31,7 @@ from job import Job, JobExtra, Joblog, NewJob, SetFELog
|
|||||||
from path import PathType, Path
|
from path import PathType, Path
|
||||||
from person import Refimg, Person, PersonRefimgLink
|
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
|
from shared import SymlinkName, ICON, PA
|
||||||
from dups import Duplicates
|
from dups import Duplicates
|
||||||
from face import Face, FaceFileLink, FaceRefimgLink, FaceOverrideType, FaceNoMatchOverride, FaceForceMatchOverride
|
from face import Face, FaceFileLink, FaceRefimgLink, FaceOverrideType, FaceNoMatchOverride, FaceForceMatchOverride
|
||||||
|
|
||||||
@@ -41,41 +41,32 @@ from face import Face, FaceFileLink, FaceRefimgLink, FaceOverrideType, FaceNoMat
|
|||||||
# Class describing PathDirLink and in the DB (via sqlalchemy)
|
# Class describing PathDirLink and in the DB (via sqlalchemy)
|
||||||
# connects the entry (dir) with a path
|
# connects the entry (dir) with a path
|
||||||
################################################################################
|
################################################################################
|
||||||
class PathDirLink(db.Model):
|
class PathDirLink(PA,db.Model):
|
||||||
__tablename__ = "path_dir_link"
|
__tablename__ = "path_dir_link"
|
||||||
path_id = db.Column(db.Integer, db.ForeignKey("path.id"), primary_key=True )
|
path_id = db.Column(db.Integer, db.ForeignKey("path.id"), primary_key=True )
|
||||||
dir_eid = db.Column(db.Integer, db.ForeignKey("dir.eid"), primary_key=True )
|
dir_eid = db.Column(db.Integer, db.ForeignKey("dir.eid"), primary_key=True )
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return f"<path_id: {self.path_id}, dir_eid: {self.dir_eid}>"
|
|
||||||
|
|
||||||
################################################################################
|
################################################################################
|
||||||
# Class describing EntryDirLInk and in the DB (via sqlalchemy)
|
# Class describing EntryDirLInk and in the DB (via sqlalchemy)
|
||||||
# connects (many) entry contained in a directory (which is also an entry)
|
# connects (many) entry contained in a directory (which is also an entry)
|
||||||
################################################################################
|
################################################################################
|
||||||
class EntryDirLink(db.Model):
|
class EntryDirLink(PA,db.Model):
|
||||||
__tablename__ = "entry_dir_link"
|
__tablename__ = "entry_dir_link"
|
||||||
entry_id = db.Column(db.Integer, db.ForeignKey("entry.id"), primary_key=True )
|
entry_id = db.Column(db.Integer, db.ForeignKey("entry.id"), primary_key=True )
|
||||||
dir_eid = db.Column(db.Integer, db.ForeignKey("dir.eid"), primary_key=True )
|
dir_eid = db.Column(db.Integer, db.ForeignKey("dir.eid"), primary_key=True )
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return f"<entry_id: {self.entry_id}, dir_eid: {self.dir_eid}>"
|
|
||||||
|
|
||||||
################################################################################
|
################################################################################
|
||||||
# Class describing Dir and in the DB (via sqlalchemy)
|
# Class describing Dir and in the DB (via sqlalchemy)
|
||||||
# rel_path: rest of dir after path, e.g. if path = /..../storage, then
|
# rel_path: rest of dir after path, e.g. if path = /..../storage, then
|
||||||
# rel_path could be 2021/20210101-new-years-day-pics
|
# rel_path could be 2021/20210101-new-years-day-pics
|
||||||
# in_path: only in this structure, not DB, quick ref to the path this dir is in
|
# in_path: only in this structure, not DB, quick ref to the path this dir is in
|
||||||
################################################################################
|
################################################################################
|
||||||
class Dir(db.Model):
|
class Dir(PA,db.Model):
|
||||||
__tablename__ = "dir"
|
__tablename__ = "dir"
|
||||||
eid = db.Column(db.Integer, db.ForeignKey("entry.id"), primary_key=True )
|
eid = db.Column(db.Integer, db.ForeignKey("entry.id"), primary_key=True )
|
||||||
rel_path = db.Column(db.String, unique=True )
|
rel_path = db.Column(db.String, unique=True )
|
||||||
in_path = db.relationship("Path", secondary="path_dir_link", uselist=False)
|
in_path = db.relationship("Path", secondary="path_dir_link", uselist=False)
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return f"<eid: {self.eid}, rel_path: {self.rel_path}, in_path: {self.in_path}>"
|
|
||||||
|
|
||||||
################################################################################
|
################################################################################
|
||||||
# Class describing Entry and in the DB (via sqlalchemy)
|
# Class describing Entry and in the DB (via sqlalchemy)
|
||||||
# an entry is the common bits between files and dirs
|
# an entry is the common bits between files and dirs
|
||||||
@@ -85,7 +76,7 @@ class Dir(db.Model):
|
|||||||
# in_dir - is the Dir that this entry is located in (convenience for class only)
|
# in_dir - is the Dir that this entry is located in (convenience for class only)
|
||||||
# FullPathOnFS(): method to get path on the FS for this Entry
|
# FullPathOnFS(): method to get path on the FS for this Entry
|
||||||
################################################################################
|
################################################################################
|
||||||
class Entry(db.Model):
|
class Entry(PA,db.Model):
|
||||||
__tablename__ = "entry"
|
__tablename__ = "entry"
|
||||||
id = db.Column(db.Integer, db.Sequence('file_id_seq'), primary_key=True )
|
id = db.Column(db.Integer, db.Sequence('file_id_seq'), primary_key=True )
|
||||||
name = db.Column(db.String, unique=False, nullable=False )
|
name = db.Column(db.String, unique=False, nullable=False )
|
||||||
@@ -106,9 +97,6 @@ class Entry(db.Model):
|
|||||||
s=self.dir_details.in_path.path_prefix
|
s=self.dir_details.in_path.path_prefix
|
||||||
return s
|
return s
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return f"<id: {self.id}, name: {self.name}, type={self.type}, dir_details={self.dir_details}, file_details={self.file_details}, in_dir={self.in_dir}"
|
|
||||||
|
|
||||||
################################################################################
|
################################################################################
|
||||||
# Class describing File and in the DB (via sqlalchemy)
|
# Class describing File and in the DB (via sqlalchemy)
|
||||||
# all files are entries, this is the extra bits only for a file, of note:
|
# all files are entries, this is the extra bits only for a file, of note:
|
||||||
@@ -117,7 +105,7 @@ class Entry(db.Model):
|
|||||||
# info can be from exif, or file system, or file name (rarely)
|
# info can be from exif, or file system, or file name (rarely)
|
||||||
# faces: convenience field to show connected face(s) for this file
|
# faces: convenience field to show connected face(s) for this file
|
||||||
################################################################################
|
################################################################################
|
||||||
class File(db.Model):
|
class File(PA,db.Model):
|
||||||
__tablename__ = "file"
|
__tablename__ = "file"
|
||||||
eid = db.Column(db.Integer, db.ForeignKey("entry.id"), primary_key=True )
|
eid = db.Column(db.Integer, db.ForeignKey("entry.id"), primary_key=True )
|
||||||
size_mb = db.Column(db.Integer, unique=False, nullable=False)
|
size_mb = db.Column(db.Integer, unique=False, nullable=False)
|
||||||
@@ -129,20 +117,27 @@ class File(db.Model):
|
|||||||
woy = db.Column(db.Integer)
|
woy = db.Column(db.Integer)
|
||||||
faces = db.relationship ("Face", secondary="face_file_link" )
|
faces = db.relationship ("Face", secondary="face_file_link" )
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return f"<eid: {self.eid}, size_mb={self.size_mb}, hash={self.hash}, year={self.year}, month={self.month}, day={self.day}, woy={self.woy}, faces={self.faces}>"
|
|
||||||
|
|
||||||
################################################################################
|
################################################################################
|
||||||
# Class describing FileType and in the DB (via sqlalchemy)
|
# Class describing FileType and in the DB (via sqlalchemy)
|
||||||
# pre-defined list of file types (image, dir, etc.)
|
# pre-defined list of file types (image, dir, etc.)
|
||||||
################################################################################
|
################################################################################
|
||||||
class FileType(db.Model):
|
class FileType(PA,db.Model):
|
||||||
__tablename__ = "file_type"
|
__tablename__ = "file_type"
|
||||||
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 )
|
||||||
|
|
||||||
def __repr__(self):
|
class AmendmentType(PA,db.Model):
|
||||||
return f"<id: {self.id}, name={self.name}>"
|
__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")
|
||||||
|
|
||||||
|
|
||||||
################################################################################
|
################################################################################
|
||||||
@@ -249,6 +244,18 @@ class FileSchema(ma.SQLAlchemyAutoSchema):
|
|||||||
load_instance = True
|
load_instance = True
|
||||||
faces = ma.Nested(FaceSchema,many=True,allow_none=True)
|
faces = ma.Nested(FaceSchema,many=True,allow_none=True)
|
||||||
|
|
||||||
|
class AmendmentTypeSchema(ma.SQLAlchemyAutoSchema):
|
||||||
|
class Meta:
|
||||||
|
model = AmendmentType
|
||||||
|
load_instance = True
|
||||||
|
|
||||||
|
class EntryAmendmentSchema(ma.SQLAlchemyAutoSchema):
|
||||||
|
class Meta:
|
||||||
|
model = EntryAmendment
|
||||||
|
load_instance = True
|
||||||
|
eid = ma.auto_field()
|
||||||
|
type = ma.Nested(AmendmentTypeSchema)
|
||||||
|
|
||||||
################################################################################
|
################################################################################
|
||||||
# Schema for Entry so we can json for data to the client
|
# Schema for Entry so we can json for data to the client
|
||||||
################################################################################
|
################################################################################
|
||||||
@@ -309,7 +316,14 @@ def process_ids():
|
|||||||
# Sort the entries according to the order of ids
|
# Sort the entries according to the order of ids
|
||||||
sorted_data = [entry_map[id_] for id_ in ids if id_ in entry_map]
|
sorted_data = [entry_map[id_] for id_ in ids if id_ in entry_map]
|
||||||
|
|
||||||
return jsonify(entries_schema.dump(sorted_data))
|
# get any pending entry amendments
|
||||||
|
stmt = select(EntryAmendment).join(AmendmentType)
|
||||||
|
ea = db.session.execute(stmt).unique().scalars().all()
|
||||||
|
ea_schema = EntryAmendmentSchema(many=True)
|
||||||
|
ea_data=ea_schema.dump(ea)
|
||||||
|
print( ea_data )
|
||||||
|
|
||||||
|
return jsonify(entries=entries_schema.dump(sorted_data), amend=ea_data)
|
||||||
|
|
||||||
|
|
||||||
################################################################################
|
################################################################################
|
||||||
@@ -328,21 +342,21 @@ def get_dir_entries():
|
|||||||
# if we are going back, find the parent id and use that instead
|
# if we are going back, find the parent id and use that instead
|
||||||
if back:
|
if back:
|
||||||
# get parent of this dir, to go back
|
# get parent of this dir, to go back
|
||||||
stmt=( select(EntryDirLink.dir_eid).filter(EntryDirLink.entry_id==dir_id) )
|
stmt=select(EntryDirLink.dir_eid).filter(EntryDirLink.entry_id==dir_id)
|
||||||
dir_id = db.session.execute(stmt).scalars().one_or_none()
|
dir_id = db.session.execute(stmt).scalars().one_or_none()
|
||||||
if not dir_id:
|
if not dir_id:
|
||||||
# return valid as false, we need to let user know this is not an empty dir, it does not exist
|
# return valid as false, we need to let user know this is not an empty dir, it does not exist
|
||||||
return jsonify( valid=False, entry_list=[] )
|
return jsonify( valid=False, entry_list=[] )
|
||||||
|
|
||||||
# Just double-check this is still in the DB, in case it got deleted since client made view
|
# Just double-check this is still in the DB, in case it got deleted since client made view
|
||||||
stmt=( select(Entry.id).where(Entry.id==dir_id) )
|
stmt=select(Entry.id).where(Entry.id==dir_id)
|
||||||
ent_id = db.session.execute(stmt).scalars().one_or_none()
|
ent_id = db.session.execute(stmt).scalars().one_or_none()
|
||||||
if not ent_id:
|
if not ent_id:
|
||||||
# return valid as false, we need to let user know this is not an empty dir, it does not exist
|
# return valid as false, we need to let user know this is not an empty dir, it does not exist
|
||||||
return jsonify( valid=False, entry_list=[] )
|
return jsonify( valid=False, entry_list=[] )
|
||||||
|
|
||||||
# get content of dir_id
|
# get content of dir_id
|
||||||
stmt=( select(Entry.id).join(EntryDirLink).filter(EntryDirLink.dir_eid==dir_id) )
|
stmt=select(Entry.id).join(EntryDirLink).filter(EntryDirLink.dir_eid==dir_id)
|
||||||
stmt=stmt.order_by(*order_map.get(noo) )
|
stmt=stmt.order_by(*order_map.get(noo) )
|
||||||
return jsonify( valid=True, entry_list=db.session.execute(stmt).scalars().all() )
|
return jsonify( valid=True, entry_list=db.session.execute(stmt).scalars().all() )
|
||||||
|
|
||||||
@@ -435,7 +449,7 @@ def GetQueryData( OPT ):
|
|||||||
|
|
||||||
if OPT.folders:
|
if OPT.folders:
|
||||||
# start folder view with only the root folder
|
# start folder view with only the root folder
|
||||||
stmt=( select(Entry.id).join(EntryDirLink).filter(EntryDirLink.dir_eid==dir_id) )
|
stmt=select(Entry.id).join(EntryDirLink).filter(EntryDirLink.dir_eid==dir_id)
|
||||||
else:
|
else:
|
||||||
# get every File that is in the OPT.prefix Path
|
# get every File that is in the OPT.prefix Path
|
||||||
stmt=(
|
stmt=(
|
||||||
|
|||||||
@@ -161,13 +161,13 @@
|
|||||||
c4.142,0,7.5-3.357,7.5-7.5S339.642,328,335.5,328z"/>
|
c4.142,0,7.5-3.357,7.5-7.5S339.642,328,335.5,328z"/>
|
||||||
<g style="fill:#00000025;" transform="matrix(16, 0, 0, 16, 120, 115)"><path d="M4.502 9a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3z"/><path d="M14.002 13a2 2 0 0 1-2 2h-10a2 2 0 0 1-2-2V5A2 2 0 0 1 2 3a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2v8a2 2 0 0 1-1.998 2zM14 2H4a1 1 0 0 0-1 1h9.002a2 2 0 0 1 2 2v7A1 1 0 0 0 15 11V3a1 1 0 0 0-1-1zM2.002 4a1 1 0 0 0-1 1v8l2.646-2.354a.5.5 0 0 1 .63-.062l2.66 1.773 3.71-3.71a.5.5 0 0 1 .577-.094l1.777 1.947V5a1 1 0 0 0-1-1h-10z"/></g>
|
<g style="fill:#00000025;" transform="matrix(16, 0, 0, 16, 120, 115)"><path d="M4.502 9a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3z"/><path d="M14.002 13a2 2 0 0 1-2 2h-10a2 2 0 0 1-2-2V5A2 2 0 0 1 2 3a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2v8a2 2 0 0 1-1.998 2zM14 2H4a1 1 0 0 0-1 1h9.002a2 2 0 0 1 2 2v7A1 1 0 0 0 15 11V3a1 1 0 0 0-1-1zM2.002 4a1 1 0 0 0-1 1v8l2.646-2.354a.5.5 0 0 1 .63-.062l2.66 1.773 3.71-3.71a.5.5 0 0 1 .577-.094l1.777 1.947V5a1 1 0 0 0-1-1h-10z"/></g>
|
||||||
</svg>
|
</svg>
|
||||||
<svg id="flip_h" fill="currentColor" viewBox='0 0 512 512'>
|
<svg id="flip_h" viewBox='0 0 512 512'>
|
||||||
<path fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' stroke-width='32' d='M304 48l112 112-112 112M398.87 160H96M208 464L96 352l112-112M114 352h302'/>
|
<path fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' stroke-width='32' d='M304 48l112 112-112 112M398.87 160H96M208 464L96 352l112-112M114 352h302'/>
|
||||||
</svg>
|
</svg>
|
||||||
<svg id="flip_v" fill="currentColor" viewBox='0 0 512 512'>
|
<svg id="flip_v" viewBox='0 0 512 512'>
|
||||||
<path fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' stroke-width='32' d='M464 208L352 96 240 208M352 113.13V416M48 304l112 112 112-112M160 398V96'/>
|
<path fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' stroke-width='32' d='M464 208L352 96 240 208M352 113.13V416M48 304l112 112 112-112M160 398V96'/>
|
||||||
</svg>
|
</svg>
|
||||||
<svg id="fullscreen" fill="currentColor" viewBox="0 0 16 16">
|
<svg id="fullscreen" viewBox="0 0 16 16">
|
||||||
<path fill-rule="evenodd" d="M5.828 10.172a.5.5 0 0 0-.707 0l-4.096 4.096V11.5a.5.5 0 0 0-1 0v3.975a.5.5 0 0 0 .5.5H4.5a.5.5 0 0 0 0-1H1.732l4.096-4.096a.5.5 0 0 0 0-.707zm4.344 0a.5.5 0 0 1 .707 0l4.096 4.096V11.5a.5.5 0 1 1 1 0v3.975a.5.5 0 0 1-.5.5H11.5a.5.5 0 0 1 0-1h2.768l-4.096-4.096a.5.5 0 0 1 0-.707zm0-4.344a.5.5 0 0 0 .707 0l4.096-4.096V4.5a.5.5 0 1 0 1 0V.525a.5.5 0 0 0-.5-.5H11.5a.5.5 0 0 0 0 1h2.768l-4.096 4.096a.5.5 0 0 0 0 .707zm-4.344 0a.5.5 0 0 1-.707 0L1.025 1.732V4.5a.5.5 0 0 1-1 0V.525a.5.5 0 0 1 .5-.5H4.5a.5.5 0 0 1 0 1H1.732l4.096 4.096a.5.5 0 0 1 0 .707z"/>
|
<path fill-rule="evenodd" d="M5.828 10.172a.5.5 0 0 0-.707 0l-4.096 4.096V11.5a.5.5 0 0 0-1 0v3.975a.5.5 0 0 0 .5.5H4.5a.5.5 0 0 0 0-1H1.732l4.096-4.096a.5.5 0 0 0 0-.707zm4.344 0a.5.5 0 0 1 .707 0l4.096 4.096V11.5a.5.5 0 1 1 1 0v3.975a.5.5 0 0 1-.5.5H11.5a.5.5 0 0 1 0-1h2.768l-4.096-4.096a.5.5 0 0 1 0-.707zm0-4.344a.5.5 0 0 0 .707 0l4.096-4.096V4.5a.5.5 0 1 0 1 0V.525a.5.5 0 0 0-.5-.5H11.5a.5.5 0 0 0 0 1h2.768l-4.096 4.096a.5.5 0 0 0 0 .707zm-4.344 0a.5.5 0 0 1-.707 0L1.025 1.732V4.5a.5.5 0 0 1-1 0V.525a.5.5 0 0 1 .5-.5H4.5a.5.5 0 0 1 0 1H1.732l4.096 4.096a.5.5 0 0 1 0 .707z"/>
|
||||||
</svg>
|
</svg>
|
||||||
<svg id="unknown_ftype" fill="grey" viewBox="0 0 16 16">
|
<svg id="unknown_ftype" fill="grey" viewBox="0 0 16 16">
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 25 KiB |
@@ -213,44 +213,82 @@ function DetailsDBox()
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// DoSel is called when a click event occurs, and sets the selection via adding
|
// DoSel is called when a click event occurs, and sets the selection via adding
|
||||||
// 'highlight' to the class of the appropriate thumbnails
|
// 'highlight' to the class of the appropriate thumbnails
|
||||||
// e == event (can see if shift/ctrl held down while left-clicking
|
// e == event (can see if shift/ctrl held down while left-clicking
|
||||||
// el == element the click is on
|
// el == element the click is on
|
||||||
// this allows single-click to select, ctrl-click to (de)select 1 item, and
|
// this allows single-click to select, ctrl-click to (de)select 1 item, and
|
||||||
// shift-click to add all elements between highlighted area and clicked area,
|
// shift-click to add all elements between highlighted area and clicked el,
|
||||||
// whether you click after highlight or before
|
// whether you click before highlight or after, or inside a gap and then back
|
||||||
function DoSel(e, el)
|
// or forward to the closest higlighted entry - also, only works on entry class,
|
||||||
{
|
// so it ignores figures that we take entry off while we transform, etc it
|
||||||
if( e.ctrlKey || document.fake_ctrl === 1 )
|
function DoSel(e, el) {
|
||||||
{
|
const id = $(el).attr('id');
|
||||||
$(el).toggleClass('highlight')
|
const entries = $('.entry');
|
||||||
if( document.fake_ctrl === 1 )
|
|
||||||
document.fake_ctrl=0
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if( e.shiftKey || document.fake_shift === 1 )
|
|
||||||
{
|
|
||||||
st=Number($('.highlight').first().attr('ecnt'))
|
|
||||||
end=Number($('.highlight').last().attr('ecnt'))
|
|
||||||
clicked=Number($(el).attr('ecnt'))
|
|
||||||
// if we shift-click first element, then st/end are NaN, so just highlightthe one clicked
|
|
||||||
if( isNaN(st) )
|
|
||||||
{
|
|
||||||
$('.entry').slice( clicked, clicked+1 ).addClass('highlight')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if( clicked > end )
|
|
||||||
$('.entry').slice( end, clicked+1 ).addClass('highlight')
|
|
||||||
else
|
|
||||||
$('.entry').slice( clicked, st ).addClass('highlight')
|
|
||||||
|
|
||||||
if( document.fake_shift === 1 )
|
// Collect currently highlighted entries
|
||||||
document.fake_shift=0
|
const currentHighlights = $('.highlight');
|
||||||
return
|
const highlighted = new Set();
|
||||||
|
currentHighlights.each(function() {
|
||||||
|
highlighted.add($(this).attr('id'));
|
||||||
|
});
|
||||||
|
|
||||||
|
// Ctrl+click: toggle highlight for the clicked entry
|
||||||
|
if (e.ctrlKey || document.fake_ctrl === 1) {
|
||||||
|
$(el).toggleClass('highlight');
|
||||||
|
if (highlighted.has(id)) {
|
||||||
|
highlighted.delete(id);
|
||||||
|
} else {
|
||||||
|
highlighted.add(id);
|
||||||
|
}
|
||||||
|
if (document.fake_ctrl === 1) {
|
||||||
|
document.fake_ctrl = 0;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Shift+click: select a range
|
||||||
|
else if (e.shiftKey || document.fake_shift === 1) {
|
||||||
|
if (currentHighlights.length === 0) {
|
||||||
|
// If no highlights, just highlight the clicked entry
|
||||||
|
$(el).addClass('highlight');
|
||||||
|
highlighted.add(id);
|
||||||
|
} else {
|
||||||
|
// Find the nearest highlighted entry
|
||||||
|
const clickedIndex = entries.index($(el));
|
||||||
|
let nearestHighlightIndex = -1;
|
||||||
|
let minDistance = Infinity;
|
||||||
|
|
||||||
|
currentHighlights.each(function() {
|
||||||
|
const highlightIndex = entries.index($(this));
|
||||||
|
const distance = Math.abs(highlightIndex - clickedIndex);
|
||||||
|
if (distance < minDistance) {
|
||||||
|
minDistance = distance;
|
||||||
|
nearestHighlightIndex = highlightIndex;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Highlight the range between the nearest highlighted entry and the clicked entry
|
||||||
|
const from = Math.min(clickedIndex, nearestHighlightIndex);
|
||||||
|
const to = Math.max(clickedIndex, nearestHighlightIndex);
|
||||||
|
|
||||||
|
for (let i = from; i <= to; i++) {
|
||||||
|
const entryId = entries.eq(i).attr('id');
|
||||||
|
highlighted.add(entryId);
|
||||||
|
entries.eq(i).addClass('highlight');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (document.fake_shift === 1) {
|
||||||
|
document.fake_shift = 0;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Single click: clear all highlights and highlight the clicked entry
|
||||||
|
else {
|
||||||
|
$('.highlight').removeClass('highlight');
|
||||||
|
highlighted.clear();
|
||||||
|
$(el).addClass('highlight');
|
||||||
|
highlighted.add(id);
|
||||||
}
|
}
|
||||||
$('.highlight').removeClass('highlight')
|
|
||||||
$(el).addClass('highlight')
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// if a selection exists, enable move & del/restore buttons otherwise disable them
|
// if a selection exists, enable move & del/restore buttons otherwise disable them
|
||||||
@@ -323,7 +361,7 @@ function NoSel() {
|
|||||||
* ecnt - Entry counter (e.g., { val: 0 }).
|
* ecnt - Entry counter (e.g., { val: 0 }).
|
||||||
* returns {string} - Generated HTML string.
|
* returns {string} - Generated HTML string.
|
||||||
*/
|
*/
|
||||||
function addFigure( obj, last, ecnt)
|
function addFigure( obj, last, ecnt )
|
||||||
{
|
{
|
||||||
let html = "";
|
let html = "";
|
||||||
|
|
||||||
@@ -383,6 +421,24 @@ function addFigure( obj, last, ecnt)
|
|||||||
}
|
}
|
||||||
|
|
||||||
$('#figures').append( html )
|
$('#figures').append( html )
|
||||||
|
|
||||||
|
// check if there is a pending amendment for this entry, if so mark it up
|
||||||
|
// (e.g. its being deleted, rotated, etc) - details in the am obj
|
||||||
|
for (const am of document.amendments)
|
||||||
|
{
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -613,7 +669,7 @@ 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) { getEntriesByIdSuccessHandler( res, pageNumber, successCallback, viewingIdx ) },
|
success: function(res) { document.amendments=res.amend; 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
|
||||||
}
|
}
|
||||||
|
|||||||
47
tables.sql
47
tables.sql
@@ -1,4 +1,4 @@
|
|||||||
ALTER DATABASE pa SET TIMEZONE TO 'aUSTRALIA/vICTORIA';
|
ALTER DATABASE pa SET TIMEZONE TO 'Australia/Victoria';
|
||||||
|
|
||||||
CREATE SEQUENCE pa_user_id_seq;
|
CREATE SEQUENCE pa_user_id_seq;
|
||||||
CREATE SEQUENCE pa_user_state_id_seq;
|
CREATE SEQUENCE pa_user_state_id_seq;
|
||||||
@@ -21,8 +21,8 @@ CREATE SEQUENCE query_id_seq;
|
|||||||
|
|
||||||
-- these are hard-coded at present, not sure I can reflexively find models from API?
|
-- these are hard-coded at present, not sure I can reflexively find models from API?
|
||||||
CREATE TABLE ai_model ( id INTEGER, name VARCHAR(24), description VARCHAR(80), CONSTRAINT pk_ai_model PRIMARY KEY(id) );
|
CREATE TABLE ai_model ( id INTEGER, name VARCHAR(24), description VARCHAR(80), CONSTRAINT pk_ai_model PRIMARY KEY(id) );
|
||||||
INSERT INTO ai_model VALUES ( 1, 'HOG', 'NORMAL' );
|
INSERT INTO ai_model VALUES ( 1, 'hog', 'normal' );
|
||||||
INSERT INTO ai_model VALUES ( 2, 'CNN', 'MORE ACCURATE / MUCH SLOWER' );
|
INSERT INTO ai_model VALUES ( 2, 'cnn', 'more accurate / much slower' );
|
||||||
|
|
||||||
CREATE TABLE settings(
|
CREATE TABLE settings(
|
||||||
id INTEGER,
|
id INTEGER,
|
||||||
@@ -122,10 +122,10 @@ CREATE TABLE face_refimg_link( face_id INTEGER, refimg_id INTEGER, face_distance
|
|||||||
CONSTRAINT fk_frl_refimg_id FOREIGN KEY (refimg_id) REFERENCES refimg(id) );
|
CONSTRAINT fk_frl_refimg_id FOREIGN KEY (refimg_id) REFERENCES refimg(id) );
|
||||||
|
|
||||||
CREATE TABLE face_override_type ( id INTEGER, name VARCHAR UNIQUE, CONSTRAINT pk_face_override_type_id PRIMARY KEY(id) );
|
CREATE TABLE face_override_type ( id INTEGER, name VARCHAR UNIQUE, CONSTRAINT pk_face_override_type_id PRIMARY KEY(id) );
|
||||||
INSERT INTO face_override_type VALUES ( (SELECT NEXTVAL('face_override_type_id_seq')), 'mANUAL MATCH TO EXISTING PERSON' );
|
INSERT INTO face_override_type VALUES ( (SELECT NEXTVAL('face_override_type_id_seq')), 'Manual match to existing person' );
|
||||||
INSERT INTO face_override_type VALUES ( (SELECT NEXTVAL('face_override_type_id_seq')), 'nOT A FACE' );
|
INSERT INTO face_override_type VALUES ( (SELECT NEXTVAL('face_override_type_id_seq')), 'Not a face' );
|
||||||
INSERT INTO face_override_type VALUES ( (SELECT NEXTVAL('face_override_type_id_seq')), 'tOO YOUNG' );
|
INSERT INTO face_override_type VALUES ( (SELECT NEXTVAL('face_override_type_id_seq')), 'Too young' );
|
||||||
INSERT INTO face_override_type VALUES ( (SELECT NEXTVAL('face_override_type_id_seq')), 'iGNORE FACE' );
|
INSERT INTO face_override_type VALUES ( (SELECT NEXTVAL('face_override_type_id_seq')), 'Ignore face' );
|
||||||
|
|
||||||
-- keep non-redundant FACE because, when we rebuild data we may have a null FACE_ID, but still want to connect to this override
|
-- keep non-redundant FACE because, when we rebuild data we may have a null FACE_ID, but still want to connect to this override
|
||||||
-- from a previous AI pass... (would happen if we delete a file and then reimport/scan it), OR, more likely we change (say) a threshold, etc.
|
-- from a previous AI pass... (would happen if we delete a file and then reimport/scan it), OR, more likely we change (say) a threshold, etc.
|
||||||
@@ -165,20 +165,33 @@ CREATE TABLE joblog ( id INTEGER, job_id INTEGER, log_date TIMESTAMPTZ, log VARC
|
|||||||
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, level VARCHAR(16), message VARCHAR(8192), persistent BOOLEAN, cant_close BOOLEAN,
|
CREATE TABLE pa_job_manager_fe_message ( id INTEGER, job_id INTEGER, level VARCHAR(16), message VARCHAR(8192), persistent BOOLEAN, cant_close BOOLEAN,
|
||||||
CONSTRAINT 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),
|
||||||
|
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, which, what, colour ) VALUES ( 2, 'img', 'rot90.png', '#009EFF' );
|
||||||
|
INSERT INTO amendment_type ( id, which, what, colour ) VALUES ( 3, 'img', 'rot180.png', '#009EFF' );
|
||||||
|
INSERT INTO amendment_type ( id, which, what, colour ) VALUES ( 4, 'img', 'rot270.png', '#009EFF' );
|
||||||
|
INSERT INTO amendment_type ( id, which, what, colour ) VALUES ( 5, 'icon', 'flip_h', '#009EFF' );
|
||||||
|
INSERT INTO amendment_type ( id, which, what, colour ) VALUES ( 6, 'icon', 'flip_v', '#009EFF' );
|
||||||
|
|
||||||
|
CREATE TABLE entry_amendment ( eid INTEGER, amend_type INTEGER,
|
||||||
|
CONSTRAINT pk_entry_amendment_eid_name PRIMARY KEY(eid,amend_type),
|
||||||
|
CONSTRAINT fk_entry_amendment_amendment_type FOREIGN KEY(amend_type) REFERENCES amendment_type(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' );
|
||||||
INSERT INTO path_type VALUES ( (SELECT NEXTVAL('path_type_id_seq')), 'sTORAGE' );
|
INSERT INTO path_type VALUES ( (SELECT NEXTVAL('path_type_id_seq')), 'Storage' );
|
||||||
INSERT INTO path_type VALUES ( (SELECT NEXTVAL('path_type_id_seq')), 'bIN' );
|
INSERT INTO path_type VALUES ( (SELECT NEXTVAL('path_type_id_seq')), 'Bin' );
|
||||||
INSERT INTO path_type VALUES ( (SELECT NEXTVAL('path_type_id_seq')), 'mETADATA' );
|
INSERT INTO path_type VALUES ( (SELECT NEXTVAL('path_type_id_seq')), 'Metadata' );
|
||||||
|
|
||||||
-- default data for types of files
|
-- default data for types of files
|
||||||
INSERT INTO file_type VALUES ( (SELECT NEXTVAL('file_type_id_seq')), 'iMAGE' );
|
INSERT INTO file_type VALUES ( (SELECT NEXTVAL('file_type_id_seq')), 'Image' );
|
||||||
INSERT INTO file_type VALUES ( (SELECT NEXTVAL('file_type_id_seq')), 'vIDEO' );
|
INSERT INTO file_type VALUES ( (SELECT NEXTVAL('file_type_id_seq')), 'Video' );
|
||||||
INSERT INTO file_type VALUES ( (SELECT NEXTVAL('file_type_id_seq')), 'dIRECTORY' );
|
INSERT INTO file_type VALUES ( (SELECT NEXTVAL('file_type_id_seq')), 'Directory' );
|
||||||
INSERT INTO file_type VALUES ( (SELECT NEXTVAL('file_type_id_seq')), 'uNKNOWN' );
|
INSERT INTO file_type VALUES ( (SELECT NEXTVAL('file_type_id_seq')), 'Unknown' );
|
||||||
|
|
||||||
-- fake data only for making testing easier
|
-- fake data only for making testing easier
|
||||||
--INSERT INTO person VALUES ( (SELECT NEXTVAL('person_id_seq')), 'dad', 'Damien', 'De Paoli' );
|
--INSERT INTO person VALUES ( (SELECT NEXTVAL('person_id_seq')), 'dad', 'Damien', 'De Paoli' );
|
||||||
@@ -186,7 +199,7 @@ INSERT INTO file_type VALUES ( (SELECT NEXTVAL('file_type_id_seq')), 'uNKNOWN' )
|
|||||||
--INSERT INTO person VALUES ( (SELECT NEXTVAL('person_id_seq')), 'cam', 'Cameron', 'De Paoli' );
|
--INSERT INTO person VALUES ( (SELECT NEXTVAL('person_id_seq')), 'cam', 'Cameron', 'De Paoli' );
|
||||||
--INSERT INTO person VALUES ( (SELECT NEXTVAL('person_id_seq')), 'mich', 'Michelle', 'De Paoli' );
|
--INSERT INTO person VALUES ( (SELECT NEXTVAL('person_id_seq')), 'mich', 'Michelle', 'De Paoli' );
|
||||||
-- DEV(ddp):
|
-- DEV(ddp):
|
||||||
INSERT INTO settings ( ID, BASE_PATH, IMPORT_PATH, STORAGE_PATH, RECYCLE_BIN_PATH, METADATA_PATH, AUTO_ROTATE, DEFAULT_REFIMG_MODEL, DEFAULT_SCAN_MODEL, DEFAULT_THRESHOLD, FACE_SIZE_LIMIT, SCHEDULED_IMPORT_SCAN, SCHEDULED_STORAGE_SCAN, SCHEDULED_BIN_CLEANUP, BIN_CLEANUP_FILE_AGE, JOB_ARCHIVE_AGE ) VALUES ( (SELECT NEXTVAL('settings_id_seq')), '/HOME/DDP/SRC/PHOTOASSISTANT/', 'IMAGES_TO_PROCESS/', 'PHOTOS/', '.PA_BIN/', '.PA_METADATA/', TRUE, 1, 1, '0.55', 43, 1, 1, 7, 30, 3 );
|
INSERT INTO settings ( id, base_path, import_path, storage_path, recycle_bin_path, metadata_path, auto_rotate, default_refimg_model, default_scan_model, default_threshold, face_size_limit, scheduled_import_scan, scheduled_storage_scan, scheduled_bin_cleanup, bin_cleanup_file_age, job_archive_age ) VALUES ( (SELECT NEXTVAL('settings_id_seq')), '/home/ddp/src/photoassistant/', 'images_to_process/', 'photos/', '.pa_bin/', '.pa_metadata/', true, 1, 1, '0.55', 43, 1, 1, 7, 30, 3 );
|
||||||
-- DEV(cam):
|
-- DEV(cam):
|
||||||
--INSERT INTO settings ( id, base_path, import_path, storage_path, recycle_bin_path, metadata_path, auto_rotate, default_refimg_model, default_scan_model, default_threshold, face_size_limit, scheduled_import_scan, scheduled_storage_scan, scheduled_bin_cleanup, bin_cleanup_file_age, job_archive_age ) VALUES ( (select nextval('SETTINGS_ID_SEQ')), 'c:/Users/cam/Desktop/code/python/photoassistant/', 'c:\images_to_process', 'photos/', '.pa_bin/', '.pa_metadata/', TRUE, 1, 1, '0.55', 43, 1, 1, 7, 30, 3 );
|
--INSERT INTO settings ( id, base_path, import_path, storage_path, recycle_bin_path, metadata_path, auto_rotate, default_refimg_model, default_scan_model, default_threshold, face_size_limit, scheduled_import_scan, scheduled_storage_scan, scheduled_bin_cleanup, bin_cleanup_file_age, job_archive_age ) VALUES ( (select nextval('SETTINGS_ID_SEQ')), 'c:/Users/cam/Desktop/code/python/photoassistant/', 'c:\images_to_process', 'photos/', '.pa_bin/', '.pa_metadata/', TRUE, 1, 1, '0.55', 43, 1, 1, 7, 30, 3 );
|
||||||
-- PROD:
|
-- PROD:
|
||||||
|
|||||||
Reference in New Issue
Block a user