-
Notifications
You must be signed in to change notification settings - Fork 29
feat(teams): add team icon, recommended status and favorites features #204
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
c387c01
a8d6702
dab1e23
99c2c3d
3a08be3
6cb06e3
325865e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,46 @@ | ||
| # SPDX-FileCopyrightText: 2025 Weibo, Inc. | ||
| # | ||
| # SPDX-License-Identifier: Apache-2.0 | ||
|
|
||
| """add_user_team_favorites_table | ||
|
|
||
| Revision ID: 2a3b4c5d6e7f | ||
| Revises: 1a2b3c4d5e6f | ||
| Create Date: 2025-07-16 10:00:00.000000 | ||
|
|
||
| """ | ||
| from alembic import op | ||
| import sqlalchemy as sa | ||
|
|
||
|
|
||
| # revision identifiers, used by Alembic. | ||
| revision = '2a3b4c5d6e7f' | ||
| down_revision = '1a2b3c4d5e6f' | ||
| branch_labels = None | ||
| depends_on = None | ||
|
|
||
|
|
||
| def upgrade() -> None: | ||
| # Create user_team_favorites table | ||
| op.create_table( | ||
| 'user_team_favorites', | ||
| sa.Column('id', sa.Integer(), nullable=False), | ||
| sa.Column('user_id', sa.Integer(), nullable=False), | ||
| sa.Column('team_id', sa.Integer(), nullable=False), | ||
| sa.Column('created_at', sa.DateTime(), nullable=True), | ||
| sa.PrimaryKeyConstraint('id'), | ||
| mysql_charset='utf8mb4', | ||
| mysql_collate='utf8mb4_unicode_ci' | ||
| ) | ||
| op.create_index('ix_user_team_favorites_id', 'user_team_favorites', ['id'], unique=False) | ||
| op.create_index('ix_user_team_favorites_user_id', 'user_team_favorites', ['user_id'], unique=False) | ||
| op.create_index('ix_user_team_favorites_team_id', 'user_team_favorites', ['team_id'], unique=False) | ||
| op.create_index('idx_user_team_favorite', 'user_team_favorites', ['user_id', 'team_id'], unique=True) | ||
|
|
||
|
|
||
| def downgrade() -> None: | ||
| op.drop_index('idx_user_team_favorite', table_name='user_team_favorites') | ||
| op.drop_index('ix_user_team_favorites_team_id', table_name='user_team_favorites') | ||
| op.drop_index('ix_user_team_favorites_user_id', table_name='user_team_favorites') | ||
| op.drop_index('ix_user_team_favorites_id', table_name='user_team_favorites') | ||
| op.drop_table('user_team_favorites') |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| # SPDX-FileCopyrightText: 2025 Weibo, Inc. | ||
| # | ||
| # SPDX-License-Identifier: Apache-2.0 | ||
|
|
||
| from datetime import datetime | ||
|
|
||
| from sqlalchemy import Column, DateTime, Index, Integer | ||
|
|
||
| from app.db.base import Base | ||
|
|
||
|
|
||
| class UserTeamFavorite(Base): | ||
| """User team favorite model for maintaining user-team favorite relationships""" | ||
|
|
||
| __tablename__ = "user_team_favorites" | ||
|
|
||
| id = Column(Integer, primary_key=True, index=True) | ||
| user_id = Column(Integer, nullable=False, index=True) # User who favorited the team | ||
| team_id = Column(Integer, nullable=False, index=True) # Team that was favorited | ||
| created_at = Column(DateTime, default=datetime.now) | ||
|
|
||
| __table_args__ = ( | ||
| Index("idx_user_team_favorite", "user_id", "team_id", unique=True), | ||
| {"mysql_charset": "utf8mb4", "mysql_collate": "utf8mb4_unicode_ci"}, | ||
| ) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -136,7 +136,12 @@ def create_with_user( | |
| # Create Team JSON | ||
| team_json = { | ||
| "kind": "Team", | ||
| "spec": {"members": members, "collaborationModel": collaboration_model}, | ||
| "spec": { | ||
| "members": members, | ||
| "collaborationModel": collaboration_model, | ||
| "icon": getattr(obj_in, "icon", None), | ||
| "isRecommended": getattr(obj_in, "is_recommended", False), | ||
| }, | ||
| "status": {"state": "Available"}, | ||
| "metadata": {"name": obj_in.name, "namespace": "default"}, | ||
| "apiVersion": "agent.wecode.io/v1", | ||
|
|
@@ -511,6 +516,14 @@ def update_with_user( | |
|
|
||
| team_crd.spec.collaborationModel = collaboration_model | ||
|
|
||
| # Handle icon update | ||
| if "icon" in update_data: | ||
| team_crd.spec.icon = update_data["icon"] | ||
|
|
||
| # Handle is_recommended update | ||
| if "is_recommended" in update_data: | ||
| team_crd.spec.isRecommended = update_data["is_recommended"] | ||
|
|
||
|
Comment on lines
+519
to
+526
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Avoid persisting
Consider only updating when a concrete boolean is provided: - # Handle is_recommended update
- if "is_recommended" in update_data:
- team_crd.spec.isRecommended = update_data["is_recommended"]
+ # Handle is_recommended update (ignore explicit nulls)
+ if "is_recommended" in update_data and update_data["is_recommended"] is not None:
+ team_crd.spec.isRecommended = update_data["is_recommended"]You can decide separately whether you want a way to “reset” this flag (e.g., treat 🤖 Prompt for AI Agents |
||
| # Save the updated team CRD | ||
| team.json = team_crd.model_dump(mode="json") | ||
| team.updated_at = datetime.now() | ||
|
|
@@ -886,6 +899,8 @@ def _convert_to_team_dict( | |
| "created_at": team.created_at, | ||
| "updated_at": team.updated_at, | ||
| "agent_type": agent_type, # Add agent_type field | ||
| "icon": team_crd.spec.icon, # Lucide icon name | ||
| "is_recommended": team_crd.spec.isRecommended, # Whether this team is recommended | ||
| } | ||
|
|
||
| def _get_bot_summary(self, bot: Kind, db: Session, user_id: int) -> Dict[str, Any]: | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,91 @@ | ||
| # SPDX-FileCopyrightText: 2025 Weibo, Inc. | ||
| # | ||
| # SPDX-License-Identifier: Apache-2.0 | ||
|
|
||
| from typing import Any, Dict, List, Set | ||
|
|
||
| from fastapi import HTTPException | ||
| from sqlalchemy.orm import Session | ||
|
|
||
| from app.models.kind import Kind | ||
| from app.models.user_team_favorite import UserTeamFavorite | ||
|
|
||
|
|
||
| class TeamFavoriteService: | ||
| """Service for team favorite operations""" | ||
|
|
||
| def add_favorite( | ||
| self, db: Session, *, team_id: int, user_id: int | ||
| ) -> Dict[str, Any]: | ||
| """Add a team to user's favorites""" | ||
| # Check if team exists | ||
| team = ( | ||
| db.query(Kind) | ||
| .filter(Kind.id == team_id, Kind.kind == "Team", Kind.is_active == True) | ||
| .first() | ||
| ) | ||
|
|
||
| if not team: | ||
| raise HTTPException(status_code=404, detail="Team not found") | ||
|
|
||
| # Check if already favorited | ||
| existing = ( | ||
| db.query(UserTeamFavorite) | ||
| .filter( | ||
| UserTeamFavorite.user_id == user_id, UserTeamFavorite.team_id == team_id | ||
| ) | ||
| .first() | ||
| ) | ||
|
|
||
| if existing: | ||
| return {"message": "Team already in favorites", "is_favorited": True} | ||
|
|
||
| # Create favorite record | ||
| favorite = UserTeamFavorite(user_id=user_id, team_id=team_id) | ||
| db.add(favorite) | ||
| db.commit() | ||
|
|
||
| return {"message": "Team added to favorites", "is_favorited": True} | ||
|
Comment on lines
+40
to
+48
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
# Search for UserTeamFavorite model and foreign key definitions
echo "=== Searching for UserTeamFavorite model ==="
fd -t py -i "*favorite*" | head -20
echo -e "\n=== Searching for ForeignKey references in Python files ==="
rg -n "ForeignKey|foreign_key" --type py -i "team_id|user_team_favorite" -A 2 -B 2
echo -e "\n=== Searching for model class definitions ==="
rg -n "class UserTeamFavorite\|class.*Favorite" --type py -A 10Repository: wecode-ai/Wegent Length of output: 466 🏁 Script executed: #!/bin/bash
# Search for the file under review
echo "=== Looking for team_favorite.py ==="
find . -name "team_favorite.py" -type f
echo -e "\n=== Looking for model files with 'favorite' in name ==="
find . -name "*favorite*.py" -type f
echo -e "\n=== Searching for UserTeamFavorite class definition ==="
rg -n "class UserTeamFavorite" --type python -A 15
echo -e "\n=== Searching for ForeignKey on team_id ==="
rg -n "team_id.*ForeignKey\|ForeignKey.*team_id" --type python -B 2 -A 2
echo -e "\n=== General search for ForeignKey usage ==="
rg -n "ForeignKey" --type python -B 1 -A 1 | head -50Repository: wecode-ai/Wegent Length of output: 146 🏁 Script executed: #!/bin/bash
# Read the file under review
echo "=== Reading backend/app/services/team_favorite.py ==="
cat -n backend/app/services/team_favorite.py
echo -e "\n=== List files in backend/app ==="
ls -la backend/app/
echo -e "\n=== Look for database model files ==="
find backend/app -name "*.py" -path "*/models/*" -o -name "*model*.py" | head -10Repository: wecode-ai/Wegent Length of output: 4557 🏁 Script executed: #!/bin/bash
# Read the UserTeamFavorite model
echo "=== Reading backend/app/models/user_team_favorite.py ==="
cat -n backend/app/models/user_team_favorite.py
echo -e "\n=== Look for migration or schema files ==="
find backend -name "*migration*" -o -name "*alembic*" -o -name "*schema*" | head -20
echo -e "\n=== Check db directory ==="
ls -la backend/app/db/Repository: wecode-ai/Wegent Length of output: 1531 🏁 Script executed: #!/bin/bash
# Read the migration file
echo "=== Reading Alembic migration file ==="
cat -n backend/alembic/versions/0c086b93f8b9_initial_migration.pyRepository: wecode-ai/Wegent Length of output: 6472 🏁 Script executed: #!/bin/bash
# List all migration files
echo "=== All migration files ==="
ls -la backend/alembic/versions/
echo -e "\n=== Search for user_team_favorites in all migration files ==="
grep -r "user_team_favorites" backend/alembic/
echo -e "\n=== Search for user_team_favorites in all Python files ==="
grep -r "user_team_favorites" backend/app/ --include="*.py" | grep -i "create\|table\|migration"Repository: wecode-ai/Wegent Length of output: 2408 🏁 Script executed: #!/bin/bash
# Read the migration file for user_team_favorites
cat -n backend/alembic/versions/2a3b4c5d6e7f_add_user_team_favorites_table.pyRepository: wecode-ai/Wegent Length of output: 2060 No foreign key constraint exists on The migration file (backend/alembic/versions/2a3b4c5d6e7f_add_user_team_favorites_table.py) creates the table with only indexes and a unique constraint on (user_id, team_id), but no ForeignKey constraint on team_id. This confirms the race condition scenario: a team could be deleted after the existence check at lines 22–26 but before the commit at line 46, resulting in an orphaned favorite record pointing to a non-existent team. Adding a foreign key constraint to enforce referential integrity would prevent this edge case. 🤖 Prompt for AI Agents |
||
|
|
||
| def remove_favorite( | ||
| self, db: Session, *, team_id: int, user_id: int | ||
| ) -> Dict[str, Any]: | ||
| """Remove a team from user's favorites""" | ||
| favorite = ( | ||
| db.query(UserTeamFavorite) | ||
| .filter( | ||
| UserTeamFavorite.user_id == user_id, UserTeamFavorite.team_id == team_id | ||
| ) | ||
| .first() | ||
| ) | ||
|
|
||
| if not favorite: | ||
| return {"message": "Team not in favorites", "is_favorited": False} | ||
|
|
||
| db.delete(favorite) | ||
| db.commit() | ||
|
|
||
| return {"message": "Team removed from favorites", "is_favorited": False} | ||
|
|
||
| def get_user_favorite_team_ids(self, db: Session, *, user_id: int) -> Set[int]: | ||
| """Get set of team IDs that user has favorited""" | ||
| favorites = ( | ||
| db.query(UserTeamFavorite.team_id) | ||
| .filter(UserTeamFavorite.user_id == user_id) | ||
| .all() | ||
| ) | ||
| return {f.team_id for f in favorites} | ||
|
|
||
| def is_team_favorited(self, db: Session, *, team_id: int, user_id: int) -> bool: | ||
| """Check if a team is in user's favorites""" | ||
| favorite = ( | ||
| db.query(UserTeamFavorite) | ||
| .filter( | ||
| UserTeamFavorite.user_id == user_id, UserTeamFavorite.team_id == team_id | ||
| ) | ||
| .first() | ||
| ) | ||
| return favorite is not None | ||
|
|
||
|
|
||
| team_favorite_service = TeamFavoriteService() | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Use
datetime.utcnowor timezone-aware datetime instead ofdatetime.now.Using
datetime.nowstores local server time, which can cause inconsistencies across different server timezones. Consider usingdatetime.utcnowfor consistency with typical database practices.📝 Committable suggestion
🤖 Prompt for AI Agents