Compare commits

...

4 Commits

5 changed files with 158 additions and 177 deletions

21
TODO
View File

@@ -1,15 +1,20 @@
###
# get override data into view
# should start with an empty DB and test
#
#1 get override data into view
# also all the add ref img/add override, etc are non-functional - FIX the override* stuff first to get table/naming consistency as that is half the problem
# NMO data -> there is an NMO object (just NMO names/types - |json), then there is per face level data - this should be a reference from Face and Schema/marshmallow
#
#2 should start with an empty DB and test
# definitely no dirs in storage_sp I now pass root_eid=0 for this
# BUT - need GUI to work - may even be good to put an alert up - its so odd to have not root dir ONLY happens when no data
# empty directories (2017/20171015-test/...) showing "No matches for: 'undefined'" <- should only comes up for search in URL???
# think I killed pa_job_manager without passing an eid to a transform job, shouldn't crash
#3 empty directories (2017/20171015-test/...) showing "No matches for: 'undefined'" <- should only comes up for search in URL???
#
#4 TEST everything (don't forget keybindings,e.g. delete)
# -- go into viewer code from a files_rbp - had red bin, bot green on viewer.
#
#5 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...
# also all the add ref img/add override, etc are non-functional - FIX the override* stuff first to get table/naming consistency as that is half the problem
# convert move_paths to a json setup
# ALSO revisit this move_paths to be as safe as possible ultimately, triple-check there are no leading / or .. 's
# TEST everything (don't forget keybindings,e.g. delete)
#
###
### major fix - go to everywhere I call GetEntries(), and redo the logic totally...

View File

