Skip to content

Commit 70389bf

Browse files
authored
Merge pull request #198 from cuappdev/friends
Added friends storing
2 parents 56975a0 + 159a0d2 commit 70389bf

File tree

8 files changed

+430
-5
lines changed

8 files changed

+430
-5
lines changed

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,4 +16,5 @@ build/
1616
Archive
1717
scripts
1818
*.sqlite3
19-
service-account-key.json
19+
service-account-key.json
20+
docker-compose.yml

app_factory.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import logging
22
from datetime import timedelta, timezone
33
from flask_jwt_extended import JWTManager
4-
from src.utils.constants import SERVICE_ACCOUNT_PATH
4+
from src.utils.constants import SERVICE_ACCOUNT_PATH, JWT_SECRET_KEY
55
from datetime import datetime
66
from flask import Flask, render_template
77
from graphene import Schema
@@ -71,6 +71,7 @@ def create_app(run_migrations=False):
7171
schema = Schema(query=Query, mutation=Mutation)
7272
swagger = Swagger(app)
7373

74+
app.config['JWT_SECRET_KEY'] = JWT_SECRET_KEY
7475
app.config["JWT_ACCESS_TOKEN_EXPIRES"] = timedelta(hours=1)
7576
app.config["JWT_REFRESH_TOKEN_EXPIRES"] = timedelta(days=30)
7677

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
"""Add friends table
2+
3+
Revision ID: 01234abcdef1
4+
Revises: previous_revision_id_here
5+
Create Date: 2025-03-29 00:45:00.000000
6+
7+
"""
8+
from alembic import op
9+
import sqlalchemy as sa
10+
from datetime import datetime
11+
12+
13+
# revision identifiers, used by Alembic.
14+
revision = '01234abcdef1'
15+
down_revision = 'add99ce06ff5'
16+
branch_labels = None
17+
depends_on = None
18+
19+
20+
def upgrade():
21+
# Create friends table
22+
op.create_table('friends',
23+
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
24+
sa.Column('user_id', sa.Integer(), nullable=False),
25+
sa.Column('friend_id', sa.Integer(), nullable=False),
26+
sa.Column('created_at', sa.DateTime(), nullable=True, default=datetime.utcnow),
27+
sa.Column('is_accepted', sa.Boolean(), nullable=True, default=False),
28+
sa.Column('accepted_at', sa.DateTime(), nullable=True),
29+
sa.ForeignKeyConstraint(['user_id'], ['users.id'], ondelete='CASCADE'),
30+
sa.ForeignKeyConstraint(['friend_id'], ['users.id'], ondelete='CASCADE'),
31+
sa.PrimaryKeyConstraint('id')
32+
)
33+
34+
35+
def downgrade():
36+
# Drop friends table
37+
op.drop_table('friends')
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
"""Add friends table
2+
3+
Revision ID: add_friends_table
4+
Revises: 3c406131c004
5+
Create Date: 2025-03-29 00:55:00.000000
6+
7+
"""
8+
from alembic import op
9+
import sqlalchemy as sa
10+
from datetime import datetime
11+
12+
13+
# revision identifiers, used by Alembic.
14+
revision = 'add_friends_table'
15+
down_revision = '3c406131c004'
16+
branch_labels = None
17+
depends_on = None
18+
19+
20+
def upgrade():
21+
# Create friends table
22+
op.create_table('friends',
23+
sa.Column('user_id', sa.Integer(), nullable=False),
24+
sa.Column('friend_id', sa.Integer(), nullable=False),
25+
sa.Column('created_at', sa.DateTime(), nullable=True, server_default=sa.text('CURRENT_TIMESTAMP')),
26+
sa.Column('is_accepted', sa.Boolean(), nullable=True, server_default=sa.text('false')),
27+
sa.Column('accepted_at', sa.DateTime(), nullable=True),
28+
sa.ForeignKeyConstraint(['user_id'], ['users.id'], ondelete='CASCADE'),
29+
sa.ForeignKeyConstraint(['friend_id'], ['users.id'], ondelete='CASCADE'),
30+
sa.PrimaryKeyConstraint('user_id', 'friend_id')
31+
)
32+
33+
34+
def downgrade():
35+
# Drop friends table
36+
op.drop_table('friends')

schema.graphql

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,21 @@ enum FacilityType {
136136
COURT
137137
}
138138

