11import graphene
2+ import base64
23import os
34from flask_jwt_extended import create_access_token , create_refresh_token , get_jwt_identity , get_jwt , jwt_required
45from functools import wraps
5- from datetime import datetime , timedelta , timezone
6+ from datetime import datetime , timedelta , time , timezone
67from graphene_sqlalchemy import SQLAlchemyObjectType
78from graphql import GraphQLError
89from src .models .capacity import Capacity as CapacityModel
2930import requests
3031from firebase_admin import messaging
3132import logging
33+ from zoneinfo import ZoneInfo
3234from sqlalchemy import func , cast , Date
3335
36+ local_tz = ZoneInfo ("America/New_York" )
3437
3538def resolve_enum_value (entry ):
3639 """Return the raw value for Enum objects while leaving plain strings untouched."""
@@ -66,7 +69,8 @@ def to_local_time(dt):
6669 return None
6770
6871 # Convert to local timezone (server-local)
69- return dt_utc .astimezone ()
72+ return dt_utc .astimezone (local_tz )
73+
7074
7175def goal_at (goal_history , window_start_date ):
7276 """
@@ -83,6 +87,7 @@ def goal_at(goal_history, window_start_date):
8387
8488 return goal_history [- 1 ][0 ]
8589
90+
8691# MARK: - Gym
8792
8893
@@ -248,6 +253,7 @@ class Meta:
248253 def resolve_effective_at (self , info ):
249254 return to_local_time (self .effective_at )
250255
256+
251257# MARK: - User
252258
253259
@@ -258,18 +264,24 @@ class Meta:
258264 friendships = graphene .List (lambda : Friendship )
259265 friends = graphene .List (lambda : User )
260266 total_gym_days = graphene .Int (
261- required = True ,
262- description = "Get the total number of gym days (unique workout days) for user."
267+ required = True , description = "Get the total number of gym days (unique workout days) for user."
263268 )
264- streak_start = graphene .Date (
265- description = "The start date of the most recent active streak, up until the current date."
269+ streak_start = graphene .DateTime (
270+ description = "The start datetime of the most recent active streak (midnight of the day in local timezone) , up until the current date."
266271 )
272+ workout_history = graphene .List (lambda : Workout )
273+
274+ def resolve_workout_history (self , info ):
275+ query = Workout .get_query (info ).filter (WorkoutModel .user_id == self .id ).order_by (WorkoutModel .workout_time .desc ())
276+ return query .all ()
267277
268278 def resolve_total_gym_days (self , info ):
269279 return (
270280 Workout .get_query (info )
271281 .filter (WorkoutModel .user_id == self .id )
272- .with_entities (func .count (func .distinct (cast (WorkoutModel .workout_time , Date )))) # We cast the datetiem object as a Date object to get the unique days
282+ .with_entities (
283+ func .count (func .distinct (cast (WorkoutModel .workout_time , Date )))
284+ ) # We cast the datetiem object as a Date object to get the unique days
273285 .scalar ()
274286 )
275287
@@ -290,7 +302,7 @@ def resolve_active_streak(self, info):
290302 if not workout_date_rows :
291303 return 0
292304
293- workout_dates = [row [0 ] for row in workout_date_rows ]
305+ workout_dates = [row [0 ] for row in workout_date_rows ]
294306
295307 goal_hist = (
296308 db_session .query (UserWorkoutGoalHistoryModel .workout_goal , UserWorkoutGoalHistoryModel .effective_at )
@@ -316,7 +328,7 @@ def resolve_active_streak(self, info):
316328
317329 day_iterator = day_pointer
318330 count_in_window = 0
319-
331+
320332 while day_iterator < total_workout_days and workout_dates [day_iterator ] >= window_start :
321333 count_in_window += 1
322334 day_iterator += 1
@@ -357,10 +369,7 @@ def resolve_streak_start(self, info):
357369 return None
358370
359371 goal_hist = (
360- db_session .query (
361- UserWorkoutGoalHistoryModel .workout_goal ,
362- UserWorkoutGoalHistoryModel .effective_at ,
363- )
372+ db_session .query (UserWorkoutGoalHistoryModel .workout_goal , UserWorkoutGoalHistoryModel .effective_at )
364373 .filter (UserWorkoutGoalHistoryModel .user_id == user .id )
365374 .order_by (UserWorkoutGoalHistoryModel .effective_at .desc ())
366375 .all ()
@@ -430,8 +439,8 @@ def goal_for_window_start(ws_date):
430439 return None
431440
432441 last_streak_start_date = workout_dates [idx_last_streak_start ]
433-
434- return last_streak_start_date
442+ local_midnight = datetime . combine ( last_streak_start_date , time . min , tzinfo = local_tz )
443+ return local_midnight
435444
436445 def resolve_max_streak (self , info ):
437446 user = User .get_query (info ).filter (UserModel .id == self .id ).first ()
@@ -450,7 +459,7 @@ def resolve_max_streak(self, info):
450459 if not workout_date_rows :
451460 return 0
452461
453- workout_dates = [row [0 ] for row in workout_date_rows ]
462+ workout_dates = [row [0 ] for row in workout_date_rows ]
454463
455464 goal_hist = (
456465 db_session .query (UserWorkoutGoalHistoryModel .workout_goal , UserWorkoutGoalHistoryModel .effective_at )
@@ -484,7 +493,7 @@ def resolve_max_streak(self, info):
484493 count_in_window += 1
485494 day_iterator += 1
486495
487- goal_days = goal_at (goal_hist , window_start )
496+ goal_days = goal_at (goal_hist , window_start )
488497
489498 if count_in_window == 0 :
490499 max_met_goal = max (max_met_goal , run_met_goal )
@@ -554,6 +563,7 @@ def resolve_friend(self, info):
554563 def resolve_accepted_at (self , info ):
555564 return to_local_time (self .accepted_at )
556565
566+
557567# MARK: - Giveaway
558568
559569
@@ -703,7 +713,7 @@ def resolve_get_weekly_workout_days(self, info, id):
703713
704714 def resolve_get_all_reports (self , info ):
705715 query = ReportModel .query .all ()
706- return query
716+ return query
707717
708718 def resolve_get_hourly_average_capacities_by_facility_id (self , info , facility_id ):
709719 valid_facility_ids = [14492437 , 8500985 , 7169406 , 10055021 , 2323580 , 16099753 , 15446768 , 12572681 ]
@@ -831,18 +841,22 @@ def mutate(self, info, name, net_id, email, encoded_image=None):
831841 upload_url = os .getenv ("DIGITAL_OCEAN_URL" )
832842 if not upload_url :
833843 raise GraphQLError ("Upload URL not configured." )
834- payload = { "bucket" : os . getenv ( "BUCKET_NAME" ), "image" : encoded_image } # Base64-encoded image string
844+
835845 headers = {"Content-Type" : "application/json" }
846+
847+ image_bytes = base64 .b64decode (encoded_image )
848+ files = {"image" : ("profile.png" , image_bytes , "image/png" )}
849+ data = {"bucket" : os .getenv ("BUCKET_NAME" )}
836850 try :
837- response = requests .post (upload_url , json = payload , headers = headers )
851+ response = requests .post (upload_url , files = files , data = data )
838852 response .raise_for_status ()
839853 json_response = response .json ()
840854 final_photo_url = json_response .get ("data" )
841855 if not final_photo_url :
842856 raise GraphQLError ("No URL returned from upload service." )
843857 except requests .exceptions .RequestException as e :
844858 print (f"Request failed: { e } " )
845- raise GraphQLError ("Failed to upload photo. " )
859+ raise GraphQLError (f "Failed to upload photo: { e } " )
846860
847861 new_user = UserModel (name = name , net_id = net_id , email = email , encoded_image = final_photo_url )
848862 db_session .add (new_user )
@@ -1001,8 +1015,7 @@ class SetWorkoutGoals(graphene.Mutation):
10011015 class Arguments :
10021016 user_id = graphene .Int (required = True , description = "The ID of the user." )
10031017 workout_goal = graphene .Int (
1004- required = True ,
1005- description = "The new workout goal for the user in terms of number of days per week." ,
1018+ required = True , description = "The new workout goal for the user in terms of number of days per week."
10061019 )
10071020
10081021 Output = User
@@ -1045,11 +1058,7 @@ def mutate(self, info, user_id, workout_goal):
10451058 user .workout_goal = workout_goal
10461059
10471060 db_session .add (
1048- UserWorkoutGoalHistoryModel (
1049- user_id = user .id ,
1050- workout_goal = workout_goal ,
1051- effective_at = effective_at ,
1052- )
1061+ UserWorkoutGoalHistoryModel (user_id = user .id , workout_goal = workout_goal , effective_at = effective_at )
10531062 )
10541063
10551064 db_session .commit ()
0 commit comments