"""Public portal API — no auth required.""" from flask import Blueprint, jsonify, request from models import Category, Option, Resource, SideLink, SiteSetting bp = Blueprint("portal", __name__, url_prefix="/api/portal") @bp.route("/bootstrap") def bootstrap(): """Everything the portal page needs in one request.""" settings = {s.key: s.value for s in SiteSetting.query.all()} categories = ( Category.query.filter_by(is_active=True) .order_by(Category.sort_order, Category.id) .all() ) links = ( SideLink.query.filter_by(is_active=True) .order_by(SideLink.sort_order, SideLink.id) .all() ) return jsonify({ "settings": settings, "categories": [c.to_dict() for c in categories], "links": [l.to_dict() for l in links], }) @bp.route("/categories//resources") def category_resources(cid): """Resources for a category, faceted by selected option ids.""" category = Category.query.get_or_404(cid) selected = [] raw = request.args.get("option_ids", "") keyword = request.args.get("keyword", "").strip() if raw.strip(): try: selected = [int(x) for x in raw.split(",") if x.strip()] except ValueError: selected = [] query = Resource.query.filter_by(category_id=cid, is_active=True) if keyword: query = query.filter( Resource.title.like(f"%{keyword}%") | Resource.description.like(f"%{keyword}%") | Resource.content.like(f"%{keyword}%") ) resources = query.order_by(Resource.sort_order, Resource.id).all() if selected: chosen = Option.query.filter(Option.id.in_(selected)).all() by_row = {} for o in chosen: by_row.setdefault(o.row_id, []).append(o.id) def matches(res): tagged_ids = {o.id for o in res.options} tagged_row_ids = {o.row_id for o in res.options} for row_id, opt_ids in by_row.items(): if row_id not in tagged_row_ids: continue # untagged in this row -> applies to all if not tagged_ids.intersection(opt_ids): return False return True resources = [r for r in resources if matches(r)] return jsonify({ "category": category.to_dict(), "resources": [r.to_dict() for r in resources], "selected_option_ids": selected, }) @bp.route("/search") def global_search(): """Global search across all resources.""" keyword = request.args.get("keyword", "").strip() if not keyword or len(keyword) < 2: return jsonify({"results": [], "keyword": keyword}) results = Resource.query.filter_by(is_active=True).filter( Resource.title.like(f"%{keyword}%") | Resource.description.like(f"%{keyword}%") | Resource.content.like(f"%{keyword}%") ).order_by(Resource.category_id, Resource.sort_order).limit(50).all() return jsonify({ "keyword": keyword, "count": len(results), "results": [r.to_dict() for r in results] })