Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion backend/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@ Flask==3.0.0
Flask-CORS==4.0.0
Flask-SQLAlchemy==3.1.1
python-dotenv==1.0.0
SQLAlchemy==2.0.23
SQLAlchemy==2.0.35
68 changes: 68 additions & 0 deletions backend/routes.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from flask import Blueprint, request, jsonify
from database import db
from sqlalchemy import or_
Copy link

Copilot AI Mar 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unused import: or_ is added but not referenced anywhere in this file. Please remove it to avoid lint issues and keep imports minimal.

Suggested change
from sqlalchemy import or_

Copilot uses AI. Check for mistakes.
from models import StudySpot
from datetime import datetime
import logging
Expand Down Expand Up @@ -59,6 +60,73 @@ def health_check():

TIME_PATTERN = re.compile(r'^([01]\d|2[0-3]):([0-5]\d)$')

@api_bp.route('/schema', methods=['GET'])
def get_schema():
"""Return database schema information."""
try:
Comment on lines +63 to +66
Copy link

Copilot AI Mar 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The /api/schema endpoint exposes internal database structure (table/column names, types, defaults) and is currently publicly accessible with permissive CORS. This is sensitive information that can aid attackers; consider removing this from the public API or gating it behind admin auth / an environment flag (e.g., only enabled in development).

Copilot uses AI. Check for mistakes.
# Get all tables
inspector = db.inspect(db.engine)
tables = inspector.get_table_names()
Copy link

Copilot AI Mar 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

db.inspect(db.engine) is not a valid Flask-SQLAlchemy API (the SQLAlchemy() instance in backend/database.py doesn’t expose inspect). This will raise an AttributeError at runtime; use SQLAlchemy’s inspect(db.engine) (import inspect from sqlalchemy) or sqlalchemy.inspect instead.

Copilot uses AI. Check for mistakes.

schema = {}
for table in tables:
columns = inspector.get_columns(table)
schema[table] = {
'columns': [
{
'name': col['name'],
'type': str(col['type']),
'nullable': col['nullable'],
'default': str(col['default']) if col['default'] else None
Copy link

Copilot AI Mar 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

'default': str(col['default']) if col['default'] else None will incorrectly treat falsy defaults (e.g., 0, False, empty string) as missing. Use an explicit is not None check so real defaults aren’t dropped.

Suggested change
'default': str(col['default']) if col['default'] else None
'default': str(col['default']) if col['default'] is not None else None

Copilot uses AI. Check for mistakes.
}
for col in columns
]
}

return jsonify(schema), 200
except Exception as e:
logger.error(f"Error getting schema: {str(e)}")
return jsonify({'error': 'Failed to get schema'}), 500

@api_bp.route('/study_spots/raw', methods=['GET'])
def get_raw_study_spots():
"""Return raw StudySpot data as-is from database."""
try:
spots = StudySpot.query.all()
return jsonify([s.to_dict() for s in spots]), 200
Comment on lines +91 to +96
Copy link

Copilot AI Mar 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

/study_spots/raw currently returns the same payload as the existing /study_spots endpoint (to_dict() for all rows), but adds another route that fetches the entire table without pagination. If this is meant for debugging/admin use, consider gating it (auth/env flag) or adding pagination/limits; otherwise it’s redundant and increases maintenance surface.

Copilot uses AI. Check for mistakes.
except Exception as e:
logger.error(f"Error fetching raw study spots: {str(e)}")
return jsonify({'error': 'Failed to fetch study spots'}), 500

@api_bp.route('/study_spots/distinct/<column>', methods=['GET'])
def get_distinct_values(column):
try:
allowed_columns = {'spot_type', 'noise_level', 'tags', 'access_hours'}
if column not in allowed_columns:
return jsonify({'error': 'Invalid column name'}), 400

if column == 'access_hours':
# For open now, return boolean options
return jsonify(['true', 'false']), 200
Copy link

Copilot AI Mar 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

/study_spots/distinct/access_hours returns ['true','false'], which doesn’t represent distinct access_hours values and also encodes booleans as strings. If the intent is an “open now” filter, consider using a dedicated route/column name (e.g., open_now) and return JSON booleans (true/false) to avoid confusing API consumers.

Suggested change
# For open now, return boolean options
return jsonify(['true', 'false']), 200
# For open now, return boolean options as JSON booleans
return jsonify([True, False]), 200

Copilot uses AI. Check for mistakes.
elif column in {'spot_type', 'tags'}:
# Handle JSON arrays
spots = StudySpot.query.all()
values = set()
Comment on lines +111 to +114
Copy link

Copilot AI Mar 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For spot_type/tags, this endpoint loads all StudySpot rows into memory to compute distinct values. That won’t scale as the table grows; prefer doing this in the database (e.g., via JSON table-valued functions / json_each/jsonb_array_elements depending on the DB) or add a reasonable cap and/or caching strategy.

Copilot uses AI. Check for mistakes.
for spot in spots:
col_data = getattr(spot, column)
if col_data:
for item in col_data:
if item:
values.add(str(item))
else:
# Handle regular string columns
values = db.session.query(getattr(StudySpot, column)).distinct().all()
values = [v[0] for v in values if v[0] is not None]

return jsonify(sorted(list(values))), 200
except Exception as e:
logger.error(f"Error fetching distinct values for column {column}: {str(e)}")
return jsonify({'error': 'Failed to fetch distinct values'}), 500

def _normalize_access_hours(value):
"""
Expand Down