Skip to content

Commit dcb94eb

Browse files
authored
Merge pull request #186 from cuappdev/user-streaks
Added user streaks including their current and max streaks
2 parents ab33064 + 2a1820f commit dcb94eb

File tree

5 files changed

+156
-3
lines changed

5 files changed

+156
-3
lines changed
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
"""Added active_streak and max_streak to users
2+
3+
Revision ID: 6b01a81bb92b
4+
Revises: 31b1fa20772f
5+
Create Date: 2025-03-04 22:45:06.601964
6+
7+
"""
8+
from alembic import op
9+
import sqlalchemy as sa
10+
11+
12+
# revision identifiers, used by Alembic.
13+
revision = '6b01a81bb92b'
14+
down_revision = '31b1fa20772f'
15+
branch_labels = None
16+
depends_on = None
17+
18+
19+
def upgrade():
20+
# ### commands auto generated by Alembic - please adjust! ###
21+
op.drop_constraint('report_user_id_fkey', 'report', type_='foreignkey')
22+
op.drop_column('report', 'user_id')
23+
op.add_column('users', sa.Column('active_streak', sa.Integer(), nullable=True))
24+
op.add_column('users', sa.Column('max_streak', sa.Integer(), nullable=True))
25+
# ### end Alembic commands ###
26+
27+
28+
def downgrade():
29+
# ### commands auto generated by Alembic - please adjust! ###
30+
op.add_column('report', sa.Column('user_id', sa.INTEGER(), autoincrement=False, nullable=False))
31+
op.create_foreign_key('report_user_id_fkey', 'report', 'users', ['user_id'], ['id'])
32+
op.drop_column('users', 'active_streak')
33+
op.drop_column('users', 'max_streak')
34+
# ### end Alembic commands ###

