from wtforms import SubmitField, StringField, IntegerField, FloatField, HiddenField, validators, Form, SelectField from flask_wtf import FlaskForm from flask import request, render_template, redirect, url_for from main import db, app, ma from sqlalchemy import Sequence from sqlalchemy.exc import SQLAlchemyError from status import st, Status from flask_login import login_required, current_user # pylint: disable=no-member ################################################################################ # Class describing AI_MODEL in the database, and via sqlalchemy, connected to the DB as well ################################################################################ class AIModel(db.Model): __tablename__ = "ai_model" id = db.Column(db.Integer, primary_key=True ) name = db.Column(db.String) def __repr__(self): return f"" ################################################################################ # Class describing Settings in the database, and via sqlalchemy, connected to the DB as well ################################################################################ class Settings(db.Model): id = db.Column(db.Integer, db.Sequence('settings_id_seq'), primary_key=True ) base_path = db.Column(db.String) import_path = db.Column(db.String) storage_path = db.Column(db.String) recycle_bin_path = db.Column(db.String) default_refimg_model = db.Column(db.Integer,db.ForeignKey('ai_model.id'), unique=True, nullable=False) default_scan_model = db.Column(db.Integer,db.ForeignKey('ai_model.id'), unique=True, nullable=False) default_threshold = db.Column(db.Integer) scheduled_import_scan = db.Column(db.Integer) scheduled_storage_scan = db.Column(db.Integer) scheduled_bin_cleanup = db.Column(db.Integer) bin_cleanup_file_age = db.Column(db.Integer) job_archive_age = db.Column(db.Integer) def __repr__(self): return f"" ################################################################################ # Helper class that inherits a .dump() method to turn class Settings into json / useful in jinja2 ################################################################################ class SettingsSchema(ma.SQLAlchemyAutoSchema): class Meta: model = Settings ordered = True settings_schema = SettingsSchema(many=True) ################################################################################ # Helper class that defines a form for Settings, used to make html
# with field validation (via wtforms) ################################################################################ class SettingsForm(FlaskForm): id = HiddenField() base_path = StringField('Base Path to use below:', [validators.DataRequired()]) import_path = StringField('Path(s) to import from:', [validators.DataRequired()]) storage_path = StringField('Path to store sorted images to:', [validators.DataRequired()]) recycle_bin_path = StringField('Path to temporarily store deleted images in:', [validators.DataRequired()]) default_refimg_model = SelectField( 'Default model to use for reference images', choices=[(c.id, c.name) for c in AIModel.query.order_by('id')] ) default_scan_model = SelectField( 'Default model to use for all scanned images', choices=[(c.id, c.name) for c in AIModel.query.order_by('id')] ) default_threshold = StringField('Face Distance threshold (below is a match):', [validators.DataRequired()]) scheduled_import_scan = IntegerField('Days between forced scan of import path', [validators.DataRequired()]) scheduled_storage_scan = IntegerField('Days between forced scan of storage path', [validators.DataRequired()]) scheduled_bin_cleanup = IntegerField('Days between checking to clean Recycle Bin:', [validators.DataRequired()]) bin_cleanup_file_age = IntegerField('Age of files to clean out of the Recycle Bin', [validators.DataRequired()]) job_archive_age = IntegerField('Age of jobs to archive', [validators.DataRequired()]) submit = SubmitField('Save' ) ################################################################################ # /settings -> show current settings ################################################################################ @app.route("/settings", methods=["GET", "POST"]) @login_required def settings(): form = SettingsForm(request.form) page_title='Settings' HELP={} HELP['base_path']="Optional: if the paths below are absolute, then this field is ignored. If they are relative, then this field is prepended" HELP['import_path']="Path(s) to import files from. If starting with /, then used literally, otherwise base path is prepended" HELP['storage_path']="Path(s) to store sorted files to. If starting with /, then used literally, otherwise base path is prepended" HELP['recycle_bin_path']="Path where deleted files are moved to. If starting with /, then used literally, otherwise base path is prepended" HELP['default_refimg_model']="Default face recognition model used for reference images - cnn is slower/more accurate, hog is faster/less accurate - we scan (small) refimg once, so cnn is okay" HELP['default_scan_model']="Default face recognition model used for scanned images - cnn is slower/more accurate, hog is faster/less accurate - we scan (large) scanned images lots, so cnn NEEDS gpu/mem" HELP['default_threshold']="The distance below which a face is considered a match. The default is usually 0.6, we are trying for about 0.55 with kids. YMMV" HELP['scheduled_import_scan']="The # of days between forced scans of the import path for new images" HELP['scheduled_storage_scan']="The # of days between forced scans of the storage path for any file system changes outside of Photo Assistant" HELP['scheduled_bin_cleanup']="The # of days between running a job to delete old files from the Recycle Bin" HELP['bin_cleanup_file_age']="The # of days a file has to exist in the Recycle Bin before it can be really deleted" HELP['job_archive_age']="The # of days a job has to exist for to be archived" if request.method == 'POST' and form.validate(): try: s = Settings.query.one() if 'submit' in request.form: st.SetMessage("Successfully Updated Settings" ) s.import_path = request.form['import_path'] s.storage_path = request.form['storage_path'] s.recycle_bin_path = request.form['recycle_bin_path'] s.default_refimg_model = request.form['default_refimg_model'] s.default_scan_model = request.form['default_scan_model'] s.default_threshold = request.form['default_threshold'] s.scheduled_import_scan = request.form['scheduled_import_scan'] s.scheduled_storage_scan = request.form['scheduled_storage_scan'] s.scheduled_bin_cleanup = request.form['scheduled_bin_cleanup'] s.bin_cleanup_file_age = request.form['bin_cleanup_file_age'] s.job_archive_age = request.form['job_archive_age'] db.session.commit() return redirect( url_for( 'settings' ) ) except SQLAlchemyError as e: st.SetMessage( f"Failed to modify Setting: {e.orig}", "danger" ) return render_template("settings.html", form=form, page_title=page_title, HELP=HELP) else: form = SettingsForm( obj=Settings.query.first() ) return render_template("settings.html", form=form, page_title = page_title, HELP=HELP) ############################################################################## # SettingsRBPath(): return modified array of paths (take each path in # recycle_bin_path and add base_path if needed) ############################################################################## def SettingsRBPath(): settings = Settings.query.first() if settings == None: print("Cannot create file data with no settings / recycle bin path is missing") return # path setting is an absolute path, just use it, otherwise prepend base_path first if settings.recycle_bin_path[0] == '/': path = settings.recycle_bin_path else: path = settings.base_path+settings.recycle_bin_path return path ############################################################################## # SettingsSPath(): return modified array of paths (take each path in # storage_path and add base_path if needed) ############################################################################## def SettingsSPath(): paths=[] settings = Settings.query.first() if settings == None: print("Cannot create file data with no settings / storage path is missing") return for p in settings.storage_path.split("#"): if p[0] == '/': paths.append(p) else: paths.append(settings.base_path+p) return paths ############################################################################## # SettingsIPath(): return modified array of paths (take each path in # import_path and add base_path if needed) ############################################################################## def SettingsIPath(): paths=[] settings = Settings.query.first() if settings == None: print ("Cannot create file data with no settings / import path is missing") return for p in settings.import_path.split("#"): if p[0] == '/': paths.append(p) else: paths.append(settings.base_path+p) return paths