139+
type Friendship {
140+
id: ID!
141+
userId: Int!
142+
friendId: Int!
143+
createdAt: DateTime
144+
isAccepted: Boolean
145+
acceptedAt: DateTime
146+
user: User
147+
friend: User
148+
}
149+
150+
type GetPendingFriendRequests {
151+
pendingRequests: [Friendship]
152+
}
153+
139154
type Giveaway {
140155
id: ID!
141156
name: String!
@@ -214,6 +229,10 @@ type Mutation {
214229
createCapacityReminder(capacityPercent: Int!, daysOfWeek: [String]!, fcmToken: String!, gyms: [String]!): CapacityReminder
215230
editCapacityReminder(capacityPercent: Int!, daysOfWeek: [String]!, gyms: [String]!, reminderId: Int!): CapacityReminder
216231
deleteCapacityReminder(reminderId: Int!): CapacityReminder
232+
addFriend(friendId: Int!, userId: Int!): Friendship
233+
acceptFriendRequest(friendshipId: Int!): Friendship
234+
removeFriend(friendId: Int!, userId: Int!): RemoveFriend
235+
getPendingFriendRequests(userId: Int!): GetPendingFriendRequests
217236
}
218237

219238
type OpenHours {
@@ -245,6 +264,7 @@ enum PriceType {
245264
type Query {
246265
getAllGyms: [Gym]
247266
getUserByNetId(netId: String): [User]
267+
getUsersFriends(id: Int): [User]
248268
getUsersByGiveawayId(id: Int): [User]
249269
getWeeklyWorkoutDays(id: Int): [String]
250270
getWorkoutsById(id: Int): [Workout]
@@ -253,12 +273,17 @@ type Query {
253273
getWorkoutGoals(id: Int!): [String]
254274
getUserStreak(id: Int!): JSONString
255275
getHourlyAverageCapacitiesByFacilityId(facilityId: Int): [HourlyAverageCapacity]
276+
getUserFriends(userId: Int!): [User]
256277
}
257278

258279
type RefreshAccessToken {
259280
newAccessToken: String
260281
}
261282

283+
type RemoveFriend {
284+
success: Boolean
285+
}
286+
262287
type Report {
263288
id: ID!
264289
createdAt: DateTime!
@@ -286,6 +311,10 @@ type User {
286311
workoutGoal: [DayOfWeekGraphQLEnum]
287312
encodedImage: String
288313
giveaways: [Giveaway]
314+
friendRequestsSent: [Friendship]
315+
friendRequestsReceived: [Friendship]
316+
friendships: [Friendship]
317+
friends: [User]
289318
}
290319

291320
type Workout {

src/models/friends.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
from sqlalchemy import Column, Integer, ForeignKey, DateTime, Boolean, UniqueConstraint
2+
from sqlalchemy.orm import relationship
3+
from src.database import Base
4+
from datetime import datetime
5+
6+
class Friendship(Base):
7+
"""
8+
A friendship relationship between two users.
9+
10+
Attributes:
11+
- `id` The ID of the friendship.
12+
- `user_id` The ID of the user who initiated the friendship.
13+
- `friend_id` The ID of the user who received the friendship request.
14+
- `created_at` When the friendship was created.
15+
- `is_accepted` Whether the friendship has been accepted.
16+
- `accepted_at` When the friendship was accepted.
17+
"""
18+
19+
__tablename__ = "friends"
20+
21+
id = Column(Integer, primary_key=True, autoincrement=True)
22+
user_id = Column(Integer, ForeignKey("users.id"), nullable=False)
23+
friend_id = Column(Integer, ForeignKey("users.id"), nullable=False)
24+
__table_args__ = (UniqueConstraint('user_id', 'friend_id', name='unique_friendship'),)
25+
26+
created_at = Column(DateTime, default=datetime.utcnow)
27+
is_accepted = Column(Boolean, default=False)
28+
accepted_at = Column(DateTime, nullable=True)
29+
30+
user = relationship("User", foreign_keys=[user_id], back_populates="friend_requests_sent")
31+
friend = relationship("User", foreign_keys=[friend_id], back_populates="friend_requests_received")
32+
33+
def accept(self):
34+
"""Accept a friendship request."""
35+
self.is_accepted = True
36+
self.accepted_at = datetime.utcnow()

src/models/user.py

Lines changed: 48 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1-
from sqlalchemy import Column, Integer, String, ARRAY, Enum
2-
from sqlalchemy.orm import backref, relationship
1+
from sqlalchemy import Column, Integer, String, ARRAY, Enum, ForeignKey
2+
from sqlalchemy.orm import relationship
33
from src.database import Base
44
from src.models.enums import DayOfWeekEnum
5+
from src.models.friends import Friendship
56

67
class User(Base):
78
"""
@@ -30,4 +31,48 @@ class User(Base):
3031
active_streak = Column(Integer, nullable=True)
3132
max_streak = Column(Integer, nullable=True)
3233
workout_goal = Column(ARRAY(Enum(DayOfWeekEnum)), nullable=True)
33-
encoded_image = Column(String, nullable=True)
34+
encoded_image = Column(String, nullable=True)
35+
36+
friend_requests_sent = relationship("Friendship",
37+
foreign_keys="Friendship.user_id",
38+
back_populates="user")
39+
40+
friend_requests_received = relationship("Friendship",
41+
foreign_keys="Friendship.friend_id",
42+
back_populates="friend")
43+
44+
def add_friend(self, friend):
45+
# Check if friendship already exists
46+
existing = Friendship.query.filter(
47+
(Friendship.user_id == self.id) &
48+
(Friendship.friend_id == friend.id)
49+
).first()
50+
51+
if not existing:
52+
new_friendship = Friendship(user_id=self.id, friend_id=friend.id)
53+
# Add to database session here or return for external handling
54+
return new_friendship
55+
56+
def remove_friend(self, friend):
57+
friendship = Friendship.query.filter(
58+
(Friendship.user_id == self.id) &
59+
(Friendship.friend_id == friend.id)
60+
).first()
61+
62+
if friendship:
63+
# Delete from database session here or return for external handling
64+
return friendship
65+
66+
def get_friends(self):
67+
# Get users this user has added as friends
68+
direct_friends_query = Friendship.query.filter_by(user_id=self.id)
69+
direct_friends = [friendship.friend for friendship in direct_friends_query]
70+
71+
# Get users who have added this user as a friend
72+
reverse_friends_query = Friendship.query.filter_by(friend_id=self.id)
73+
reverse_friends = [friendship.user for friendship in reverse_friends_query]
74+
75+
# Combine both lists and remove duplicates
76+
all_friends = list(set(direct_friends + reverse_friends))
77+
78+
return all_friends

0 commit comments

Comments
 (0)