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
+8 -1
View File
@@ -11,7 +11,10 @@ from typing import Dict, Any, List, Type, Optional
import inspect as py_inspect
from ..db import SessionLocal, Base
from ..models import User, Team, TeamMembership, QRCode, QRSet, TeamAchievement, Event
from ..models import (
User, Team, TeamMembership, QRCode, QRSet, TeamAchievement, Event,
OAuthAccount, TeamJoinRequest, EventAttendee, UserPoints
)
from ..templates_config import templates
router = APIRouter()
@@ -25,6 +28,10 @@ MODELS = {
'qr_set': (QRSet, "QR Sets"),
'team_achievement': (TeamAchievement, "Team Achievements"),
'event': (Event, "Events"),
'oauth_account': (OAuthAccount, "OAuth Accounts"),
'team_join_request': (TeamJoinRequest, "Team Join Requests"),
'event_attendee': (EventAttendee, "Event Attendees"),
'user_points': (UserPoints, "User Points"),
}
def get_db():
+24 -5
View File
@@ -17,15 +17,25 @@ from ..templates_config import templates
from ..security import verify_password, get_password_hash
from ..utils.mail import send_password_reset_email
router = APIRouter(prefix="/auth", tags=["Auth"])
router = APIRouter(tags=["Auth"])
@router.get("/login", response_class=HTMLResponse)
async def login_page(request: Request, error: Optional[str] = None, message: Optional[str] = None):
async def login_page(request: Request, error: Optional[str] = None, message: Optional[str] = None, db: Session = Depends(get_db)):
"""Login page route"""
# Check if user is already logged in
user = None
user_id = request.session.get("user_id")
if user_id:
user = db.query(User).get(user_id)
# If user is already logged in, show a message
if not message:
message = "You are already logged in."
return templates.TemplateResponse(
"auth/login.html",
{"request": request, "error": error, "message": message,
"show_oauth": True, "oauth_provider_name": "Authentik"}
"show_oauth": True, "oauth_provider_name": "Authentik",
"user": user} # Add user to context
)
@router.post("/login", response_class=HTMLResponse)
@@ -100,11 +110,20 @@ async def login_post(
return RedirectResponse(next_page, status_code=HTTP_303_SEE_OTHER)
@router.get("/register", response_class=HTMLResponse)
async def register_page(request: Request, error: Optional[str] = None):
async def register_page(request: Request, error: Optional[str] = None, db: Session = Depends(get_db)):
"""Registration page route"""
# Check if user is already logged in
user = None
user_id = request.session.get("user_id")
if user_id:
user = db.query(User).get(user_id)
# If no specific error is set, inform user they're already registered
if not error:
error = "You are already registered and logged in. You can logout first if you want to create a new account."
return templates.TemplateResponse(
"auth/register.html",
{"request": request, "error": error}
{"request": request, "error": error, "user": user} # Add user to context
)
@router.post("/register", response_class=HTMLResponse)
+32
View File
@@ -0,0 +1,32 @@
"""
Router for convenience redirects to simplify common URL patterns.
"""
from fastapi import APIRouter
from fastapi.responses import RedirectResponse
router = APIRouter(tags=["Convenience"])
@router.get("/scan")
async def scan_redirect():
"""Redirect /scan to /dashboard/scan"""
return RedirectResponse("/dashboard/scan", status_code=303)
@router.get("/login")
async def login_redirect():
"""Redirect /login to /auth/login"""
return RedirectResponse("/auth/login", status_code=303)
@router.get("/register")
async def register_redirect():
"""Redirect /register to /auth/register"""
return RedirectResponse("/auth/register", status_code=303)
@router.get("/profile")
async def profile_redirect():
"""Redirect /profile to /auth/profile"""
return RedirectResponse("/auth/profile", status_code=303)
@router.get("/logout")
async def logout_redirect():
"""Redirect /logout to /auth/logout"""
return RedirectResponse("/auth/logout", status_code=303)
+262 -42
View File
@@ -2,17 +2,22 @@
"""
Teams management for users and admins.
"""
from fastapi import APIRouter, Depends, Request, Form, HTTPException
from fastapi import APIRouter, Depends, Request, Form, HTTPException, status, BackgroundTasks
from fastapi.responses import HTMLResponse, RedirectResponse
from sqlalchemy.orm import Session
from sqlalchemy import func, desc, inspect
from sqlalchemy import func, desc, inspect, or_, and_
from datetime import datetime, timedelta
import random # For demo data
import secrets
from starlette.status import HTTP_303_SEE_OTHER
from starlette.middleware.sessions import SessionMiddleware
from ..db import SessionLocal
from ..models import Team, TeamMembership, User, QRCode, TeamAchievement, TeamMember
from ..db import SessionLocal, get_db
from ..models import Team, TeamMembership, User, QRCode, TeamAchievement, TeamMember, TeamJoinRequest
from ..schemas import TeamCreate
from ..templates_config import templates
from ..utils.auth import get_current_user, is_team_captain
from ..utils.mail import send_team_join_request_notification, send_join_request_response
router = APIRouter()
@@ -60,60 +65,260 @@ def list_teams(request: Request, db: Session = Depends(get_db)):
}
)
@router.post("/create")
def create_team(request: Request, name: str = Form(...), db: Session = Depends(get_db)):
# 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 the user
user = db.query(User).get(user_id)
if not user:
return RedirectResponse("/auth/login", status_code=303)
@router.post("/create", response_class=HTMLResponse)
async def create_team_post(
request: Request,
name: str = Form(...),
description: str = Form(""),
logo_url: str = Form(""),
is_open: bool = Form(False), # Added is_open field
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 to teams page with error message
# In a real app, you'd add error handling/flash messages
return RedirectResponse("/teams/?error=Team+name+already+exists", status_code=303)
return RedirectResponse("/teams/?error=Team+name+already+exists", status_code=HTTP_303_SEE_OTHER)
# Create team with the user as owner
new_team = Team(name=name, owner_id=user_id)
db.add(new_team)
# Create team
team = Team(
name=name,
description=description,
logo_url=logo_url,
is_open=is_open # Save is_open value
)
db.add(team)
db.commit()
db.refresh(new_team)
db.refresh(team)
# Make the user an admin of the team in TeamMembership
team_membership = TeamMembership(
user_id=user_id,
team_id=new_team.id,
user_id=current_user.id,
team_id=team.id,
is_admin=True # User becomes admin of the team
)
db.add(team_membership)
# Check if TeamMember model exists in the database
try:
# Use a safer approach to check if the model exists and is usable
if 'team_members' in inspect(db.bind).get_table_names():
# Create TeamMember relationship as well
team_member = TeamMember(
user_id=user_id,
team_id=new_team.id,
is_captain=True # User becomes captain in TeamMember model
)
db.add(team_member)
except Exception as e:
print(f"Could not create TeamMember record: {str(e)}")
# Continue even if this fails - TeamMembership is primary relationship
# Create TeamMember relationship as well
team_member = TeamMember(
user_id=current_user.id,
team_id=team.id,
is_captain=True # Use is_captain instead of role
)
db.add(team_member)
db.commit()
return RedirectResponse("/teams/", status_code=303)
return RedirectResponse("/teams/", status_code=HTTP_303_SEE_OTHER)
@router.post("/teams/{team_id}/edit", response_class=HTMLResponse)
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), # Added is_open field
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,
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 # Update is_open value
db.commit()
return RedirectResponse(f"/teams/{team_id}", status_code=HTTP_303_SEE_OTHER)
@router.get("/{team_id}/join", response_class=HTMLResponse)
async def join_team_page(
request: Request,
team_id: int,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""Page for joining a team"""
if not current_user:
return RedirectResponse(f"/auth/login?next=/teams/{team_id}/join", 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(TeamMember).filter(
TeamMember.team_id == team_id,
TeamMember.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}
)
@router.post("/{team_id}/join", response_class=HTMLResponse)
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(TeamMember).filter(
TeamMember.team_id == team_id,
TeamMember.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 = TeamMember(
team_id=team_id,
user_id=current_user.id,
is_captain=False # Use is_captain instead of role
)
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
captains = db.query(User).join(TeamMember).filter(
TeamMember.team_id == team_id,
TeamMember.is_captain == True # Use is_captain instead of role
).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
)
@router.post("/join/{team_id}")
def join_team(request: Request, team_id: int, db: Session = Depends(get_db)):
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:
@@ -136,14 +341,27 @@ def join_team(request: Request, team_id: int, db: Session = Depends(get_db)):
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
# Create membership for open teams
new_member = TeamMembership(
user_id=user_id,
team_id=team.id,
is_admin=False
)
db.add(new_member)
# Also create TeamMember record for consistency
team_member = TeamMember(
user_id=user_id,
team_id=team.id,
is_captain=False # Use is_captain instead of role
)
db.add(team_member)
db.commit()
# Redirect to the team detail page
@@ -347,6 +565,7 @@ def team_detail(request: Request, team_id: int, db: Session = Depends(get_db)):
# Safely get team attributes
is_public = getattr(team, 'is_public', False)
is_open = getattr(team, 'is_open', False) # Add this line to get is_open status
created_at = getattr(team, 'created_at', None)
# Calculate days since team was founded
@@ -382,6 +601,7 @@ def team_detail(request: Request, team_id: int, db: Session = Depends(get_db)):
"is_team_member": is_team_member,
"days_ago": days_ago,
"founded_date": founded_date_str,
"user": user # Add user to the context
"user": user, # Add user to the context
"is_open": is_open # Pass is_open to the template
}
)
+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
}
)