diff --git a/backend/app/models/persona.py b/backend/app/models/persona.py index d6761cc4..9d69248e 100755 --- a/backend/app/models/persona.py +++ b/backend/app/models/persona.py @@ -7,12 +7,39 @@ logger = logging.getLogger(__name__) # Allowed fields for create/update (mass assignment protection) PERSONA_ALLOWED_FIELDS = { + # Core demographics "name", "age", "gender", "occupation", "education", "location", - "techSavviness", "personality", "interests", "brandLoyalty", - "priceConsciousness", "environmentalConcern", "hasPurchasingPower", - "hasChildren", "thinkFeelDo", "description", "imageUrl", - "folder_ids", "llm_model", "traits", "background", "goals", - "communication_style", "values", "demographics", "marketplace", + "ethnicity", "socialGrade", + # Psychographic sliders + "techSavviness", "brandLoyalty", "priceConsciousness", + "environmentalConcern", "hasPurchasingPower", "hasChildren", + # Personality + "personality", "oceanTraits", + # Interests and rich text + "interests", "description", "imageUrl", + # AI-generated narrative fields + "goals", "frustrations", "motivations", "scenarios", "scenarioType", + "thinkFeelDo", + # Household / lifestyle + "householdComposition", "householdIncome", "livingSituation", + "mediaConsumption", "deviceUsage", "shoppingHabits", + "brandPreferences", "communicationPreferences", "additionalInformation", + # Extended attitudinal fields + "coreValues", "lifestyleChoices", "socialActivities", + "categoryKnowledge", "paymentMethods", "purchaseBehaviour", + "decisionInfluences", "painPoints", "journeyContext", + "keyTouchpoints", "selfDeterminationNeeds", "fears", "narrative", + # AI-synthesised summary fields (for persona cards) + "aiSynthesizedBio", "qualitativeAttributes", "topPersonalityTraits", + # Generation provenance + "audience_brief", "research_objective", + # Organisation + "folder_ids", "folderId", "folder_id", + # Cohorta-specific + "marketplace", + # Legacy / misc + "llm_model", "traits", "background", + "communication_style", "values", "demographics", } diff --git a/backend/app/routes/personas.py b/backend/app/routes/personas.py index b32bc2e3..8392255f 100755 --- a/backend/app/routes/personas.py +++ b/backend/app/routes/personas.py @@ -48,21 +48,80 @@ async def get_all_personas(): logger.error(f"Error in get_all_personas: {e}") return jsonify({"error": str(e)}), 500 + +@personas_bp.route('/library', methods=['GET']) +@jwt_required() +@active_required +async def get_library_personas(): + """Return all personas from all users, enriched with owner username.""" + try: + from app.db import get_db + db = await get_db() + + limit = min(int(request.args.get('limit', 500)), 1000) + + # Single query for all users to build the username map + user_map = {} + async for u in db.users.find({}, {'_id': 1, 'username': 1, 'email': 1}): + uid = str(u['_id']) + user_map[uid] = u.get('username') or u.get('email', 'Unknown') + + # Fetch personas — only fields needed for the library card + LIBRARY_FIELDS = { + 'name': 1, 'age': 1, 'gender': 1, 'occupation': 1, 'location': 1, + 'personality': 1, 'interests': 1, 'avatar': 1, + 'oceanTraits': 1, 'aiSynthesizedBio': 1, + 'qualitativeAttributes': 1, 'topPersonalityTraits': 1, + 'research_objective': 1, 'audience_brief': 1, 'marketplace': 1, + 'created_by': 1, 'created_at': 1, + } + personas = [] + async for p in db.personas.find({}, LIBRARY_FIELDS).sort('created_at', -1).limit(limit): + p['_id'] = str(p['_id']) + p['owner_name'] = user_map.get(p.get('created_by', ''), 'Unknown') + personas.append(p) + + return jsonify(make_serializable(personas)), 200 + except Exception as e: + logger.error(f"Error in get_library_personas: {e}") + return jsonify({"error": str(e)}), 500 + + +@personas_bp.route('//clone', methods=['POST']) +@jwt_required() +@active_required +@with_user_context +async def clone_persona(persona_id): + """Clone any persona into the current user's library.""" + user_id = get_jwt_identity() + try: + original = await Persona.find_by_id(persona_id) + if not original: + return jsonify({"error": "Persona not found"}), 404 + + clone = {k: v for k, v in original.items() + if k not in ('_id', 'created_at', 'created_by', 'folder_ids', 'updated_at')} + clone['folder_ids'] = [] + + new_id = await Persona.create(clone, user_id) + clone['_id'] = str(new_id) + return jsonify({"message": "Persona cloned", "persona": make_serializable(clone), "persona_id": str(new_id)}), 201 + except Exception as e: + logger.error(f"Error cloning persona {persona_id}: {e}") + return jsonify({"error": str(e)}), 500 + + @personas_bp.route('/', methods=['GET']) @jwt_required() async def get_persona(persona_id): try: - user_id = get_jwt_identity() persona = await Persona.find_by_id(persona_id) if not persona: return jsonify({"message": "Persona not found"}), 404 - if persona.get("created_by") and persona.get("created_by") != user_id: - return jsonify({"message": "Permission denied"}), 403 - - # Make persona serializable - serializable_persona = make_serializable(persona) - return jsonify(serializable_persona), 200 + # Any authenticated user may read any persona (needed for shared library). + # Write/delete endpoints retain ownership checks. + return jsonify(make_serializable(persona)), 200 except Exception as e: logger.error(f"Error in get_persona: {e}") return jsonify({"error": str(e)}), 500