@@ -98,35 +98,79 @@ def file_to_table_name(filename: str) -> str:
9898#==================================================#
9999
100100def ensure_uploads_log_schema ():
101- """Create/upgrade uploads_log to the expected schema; ensure uniqueness ."""
101+ """Public catalog ( uploads_log) + audit history (uploads_audit) with triggers ."""
102102 with sqlite3 .connect (DB_NAME ) as conn :
103103 c = conn .cursor ()
104- # Create table if missing (includes UNIQUE on the key)
104+
105+ # Public catalog (what public pages read)
105106 c .execute ("""
106107 CREATE TABLE IF NOT EXISTS uploads_log (
107- id INTEGER PRIMARY KEY AUTOINCREMENT,
108- property TEXT NOT NULL,
109- tab TEXT NOT NULL,
110- filename TEXT NOT NULL,
111- uploaded_at TEXT,
108+ id INTEGER PRIMARY KEY AUTOINCREMENT,
109+ property TEXT NOT NULL,
110+ tab TEXT NOT NULL, -- 'dataset' | 'results'
111+ filename TEXT NOT NULL, -- human-visible label
112+ uploaded_at TEXT, -- first/last touch, maintained by app
113+ -- Drive-first metadata
114+ storage TEXT, -- 'drive' | 'local' (legacy)
115+ drive_id TEXT,
116+ preview_url TEXT,
117+ download_url TEXT,
118+ source TEXT,
119+ description TEXT,
112120 UNIQUE(property, tab, filename)
113121 )
114122 """ )
115- # Ensure uploaded_at column exists (for older DBs)
116- cols = {row [1 ] for row in c .execute ("PRAGMA table_info(uploads_log)" ).fetchall ()}
117- if "uploaded_at" not in cols :
118- c .execute ("ALTER TABLE uploads_log ADD COLUMN uploaded_at TEXT" )
119- # Try to migrate from legacy logged_at if it exists
120- try :
121- c .execute ("UPDATE uploads_log SET uploaded_at = COALESCE(uploaded_at, logged_at) WHERE uploaded_at IS NULL" )
122- except sqlite3 .OperationalError :
123- pass # logged_at may not exist; ignore
124123
125- # Ensure a unique index exists even if the table was created long ago
124+ # Ensure index exists even on old DBs
126125 c .execute ("""
127126 CREATE UNIQUE INDEX IF NOT EXISTS idx_uploads_unique
128127 ON uploads_log(property, tab, filename)
129128 """ )
129+
130+ # ---- Audit table
131+ c .execute ("""
132+ CREATE TABLE IF NOT EXISTS uploads_audit (
133+ id INTEGER PRIMARY KEY AUTOINCREMENT,
134+ property TEXT NOT NULL,
135+ tab TEXT NOT NULL,
136+ filename TEXT NOT NULL,
137+ action TEXT NOT NULL, -- add | update | delete
138+ at TEXT NOT NULL
139+ )
140+ """ )
141+
142+ # Helper to create triggers idempotently
143+ def ensure_trigger (name , ddl ):
144+ c .execute ("SELECT 1 FROM sqlite_master WHERE type='trigger' AND name=?" , (name ,))
145+ if not c .fetchone ():
146+ c .execute (ddl )
147+
148+ ensure_trigger ("trg_ul_insert_audit" , """
149+ CREATE TRIGGER trg_ul_insert_audit
150+ AFTER INSERT ON uploads_log
151+ BEGIN
152+ INSERT INTO uploads_audit(property, tab, filename, action, at)
153+ VALUES (NEW.property, NEW.tab, NEW.filename, 'add',
154+ COALESCE(NEW.uploaded_at, CURRENT_TIMESTAMP));
155+ END;""" )
156+
157+ ensure_trigger ("trg_ul_update_audit" , """
158+ CREATE TRIGGER trg_ul_update_audit
159+ AFTER UPDATE ON uploads_log
160+ BEGIN
161+ INSERT INTO uploads_audit(property, tab, filename, action, at)
162+ VALUES (NEW.property, NEW.tab, NEW.filename, 'update',
163+ COALESCE(NEW.uploaded_at, CURRENT_TIMESTAMP));
164+ END;""" )
165+
166+ ensure_trigger ("trg_ul_delete_audit" , """
167+ CREATE TRIGGER trg_ul_delete_audit
168+ AFTER DELETE ON uploads_log
169+ BEGIN
170+ INSERT INTO uploads_audit(property, tab, filename, action, at)
171+ VALUES (OLD.property, OLD.tab, OLD.filename, 'delete', CURRENT_TIMESTAMP);
172+ END;""" )
173+
130174 conn .commit ()
131175
132176#==================================================#
@@ -489,38 +533,38 @@ def logout():
489533
490534#########################################################
491535
492- # -- Admin-only home page (upload/import/query) --
493- @app .route ('/admin' , methods = ['GET' , 'POST' ])
536+ # --- Admin Dashboard (history-only) - --
537+ @app .route ('/admin' , methods = ['GET' ])
494538def admin_home ():
495539 if not session .get ('admin' ):
496540 return redirect (url_for ('login' ))
497541
498- # Get all uploads (materials) from uploads_log
499- uploads = []
542+ # Make sure catalog + audit schema exist (triggers are created here too)
543+ ensure_uploads_log_schema ()
544+
545+ # Build the history table: when first added, and whether still present publicly
500546 with sqlite3 .connect (DB_NAME ) as conn :
501547 c = conn .cursor ()
502548 c .execute ("""
503- SELECT property, tab, filename, uploaded_at
504- FROM uploads_log
505- ORDER BY uploaded_at DESC
549+ WITH hist AS (
550+ SELECT property, tab, filename,
551+ MIN(CASE WHEN action='add' THEN at END) AS first_added,
552+ MAX(at) AS last_event
553+ FROM uploads_audit
554+ GROUP BY property, tab, filename
555+ )
556+ SELECT
557+ h.filename AS file_name,
558+ COALESCE(h.first_added, h.last_event) AS uploaded_at,
559+ CASE WHEN u.rowid IS NULL THEN 'Absent' ELSE 'Present' END AS public_view_status
560+ FROM hist h
561+ LEFT JOIN uploads_log u
562+ ON u.property=h.property AND u.tab=h.tab AND u.filename=h.filename
563+ ORDER BY uploaded_at DESC, h.filename;
506564 """ )
507- uploads = c .fetchall ()
565+ audit_rows = c .fetchall ()
508566
509- # Get all music clips from the music_clips table
510- music_clips = []
511- try :
512- with sqlite3 .connect (DB_NAME ) as conn :
513- c = conn .cursor ()
514- c .execute ("SELECT filename, title, description FROM music_clips ORDER BY rowid DESC" )
515- music_clips = c .fetchall ()
516- except Exception :
517- music_clips = []
518-
519- return render_template (
520- 'admin_home.html' ,
521- uploads = uploads ,
522- music_clips = music_clips
523- )
567+ return render_template ('admin_home.html' , audit_rows = audit_rows )
524568
525569#########################################################
526570
@@ -641,10 +685,10 @@ def diag_routes():
641685
642686#########################################################
643687
644- # -- View and import (admin only ) --
688+ # -- View and import (admin + Public ) --
645689@app .route ('/materials/<property_name>/<tab>' , methods = ['GET' , 'POST' ])
646690def property_detail (property_name , tab ):
647- # ---- titles / guards ----
691+ # Titles / guards
648692 pretty_titles = {
649693 'bandgap' : 'Band Gap' ,
650694 'formation_energy' : 'Formation Energy' ,
@@ -654,99 +698,104 @@ def property_detail(property_name, tab):
654698 if property_name not in pretty_titles or tab not in ('dataset' , 'results' ):
655699 return "Not found." , 404
656700
701+ ensure_uploads_log_schema ()
702+
657703 upload_message = ""
658704 edit_message = ""
659705 is_admin = bool (session .get ('admin' ))
660706
661- # ---- admin POST handlers ----
707+ # -------- Admin-only: Drive-based upload ------- ----
662708 if is_admin and request .method == 'POST' :
663- # Inline row edit
664- if 'edit_row' in request .form :
709+ if 'add_drive' in request .form :
710+ # Form fields (Drive)
711+ drive_link = (request .form .get ('drive_link' ) or '' ).strip ()
712+ label = (request .form .get ('label' ) or '' ).strip () # human filename label (required)
713+ source = (request .form .get ('row_source' ) or '' ).strip () if tab == 'dataset' else None
714+ desc = (request .form .get ('row_description' ) or '' ).strip ()
715+
716+ # Parse Drive ID from link or raw id
717+ def _extract_drive_id (link : str ):
718+ m = re .search (r'/d/([a-zA-Z0-9_-]+)' , link ) or re .search (r'[?&]id=([a-zA-Z0-9_-]+)' , link )
719+ if m : return m .group (1 )
720+ if re .match (r'^[a-zA-Z0-9_-]{10,}$' , link ): return link
721+ return None
722+
723+ drive_id = _extract_drive_id (drive_link )
724+ if not label or not drive_id :
725+ upload_message = "❌ Provide a valid Drive link/ID and a label."
726+ else :
727+ preview_url = f"https://drive.google.com/file/d/{ drive_id } /preview"
728+ download_url = f"https://drive.google.com/uc?export=download&id={ drive_id } "
729+
730+ # Upsert into public catalog (dedup by key)
731+ with sqlite3 .connect (DB_NAME ) as conn :
732+ c = conn .cursor ()
733+ c .execute ("""
734+ INSERT INTO uploads_log
735+ (property, tab, filename, uploaded_at, storage, drive_id, preview_url, download_url, source, description)
736+ VALUES (?, ?, ?, CURRENT_TIMESTAMP, 'drive', ?, ?, ?, ?, ?)
737+ ON CONFLICT(property, tab, filename)
738+ DO UPDATE SET
739+ uploaded_at = CURRENT_TIMESTAMP,
740+ storage='drive', drive_id=excluded.drive_id,
741+ preview_url=excluded.preview_url, download_url=excluded.download_url,
742+ source=excluded.source, description=excluded.description
743+ """ , (property_name , tab , label , drive_id , preview_url , download_url , source , desc ))
744+ conn .commit ()
745+ upload_message = f"✅ Added '{ label } ' from Drive."
746+
747+ elif 'edit_row' in request .form :
748+ # Inline update (source/description)
665749 row_filename = request .form .get ('row_filename' ) or ''
666750 safe_row_filename = secure_filename (os .path .basename (row_filename ))
667751 new_desc = (request .form .get ('row_description' ) or '' ).strip ()
668-
669752 with sqlite3 .connect (DB_NAME ) as conn :
670753 c = conn .cursor ()
671754 if tab == 'dataset' :
672755 new_source = (request .form .get ('row_source' ) or '' ).strip ()
673- c .execute (
674- """
756+ c .execute ("""
675757 UPDATE uploads_log
676- SET source = ?, description = ?
677- WHERE property = ? AND tab = ? AND filename = ?
678- """ ,
679- (new_source , new_desc , property_name , tab , safe_row_filename ),
680- )
758+ SET source=?, description=?, uploaded_at=CURRENT_TIMESTAMP
759+ WHERE property=? AND tab=? AND filename=?""" ,
760+ (new_source , new_desc , property_name , tab , safe_row_filename ))
681761 else :
682- c .execute (
683- """
762+ c .execute ("""
684763 UPDATE uploads_log
685- SET description = ?
686- WHERE property = ? AND tab = ? AND filename = ?
687- """ ,
688- (new_desc , property_name , tab , safe_row_filename ),
689- )
764+ SET description=?, uploaded_at=CURRENT_TIMESTAMP
765+ WHERE property=? AND tab=? AND filename=?""" ,
766+ (new_desc , property_name , tab , safe_row_filename ))
690767 conn .commit ()
691-
692768 edit_message = f"Updated info for { safe_row_filename } ."
693769
694- # New file upload
695- elif 'file' in request .files :
696- f = request .files ['file' ]
697- if not f or f .filename == '' :
698- upload_message = "No file selected."
699- else :
700- # Validate extension by tab
701- if tab == 'dataset' :
702- is_allowed = allowed_dataset_file (f .filename )
703- allowed_types = "CSV or NPY"
704- else : # results
705- is_allowed = allowed_results_file (f .filename )
706- allowed_types = "JPG, PNG, GIF, PDF, or DOCX"
707-
708- if not is_allowed :
709- upload_message = f"File type not allowed. Only { allowed_types } supported."
710- else :
711- # Save to disk (under /uploads/<property>/<tab>/)
712- property_folder = os .path .join (app .config ['UPLOAD_FOLDER' ], property_name , tab )
713- os .makedirs (property_folder , exist_ok = True )
714- safe_filename = secure_filename (os .path .basename (f .filename ))
715- filepath = os .path .join (property_folder , safe_filename )
716- f .save (filepath )
717-
718- # Log to DB (idempotent; no Python datetime)
719- with sqlite3 .connect (DB_NAME ) as conn :
720- c = conn .cursor ()
721- c .execute (
722- """
723- INSERT INTO uploads_log (property, tab, filename, uploaded_at)
724- VALUES (?, ?, ?, CURRENT_TIMESTAMP)
725- ON CONFLICT(property, tab, filename)
726- DO UPDATE SET uploaded_at = CURRENT_TIMESTAMP
727- """ ,
728- (property_name , tab , safe_filename ),
729- )
730- conn .commit ()
731-
732- upload_message = f"File { safe_filename } uploaded for { pretty_titles [property_name ]} { tab .title ()} !"
733-
734- # ---- fetch current uploads (unique per key thanks to UNIQUE index) ----
770+ # -------- Fetch current public rows -----------
735771 with sqlite3 .connect (DB_NAME ) as conn :
736772 c = conn .cursor ()
737- c .execute (
738- """
773+ c .execute ("""
739774 SELECT filename,
740- COALESCE(source, '') AS source,
741- COALESCE(description, '') AS description,
742- uploaded_at
775+ COALESCE(source,'') AS source,
776+ COALESCE(description,'') AS description,
777+ uploaded_at,
778+ COALESCE(storage,'local') AS storage,
779+ COALESCE(preview_url,'') AS preview_url,
780+ COALESCE(download_url,'') AS download_url
743781 FROM uploads_log
744- WHERE property = ? AND tab = ?
782+ WHERE property= ? AND tab= ?
745783 ORDER BY uploaded_at DESC, filename
746- """ ,
747- (property_name , tab ),
748- )
749- uploads = c .fetchall ()
784+ """ , (property_name , tab ))
785+ rows = c .fetchall ()
786+
787+ # Normalize rows for the template
788+ uploads = []
789+ for fname , source , description , uploaded_at , storage , purl , durl in rows :
790+ uploads .append ({
791+ "filename" : fname ,
792+ "source" : source ,
793+ "description" : description ,
794+ "uploaded_at" : uploaded_at ,
795+ "storage" : storage ,
796+ "preview_url" : purl ,
797+ "download_url" : durl ,
798+ })
750799
751800 return render_template (
752801 'property_detail.html' ,
0 commit comments