"""Phase 5: Team Collaboration routes."""
from functools import wraps
from flask import Blueprint, current_app, flash, g, redirect, render_template, request, url_for
from app.auth_utils import login_required
from app.models import (
accept_invite,
add_site,
change_member_role,
create_team,
delete_team,
get_team,
get_team_members,
get_user,
get_user_sites,
get_user_teams,
invite_to_team,
remove_team_member,
)
team_bp = Blueprint("teams", __name__, url_prefix="/teams")
def team_member_required(f):
"""Require user to be an active member of a team (team_id from URL)."""
@wraps(f)
def decorated_function(*args, **kwargs):
from app.models import get_user_team_role
team_id = kwargs.get("team_id") or request.view_args.get("team_id")
if not team_id:
flash("Team not found.", "error")
return redirect(url_for("teams.list_teams"))
role = get_user_team_role(g.user_id, team_id)
if not role:
flash("You are not a member of this team.", "error")
return redirect(url_for("teams.list_teams"))
g.team_role = role
return f(*args, **kwargs)
return decorated_function
@team_bp.route("/")
@login_required
def list_teams():
"""List all teams the user belongs to."""
teams = get_user_teams(g.user_id)
user = get_user(g.user_id)
return render_template("teams/list.html", teams=teams, user=user)
@team_bp.route("/new", methods=["GET", "POST"])
@login_required
def create_team_view():
"""Create a new team."""
if request.method == "POST":
name = request.form.get("name", "").strip()
if not name:
flash("Team name is required.", "error")
return render_template("teams/create.html", user=get_user(g.user_id))
if len(name) < 2:
flash("Team name must be at least 2 characters.", "error")
return render_template("teams/create.html", user=get_user(g.user_id))
result = create_team(name, g.user_id)
if "error" in result:
flash(result["error"], "error")
else:
flash(f"Team '{name}' created!", "success")
return redirect(url_for("teams.manage_team", team_id=result["id"]))
return render_template("teams/create.html", user=get_user(g.user_id))
@team_bp.route("/<int:team_id>")
@login_required
def manage_team(team_id):
"""Team management page."""
from app.models import get_user_team_role
role = get_user_team_role(g.user_id, team_id)
if not role:
flash("You are not a member of this team.", "error")
return redirect(url_for("teams.list_teams"))
g.team_role = role
team = get_team(team_id)
if not team:
flash("Team not found.", "error")
return redirect(url_for("teams.list_teams"))
members = get_team_members(team_id)
sites = get_user_sites(g.user_id)
team_sites = [s for s in sites if s.get("team_id") == team_id]
# Count team sites for limit check
from app.models import get_user_site_count
team_site_count = sum(1 for s in team_sites)
return render_template(
"teams/manage.html",
team=team,
members=members,
team_sites=team_sites,
team_site_count=team_site_count,
role=role,
user=get_user(g.user_id),
)
@team_bp.route("/<int:team_id>/invite", methods=["POST"])
@login_required
def invite_member(team_id):
"""Invite a member to the team."""
from app.models import get_user_team_role
role = get_user_team_role(g.user_id, team_id)
if not role or role not in ("owner", "admin"):
flash("Only owners and admins can invite members.", "error")
return redirect(url_for("teams.manage_team", team_id=team_id))
email = request.form.get("email", "").strip()
member_role = request.form.get("role", "member")
if not email:
flash("Email is required.", "error")
return redirect(url_for("teams.manage_team", team_id=team_id))
result = invite_to_team(team_id, g.user_id, email, member_role)
if "error" in result:
flash(result["error"], "error")
else:
flash(f"Invite sent to {email}", "success")
return redirect(url_for("teams.manage_team", team_id=team_id))
@team_bp.route("/<int:team_id>/member/<int:member_id>/remove", methods=["POST"])
@login_required
def remove_member(team_id, member_id):
"""Remove a member from the team."""
result = remove_team_member(team_id, member_id, g.user_id)
if "error" in result:
flash(result["error"], "error")
else:
flash("Member removed.", "success")
return redirect(url_for("teams.manage_team", team_id=team_id))
@team_bp.route("/<int:team_id>/member/<int:member_id>/role", methods=["POST"])
@login_required
def update_member_role(team_id, member_id):
"""Update a member's role."""
new_role = request.form.get("role", "member")
result = change_member_role(team_id, member_id, new_role, g.user_id)
if "error" in result:
flash(result["error"], "error")
else:
flash("Role updated.", "success")
return redirect(url_for("teams.manage_team", team_id=team_id))
@team_bp.route("/<int:team_id>/delete", methods=["POST"])
@login_required
def delete_team_view(team_id):
"""Delete a team."""
result = delete_team(team_id, g.user_id)
if "error" in result:
flash(result["error"], "error")
else:
flash("Team deleted.", "success")
return redirect(url_for("teams.list_teams"))
return redirect(url_for("teams.manage_team", team_id=team_id))
@team_bp.route("/<int:team_id>/sites/new", methods=["GET", "POST"])
@login_required
@team_member_required
def new_team_site(team_id):
"""Create a new site under a team."""
from app.models import get_user_team_role
role = get_user_team_role(g.user_id, team_id)
if not role or role == "viewer":
flash("Viewers cannot create sites.", "error")
return redirect(url_for("teams.manage_team", team_id=team_id))
if request.method == "POST":
name = request.form.get("name", "").strip()
if not name:
flash("Site name is required.", "error")
return redirect(url_for("teams.manage_team", team_id=team_id))
user = get_user(g.user_id)
site = add_site(name, user["email"], user_id=g.user_id, team_id=team_id)
if "error" in site:
flash(site["error"], "error")
else:
flash(f"Site '{name}' created!", "success")
return redirect(url_for("user_sites.edit_site", site_id=site["id"]))
return redirect(url_for("teams.manage_team", team_id=team_id))
@team_bp.route("/<int:team_id>/analytics")
@login_required
@team_member_required
def team_analytics(team_id):
"""Team analytics dashboard — aggregated across all team sites."""
from app.models import get_team_analytics, get_user_team_role
role = get_user_team_role(g.user_id, team_id)
if not role:
flash("You are not a member of this team.", "error")
return redirect(url_for("teams.list_teams"))
g.team_role = role
team = get_team(team_id)
if not team:
flash("Team not found.", "error")
return redirect(url_for("teams.list_teams"))
analytics = get_team_analytics(team_id)
return render_template("teams/analytics.html", team=team, analytics=analytics)