Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
65 changes: 32 additions & 33 deletions backend/app/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,39 +129,38 @@ async def global_exception_handler(request, exc):

from app.routers import auth
from app.routers import users
from app.routers import projects
from app.routers import builders
from app.routers import builder_flare
from app.routers import messages
from app.routers import notifications
from app.routers import ai
from app.routers import followers
from app.routers import bookmarks
from app.routers import activities
from app.routers import notifications
from app.routers import conversations
from app.routers import repositories
from app.routers import organizations
from app.routers import applications
from app.routers import skills
from app.routers import users
# from app.routers import projects
# from app.routers import builders
# from app.routers import builder_flares
# from app.routers import messages
# from app.routers import notifications
# from app.routers import ai
# from app.routers import followers
# from app.routers import bookmarks
# from app.routers import activities
# from app.routers import notifications
# from app.routers import conversations
# from app.routers import repositories
# from app.routers import organizations
# from app.routers import applications
# from app.routers import skills
# from app.routers import users
from app.routers import search

app.include_router(auth.router, prefix="/api/auth", tags=["Authentication"])
app.include_router(users.router, prefix="/api/users", tags=["Users"])
app.include_router(projects.router, prefix="/api/projects", tags=["Projects"])
app.include_router(builder_flare.router, prefix="/api/flare", tags=["Builder's Flare"])
app.include_router(messages.router, prefix="/api/messages", tags=["Messages"])
app.include_router(
notifications.router, prefix="/api/notifications", tags=["Notifications"]
)
app.include_router(ai.router, prefix="/api/ai", tags=["AI"])
app.include_router(followers.router)
app.include_router(bookmarks.router)
app.include_router(activities.router)
app.include_router(notifications.router)
app.include_router(conversations.router)
app.include_router(repositories.router)
app.include_router(organizations.router)
app.include_router(applications.router)
app.include_router(skills.router)
app.include_router(users.router)
# app.include_router(projects.router, prefix="/api/projects", tags=["Projects"])
# app.include_router(builders.router, prefix="/api/builders", tags=["Builders"])
# app.include_router(builder_flares.router, prefix="/api/flare", tags=["Builder's Flare"])
# app.include_router(messages.router, prefix="/api/messages", tags=["Messages"])
# app.include_router(notifications.router, prefix="/api/notifications", tags=["Notifications"])
# app.include_router(ai.router, prefix="/api/ai", tags=["AI"])
# app.include_router(followers.router)
# app.include_router(bookmarks.router)
# app.include_router(activities.router)
# app.include_router(conversations.router)
# app.include_router(repositories.router)
# app.include_router(organizations.router)
# app.include_router(applications.router)
# app.include_router(skills.router)
app.include_router(search.router)
21 changes: 21 additions & 0 deletions backend/app/routers/search.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
from fastapi import APIRouter, Depends, Query
from sqlalchemy.orm import Session

from app.database.session import get_db
from app.schemas.search import SearchResult
from app.services.search_service import SearchService

router = APIRouter(
prefix="/search",
tags=["Search"],
)

@router.get(
"/",
response_model=SearchResult,
)
def global_search(
q: str = Query(..., min_length=1),
db: Session = Depends(get_db),
):
return SearchService.search(db, q)
52 changes: 52 additions & 0 deletions backend/app/schemas/project.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
from __future__ import annotations

import uuid
from datetime import datetime
from typing import Optional
from pydantic import BaseModel, ConfigDict, Field, HttpUrl
from app.models.project import ProjectStage, ProjectVisibility

class ProjectBase(BaseModel):
title: str = Field(..., min_length=3, max_length=200)
slug: str = Field(..., min_length=3, max_length=200)
tagline: Optional[str] = Field(default=None, max_length=255)
description: str
stage: ProjectStage = ProjectStage.IDEA
visibility: ProjectVisibility = ProjectVisibility.PUBLIC
tech_stack: Optional[str] = None
repository_url: Optional[HttpUrl] = None
website_url: Optional[HttpUrl] = None
demo_url: Optional[HttpUrl] = None
max_team_size: int = 5
hiring: bool = True

class ProjectCreate(ProjectBase):
pass

class ProjectUpdate(BaseModel):
title: Optional[str] = Field(None, min_length=3, max_length=200)
tagline: Optional[str] = Field(None, max_length=255)
description: Optional[str] = None
stage: Optional[ProjectStage] = None
visibility: Optional[ProjectVisibility] = None
tech_stack: Optional[str] = None
repository_url: Optional[HttpUrl] = None
website_url: Optional[HttpUrl] = None
demo_url: Optional[HttpUrl] = None
max_team_size: Optional[int] = None
hiring: Optional[bool] = None

class ProjectResponse(ProjectBase):
model_config = ConfigDict(from_attributes=True)
id: uuid.UUID
owner_id: uuid.UUID
team_size: int
logo_url: Optional[str] = None
banner_url: Optional[str] = None
stars: int
views: int
applications_count: int
is_featured: bool
is_archived: bool
created_at: datetime
updated_at: datetime
41 changes: 41 additions & 0 deletions backend/app/schemas/search.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import uuid
from datetime import datetime
from typing import Optional
from pydantic import BaseModel, ConfigDict
from app.schemas.user import UserResponse
from app.schemas.project import ProjectResponse

class SkillResponse(BaseModel):
model_config = ConfigDict(from_attributes=True)
id: uuid.UUID
name: str
slug: str
category: Optional[str] = None
description: Optional[str] = None
icon: Optional[str] = None

