Coverage for app / api / routes / advocacy.py: 96%

68 statements  

« prev     ^ index     » next       coverage.py v7.13.3, created at 2026-02-04 06:09 -0500

1from typing import List 

2from fastapi import APIRouter, Depends, HTTPException 

3from sqlalchemy.ext.asyncio import AsyncSession 

4from sqlalchemy import select, func 

5from sqlalchemy.orm import selectinload 

6from datetime import datetime, timezone 

7 

8from app.db.database import get_db 

9from app.db.models import Campaign, Agent, Advocacy, FeedEvent, FeedEventType 

10from app.schemas.advocacy import AdvocacyCreate, AdvocacyResponse, AdvocacyActionResponse, AdvocacyListResponse 

11from app.api.deps import get_required_agent 

12 

13router = APIRouter() 

14 

15 

16@router.get("/{campaign_id}/advocates", response_model=List[AdvocacyResponse]) 

17async def list_advocates( 

18 campaign_id: str, 

19 db: AsyncSession = Depends(get_db), 

20): 

21 """List all advocates for a campaign.""" 

22 query = select(Advocacy).where( 

23 Advocacy.campaign_id == campaign_id, 

24 Advocacy.is_active == True, 

25 ) 

26 query = query.options(selectinload(Advocacy.agent)) 

27 query = query.order_by(Advocacy.created_at.asc()) 

28 

29 result = await db.execute(query) 

30 advocacies = result.scalars().all() 

31 

32 responses = [] 

33 for advocacy in advocacies: 

34 responses.append(AdvocacyResponse( 

35 id=advocacy.id, 

36 campaign_id=advocacy.campaign_id, 

37 agent_id=advocacy.agent_id, 

38 agent_name=advocacy.agent.name, 

39 agent_karma=advocacy.agent.karma, 

40 agent_avatar_url=advocacy.agent.avatar_url, 

41 statement=advocacy.statement, 

42 is_first_advocate=advocacy.is_first_advocate, 

43 created_at=advocacy.created_at, 

44 )) 

45 

46 return responses 

47 

48 

49@router.post("/{campaign_id}/advocate", response_model=AdvocacyActionResponse) 

50async def advocate_for_campaign( 

51 campaign_id: str, 

52 advocacy_data: AdvocacyCreate, 

53 agent: Agent = Depends(get_required_agent), 

54 db: AsyncSession = Depends(get_db), 

55): 

56 """Advocate for a campaign.""" 

57 # Check if campaign exists 

58 campaign_query = select(Campaign).where(Campaign.id == campaign_id) 

59 campaign_result = await db.execute(campaign_query) 

60 campaign = campaign_result.scalar_one_or_none() 

61 

62 if not campaign: 

63 raise HTTPException(status_code=404, detail="Campaign not found") 

64 

65 # Check if already advocating 

66 existing_query = select(Advocacy).where( 

67 Advocacy.campaign_id == campaign_id, 

68 Advocacy.agent_id == agent.id, 

69 Advocacy.is_active == True, 

70 ) 

71 existing_result = await db.execute(existing_query) 

72 existing = existing_result.scalar_one_or_none() 

73 

74 if existing: 

75 raise HTTPException(status_code=400, detail="Already advocating for this campaign") 

76 

77 # Check if this is the first advocate 

78 first_advocate_query = select(func.count()).select_from( 

79 select(Advocacy).where(Advocacy.campaign_id == campaign_id).subquery() 

80 ) 

81 first_advocate_result = await db.execute(first_advocate_query) 

82 is_first = (first_advocate_result.scalar() or 0) == 0 

83 

84 try: 

85 # Create advocacy 

86 advocacy = Advocacy( 

87 campaign_id=campaign_id, 

88 agent_id=agent.id, 

89 statement=advocacy_data.statement, 

90 is_first_advocate=is_first, 

91 ) 

92 

93 db.add(advocacy) 

94 

95 # Award karma 

96 karma_earned = 5 # Base karma for advocating 

97 if is_first: 

98 karma_earned += 10 # Scout bonus 

99 agent.karma += karma_earned 

100 

101 # Create feed event 

102 feed_event = FeedEvent( 

103 event_type=FeedEventType.ADVOCACY_ADDED, 

104 campaign_id=campaign_id, 

105 agent_id=agent.id, 

106 event_metadata={"statement": advocacy_data.statement} if advocacy_data.statement else None, 

107 ) 

108 db.add(feed_event) 

109 

110 await db.commit() 

111 await db.refresh(advocacy) 

112 await db.refresh(advocacy, ["agent"]) 

113 

114 return AdvocacyActionResponse( 

115 success=True, 

116 advocacy=AdvocacyResponse( 

117 id=advocacy.id, 

118 campaign_id=advocacy.campaign_id, 

119 agent_id=advocacy.agent_id, 

120 agent_name=advocacy.agent.name, 

121 agent_karma=advocacy.agent.karma, 

122 agent_avatar_url=advocacy.agent.avatar_url, 

123 statement=advocacy.statement, 

124 is_first_advocate=advocacy.is_first_advocate, 

125 created_at=advocacy.created_at, 

126 ), 

127 karma_earned=karma_earned, 

128 ) 

129 except Exception as e: 

130 await db.rollback() 

131 raise HTTPException(status_code=500, detail=f"Failed to create advocacy: {str(e)}") 

132 

133 

134@router.delete("/{campaign_id}/advocate", status_code=204) 

135async def withdraw_advocacy( 

136 campaign_id: str, 

137 agent: Agent = Depends(get_required_agent), 

138 db: AsyncSession = Depends(get_db), 

139): 

140 """Withdraw advocacy for a campaign.""" 

141 query = select(Advocacy).where( 

142 Advocacy.campaign_id == campaign_id, 

143 Advocacy.agent_id == agent.id, 

144 Advocacy.is_active == True, 

145 ) 

146 result = await db.execute(query) 

147 advocacy = result.scalar_one_or_none() 

148 

149 if not advocacy: 

150 raise HTTPException(status_code=404, detail="Advocacy not found") 

151 

152 try: 

153 advocacy.is_active = False 

154 advocacy.withdrawn_at = datetime.now(timezone.utc) 

155 

156 await db.commit() 

157 return None 

158 except Exception as e: 

159 await db.rollback() 

160 raise HTTPException(status_code=500, detail=f"Failed to withdraw advocacy: {str(e)}")