Implement team join request functionality with views and actions

- Added join_requests.html template for displaying pending join requests.
- Created join_team.html template for users to submit join requests.
- Implemented request_processed.html template to show the result of join request processing.
- Developed authentication utilities for user session management.
- Introduced convenience redirects for common URL patterns.
- Established team management routes and actions for creating, editing, and joining teams.
- Added functionality for approving and denying join requests with email notifications.
- Enhanced team views to include user permissions and team member details.
- Implemented utility functions for team-related operations such as calculating total points and team rank.
This commit is contained in:
Christian Krakau-Louis
2025-04-14 17:00:57 +02:00
parent 5cf3f944b1
commit 7323c12168
26 changed files with 2138 additions and 319 deletions
+6
View File
@@ -0,0 +1,6 @@
"""
Teams module for LeagueLedger - handles team management functionality.
"""
from .routes import router
__all__ = ['router']
+495
View File
@@ -0,0 +1,495 @@
"""Team-related actions for team management"""
from fastapi import Request, Depends, Form, HTTPException, BackgroundTasks
from fastapi.responses import RedirectResponse
from sqlalchemy.orm import Session
import secrets
from starlette.status import HTTP_303_SEE_OTHER
from fastapi.templating import Jinja2Templates
from ...models import Team, TeamMembership, User, TeamJoinRequest
from ...utils.auth import get_current_user
from ...utils.mail import send_team_join_request_notification, send_join_request_response
from ...templates_config import templates
from .routes import get_db
from . import utils
async def create_team_post(
request: Request,
name: str = Form(...),
description: str = Form(""),
logo_url: str = Form(""),
is_open: bool = Form(False),
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""Handle team creation form submission"""
if not current_user:
return RedirectResponse("/auth/login", status_code=HTTP_303_SEE_OTHER)
# Check if team name already exists
existing_team = db.query(Team).filter(Team.name == name).first()
if existing_team:
return RedirectResponse("/teams/?error=Team+name+already+exists", status_code=HTTP_303_SEE_OTHER)
# Create team with only the fields that exist in the model
team_data = {
"name": name,
"description": description,
"is_open": is_open,
"owner_id": current_user.id
}
# Only add logo_url if it exists in the Team model
from sqlalchemy import inspect
team_columns = [c.key for c in inspect(Team).columns]
if "logo_url" in team_columns:
team_data["logo_url"] = logo_url
team = Team(**team_data)
db.add(team)
db.commit()
db.refresh(team)
# Make the user an admin and captain of the team
team_membership = TeamMembership(
user_id=current_user.id,
team_id=team.id,
is_admin=True,
is_captain=True
)
db.add(team_membership)
db.commit()
return RedirectResponse("/teams/", status_code=HTTP_303_SEE_OTHER)
async def edit_team_post(
request: Request,
team_id: int,
name: str = Form(...),
description: str = Form(""),
logo_url: str = Form(""),
is_open: bool = Form(False),
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""Handle team edit form submission"""
if not current_user:
return RedirectResponse("/auth/login", status_code=HTTP_303_SEE_OTHER)
team = db.query(Team).filter(Team.id == team_id).first()
if not team:
raise HTTPException(status_code=404, detail="Team not found")
# Check if user is admin
membership = db.query(TeamMembership).filter_by(
team_id=team.id,
user_id=current_user.id,
is_admin=True
).first()
if not membership:
raise HTTPException(status_code=403, detail="You don't have permission to update this team")
# Update team details
team.name = name
team.description = description
team.logo_url = logo_url
team.is_open = is_open
db.commit()
return RedirectResponse(f"/teams/{team_id}", status_code=HTTP_303_SEE_OTHER)
async def join_team_request(
request: Request,
team_id: int,
background_tasks: BackgroundTasks,
message: str = Form(""),
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""Process join team request"""
if not current_user:
return RedirectResponse("/auth/login", status_code=HTTP_303_SEE_OTHER)
# Check if team exists
team = db.query(Team).filter(Team.id == team_id, Team.is_active == True).first()
if not team:
return templates.TemplateResponse(
"error.html",
{"request": request, "error": "Team not found"}
)
# Check if user is already a member
existing_membership = db.query(TeamMembership).filter(
TeamMembership.team_id == team_id,
TeamMembership.user_id == current_user.id
).first()
if existing_membership:
return RedirectResponse(f"/teams/{team_id}", status_code=HTTP_303_SEE_OTHER)
# Open team - directly add the user
if team.is_open:
# Add user to team
new_member = TeamMembership(
team_id=team_id,
user_id=current_user.id,
is_admin=False,
is_captain=False
)
db.add(new_member)
db.commit()
return RedirectResponse(
f"/teams/{team_id}?message=You+have+joined+the+team+successfully",
status_code=HTTP_303_SEE_OTHER
)
# Closed team - create join request
# Check for existing pending request
existing_request = db.query(TeamJoinRequest).filter(
TeamJoinRequest.team_id == team_id,
TeamJoinRequest.user_id == current_user.id,
TeamJoinRequest.status == "pending"
).first()
if existing_request:
return RedirectResponse(
f"/teams/{team_id}?message=Your+join+request+is+pending+approval",
status_code=HTTP_303_SEE_OTHER
)
# Create request token
request_token = secrets.token_urlsafe(32)
# Create join request
join_request = TeamJoinRequest(
team_id=team_id,
user_id=current_user.id,
message=message,
request_token=request_token
)
db.add(join_request)
db.commit()
# Get team captains to notify - Updated to use TeamMembership instead of TeamMember
captains = db.query(User).join(TeamMembership).filter(
TeamMembership.team_id == team_id,
TeamMembership.is_captain == True
).all()
if not captains:
print("No captains found for the team. Unable to send notifications.")
else:
# Send email notifications to all captains
for captain in captains:
try:
await send_team_join_request_notification(
captain_email=captain.email,
captain_name=captain.username,
requester_name=current_user.username,
team_name=team.name,
message=message,
approval_token=request_token,
background_tasks=background_tasks
)
# Log success
print(f"Team join request notification sent to {captain.email}")
except Exception as e:
# Log the error but continue
print(f"Failed to send notification to {captain.email}: {str(e)}")
return RedirectResponse(
f"/teams/{team_id}?message=Your+join+request+has+been+submitted+for+approval",
status_code=HTTP_303_SEE_OTHER
)
def direct_join_team(request: Request, team_id: int, db: Session = Depends(get_db)):
"""Handle direct team join requests"""
# Check if user is logged in
user_id = request.session.get("user_id")
if not user_id:
return RedirectResponse("/auth/login?next=/teams", status_code=303)
# Get user
user = db.query(User).get(user_id)
if not user:
return RedirectResponse("/auth/login", status_code=303)
# Find the team
team = db.query(Team).filter_by(id=team_id).first()
if not team:
return RedirectResponse("/teams/?error=Team+not+found", status_code=303)
# Check if membership exists
existing = db.query(TeamMembership)\
.filter_by(user_id=user_id, team_id=team.id)\
.first()
if existing:
return RedirectResponse("/teams/?error=You+are+already+a+member+of+this+team", status_code=303)
# Check if team is closed (not open)
if not team.is_open:
# For closed teams, redirect to the join request page
return RedirectResponse(f"/teams/{team_id}/join", status_code=303)
# Create membership for open teams
new_member = TeamMembership(
user_id=user_id,
team_id=team.id,
is_admin=False,
is_captain=False
)
db.add(new_member)
db.commit()
# Redirect to the team detail page
return RedirectResponse(f"/teams/{team_id}", status_code=303)
def leave_team(request: Request, team_id: int, db: Session = Depends(get_db)):
"""Allow a user to leave a team"""
# Check if user is logged in
user_id = request.session.get("user_id")
if not user_id:
return RedirectResponse("/auth/login?next=/teams", status_code=303)
# Get team
team = db.query(Team).filter_by(id=team_id).first()
if not team:
raise HTTPException(status_code=404, detail="Team not found")
# Can't leave if you're the owner
if team.owner_id == user_id:
return RedirectResponse(f"/teams/{team_id}?error=Team+owner+cannot+leave", status_code=303)
# Find membership
membership = db.query(TeamMembership)\
.filter(TeamMembership.user_id == user_id, TeamMembership.team_id == team_id)\
.first()
if not membership:
return RedirectResponse("/teams/?error=You+are+not+a+member+of+this+team", status_code=303)
# Delete the team membership
db.delete(membership)
db.commit()
return RedirectResponse("/teams/?message=Successfully+left+the+team", status_code=303)
def update_team(
request: Request,
team_id: int,
team_name: str = Form(...),
is_public: bool = Form(False),
db: Session = Depends(get_db)
):
"""Update team details."""
team = db.query(Team).filter_by(id=team_id).first()
if not team:
raise HTTPException(status_code=404, detail="Team not found")
# Check if user is admin
membership = db.query(TeamMembership).filter_by(
team_id=team.id,
user_id=request.session.get("user_id"),
is_admin=True
).first()
if not membership:
raise HTTPException(status_code=403, detail="You don't have permission to update this team")
# Update team details
team.name = team_name
team.is_public = is_public
db.commit()
return RedirectResponse(f"/teams/{team_id}", status_code=303)
async def approve_join_request(request: Request, token: str, db: Session = Depends(get_db)):
"""Approve a team join request using the provided token"""
# Find the join request by token
join_request = db.query(TeamJoinRequest).filter_by(request_token=token).first()
if not join_request:
return templates.TemplateResponse(
"error.html",
{"request": request, "error": "Invalid or expired join request"}
)
if join_request.status != "pending":
return templates.TemplateResponse(
"error.html",
{"request": request, "error": "This request has already been processed"}
)
# Get the team
team = db.query(Team).filter_by(id=join_request.team_id).first()
if not team:
return templates.TemplateResponse(
"error.html",
{"request": request, "error": "Team not found"}
)
# Check if the user has permission to approve requests
# Get user from session
user_id = request.session.get("user_id")
if not user_id:
return RedirectResponse("/auth/login", status_code=303)
user = db.query(User).get(user_id)
if not user:
return RedirectResponse("/auth/login", status_code=303)
# Check if user is admin, owner or captain
is_user_admin, is_user_owner, is_captain = utils.check_user_permissions(db, user, join_request.team_id, team)
if not (is_user_admin or is_user_owner or is_captain):
return templates.TemplateResponse(
"error.html",
{"request": request, "error": "You don't have permission to approve join requests"}
)
# Get the user who requested to join
requester = db.query(User).filter_by(id=join_request.user_id).first()
if not requester:
return templates.TemplateResponse(
"error.html",
{"request": request, "error": "Requesting user not found"}
)
# Check if user is already a member
existing_membership = db.query(TeamMembership).filter(
TeamMembership.team_id == join_request.team_id,
TeamMembership.user_id == join_request.user_id
).first()
if existing_membership:
# Update request status
join_request.status = "approved"
db.commit()
return templates.TemplateResponse(
"teams/request_processed.html",
{
"request": request,
"message": f"{requester.username} is already a member of {team.name}",
"team_id": team.id
}
)
# Create team membership
new_member = TeamMembership(
team_id=join_request.team_id,
user_id=join_request.user_id,
is_admin=False,
is_captain=False
)
db.add(new_member)
# Update request status
join_request.status = "approved"
db.commit()
# Send notification to the user (if email sending is available)
try:
background_tasks = BackgroundTasks()
await send_join_request_response(
user_email=requester.email,
user_name=requester.username,
team_name=team.name,
approved=True,
background_tasks=background_tasks
)
except Exception as e:
print(f"Failed to send approval notification: {str(e)}")
return templates.TemplateResponse(
"teams/request_processed.html",
{
"request": request,
"message": f"Successfully approved {requester.username}'s request to join {team.name}",
"team_id": team.id
}
)
async def deny_join_request(request: Request, token: str, db: Session = Depends(get_db)):
"""Deny a team join request using the provided token"""
# Find the join request by token
join_request = db.query(TeamJoinRequest).filter_by(request_token=token).first()
if not join_request:
return templates.TemplateResponse(
"error.html",
{"request": request, "error": "Invalid or expired join request"}
)
if join_request.status != "pending":
return templates.TemplateResponse(
"error.html",
{"request": request, "error": "This request has already been processed"}
)
# Get the team
team = db.query(Team).filter_by(id=join_request.team_id).first()
if not team:
return templates.TemplateResponse(
"error.html",
{"request": request, "error": "Team not found"}
)
# Check if the user has permission to deny requests
# Get user from session
user_id = request.session.get("user_id")
if not user_id:
return RedirectResponse("/auth/login", status_code=303)
user = db.query(User).get(user_id)
if not user:
return RedirectResponse("/auth/login", status_code=303)
# Check if user is admin, owner or captain
is_user_admin, is_user_owner, is_captain = utils.check_user_permissions(db, user, join_request.team_id, team)
if not (is_user_admin or is_user_owner or is_captain):
return templates.TemplateResponse(
"error.html",
{"request": request, "error": "You don't have permission to deny join requests"}
)
# Get the user who requested to join
requester = db.query(User).filter_by(id=join_request.user_id).first()
if not requester:
return templates.TemplateResponse(
"error.html",
{"request": request, "error": "Requesting user not found"}
)
# Update request status
join_request.status = "denied"
db.commit()
# Send notification to the user (if email sending is available)
try:
background_tasks = BackgroundTasks()
await send_join_request_response(
user_email=requester.email,
user_name=requester.username,
team_name=team.name,
approved=False,
background_tasks=background_tasks
)
except Exception as e:
print(f"Failed to send denial notification: {str(e)}")
return templates.TemplateResponse(
"teams/request_processed.html",
{
"request": request,
"message": f"Successfully denied {requester.username}'s request to join {team.name}",
"team_id": team.id
}
)
+57
View File
@@ -0,0 +1,57 @@
from fastapi import APIRouter, Depends, Request
from fastapi.responses import HTMLResponse
from sqlalchemy.orm import Session
from ...db import SessionLocal, get_db
from . import views, actions
from ...auth.utils import get_current_user_from_session
router = APIRouter()
def get_db():
db = SessionLocal()
try:
yield db
finally:
db.close()
# Main routes for teams
@router.get("/", response_class=HTMLResponse)
def list_teams(request: Request, db: Session = Depends(get_db)):
"""List all available teams"""
# No need to explicitly get current user - it's in request.state.user
# from middleware and will be passed to the template
return views.list_teams_view(request, db)
@router.get("/{team_id}", response_class=HTMLResponse)
def team_detail(request: Request, team_id: int, db: Session = Depends(get_db)):
"""Show details for a specific team."""
# User is already available in request.state.user
return views.team_detail_view(request, team_id, db)
@router.get("/{team_id}/join", response_class=HTMLResponse)
async def join_team_page(request: Request, team_id: int, db: Session = Depends(get_db)):
"""Page for joining a team"""
# User is already available in request.state.user
return await views.join_team_page_view(request, team_id, db)
# Action routes
router.add_api_route("/create", actions.create_team_post, methods=["POST"], response_class=HTMLResponse)
router.add_api_route("/{team_id}/edit", actions.edit_team_post, methods=["POST"], response_class=HTMLResponse)
router.add_api_route("/{team_id}/join", actions.join_team_request, methods=["POST"], response_class=HTMLResponse)
router.add_api_route("/join/{team_id}", actions.direct_join_team, methods=["POST"])
router.add_api_route("/{team_id}/leave", actions.leave_team, methods=["POST"])
router.add_api_route("/{team_id}/update", actions.update_team, methods=["POST"])
# Add these new routes for handling join requests
@router.get("/approve-request/{token}", response_class=HTMLResponse)
async def approve_join_request(request: Request, token: str, db: Session = Depends(get_db)):
"""Approve a team join request using the provided token"""
# User is already available in request.state.user
return await actions.approve_join_request(request, token, db)
@router.get("/deny-request/{token}", response_class=HTMLResponse)
async def deny_join_request(request: Request, token: str, db: Session = Depends(get_db)):
"""Deny a team join request using the provided token"""
# User is already available in request.state.user
return await actions.deny_join_request(request, token, db)
+166
View File
@@ -0,0 +1,166 @@
"""Helper functions for team views and actions"""
from sqlalchemy.orm import Session
from sqlalchemy import func, inspect
from datetime import datetime, timedelta
import random
from ...models import Team, TeamMembership, User, QRCode
def get_team_members_with_details(db: Session, team_id: int):
"""Get team members with additional details"""
memberships = db.query(TeamMembership).filter_by(team_id=team_id).all()
team_members = []
for membership in memberships:
member = db.query(User).filter_by(id=membership.user_id).first()
if member:
# Use joined_at if available, otherwise use placeholder
joined_date = membership.joined_at or datetime.now() - timedelta(days=random.randint(30, 180))
if isinstance(joined_date, datetime):
month_name = joined_date.strftime("%b")
year = joined_date.strftime("%Y")
else:
month_name = "Apr"
year = "2023"
team_members.append({
"user": member,
"is_admin": membership.is_admin,
"is_captain": membership.is_captain,
"joined": f"{month_name} {year}"
})
return team_members
def check_user_permissions(db: Session, user, team_id: int, team):
"""Check if user is admin or owner of the team"""
is_user_admin = False
is_user_owner = False
is_captain = False
if user:
# Check admin status
membership = db.query(TeamMembership).filter_by(
team_id=team_id,
user_id=user.id,
is_admin=True
).first()
is_user_admin = membership is not None
# Check captain status
captain_membership = db.query(TeamMembership).filter_by(
team_id=team_id,
user_id=user.id,
is_captain=True
).first()
is_captain = captain_membership is not None
# Check owner status
is_user_owner = hasattr(team, 'owner_id') and team.owner_id == user.id
if is_user_owner:
is_user_admin = True # Owner has admin privileges
return is_user_admin, is_user_owner, is_captain
def get_team_total_points(db: Session, team_id: int):
"""Get total points for a team"""
total_points = db.query(func.sum(QRCode.points)).filter(
QRCode.redeemed_at_team == team_id
).scalar() or 0
return total_points
def calculate_team_rank(db: Session, team_id: int):
"""Calculate team rank based on points"""
try:
# First, get the aggregated points for all teams
team_points = db.query(
QRCode.redeemed_at_team,
func.sum(QRCode.points).label('total')
).filter(
QRCode.redeemed_at_team != None
).group_by(QRCode.redeemed_at_team).all()
# Sort them by points (descending)
sorted_teams = sorted(team_points, key=lambda x: x.total or 0, reverse=True)
# Find our team's position
team_rank = 1
for idx, team_data in enumerate(sorted_teams):
if team_data.redeemed_at_team == team_id:
team_rank = idx + 1
break
return team_rank
except Exception as e:
print(f"Error calculating team rank: {e}")
return 1 # Default to 1st place on error
def get_team_points_history(db: Session, team_id: int):
"""Get points history for a team"""
# Default values
points_this_month = 65
point_change = 15
point_change_positive = True
# Try to get actual data if available
try:
now = datetime.now()
first_day_of_month = datetime(now.year, now.month, 1)
# Use raw SQL to check if column exists and get points
has_redeemed_at = False
inspector = inspect(db.bind)
if 'redeemed_at' in [col['name'] for col in inspector.get_columns('qr_codes')]:
has_redeemed_at = True
if has_redeemed_at:
points_this_month = db.query(func.sum(QRCode.points)).filter(
QRCode.redeemed_at_team == team_id,
QRCode.redeemed_at >= first_day_of_month
).scalar() or points_this_month
except Exception as e:
print(f"Error calculating monthly points: {e}")
return points_this_month, point_change, point_change_positive
def get_team_activities():
"""Get team activities (currently returns mock data)"""
return [
{
"type": "points",
"points": 15,
"event": "Music Trivia Night",
"date": "September 12, 2023"
},
{
"type": "join",
"user": "Robert Brown",
"date": "July 28, 2023"
},
{
"type": "achievement",
"achievement": "1st place",
"event": "History Night",
"date": "July 15, 2023"
},
{
"type": "points",
"points": 20,
"event": "Movie Trivia Night",
"date": "July 1, 2023"
}
]
def get_team_age(team):
"""Calculate team age"""
created_at = getattr(team, 'created_at', None)
if created_at and isinstance(created_at, datetime):
days_ago = (datetime.now() - created_at).days
founded_date_str = created_at.strftime("%B %d, %Y")
else:
days_ago = 164 # Default fallback
founded_date_str = "March 22, 2023" # Default fallback
return days_ago, founded_date_str
+178
View File
@@ -0,0 +1,178 @@
"""Team-related view functions for rendering templates"""
from fastapi import Request, HTTPException
from fastapi.responses import RedirectResponse
from sqlalchemy.orm import Session
from sqlalchemy import func
from datetime import datetime, timedelta
import random
from sqlalchemy import inspect
from ...models import Team, TeamMembership, User, QRCode, TeamJoinRequest
from ...templates_config import templates
from ...utils.auth import get_current_user
from . import utils
def list_teams_view(request: Request, db: Session):
"""Render the teams list view"""
teams = db.query(Team).all()
# Get the user's teams to highlight teams they're already in
user_team_ids = []
# Get user from session for navbar
user = None
user_id = request.session.get("user_id")
if user_id:
user = db.query(User).get(user_id)
# Get teams that user is a member of
memberships = db.query(TeamMembership).filter(TeamMembership.user_id == user_id).all()
user_team_ids = [membership.team_id for membership in memberships]
# Get error message if present
error = request.query_params.get("error")
return templates.TemplateResponse(
"teams.html",
{
"request": request,
"teams": teams,
"user_team_ids": user_team_ids,
"user": user,
"error": error,
"brand_colors": {
"irish_green": "#006837",
"golden_ale": "#FFB400",
"cream_white": "#F5F0E1",
"black_stout": "#1A1A1A",
"guinness_red": "#B22222"
}
}
)
async def join_team_page_view(request: Request, team_id: int, db: Session):
"""Render the join team page"""
user_id = request.session.get("user_id")
current_user = db.query(User).get(user_id) if user_id else None
if not current_user:
return RedirectResponse(f"/auth/login?next=/teams/{team_id}/join", status_code=303)
# Check if team exists
team = db.query(Team).filter(Team.id == team_id, Team.is_active == True).first()
if not team:
return templates.TemplateResponse(
"error.html",
{"request": request, "error": "Team not found"}
)
# Check if user is already a member
existing_membership = db.query(TeamMembership).filter(
TeamMembership.team_id == team_id,
TeamMembership.user_id == current_user.id
).first()
if existing_membership:
return templates.TemplateResponse(
"teams/join_team.html",
{
"request": request,
"team": team,
"error": "You are already a member of this team"
}
)
# Check if there's a pending join request
pending_request = db.query(TeamJoinRequest).filter(
TeamJoinRequest.team_id == team_id,
TeamJoinRequest.user_id == current_user.id,
TeamJoinRequest.status == "pending"
).first()
if pending_request:
return templates.TemplateResponse(
"teams/join_team.html",
{
"request": request,
"team": team,
"error": "You already have a pending join request for this team"
}
)
return templates.TemplateResponse(
"teams/join_team.html",
{"request": request, "team": team, "is_open": team.is_open}
)
def team_detail_view(request: Request, team_id: int, db: Session):
"""Render the team detail page"""
# Get team
team = db.query(Team).filter_by(id=team_id).first()
if not team:
raise HTTPException(status_code=404, detail="Team not found")
# Get user from session for navbar
user = None
user_id = request.session.get("user_id")
is_team_member = False
if user_id:
user = db.query(User).get(user_id)
# Check if user is a team member
team_membership = db.query(TeamMembership)\
.filter(TeamMembership.user_id == user_id, TeamMembership.team_id == team_id)\
.first()
is_team_member = team_membership is not None
# Get team members with user info
team_members = utils.get_team_members_with_details(db, team_id)
# Check if user is admin or owner
is_user_admin, is_user_owner, is_captain = utils.check_user_permissions(db, user, team_id, team)
# Get team statistics
total_points = utils.get_team_total_points(db, team_id)
team_rank = utils.calculate_team_rank(db, team_id)
points_this_month, point_change, point_change_positive = utils.get_team_points_history(db, team_id)
# Activities - simple mock data for now
activities = utils.get_team_activities()
# Get team metadata
days_ago, founded_date_str = utils.get_team_age(team)
# Performance metrics
performance = {
"last_quiz": "25 points (2nd place)",
"average": "18.7 points",
"best_streak": "3 wins in a row"
}
# Safely get team attributes
is_public = getattr(team, 'is_public', False)
is_open = getattr(team, 'is_open', False)
return templates.TemplateResponse(
"team_detail.html",
{
"request": request,
"team": team,
"team_members": team_members,
"team_rank": team_rank,
"total_points": total_points,
"points_this_month": points_this_month,
"point_change": point_change,
"point_change_positive": point_change_positive,
"activities": activities,
"performance": performance,
"is_user_admin": is_user_admin,
"is_user_owner": is_user_owner,
"is_team_member": is_team_member,
"is_captain": is_captain,
"days_ago": days_ago,
"founded_date": founded_date_str,
"user": user,
"is_open": is_open
}
)