class BuilderFlareResponse(BaseModel):
model_config = ConfigDict(from_attributes=True)
id: uuid.UUID
project_id: uuid.UUID
created_by: uuid.UUID
title: str
description: str
role: str
location: Optional[str] = None
commitment: Optional[str] = None
experience_level: Optional[str] = None
openings: int = 1
applicants_count: int = 0
status: str
featured: bool
remote: bool
created_at: datetime
updated_at: datetime

class SearchResult(BaseModel):
users: list[UserResponse] = []
projects: list[ProjectResponse] = []
skills: list[SkillResponse] = []
flares: list[BuilderFlareResponse] = []

72 changes: 72 additions & 0 deletions backend/app/services/search_service.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
from sqlalchemy.orm import Session
from sqlalchemy import or_
from app.models.user import User
from app.models.project import Project
from app.models.skill import Skill
from app.models.builder_flare import BuilderFlare
from app.schemas.search import SearchResult

class SearchService:
@staticmethod
def search(db: Session, query: str) -> SearchResult:
if not query or len(query.strip()) == 0:
return SearchResult(users=[], projects=[], skills=[], flares=[])

q = query.strip()
search_pattern = f"%{q}%"

users = (
db.query(User)
.filter(
or_(
User.username.ilike(search_pattern),
User.first_name.ilike(search_pattern),
User.last_name.ilike(search_pattern),
User.headline.ilike(search_pattern)
)
)
.limit(10)
.all()
)

projects = (
db.query(Project)
.filter(
or_(
Project.title.ilike(search_pattern),
Project.description.ilike(search_pattern),
Project.tagline.ilike(search_pattern)
)
)
.limit(10)
.all()
)

skills = (
db.query(Skill)
.filter(
or_(
Skill.name.ilike(search_pattern),
Skill.category.ilike(search_pattern),
Skill.description.ilike(search_pattern)
)
)
.limit(10)
.all()
)

flares = (
db.query(BuilderFlare)
.filter(
or_(
BuilderFlare.title.ilike(search_pattern),
BuilderFlare.description.ilike(search_pattern),
BuilderFlare.role.ilike(search_pattern)
)
)
.limit(10)
.all()
)

return SearchResult(users=users, projects=projects, skills=skills, flares=flares)

1 change: 1 addition & 0 deletions backend/tests/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Test module initialization."""
8 changes: 8 additions & 0 deletions backend/tests/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import pytest
from fastapi.testclient import TestClient
from app.main import app

@pytest.fixture(scope="session")
def client():
with TestClient(app) as c:
yield c
101 changes: 101 additions & 0 deletions backend/tests/test_search.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import uuid
from app.database.session import SessionLocal
from app.models.user import User
from app.models.project import Project
from app.models.skill import Skill
from app.models.builder_flare import BuilderFlare

def test_search_empty_query(client):
response = client.get("/search/?q=xyz")
assert response.status_code == 200
data = response.json()
assert "users" in data
assert "projects" in data
assert "skills" in data
assert "flares" in data
assert type(data["users"]) is list
assert type(data["projects"]) is list
assert type(data["skills"]) is list
assert type(data["flares"]) is list

def test_search_with_results(client):
db = SessionLocal()
# Create test objects with unique names
unique_suffix = str(uuid.uuid4())[:8]

test_user = User(
first_name=f"TestFirst_{unique_suffix}",
last_name="TestLast",
username=f"testuser_{unique_suffix}",
email=f"testuser_{unique_suffix}@example.com",
password_hash="fakehash",
headline=f"Headline search_match_{unique_suffix}"
)

db.add(test_user)
db.commit()
db.refresh(test_user)

test_project = Project(
title=f"Project search_match_{unique_suffix}",
slug=f"project-slug-{unique_suffix}",
description="This is a test project description",
owner_id=test_user.id
)

test_skill = Skill(
name=f"Skill search_match_{unique_suffix}",
slug=f"skill-slug-{unique_suffix}",
category="TestCategory",
description="Test skill description"
)

db.add(test_project)
db.add(test_skill)
db.commit()
db.refresh(test_project)
db.refresh(test_skill)

test_flare = BuilderFlare(
title=f"Flare search_match_{unique_suffix}",
description="We need builders for this project",
role="Frontend Engineer",
project_id=test_project.id,
created_by=test_user.id
)

db.add(test_flare)
db.commit()
db.refresh(test_flare)

try:
# Perform query matching "search_match_<unique_suffix>"
response = client.get(f"/search/?q=search_match_{unique_suffix}")
assert response.status_code == 200
data = response.json()

# Check matching User
assert len(data["users"]) == 1
assert data["users"][0]["first_name"] == f"TestFirst_{unique_suffix}"

# Check matching Project
assert len(data["projects"]) == 1
assert data["projects"][0]["title"] == f"Project search_match_{unique_suffix}"

# Check matching Skill
assert len(data["skills"]) == 1
assert data["skills"][0]["name"] == f"Skill search_match_{unique_suffix}"

# Check matching Flare
assert len(data["flares"]) == 1
assert data["flares"][0]["title"] == f"Flare search_match_{unique_suffix}"

finally:
# Cleanup
db.delete(test_flare)
db.delete(test_skill)
db.delete(test_project)
db.delete(test_user)
db.commit()
db.close()

Loading