@@ -38,6 +38,65 @@ def allowed_music_file(filename):
3838
3939# ========== Helper Functions ========== #
4040
41+ _SQLITE_RESERVED_PREFIXES = ("sqlite_" ,)
42+
43+ def tableize_basename (name : str ) -> str :
44+ """
45+ Convert a *basename only* (e.g., 'Featurized Band Gap Data.csv') into a
46+ safe SQLite table name. Preserves case (to match your existing tables).
47+ Ensures:
48+ - No path separators
49+ - Allowed chars: [A-Za-z0-9_]
50+ - No leading 'sqlite_' prefix
51+ - Not empty; if empty, returns 't_unnamed'
52+ - Collapses multiple underscores
53+ - Appends an extension suffix (_csv/_npy) if the original had one
54+ """
55+
56+ base = os .path .basename (name or "" ).strip ()
57+ if not base :
58+ return "t_unnamed"
59+
60+ # Split extension (if any) for suffixing
61+ stem , ext = os .path .splitext (base )
62+ ext_suffix = ""
63+ if ext :
64+ e = ext .lstrip ("." ).lower ()
65+ if e in ("csv" , "npy" ):
66+ ext_suffix = f"_{ e } "
67+ else :
68+ # Non-dataset extension: still keep it to avoid collisions
69+ ext_suffix = f"_{ e } "
70+
71+ # Replace separators and disallowed chars with underscores
72+ s = stem .replace ("." , "_" ).replace ("-" , "_" ).replace (" " , "_" )
73+ s = re .sub (r"[^0-9A-Za-z_]" , "_" , s )
74+ s = re .sub (r"_+" , "_" , s ).strip ("_" )
75+
76+ if not s :
77+ s = "t_unnamed"
78+
79+ # Avoid reserved internal prefix
80+ lowered = s .lower ()
81+ if any (lowered .startswith (p ) for p in _SQLITE_RESERVED_PREFIXES ):
82+ s = "t_" + s
83+
84+ # names in reasonable length
85+ if len (s ) > 120 :
86+ s = s [:120 ].rstrip ("_" )
87+
88+ return f"{ s } { ext_suffix } "
89+
90+ def file_to_table_name (filename : str ) -> str :
91+ """
92+ Small wrapper that ensures we only pass a basename to the canonical function.
93+ Use this everywhere you need to turn a filename into a table name.
94+ """
95+ import os
96+ return tableize_basename (os .path .basename (filename or "" ))
97+
98+ #==================================================#
99+
41100def ensure_uploads_log_schema ():
42101 """Create/upgrade uploads_log to the expected schema; ensure uniqueness."""
43102 with sqlite3 .connect (DB_NAME ) as conn :
@@ -141,12 +200,6 @@ def auto_import_uploads():
141200 ALLOWED_IMPORT_EXTS = {'csv' , 'npy' }
142201 imported = 0
143202
144- def tableize (name : str ) -> str :
145- # Stable, safe table name from filename only (not full path)
146- # e.g. "bandgap.csv" -> "bandgap_csv"
147- t = name .replace ('.' , '_' ).replace ('-' , '_' ).replace (' ' , '_' )
148- return re .sub (r'[^0-9a-zA-Z_]' , '_' , t )
149-
150203 with sqlite3 .connect (DB_NAME ) as conn :
151204 c = conn .cursor ()
152205 # Track file mtimes to avoid unnecessary re-imports
@@ -174,7 +227,7 @@ def tableize(name: str) -> str:
174227 filepath = os .path .join (root , filename )
175228 relpath = os .path .relpath (filepath , UPLOAD_FOLDER )
176229 mtime = os .path .getmtime (filepath )
177- table_name = tableize (filename )
230+ table_name = file_to_table_name (filename )
178231
179232 # Check etag (mtime)
180233 c .execute ("SELECT mtime FROM import_etag WHERE relpath=?" , (relpath ,))
@@ -775,6 +828,60 @@ def public_clips():
775828
776829#########################################################
777830
831+ @app .route ('/view_table/<path:filename>' , methods = ['GET' ])
832+ def view_table (filename ):
833+ """
834+ Used by the 'View' link in property_detail.html.
835+ Accepts 'property/tab/file.csv' and renders the dataset:
836+ - Prefer reading the imported SQLite table (created by auto_import_uploads()).
837+ - If missing, fall back to reading the file from uploads/ directly.
838+ """
839+ admin = bool (session .get ('admin' ))
840+ safe_name = os .path .basename (filename )
841+ table = tableize_basename (safe_name )
842+
843+ df = None
844+
845+ # Try SQLite first
846+ try :
847+ with sqlite3 .connect (DB_NAME ) as conn :
848+ df = pd .read_sql_query (f'SELECT * FROM "{ table } "' , conn )
849+ except Exception :
850+ # Fallback: read the source file
851+ path = os .path .join (app .config ['UPLOAD_FOLDER' ], filename )
852+ if not os .path .isfile (path ):
853+ abort (404 )
854+ ext = (safe_name .rsplit ('.' , 1 )[- 1 ] if '.' in safe_name else '' ).lower ()
855+ try :
856+ if ext == 'csv' :
857+ df = pd .read_csv (path )
858+ elif ext == 'npy' :
859+ arr = np .load (path , allow_pickle = True )
860+ if isinstance (arr , np .ndarray ):
861+ if arr .ndim == 2 :
862+ df = pd .DataFrame (arr )
863+ elif arr .ndim == 1 and hasattr (arr .dtype , 'names' ) and arr .dtype .names :
864+ df = pd .DataFrame (arr .tolist (), columns = list (arr .dtype .names ))
865+ else :
866+ df = pd .DataFrame (arr )
867+ else :
868+ return "Unsupported NPY structure." , 415
869+ else :
870+ return f"Unsupported dataset type: { ext } " , 415
871+ except Exception as e :
872+ return f"Failed to open dataset: { e } " , 500
873+
874+ return render_template (
875+ 'view_table.html' ,
876+ tables = [df .to_html (classes = 'data' , index = False )],
877+ titles = getattr (df , 'columns' , []),
878+ filename = safe_name ,
879+ imported_table = table ,
880+ admin = admin
881+ )
882+
883+ #########################################################
884+
778885@app .route ('/dataset/<table>' )
779886def public_view (table ):
780887 # Anyone can view any table
0 commit comments