@@ -73,7 +73,7 @@ def ensure_uploads_log_schema():
7373def auto_log_material_files ():
7474 """
7575 Walk UPLOAD_FOLDER and upsert one row per (property, tab, filename).
76- Idempotent: safe to call many times; never throws UNIQUE errors .
76+ Idempotent and uses SQLite CURRENT_TIMESTAMP (no Python datetime) .
7777 """
7878 ensure_uploads_log_schema ()
7979
@@ -82,7 +82,7 @@ def auto_log_material_files():
8282 return {"status" : "skip" , "reason" : "UPLOAD_FOLDER missing" , "added_or_updated" : 0 }
8383
8484 allowed_exts = (ALLOWED_DATASET_EXTENSIONS | ALLOWED_RESULTS_EXTENSIONS )
85- rows = [] # (property, tab, filename, uploaded_at )
85+ rows = [] # (property, tab, filename)
8686
8787 for root , _dirs , files in os .walk (root_dir ):
8888 for fname in files :
@@ -100,7 +100,8 @@ def auto_log_material_files():
100100
101101 if len (parts ) >= 3 :
102102 prop , tab , filename = parts [0 ], parts [1 ], parts [2 ]
103- rows .append ((prop , tab , filename , datetime .utcnow ().isoformat (timespec = "seconds" )))
103+ if tab in ("dataset" , "results" ):
104+ rows .append ((prop , tab , filename ))
104105
105106 if not rows :
106107 return {"status" : "ok" , "added_or_updated" : 0 }
@@ -110,9 +111,9 @@ def auto_log_material_files():
110111 c = conn .cursor ()
111112 upsert = """
112113 INSERT INTO uploads_log (property, tab, filename, uploaded_at)
113- VALUES (?, ?, ?, ? )
114+ VALUES (?, ?, ?, CURRENT_TIMESTAMP )
114115 ON CONFLICT(property, tab, filename)
115- DO UPDATE SET uploaded_at = excluded.uploaded_at
116+ DO UPDATE SET uploaded_at = CURRENT_TIMESTAMP
116117 """
117118 for r in rows :
118119 c .execute (upsert , r )
@@ -246,34 +247,38 @@ def _startup_once():
246247
247248# ========== ROUTES ==========
248249
249-
250+ #########################################################
250251# Admin only rescanning for duplicates and re-importing
251252
252- @app .route ("/admin/rescan_uploads" )
253- def admin_rescan_uploads ():
254- if not session .get ("admin" ):
255- return redirect (url_for ("login" ))
256-
257- def count_rows ():
253+ @app .route ('/admin/rescan_uploads' )
254+ def rescan_uploads ():
255+ """
256+ Re-scan the UPLOAD_FOLDER and upsert entries into uploads_log.
257+ Returns JSON with counts. Uses CURRENT_TIMESTAMP in SQL.
258+ """
259+ try :
260+ # observe before/after for quick sanity checks
258261 with sqlite3 .connect (DB_NAME ) as conn :
259262 cur = conn .cursor ()
260263 cur .execute ("SELECT COUNT(*) FROM uploads_log" )
261- return cur .fetchone ()[0 ]
264+ before = cur .fetchone ()[0 ]
262265
263- before = count_rows ()
264- try :
265266 result = auto_log_material_files ()
267+
268+ with sqlite3 .connect (DB_NAME ) as conn :
269+ cur = conn .cursor ()
270+ cur .execute ("SELECT COUNT(*) FROM uploads_log" )
271+ after = cur .fetchone ()[0 ]
272+
273+ return jsonify ({
274+ "status" : result .get ("status" , "ok" ),
275+ "added_or_updated" : result .get ("added_or_updated" , 0 ),
276+ "rows_before" : before ,
277+ "rows_after" : after
278+ })
266279 except Exception as e :
267280 return jsonify ({"status" : f"auto_log_material_files failed: { e } " }), 500
268- after = count_rows ()
269-
270- return jsonify ({
271- "status" : result .get ("status" ),
272- "added_or_updated" : result .get ("added_or_updated" ),
273- "rows_before" : before ,
274- "rows_after" : after
275- })
276-
281+ #########################################################
277282# -- Admin login/logout --
278283@app .route ('/login' , methods = ['GET' , 'POST' ])
279284def login ():
@@ -291,7 +296,7 @@ def logout():
291296 session .pop ('admin' , None )
292297 flash ("Logged out." )
293298 return redirect (url_for ('public_home' ))
294-
299+ #########################################################
295300# -- Admin-only home page (upload/import/query) --
296301@app .route ('/admin' , methods = ['GET' , 'POST' ])
297302def admin_home ():
@@ -324,106 +329,8 @@ def admin_home():
324329 uploads = uploads ,
325330 music_clips = music_clips
326331 )
327-
328- # -- View and import (admin only) --
329- @app .route ('/view/<path:filename>' , methods = ['GET' , 'POST' ])
330- def view_table (filename ):
331- admin = session .get ('admin' , False )
332- filepath = os .path .join (app .config ['UPLOAD_FOLDER' ], filename )
333- ext = filename .rsplit ('.' , 1 )[1 ].lower ()
334- table_name = filename .replace ('.' , '_' ).replace ('-' , '_' ).replace ('/' , '_' ).replace ('\\ ' , '_' )
335-
336- try :
337- if ext == 'csv' :
338- df = pd .read_csv (filepath )
339- elif ext == 'npy' :
340- arr = np .load (filepath , allow_pickle = True )
341- if isinstance (arr , np .ndarray ):
342- if arr .ndim == 2 :
343- df = pd .DataFrame (arr )
344- elif arr .ndim == 1 and hasattr (arr [0 ], 'dtype' ) and arr [0 ].dtype .names :
345- df = pd .DataFrame (arr )
346- else :
347- df = pd .DataFrame (arr )
348- else :
349- return "Unsupported NPY format for display."
350- else :
351- return "Unsupported file type."
352- except Exception as e :
353- return f"Could not read file: { e } "
354-
355- # Only allow import if admin
356- if admin and request .method == 'POST' and 'import_sql' in request .form :
357- with sqlite3 .connect (DB_NAME ) as conn :
358- df .to_sql (table_name , conn , if_exists = 'replace' , index = False )
359- flash (f"Table '{ table_name } ' imported to SQLite." )
360-
361- return render_template ('view_table.html' ,
362- tables = [df .to_html (classes = 'data' )],
363- titles = df .columns .values ,
364- filename = filename ,
365- imported_table = table_name ,
366- admin = admin )
367-
368-
369- # -- SQL query tool (admin only) --
370- @app .route ('/query' , methods = ['GET' , 'POST' ])
371- def query_sql ():
372- if not session .get ('admin' ):
373- return redirect (url_for ('login' ))
374-
375- # List all tables for dropdown or info
376- tables = []
377- with sqlite3 .connect (DB_NAME ) as conn :
378- c = conn .cursor ()
379- c .execute ("SELECT name FROM sqlite_master WHERE type='table';" )
380- tables = [r [0 ] for r in c .fetchall ()]
381-
382- sql = ""
383- result_html = ""
384- error_msg = ""
385-
386- if request .method == 'POST' :
387- sql = request .form ['sql' ]
388- try :
389- with sqlite3 .connect (DB_NAME ) as conn :
390- c = conn .cursor ()
391- c .execute (sql )
392- # Try to fetch rows, if any
393- try :
394- rows = c .fetchall ()
395- if rows :
396- # Get column names
397- columns = [desc [0 ] for desc in c .description ]
398- import pandas as pd
399- df = pd .DataFrame (rows , columns = columns )
400- result_html = df .to_html (classes = 'data' )
401- else :
402- result_html = "<p><b>Query executed successfully.</b></p>"
403- except Exception :
404- result_html = "<p><b>Query executed successfully.</b></p>"
405- conn .commit ()
406- except Exception as e :
407- error_msg = str (e )
408- return render_template (
409- 'sql_query.html' ,
410- tables = tables ,
411- sql = sql ,
412- result_html = result_html ,
413- error_msg = error_msg ,
414- admin = True
415- )
416-
417- # ========== PUBLIC ROUTES (view/download only) ==========
418-
419- @app .route ('/' )
420- def public_home ():
421- return render_template ('landing.html' )
422- #########################################################
423- @app .route ('/materials' )
424- def materials_portal ():
425- return render_template ('materials_portal.html' )
426332#########################################################
333+ # -- View and import (admin only) --
427334@app .route ('/materials/<property_name>/<tab>' , methods = ['GET' , 'POST' ])
428335def property_detail (property_name , tab ):
429336 # ---- titles / guards ----
@@ -438,35 +345,39 @@ def property_detail(property_name, tab):
438345
439346 upload_message = ""
440347 edit_message = ""
441- is_admin = session .get ('admin' , False )
348+ is_admin = bool ( session .get ('admin' ) )
442349
443350 # ---- admin POST handlers ----
444351 if is_admin and request .method == 'POST' :
445352 # Inline row edit
446353 if 'edit_row' in request .form :
447- row_filename = request .form .get ('row_filename' )
448- safe_row_filename = secure_filename (os .path .basename (row_filename or "" ))
449-
354+ row_filename = request .form .get ('row_filename' ) or ''
355+ safe_row_filename = secure_filename (os .path .basename (row_filename ))
450356 new_desc = (request .form .get ('row_description' ) or '' ).strip ()
451- if tab == 'dataset' :
452- new_source = (request .form .get ('row_source' ) or '' ).strip ()
453- with sqlite3 .connect (DB_NAME ) as conn :
454- c = conn .cursor ()
455- c .execute ("""
357+
358+ with sqlite3 .connect (DB_NAME ) as conn :
359+ c = conn .cursor ()
360+ if tab == 'dataset' :
361+ new_source = (request .form .get ('row_source' ) or '' ).strip ()
362+ c .execute (
363+ """
456364 UPDATE uploads_log
457365 SET source = ?, description = ?
458366 WHERE property = ? AND tab = ? AND filename = ?
459- """ , ( new_source , new_desc , property_name , tab , safe_row_filename ))
460- conn . commit ()
461- else :
462- with sqlite3 . connect ( DB_NAME ) as conn :
463- c = conn . cursor ()
464- c . execute ( """
367+ """ ,
368+ ( new_source , new_desc , property_name , tab , safe_row_filename ),
369+ )
370+ else :
371+ c . execute (
372+ """
465373 UPDATE uploads_log
466374 SET description = ?
467375 WHERE property = ? AND tab = ? AND filename = ?
468- """ , (new_desc , property_name , tab , safe_row_filename ))
469- conn .commit ()
376+ """ ,
377+ (new_desc , property_name , tab , safe_row_filename ),
378+ )
379+ conn .commit ()
380+
470381 edit_message = f"Updated info for { safe_row_filename } ."
471382
472383 # New file upload
@@ -479,12 +390,9 @@ def property_detail(property_name, tab):
479390 if tab == 'dataset' :
480391 is_allowed = allowed_dataset_file (f .filename )
481392 allowed_types = "CSV or NPY"
482- elif tab == ' results' :
393+ else : # results
483394 is_allowed = allowed_results_file (f .filename )
484395 allowed_types = "JPG, PNG, GIF, PDF, or DOCX"
485- else :
486- is_allowed = False
487- allowed_types = ""
488396
489397 if not is_allowed :
490398 upload_message = f"File type not allowed. Only { allowed_types } supported."
@@ -496,41 +404,37 @@ def property_detail(property_name, tab):
496404 filepath = os .path .join (property_folder , safe_filename )
497405 f .save (filepath )
498406
499- # Log to DB (idempotent)
407+ # Log to DB (idempotent; no Python datetime )
500408 with sqlite3 .connect (DB_NAME ) as conn :
501409 c = conn .cursor ()
502410 c .execute (
503411 """
504412 INSERT INTO uploads_log (property, tab, filename, uploaded_at)
505- VALUES (?, ?, ?, ? )
413+ VALUES (?, ?, ?, CURRENT_TIMESTAMP )
506414 ON CONFLICT(property, tab, filename)
507- DO UPDATE SET uploaded_at = excluded.uploaded_at
415+ DO UPDATE SET uploaded_at = CURRENT_TIMESTAMP
508416 """ ,
509- (property_name , tab , safe_filename , datetime . datetime . now (). isoformat () ),
417+ (property_name , tab , safe_filename ),
510418 )
511419 conn .commit ()
512420
513421 upload_message = f"File { safe_filename } uploaded for { pretty_titles [property_name ]} { tab .title ()} !"
514422
515- # ---- fetch current uploads (dedup to 1 row per filename; keep newest ) ----
423+ # ---- fetch current uploads (unique per key thanks to UNIQUE index ) ----
516424 with sqlite3 .connect (DB_NAME ) as conn :
517425 c = conn .cursor ()
518- c .execute ("""
519- SELECT u.filename,
520- COALESCE(u.source, '') AS source,
521- COALESCE(u.description, '') AS description,
522- u.uploaded_at
523- FROM uploads_log AS u
524- JOIN (
525- SELECT filename, MAX(uploaded_at) AS max_ts
526- FROM uploads_log
527- WHERE property = ? AND tab = ?
528- GROUP BY filename
529- ) AS m
530- ON m.filename = u.filename AND u.uploaded_at = m.max_ts
531- WHERE u.property = ? AND u.tab = ?
532- ORDER BY u.uploaded_at DESC
533- """ , (property_name , tab , property_name , tab ))
426+ c .execute (
427+ """
428+ SELECT filename,
429+ COALESCE(source, '') AS source,
430+ COALESCE(description, '') AS description,
431+ uploaded_at
432+ FROM uploads_log
433+ WHERE property = ? AND tab = ?
434+ ORDER BY uploaded_at DESC, filename
435+ """ ,
436+ (property_name , tab ),
437+ )
534438 uploads = c .fetchall ()
535439
536440 return render_template (
@@ -796,6 +700,7 @@ def extract_drive_id(link):
796700 message = "❌ Invalid link or missing title."
797701
798702 return render_template ('add_drive_clip.html' , message = message )
703+
799704#########################################################
800705
801706# ========== MAIN ==========
0 commit comments