schema.graphql

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,16 @@ type CreateReport {
7171

7272
scalar DateTime
7373

74+
enum DayOfWeekEnum {
75+
MONDAY
76+
TUESDAY
77+
WEDNESDAY
78+
THURSDAY
79+
FRIDAY
80+
SATURDAY
81+
SUNDAY
82+
}
83+
7484
enum DayOfWeekGraphQLEnum {
7585
MONDAY
7686
TUESDAY
@@ -146,6 +156,8 @@ type HourlyAverageCapacity {
146156
history: [Float]!
147157
}
148158

159+
scalar JSONString
160+
149161
enum MuscleGroup {
150162
ABDOMINALS
151163
CHEST
@@ -205,6 +217,8 @@ type Query {
205217
getWorkoutsById(id: Int): [Workout]
206218
activities: [Activity]
207219
getAllReports: [Report]
220+
getWorkoutGoals(id: Int!): [String]
221+
getUserStreak(id: Int!): JSONString
208222
getHourlyAverageCapacitiesByFacilityId(facilityId: Int): [HourlyAverageCapacity]
209223
}
210224

@@ -230,7 +244,9 @@ type User {
230244
email: String
231245
netId: String!
232246
name: String!
233-
workoutGoal: [DayOfWeekGraphQLEnum]
247+
activeStreak: Int
248+
maxStreak: Int
249+
workoutGoal: [DayOfWeekEnum]
234250
giveaways: [Giveaway]
235251
}
236252

src/models/user.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@ class User(Base):
1414
- `net_id` The user's Net ID.
1515
- `name` The user's name.
1616
- `workout_goal` The days of the week the user has set as their personal goal.
17+
- `active_streak` The number of consecutive weeks the user has met their personal goal.
18+
- `max_streak` The maximum number of consecutive weeks the user has met their personal goal.
19+
- `workout_goal` The max number of weeks the user has met their personal goal.
1720
"""
1821

1922
__tablename__ = "users"
@@ -23,4 +26,6 @@ class User(Base):
2326
giveaways = relationship("Giveaway", secondary="giveaway_instance", back_populates="users")
2427
net_id = Column(String, nullable=False)
2528
name = Column(String, nullable=False)
29+
active_streak = Column(Integer, nullable=True)
30+
max_streak = Column(Integer, nullable=True)
2631
workout_goal = Column(ARRAY(Enum(DayOfWeekEnum)), nullable=True)

src/schema.py

Lines changed: 48 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -185,7 +185,6 @@ def resolve_pricing(self, info):
185185
class User(SQLAlchemyObjectType):
186186
class Meta:
187187
model = UserModel
188-
workout_goal = graphene.List(DayOfWeekGraphQLEnum)
189188

190189

191190
class UserInput(graphene.InputObjectType):
@@ -241,6 +240,8 @@ class Query(graphene.ObjectType):
241240
get_workouts_by_id = graphene.List(Workout, id=graphene.Int(), description="Get all of a user's workouts by ID.")
242241
activities = graphene.List(Activity)
243242
get_all_reports = graphene.List(Report, description="Get all reports.")
243+
get_workout_goals = graphene.List(graphene.String, id=graphene.Int(required=True), description="Get the workout goals of a user by ID.")
244+
get_user_streak = graphene.Field(graphene.JSONString, id=graphene.Int(required=True), description="Get the current and max workout streak of a user.")
244245
get_hourly_average_capacities_by_facility_id = graphene.List(
245246
HourlyAverageCapacity, facility_id=graphene.Int(), description="Get all facility hourly average capacities."
246247
)
@@ -296,15 +297,60 @@ def resolve_get_weekly_workout_days(self, info, id):
296297
def resolve_get_all_reports(self, info):
297298
query = ReportModel.query.all()
298299
return query
300+
301+
def resolve_get_workout_goals(self, info, id):
302+
user = User.get_query(info).filter(UserModel.id == id).first()
303+
if not user:
304+
raise GraphQLError("User with the given ID does not exist.")
305+
306+
return [day.value for day in user.workout_goal] if user.workout_goal else []
307+
308+
def resolve_get_user_streak(self, info, id):
309+
user = User.get_query(info).filter(UserModel.id == id).first()
310+
if not user:
311+
raise GraphQLError("User with the given ID does not exist.")
299312

313+
workouts = (
314+
Workout.get_query(info)
315+
.filter(WorkoutModel.user_id == user.id)
316+
.order_by(WorkoutModel.workout_time.desc())
317+
.all()
318+
)
319+
320+
if not workouts:
321+
return {"active_streak": 0, "max_streak": 0}
322+
323+
workout_dates = {workout.workout_time.date() for workout in workouts}
324+
sorted_dates = sorted(workout_dates, reverse=True)
325+
326+
today = datetime.utcnow().date()
327+
active_streak = 0
328+
max_streak = 0
329+
streak = 0
330+
prev_date = None
331+
332+
for date in sorted_dates:
333+
if prev_date and (prev_date - date).days > 1:
334+
max_streak = max(max_streak, streak)
335+
streak = 0
336+
337+
streak += 1
338+
prev_date = date
339+
340+
if date == today or (date == today - timedelta(days=1) and active_streak == 0):
341+
active_streak = streak
342+
343+
max_streak = max(max_streak, streak)
344+
345+
return {"active_streak": active_streak, "max_streak": max_streak}
346+
300347
def resolve_get_hourly_average_capacities_by_facility_id(self, info, facility_id):
301348
valid_facility_ids = [14492437, 8500985, 7169406, 10055021, 2323580, 16099753, 15446768, 12572681]
302349
if facility_id not in valid_facility_ids:
303350
raise GraphQLError("Invalid facility ID.")
304351
query = HourlyAverageCapacity.get_query(info).filter(HourlyAverageCapacityModel.facility_id == facility_id)
305352
return query.all()
306353

307-
308354
# MARK: - Mutation
309355

310356

src/utils/utils.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from src.models.gym import Gym
88
from src.models.facility import Facility, FacilityType
99
from src.models.amenity import Amenity, AmenityType
10+
from src.models.workout import Workout
1011
from src.utils.constants import ASSET_BASE_URL, EASTERN_TIMEZONE
1112

1213

@@ -140,3 +141,54 @@ def get_facility_id(name):
140141
"""
141142
facility = Facility.query.filter_by(name=name).first()
142143
return facility.id
144+
145+
def calculate_streaks(user, workouts, workout_goal):
146+
"""
147+
Calculate the current and maximum workout streaks for a user.
148+
149+
Parameters:
150+
- `user` The user object.
151+
- `workouts` The user's list of completed workouts.
152+
- `workout_goal` A list of goal days (e.g., ['Monday', 'Wednesday']).
153+
154+
Returns:
155+
- Updates `user.active_streak` and `user.max_streak`.
156+
"""
157+
if not workouts:
158+
user.active_streak = 0
159+
user.max_streak = user.max_streak or 0
160+
return
161+
162+
# Convert goal days to set of weekday numbers (Monday=0, Sunday=6)
163+
goal_days = {time.strptime(day, "%A").tm_wday for day in workout_goal}
164+
165+
# Filter workouts to only include those on goal days
166+
valid_workouts = [w for w in workouts if w.workout_time.weekday() in goal_days]
167+
168+
# Sort by workout date
169+
valid_workouts.sort(key=lambda x: x.workout_time)
170+
171+
active_streak = 1
172+
max_streak = user.max_streak or 0
173+
174+
for i in range(1, len(valid_workouts)):
175+
prev_day = valid_workouts[i - 1].workout_time
176+
curr_day = valid_workouts[i].workout_time
177+
178+
# Find the next expected goal day
179+
expected_next_day = prev_day + timedelta(days=1)
180+
while expected_next_day.weekday() not in goal_days:
181+
expected_next_day += timedelta(days=1)
182+
183+
# Check if current workout is on the expected next goal day
184+
if curr_day.date() == expected_next_day.date():
185+
active_streak += 1
186+
else:
187+
max_streak = max(max_streak, active_streak)
188+
active_streak = 1
189+
190+
# Final update
191+
max_streak = max(max_streak, active_streak)
192+
user.active_streak = active_streak
193+
user.max_streak = max_streak
194+

0 commit comments

Comments
 (0)