From ab962725e6f92c3d3fda7e9b8674c767f63b061f Mon Sep 17 00:00:00 2001 From: ryan Date: Sun, 18 Jan 2026 13:01:11 -0500 Subject: [PATCH] Add filtering and sorting to question bank - Add search, category, and type filters to questions API - Add clickable sortable table headers with sort indicators - Add filter controls UI with clear filters button Co-Authored-By: Claude Opus 4.5 --- backend/routes/questions.py | 61 ++++++- .../questionbank/QuestionBankView.jsx | 164 ++++++++++++++++-- frontend/frontend/src/services/api.js | 2 +- 3 files changed, 206 insertions(+), 21 deletions(-) diff --git a/backend/routes/questions.py b/backend/routes/questions.py index df16d5f..3bc1481 100644 --- a/backend/routes/questions.py +++ b/backend/routes/questions.py @@ -7,8 +7,65 @@ bp = Blueprint('questions', __name__, url_prefix='/api/questions') @bp.route('', methods=['GET']) def list_questions(): - """Get all questions""" - questions = Question.query.order_by(Question.created_at.desc()).all() + """Get all questions with optional filtering and sorting + + Query parameters: + - search: Search in question content and answer (case-insensitive) + - category: Filter by category name (exact match, or 'none' for uncategorized) + - type: Filter by question type (text, image, youtube_audio) + - sort_by: Field to sort by (created_at, category, type, question_content, answer) + - sort_order: Sort direction (asc, desc) - default: desc + """ + query = Question.query + + # Search filter + search = request.args.get('search', '').strip() + if search: + search_pattern = f'%{search}%' + query = query.filter( + db.or_( + Question.question_content.ilike(search_pattern), + Question.answer.ilike(search_pattern) + ) + ) + + # Category filter + category = request.args.get('category', '').strip() + if category: + if category.lower() == 'none': + query = query.filter(Question.category.is_(None)) + else: + query = query.filter(Question.category == category) + + # Type filter + question_type = request.args.get('type', '').strip() + if question_type: + try: + query = query.filter(Question.type == QuestionType(question_type)) + except ValueError: + pass # Invalid type, ignore filter + + # Sorting + sort_by = request.args.get('sort_by', 'created_at').strip() + sort_order = request.args.get('sort_order', 'desc').strip().lower() + + # Map sort_by to column + sort_columns = { + 'created_at': Question.created_at, + 'category': Question.category, + 'type': Question.type, + 'question_content': Question.question_content, + 'answer': Question.answer, + } + + sort_column = sort_columns.get(sort_by, Question.created_at) + + if sort_order == 'asc': + query = query.order_by(sort_column.asc()) + else: + query = query.order_by(sort_column.desc()) + + questions = query.all() return jsonify([q.to_dict(include_answer=True) for q in questions]), 200 diff --git a/frontend/frontend/src/components/questionbank/QuestionBankView.jsx b/frontend/frontend/src/components/questionbank/QuestionBankView.jsx index bcc6475..0508820 100644 --- a/frontend/frontend/src/components/questionbank/QuestionBankView.jsx +++ b/frontend/frontend/src/components/questionbank/QuestionBankView.jsx @@ -29,14 +29,29 @@ export default function QuestionBankView() { const [downloadJob, setDownloadJob] = useState(null); const [downloadProgress, setDownloadProgress] = useState(0); + // Filter and sort state + const [searchTerm, setSearchTerm] = useState(""); + const [filterCategory, setFilterCategory] = useState(""); + const [filterType, setFilterType] = useState(""); + const [sortBy, setSortBy] = useState("created_at"); + const [sortOrder, setSortOrder] = useState("desc"); + useEffect(() => { loadQuestions(); loadCategories(); - }, []); + }, [searchTerm, filterCategory, filterType, sortBy, sortOrder]); const loadQuestions = async () => { try { - const response = await questionsAPI.getAll(); + const params = { + sort_by: sortBy, + sort_order: sortOrder, + }; + if (searchTerm) params.search = searchTerm; + if (filterCategory) params.category = filterCategory; + if (filterType) params.type = filterType; + + const response = await questionsAPI.getAll(params); setQuestions(response.data); } catch (error) { console.error("Error loading questions:", error); @@ -295,6 +310,28 @@ export default function QuestionBankView() { } }; + const handleSort = (column) => { + if (sortBy === column) { + setSortOrder(sortOrder === "asc" ? "desc" : "asc"); + } else { + setSortBy(column); + setSortOrder("asc"); + } + }; + + const SortIndicator = ({ column }) => { + if (sortBy !== column) return ; + return {sortOrder === "asc" ? "↑" : "↓"}; + }; + + const sortableHeaderStyle = { + padding: "0.75rem", + textAlign: "left", + borderBottom: "2px solid #ddd", + cursor: "pointer", + userSelect: "none", + }; + return ( <> @@ -581,6 +618,101 @@ export default function QuestionBankView() { )}
+ {/* Filter and Sort Controls */} +
+ {/* Search */} +
+ setSearchTerm(e.target.value)} + style={{ + width: "100%", + padding: "0.5rem", + border: "1px solid #ddd", + borderRadius: "4px", + }} + /> +
+ + {/* Category Filter */} +
+ +
+ + {/* Type Filter */} +
+ +
+ + {/* Clear Filters */} + {(searchTerm || filterCategory || filterType || sortBy !== "created_at" || sortOrder !== "desc") && ( + + )} +
+
handleSort("type")} style={{ - padding: "0.75rem", - textAlign: "left", - borderBottom: "2px solid #ddd", + ...sortableHeaderStyle, width: "80px", }} > Type + handleSort("category")} style={{ - padding: "0.75rem", - textAlign: "left", - borderBottom: "2px solid #ddd", + ...sortableHeaderStyle, width: "120px", }} > Category + handleSort("question_content")} + style={sortableHeaderStyle} > Question + handleSort("answer")} + style={sortableHeaderStyle} > Answer + api.get("/questions"), + getAll: (params = {}) => api.get("/questions", { params }), getOne: (id) => api.get(`/questions/${id}`), create: (data) => api.post("/questions", data), createWithImage: (formData) =>