@@ -2,7 +2,7 @@ from flask_wtf import FlaskForm
from flask import request, render_template, redirect, send_from_directory, url_for, jsonify, make_response
from marshmallow import Schema, fields
from main import db, app, ma
from sqlalchemy import Sequence, text, select, union
from sqlalchemy import Sequence, text, select, union, or_
from sqlalchemy.exc import SQLAlchemyError
from sqlalchemy.orm import joinedload
import os
@@ -28,10 +28,10 @@ from types import SimpleNamespace
from states import States, PA_UserState
from query import Query
from job import Job, JobExtra, Joblog, NewJob, SetFELog
from path import PathType, Path, MovePathDetails
from path import PathType, Path
from person import Refimg, Person, PersonRefimgLink
from settings import Settings, SettingsIPath, SettingsSPath, SettingsRBPath
from shared import SymlinkName
from shared import SymlinkName, ICON
from dups import Duplicates
from face import Face, FaceFileLink, FaceRefimgLink, FaceOverrideType, FaceNoMatchOverride, FaceForceMatchOverride
@@ -170,6 +170,14 @@ class PathSchema(ma.SQLAlchemyAutoSchema):
class Meta: model = Path
load_instance = True
type = ma.Nested(PathType)
root_dir = fields.Method("get_root_dir")
icon_url = fields.Method("get_icon_url")
def get_icon_url(self, obj):
return url_for("internal", filename="icons.svg") + "#" + ICON[obj.type.name]
def get_root_dir(self, obj):
parts = obj.path_prefix.split('/')
return ''.join(parts[2:])
class FileTypeSchema(ma.SQLAlchemyAutoSchema):
class Meta: model = FileType
@@ -216,6 +224,11 @@ class FileSchema(ma.SQLAlchemyAutoSchema):
load_instance = True
faces = ma.Nested(FaceSchema,many=True,allow_none=True)
# used just in NMO var
class FaceOverrideTypeSchema(ma.SQLAlchemyAutoSchema):
class Meta: model = FaceOverrideType
load_instance = True
################################################################################
# Schema for Entry so we can json for data to the client
################################################################################
@@ -238,6 +251,9 @@ class EntrySchema(ma.SQLAlchemyAutoSchema):
# global - this will be use more than once below, so do it once for efficiency
entries_schema = EntrySchema(many=True)
FOT_Schema = FaceOverrideTypeSchema(many=True)
path_Schema = PathSchema(many=True)
person_Schema = PersonSchema(many=True)
################################################################################
# util function to just update the current/first/last positions needed for
@@ -314,6 +330,25 @@ def get_dir_entries():
entries = Entry.query.filter(Entry.id.in_(ids)).all()
return jsonify(entries_schema.dump(entries))
# get Face overrid details
def getFOT():
stmt = select(FaceOverrideType)
fot=db.session.execute(stmt).scalars().all()
return FOT_Schema.dump(fot)
# get import/storage path details for move dbox
def getMoveDetails():
stmt = select(Path).where( or_( Path.type.has(name="Import"), Path.type.has(name="Storage")))
mp=db.session.execute(stmt).scalars().all()
return path_Schema.dump(mp)
# get people data for the menu for AI matching (of person.tag)
def getPeople():
stmt = select(Person)
people=db.session.execute(stmt).scalars().all()
return person_Schema.dump(people)
################################################################################
# Get all relevant Entry.ids based on search_term passed in and OPT visuals
@@ -322,6 +357,9 @@ def GetSearchQueryData(OPT):
query_data={}
query_data['entry_list']=None
query_data['root_eid']=0
query_data['NMO'] = getFOT()
query_data['move_paths'] = getMoveDetails()
query_data['people'] = getPeople()
search_term = OPT.search_term
# turn * wildcard into sql wildcard of %
@@ -352,13 +390,15 @@ def GetSearchQueryData(OPT):
query_data['entry_list']=all_entries
return query_data
#################################################################################
# Get all relevant Entry.ids based on files_ip/files_sp/files_rbp and OPT visuals
#################################################################################
def GetQueryData( OPT ):
query_data={}
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
dir_stmt=(
@@ -388,7 +428,6 @@ def GetQueryData( OPT ):
stmt=stmt.order_by(*order_map.get(OPT.noo) )
query_data['entry_list']=db.session.execute(stmt).scalars().all()
return query_data
################################################################################
@@ -428,9 +467,8 @@ def file_list_ip():
def files_ip():
OPT=States( request )
people = Person.query.all()
move_paths = MovePathDetails()
query_data = GetQueryData( OPT )
return render_template("files.html", page_title=f"View Files ({OPT.path_type} Path)", OPT=OPT, people=people, move_paths=move_paths, query_data=query_data )
return render_template("files.html", page_title=f"View Files ({OPT.path_type} Path)", OPT=OPT, people=people, query_data=query_data )
################################################################################
# /files -> show thumbnail view of files from storage_path
@@ -440,9 +478,8 @@ def files_ip():
def files_sp():
OPT=States( request )
people = Person.query.all()
move_paths = MovePathDetails()
query_data = GetQueryData( OPT )
return render_template("files.html", page_title=f"View Files ({OPT.path_type} Path)", OPT=OPT, people=people, move_paths=move_paths, query_data=query_data )
return render_template("files.html", page_title=f"View Files ({OPT.path_type} Path)", OPT=OPT, people=people, query_data=query_data )
################################################################################
@@ -453,9 +490,8 @@ def files_sp():
def files_rbp():
OPT=States( request )
people = Person.query.all()
move_paths = MovePathDetails()
query_data = GetQueryData( OPT )
return render_template("files.html", page_title=f"View Files ({OPT.path_type} Path)", OPT=OPT, people=people, move_paths=move_paths, query_data=query_data )
return render_template("files.html", page_title=f"View Files ({OPT.path_type} Path)", OPT=OPT, people=people, query_data=query_data )
################################################################################
# search -> GET version -> has search_term in the URL and is therefore able to
@@ -470,8 +506,7 @@ def search(search_term):
OPT.folders = False
query_data=GetSearchQueryData( OPT )
move_paths = MovePathDetails()
return render_template("files.html", page_title='View Files', search_term=search_term, query_data=query_data, OPT=OPT, move_paths=move_paths )
return render_template("files.html", page_title='View Files', search_term=search_term, query_data=query_data, OPT=OPT )
################################################################################
# /files/scan_ip -> allows us to force a check for new files

View File

@@ -109,7 +109,7 @@ function MoveOrDelCleanUpUI()
// show the DBox for a move file, includes all thumbnails of selected files to move
// and a pre-populated folder to move them into, with text field to add a suffix
function MoveDBox(path_details, db_url)
function MoveDBox(path_details)
{
$('#dbox-title').html('Move Selected File(s) to new directory in Storage Path')
div =`
@@ -119,12 +119,12 @@ function MoveDBox(path_details, db_url)
<form id="mv_fm" class="form form-control-inline col-12">
<input id="move_path_type" name="move_path_type" type="hidden"
`
div += ' value="' + path_details[0].type + '"></input>'
div += ' value="' + path_details[0].type.name + '"></input>'
div+=GetSelnAsDiv()
yr=$('.highlight').first().attr('yr')
dt=$('.highlight').first().attr('date')
div+='<div class="row">Use Existing Directory (in the chosen path):</div><div id="existing"></div>'
GetExistingDirsAsDiv( dt, "existing", path_details[0].type )
GetExistingDirsAsDiv( dt, "existing", path_details[0].type.name )
div+=`
<div class="input-group my-3">
<alert class="alert alert-primary my-auto py-1">
@@ -133,7 +133,7 @@ function MoveDBox(path_details, db_url)
div+= '<svg id="move_path_icon" width="20" height="20" fill="currentColor"><use xlink:href="' + path_details[0].icon_url + '"></svg>'
div+= '<select id="rp_sel" name="rel_path" class="text-primary alert-primary py-1 border border-primary rounded" onChange="change_rp_sel()">'
for(p of path_details) {
div+= '<option path_type="'+p.type+'" icon_url="'+p.icon_url+'">'+p.path+'</option>'
div+= '<option path_type="'+p.type.name+'" icon_url="'+p.icon_url+'">'+p.root_dir+'</option>'
}
div+= '</select>'
div+=`
@@ -838,3 +838,87 @@ function dblClickToViewEntry(id) {
setEntryById( id )
ViewImageOrVideo()
}
// different context menu on files
$.contextMenu({
selector: '.entry',
itemClickEvent: "click",
build: function($triggerElement, e) {
// when right-clicking & no selection add one OR deal with ctrl/shift right-lick as it always changes seln
if( NoSel() || e.ctrlKey || e.shiftKey )
{
DoSel(e, e.currentTarget )
SetButtonState();
}
if( FiguresOrDirsOrBoth() == "figure" )
{
item_list = {
details: { name: "Details..." },
view: { name: "View File" },
sep: "---",
}
if( e.currentTarget.getAttribute('type') == 'Image' )
{
item_list['transform'] = {
name: "Transform",
items: {
"r90": { "name" : "Rotate 90 degrees" },
"r180": { "name" : "Rotate 180 degrees" },
"r270": { "name" : "Rotate 270 degrees" },
"fliph": { "name" : "Flip horizontally" },
"flipv": { "name" : "Flip vertically" }
}
}
}
item_list['move'] = { name: "Move selected file(s) to new folder" }
item_list['sep2'] = { sep: "---" }
}
else
item_list = {
move: { name: "Move selection(s) to new folder" }
}
item_list['ai'] = {
name: "Scan file for faces",
items: {
"ai-all": { name: "all" }
}
};
// Dynamically add entries for each person in the `people` array
people.forEach(person => {
item_list['ai'].items[`ai-${person.tag}`] = { name: person.tag };
});
if( SelContainsBinAndNotBin() ) {
item_list['both']= { name: 'Cannot delete and restore at same time', disabled: true }
} else {
if (e.currentTarget.getAttribute('path_type') == 'Bin' )
item_list['undel']= { name: "Restore selected file(s)" }
else if( e.currentTarget.getAttribute('type') != 'Directory' )
item_list['del']= { name: "Delete Selected file(s)" }
}
return {
callback: function( key, options) {
if( key == "details" ) { DetailsDBox() }
if( key == "view" ) { dblClickToViewEntry( $(this).attr('id') ) }
if( key == "move" ) { MoveDBox(move_paths) }
if( key == "del" ) { DelDBox('Delete') }
if( key == "undel") { DelDBox('Restore') }
if( key == "r90" ) { Transform(90) }
if( key == "r180" ) { Transform(180) }
if( key == "r270" ) { Transform(270) }
if( key == "fliph" ) { Transform("fliph") }
if( key == "flipv" ) { Transform("flipv") }
if( key.startsWith("ai")) { RunAIOnSeln(key) }
// dont flow this event through the dom
e.stopPropagation()
},
items: item_list
};
}
});

50
path.py
View File

@@ -42,53 +42,3 @@ class Path(db.Model):
def __repr__(self):
return f"<id: {self.id}, path_prefix: {self.path_prefix}, num_files={self.num_files}, type={self.type}>"
################################################################################
# Class describing PathDetail (quick connvenence class for MovePathDetails())
################################################################################
class PathDetail(PA):
"""Class describing details of a Path [internal class used in MovePathDetais()]"""
def __init__(self,ptype,path):
"""Initialisation function for PathDetail class
Args:
id (int): database id of row in PathDetail table / primary key
ptype (int): database id of row in PathType table / foreign key
"""
self.type:int=ptype
self.path:str=path
# construct icon_url based on type of storage path (icons.svg contains icons for each)
self.icon_url:str=url_for("internal", filename="icons.svg") + "#" + ICON[self.type]
def to_dict(self):
return {key: value for key, value in vars(self).items()}
################################################################################
# helper function to find path details for move destinations - used in html
# for move DBox to show potential storage paths to move files into
################################################################################
def MovePathDetails():
"""helper function to find path details for move destinations
used in html/javascript for move Dialog Box to show potential storage paths to move files into
Args:
None
Returns:
ret (List[PathDetail]): a list of Path Details for where files can be moved
"""
ret=[]
sps=Path.query.join(PathType).filter(PathType.name=="Storage").all()
for p in sps:
obj = PathDetail( ptype="Storage", path=p.path_prefix.replace("static/Storage/","") )
ret.append( obj.to_dict() )
ips=Path.query.join(PathType).filter(PathType.name=="Import").all()
for p in ips:
obj = PathDetail( ptype="Import", path=p.path_prefix.replace("static/Import/","") )
ret.append( obj.to_dict() )
return ret

View File

@@ -64,7 +64,7 @@
<button aria-label="next" id="next" name="next" class="next sm-txt btn btn-outline-secondary" onClick="nextPage(getPageFigures)">
<svg width="16" height="16" fill="currentColor"><use xlink:href="{{url_for('internal', filename='icons.svg')}}#next"/></svg>
</button>
<button aria-label="move" id="move" disabled name="move" class="sm-txt btn btn-outline-primary ms-4" onClick="MoveDBox(move_paths,'{{url_for('internal', filename='icons.svg')}}'); return false;">
<button aria-label="move" id="move" disabled name="move" class="sm-txt btn btn-outline-primary ms-4" onClick="MoveDBox(move_paths); return false;">
<svg width="16" height="16" fill="currentColor"><use xlink:href="{{url_for('internal', filename='icons.svg')}}#folder_plus"/></svg>
</button>
{% if "files_rbp" in request.url %}
@@ -221,19 +221,24 @@
{% block script_content %}
<script>
// GLOBALS
document.fake_shift=0
document.fake_ctrl=0
// FIXME: used by viewer code - should probably get rid of this?
var fullscreen=false;
var move_paths = {{ move_paths|tojson }};
// GLOBALS
// this is the current entry (object) we are viewing - an image/video (used when we dbl-click to view & then in next/prev in view)
document.viewing=null;
var OPT = {{ OPT.to_dict()|tojson }};
OPT.root_eid = {{ query_data.root_eid }};
// get items out of query_data into convenience javascript vars...
var move_paths = {{ query_data.move_paths|tojson }};
var NMO={{query_data.NMO|tojson}}
var people={{query_data.people|tojson}}
// this is the list of entry ids for the images for ALL matches for this query
var entryList={{query_data.entry_list}}
@@ -247,7 +252,6 @@
var grayscale=0
var throbber=0
var NMO=[]
var imp_path="static/Import/{{imp_path}}"
var st_path="static/Storage/{{st_path}}"
var bin_path="static/Bin/{{bin_path}}"
@@ -274,108 +278,11 @@
return s
}
// different context menu on files
$.contextMenu({
selector: '.entry',
itemClickEvent: "click",
build: function($triggerElement, e) {
// when right-clicking & no selection add one OR deal with ctrl/shift right-lick as it always changes seln
if( NoSel() || e.ctrlKey || e.shiftKey )
{
DoSel(e, e.currentTarget )
SetButtonState();
}
if( FiguresOrDirsOrBoth() == "figure" )
{
item_list = {
details: { name: "Details..." },
view: { name: "View File" },
sep: "---",
}
if( e.currentTarget.getAttribute('type') == 'Image' )
{
item_list['transform'] = {
name: "Transform",
items: {
"r90": { "name" : "Rotate 90 degrees" },
"r180": { "name" : "Rotate 180 degrees" },
"r270": { "name" : "Rotate 270 degrees" },
"fliph": { "name" : "Flip horizontally" },
"flipv": { "name" : "Flip vertically" }
}
}
}
item_list['move'] = { name: "Move selected file(s) to new folder" }
item_list['sep2'] = { sep: "---" }
}
else
item_list = {
move: { name: "Move selection(s) to new folder" }
}
item_list['ai'] = {
name: "Scan file for faces",
items: {
"ai-all": {"name": "all"},
{% for p in people %}
"ai-{{p.tag}}": {"name": "{{p.tag}}"},
{% endfor %}
}
}
if( SelContainsBinAndNotBin() ) {
item_list['both']= { name: 'Cannot delete and restore at same time', disabled: true }
} else {
if (e.currentTarget.getAttribute('path_type') == 'Bin' )
item_list['undel']= { name: "Restore selected file(s)" }
else if( e.currentTarget.getAttribute('type') != 'Directory' )
item_list['del']= { name: "Delete Selected file(s)" }
}
return {
callback: function( key, options) {
if( key == "details" ) { DetailsDBox() }
if( key == "view" ) { dblClickToViewEntry( $(this).attr('id') ) }
if( key == "move" ) { MoveDBox(move_paths, "{{url_for('internal', filename='icons.svg')}}") }
if( key == "del" ) { DelDBox('Delete') }
if( key == "undel") { DelDBox('Restore') }
if( key == "r90" ) { Transform(90) }
if( key == "r180" ) { Transform(180) }
if( key == "r270" ) { Transform(270) }
if( key == "fliph" ) { Transform("fliph") }
if( key == "flipv" ) { Transform("flipv") }
if( key.startsWith("ai")) { RunAIOnSeln(key) }
// dont flow this event through the dom
e.stopPropagation()
},
items: item_list
};
}
});
$( document ).keydown(function(event) { switch (event.key)
{
case "Delete":
{% if "files_rbp" in request.url %}
if( ! NoSel() ) DelDBox('Restore');
{% else %}
if( ! NoSel() ) DelDBox('Delete');
{% endif %}
break;
} })
if( isMobile() )
{
$('#shift-key').css('visibility', 'visible');
$('#ctrl-key').css('visibility', 'visible');
}
// check the size radiobutton
$(`input[name="size"][value="${OPT.size}"]`).prop('checked', true)
window.addEventListener('resize', DrawImg, false);
window.addEventListener('resize', ResizeVideo, false);
</script>
{% endblock script_content %}