diff --git a/.forgejo/workflows/deploy.yml b/.forgejo/workflows/deploy.yml index efdaa4e..0002b5d 100644 --- a/.forgejo/workflows/deploy.yml +++ b/.forgejo/workflows/deploy.yml @@ -36,7 +36,7 @@ jobs: script: | # Clone or pull repo if [ ! -d "${{ steps.target.outputs.deploy_path }}" ]; then - git clone https://git.runfoo.run/runfoo/fediversion.git ${{ steps.target.outputs.deploy_path }} + git clone https://git.runfoo.run/runfoo-org/fediversion.git ${{ steps.target.outputs.deploy_path }} fi cd ${{ steps.target.outputs.deploy_path }} git fetch origin ${{ github.ref_name }} diff --git a/backend/models.py b/backend/models.py index 37d6605..b55305e 100644 --- a/backend/models.py +++ b/backend/models.py @@ -88,6 +88,10 @@ class Vertical(SQLModel, table=True): is_active: bool = Field(default=True, description="Show in band selector") is_featured: bool = Field(default=False, description="Highlight in discovery") + # Branding + logo_url: Optional[str] = Field(default=None, description="Band logo URL for UI") + accent_color: Optional[str] = Field(default=None, description="Hex color for accents") + # Relationships shows: List["Show"] = Relationship(back_populates="vertical") songs: List["Song"] = Relationship(back_populates="vertical") @@ -421,10 +425,14 @@ class Group(SQLModel, table=True): id: Optional[int] = Field(default=None, primary_key=True) name: str = Field(index=True, unique=True) description: Optional[str] = None - privacy: str = Field(default="public") # public, private + privacy: str = Field(default="public") # public, private, invite_only created_by: int = Field(foreign_key="user.id") created_at: datetime = Field(default_factory=datetime.utcnow) + # Vertical scoping (optional - null means cross-band group) + vertical_id: Optional[int] = Field(default=None, foreign_key="vertical.id", index=True) + image_url: Optional[str] = Field(default=None, description="Group logo/image URL") + members: List["GroupMember"] = Relationship(back_populates="group") posts: List["GroupPost"] = Relationship(back_populates="group") diff --git a/backend/routers/groups.py b/backend/routers/groups.py index b5e4383..496b195 100644 --- a/backend/routers/groups.py +++ b/backend/routers/groups.py @@ -1,4 +1,4 @@ -from typing import List +from typing import List, Optional from fastapi import APIRouter, Depends, HTTPException, Query from sqlmodel import Session, select, func from database import get_session @@ -32,12 +32,33 @@ def create_group( @router.get("/", response_model=List[GroupRead]) def read_groups( offset: int = 0, - limit: int = Query(default=100, le=100), + limit: int = Query(default=100, le=100), + vertical: Optional[str] = None, session: Session = Depends(get_session) ): - # TODO: Add member count to response - groups = session.exec(select(Group).offset(offset).limit(limit)).all() - return groups + from models import Vertical + + query = select(Group) + + # Filter by vertical if specified + if vertical: + v = session.exec(select(Vertical).where(Vertical.slug == vertical)).first() + if v: + query = query.where(Group.vertical_id == v.id) + + groups = session.exec(query.offset(offset).limit(limit)).all() + + # Add member count to each group + result = [] + for g in groups: + member_count = session.exec( + select(func.count(GroupMember.id)).where(GroupMember.group_id == g.id) + ).one() + result.append({ + **g.model_dump(), + "member_count": member_count or 0 + }) + return result @router.get("/{group_id}", response_model=GroupRead) def read_group(group_id: int, session: Session = Depends(get_session)): @@ -83,6 +104,42 @@ def join_group( return {"status": "joined"} + +@router.delete("/{group_id}/leave") +def leave_group( + group_id: int, + session: Session = Depends(get_session), + current_user: User = Depends(get_current_user) +): + """Leave a group (non-admins only, admins must transfer ownership first)""" + member = session.exec( + select(GroupMember) + .where(GroupMember.group_id == group_id) + .where(GroupMember.user_id == current_user.id) + ).first() + + if not member: + raise HTTPException(status_code=404, detail="Not a member") + + if member.role == "admin": + # Check if only admin + other_admins = session.exec( + select(GroupMember) + .where(GroupMember.group_id == group_id) + .where(GroupMember.role == "admin") + .where(GroupMember.user_id != current_user.id) + ).first() + + if not other_admins: + raise HTTPException( + status_code=400, + detail="Cannot leave: you are the only admin. Transfer ownership first." + ) + + session.delete(member) + session.commit() + return {"status": "left"} + # --- Posts --- @router.post("/{group_id}/posts", response_model=GroupPostRead)