From 8cae55eba8faec72bab34d9976798d8e9857b376 Mon Sep 17 00:00:00 2001 From: Sophie Strausberg Date: Sat, 29 Mar 2025 13:58:27 -0400 Subject: [PATCH 1/7] initial commit --- src/models/workout_reminder.py | 24 ++++++++++++++++++++++++ src/scrapers/capacities_scraper.py | 2 +- 2 files changed, 25 insertions(+), 1 deletion(-) create mode 100644 src/models/workout_reminder.py diff --git a/src/models/workout_reminder.py b/src/models/workout_reminder.py new file mode 100644 index 0000000..07b7d8e --- /dev/null +++ b/src/models/workout_reminder.py @@ -0,0 +1,24 @@ +from sqlalchemy import Column, Integer, ForeignKey, TIME, Boolean +from sqlalchemy.dialects.postgresql import ARRAY +from sqlalchemy import Enum as SQLAEnum +from src.models.user import DayOfWeekEnum +from src.database import Base + +class WorkoutReminder(Base): + """ + A workout reminder for an Uplift user. + Attributes: + - `id` The ID of the workout reminder. + - `user_id` The ID of the user who owns this reminder. + - `days_of_week` The days of the week when the reminder is active. + - `reminder_time` The time of day the reminder is scheduled for. + - `is_active` Whether the reminder is currently active (default is True). + """ + + __tablename__ = "workout_reminder" + + id = Column(Integer, primary_key=True) + user_id = Column(Integer, ForeignKey("users.id"), nullable=False) + days_of_week = Column(ARRAY(SQLAEnum(DayOfWeekEnum)), nullable=False) + reminder_time = Column(TIME, nullable=False) + is_active = Column(Boolean, default=True) \ No newline at end of file diff --git a/src/scrapers/capacities_scraper.py b/src/scrapers/capacities_scraper.py index 7e6157b..69b8129 100644 --- a/src/scrapers/capacities_scraper.py +++ b/src/scrapers/capacities_scraper.py @@ -105,7 +105,7 @@ def fetch_capacities(): current_time = int(time.time()) - is_open = any(hour.start_time <= current_time <= hour.end_time for hour in facility.hours) + is_open = any(hour.start_time <= current_time < hour.end_time for hour in facility.hours) if is_open: topic_enum = gym_mapping[db_name] From c230300d957e6c58404b6dc996b1a8eaf292cf57 Mon Sep 17 00:00:00 2001 From: Sophie Strausberg Date: Mon, 31 Mar 2025 23:16:36 -0400 Subject: [PATCH 2/7] add workout reminders --- app_factory.py | 12 ++++- schema.graphql | 19 ++++++- src/models/user.py | 6 ++- src/models/workout_reminder.py | 2 +- src/schema.py | 93 ++++++++++++++++++++++++++++++++-- src/utils/messaging.py | 48 +++++++++++++++++- 6 files changed, 169 insertions(+), 11 deletions(-) diff --git a/app_factory.py b/app_factory.py index 198a41e..f420313 100644 --- a/app_factory.py +++ b/app_factory.py @@ -6,6 +6,7 @@ from flask import Flask, render_template from graphene import Schema from graphql.utils import schema_printer +from src.models.workout_reminder import WorkoutReminder from src.database import db_session, init_db from src.database import Base as db from src.database import db_url, db_user, db_password, db_name, db_host, db_port @@ -26,7 +27,7 @@ def initialize_firebase(): raise ValueError("GOOGLE_SERVICE_ACCOUNT_PATH environment variable not set.") else: firebase_app = firebase_admin.get_app() - logging.info("Firebase app created...") + logging.info("Firebase app created") return firebase_app @@ -127,6 +128,7 @@ def setup_scrapers(app): from src.scrapers.class_scraper import fetch_classes from src.scrapers.activities_scraper import fetch_activity from src.utils.utils import create_gym_table + from src.utils.messaging import send_workout_reminders from src.models.openhours import OpenHours import os @@ -197,7 +199,7 @@ def cleanup_expired_tokens(): # Update hourly average capacity every hour @scheduler.task("cron", id="update_capacity", hour="*") - def scheduled_job(): + def update_hourly_avg_capacity(): current_time = datetime.now() current_day = current_time.strftime("%A").upper() current_hour = current_time.hour @@ -207,6 +209,12 @@ def scheduled_job(): except Exception as e: logging.error(f"Error updating hourly average capacity for {current_day}, hour {current_hour}: {e}") + # Send workout reminders every morning at 12:00 AM + @scheduler.task("cron", id="send_reminders", hour=0, minute=0) + def workout_reminders(): + logging.info("Sending workout reminders...") + send_workout_reminders() + # We're now handling job execution logging within each task function # Initialize scheduler diff --git a/schema.graphql b/schema.graphql index 10289db..ce883ed 100644 --- a/schema.graphql +++ b/schema.graphql @@ -201,8 +201,8 @@ enum MuscleGroup { type Mutation { createGiveaway(name: String!): Giveaway - createUser(email: String!, encodedImage: String, name: String!, netId: String!): User - editUser(email: String, encodedImage: String, name: String, netId: String!): User + createUser(email: String!, encodedImage: String, fcmToken: String!, name: String!, netId: String!): User + editUser(email: String, encodedImage: String, fcmToken: String, name: String, netId: String!): User enterGiveaway(giveawayId: Int!, userNetId: String!): GiveawayInstance setWorkoutGoals(userId: Int!, workoutGoal: [String]!): User logWorkout(facilityId: Int!, userId: Int!, workoutTime: DateTime!): Workout @@ -214,6 +214,9 @@ type Mutation { createCapacityReminder(capacityPercent: Int!, daysOfWeek: [String]!, fcmToken: String!, gyms: [String]!): CapacityReminder toggleCapacityReminder(reminderId: Int!): CapacityReminder deleteCapacityReminder(reminderId: Int!): CapacityReminder + createWorkoutReminder(daysOfWeek: [String]!, reminderTime: Time!, userId: Int!): WorkoutReminder + toggleWorkoutReminder(reminderId: Int!): WorkoutReminder + deleteWorkoutReminder(reminderId: Int!): WorkoutReminder } type OpenHours { @@ -276,6 +279,8 @@ enum ReportType { OTHER } +scalar Time + type User { id: ID! email: String @@ -285,7 +290,9 @@ type User { maxStreak: Int workoutGoal: [DayOfWeekGraphQLEnum] encodedImage: String + fcmToken: String! giveaways: [Giveaway] + workoutReminders: [WorkoutReminder] } type Workout { @@ -294,3 +301,11 @@ type Workout { userId: Int! facilityId: Int! } + +type WorkoutReminder { + id: ID! + userId: Int! + daysOfWeek: [DayOfWeekGraphQLEnum] + reminderTime: String! + isActive: Boolean +} diff --git a/src/models/user.py b/src/models/user.py index 9e608a7..d55dec2 100644 --- a/src/models/user.py +++ b/src/models/user.py @@ -1,5 +1,5 @@ from sqlalchemy import Column, Integer, String, ARRAY, Enum -from sqlalchemy.orm import backref, relationship +from sqlalchemy.orm import relationship from src.database import Base from src.models.enums import DayOfWeekEnum @@ -30,4 +30,6 @@ class User(Base): active_streak = Column(Integer, nullable=True) max_streak = Column(Integer, nullable=True) workout_goal = Column(ARRAY(Enum(DayOfWeekEnum)), nullable=True) - encoded_image = Column(String, nullable=True) \ No newline at end of file + encoded_image = Column(String, nullable=True) + fcm_token = Column(String, nullable=False) + workout_reminders = relationship("WorkoutReminder") \ No newline at end of file diff --git a/src/models/workout_reminder.py b/src/models/workout_reminder.py index 07b7d8e..c354466 100644 --- a/src/models/workout_reminder.py +++ b/src/models/workout_reminder.py @@ -1,7 +1,7 @@ from sqlalchemy import Column, Integer, ForeignKey, TIME, Boolean from sqlalchemy.dialects.postgresql import ARRAY from sqlalchemy import Enum as SQLAEnum -from src.models.user import DayOfWeekEnum +from src.models.enums import DayOfWeekEnum from src.database import Base class WorkoutReminder(Base): diff --git a/src/schema.py b/src/schema.py index 9d4526d..611afdc 100644 --- a/src/schema.py +++ b/src/schema.py @@ -21,6 +21,7 @@ from src.models.giveaway import Giveaway as GiveawayModel from src.models.giveaway import GiveawayInstance as GiveawayInstanceModel from src.models.workout import Workout as WorkoutModel +from src.models.workout_reminder import WorkoutReminder as WorkoutReminderModel from src.models.report import Report as ReportModel from src.models.hourly_average_capacity import HourlyAverageCapacity as HourlyAverageCapacityModel from src.database import db_session @@ -248,6 +249,16 @@ class Meta: model = CapacityReminderModel +# MARK: - Workout Reminder + + +class WorkoutReminder(SQLAlchemyObjectType): + class Meta: + model = WorkoutReminderModel + + days_of_week = graphene.List(DayOfWeekGraphQLEnum) + + # MARK: - Query @@ -439,11 +450,12 @@ class Arguments: name = graphene.String(required=True) net_id = graphene.String(required=True) email = graphene.String(required=True) + fcm_token = graphene.String(required=True) encoded_image = graphene.String(required=False) Output = User - def mutate(self, info, name, net_id, email, encoded_image=None): + def mutate(self, info, name, net_id, email, fcm_token, encoded_image=None): # Check if a user with the given NetID already exists existing_user = db_session.query(UserModel).filter(UserModel.net_id == net_id).first() final_photo_url = None @@ -468,7 +480,7 @@ def mutate(self, info, name, net_id, email, encoded_image=None): print(f"Request failed: {e}") raise GraphQLError("Failed to upload photo.") - new_user = UserModel(name=name, net_id=net_id, email=email, encoded_image=final_photo_url) + new_user = UserModel(name=name, net_id=net_id, email=email, encoded_image=final_photo_url, fcm_token=fcm_token) db_session.add(new_user) db_session.commit() @@ -479,11 +491,12 @@ class Arguments: name = graphene.String(required=False) net_id = graphene.String(required=True) email = graphene.String(required=False) + fcm_token = graphene.String(required=False) encoded_image = graphene.String(required=False) Output = User - def mutate(self, info, net_id, name=None, email=None, encoded_image=None): + def mutate(self, info, net_id, name=None, email=None, fcm_token=None, encoded_image=None): existing_user = db_session.query(UserModel).filter(UserModel.net_id == net_id).first() if not existing_user: raise GraphQLError("User with given net id does not exist.") @@ -492,6 +505,8 @@ def mutate(self, info, net_id, name=None, email=None, encoded_image=None): existing_user.name = name if email is not None: existing_user.email = email + if fcm_token is not None: + existing_user.fcm_token = fcm_token if encoded_image is not None: upload_url = os.getenv("DIGITAL_OCEAN_URL") # Base URL for upload endpoint if not upload_url: @@ -784,6 +799,75 @@ def mutate(self, info, reminder_id): db_session.commit() return reminder + + +class CreateWorkoutReminder(graphene.Mutation): + class Arguments: + user_id = graphene.Int(required=True) + reminder_time = graphene.Time(required=True) + days_of_week = graphene.List(graphene.String, required=True) + + Output = WorkoutReminder + + def mutate(self, info, user_id, reminder_time, days_of_week): + # Validate user existence + user = db_session.query(UserModel).filter_by(id=user_id).first() + if not user: + raise GraphQLError("User not found.") + + # Validate days of the week + validated_workout_days = [] + for day in days_of_week: + try: + validated_workout_days.append(DayOfWeekGraphQLEnum[day.upper()].value) + except KeyError: + raise GraphQLError(f"Invalid day of the week: {day}") + + try: + reminder = WorkoutReminderModel( + user_id=user_id, reminder_time=reminder_time, days_of_week=validated_workout_days + ) + db_session.add(reminder) + db_session.commit() + except Exception as e: + db_session.rollback() + raise GraphQLError(f"Error creating workout reminder: {str(e)}") + + return reminder + + +class ToggleWorkoutReminder(graphene.Mutation): + class Arguments: + reminder_id = graphene.Int(required=True) + + Output = WorkoutReminder + + def mutate(self, info, reminder_id): + reminder = db_session.query(WorkoutReminderModel).filter_by(id=reminder_id).first() + if not reminder: + raise GraphQLError("Workout reminder not found.") + + reminder.is_active = not reminder.is_active + db_session.commit() + + return reminder + + +class DeleteWorkoutReminder(graphene.Mutation): + class Arguments: + reminder_id = graphene.Int(required=True) + + Output = WorkoutReminder + + def mutate(self, info, reminder_id): + reminder = db_session.query(WorkoutReminderModel).filter_by(id=reminder_id).first() + if not reminder: + raise GraphQLError("Workout reminder not found.") + + db_session.delete(reminder) + db_session.commit() + + return reminder class Mutation(graphene.ObjectType): @@ -801,6 +885,9 @@ class Mutation(graphene.ObjectType): create_capacity_reminder = CreateCapacityReminder.Field(description="Create a new capacity reminder.") toggle_capacity_reminder = ToggleCapacityReminder.Field(description="Toggle a capacity reminder on or off.") delete_capacity_reminder = DeleteCapacityReminder.Field(description="Delete a capacity reminder") + create_workout_reminder = CreateWorkoutReminder.Field(description="Create a new workout reminder.") + toggle_workout_reminder = ToggleWorkoutReminder.Field(description="Toggle a workout reminder on or off.") + delete_workout_reminder = DeleteWorkoutReminder.Field(description="Delete a workout reminder.") schema = graphene.Schema(query=Query, mutation=Mutation) diff --git a/src/utils/messaging.py b/src/utils/messaging.py index e23bee5..7ec4138 100644 --- a/src/utils/messaging.py +++ b/src/utils/messaging.py @@ -1,5 +1,10 @@ import logging +from datetime import datetime from firebase_admin import messaging +from src.database import db_session +from src.models.workout_reminder import WorkoutReminder +from src.models.user import User +from src.models.user import DayOfWeekEnum def send_capacity_reminder(topic_name, facility_name, current_percent): @@ -21,4 +26,45 @@ def send_capacity_reminder(topic_name, facility_name, current_percent): response = messaging.send(message) logging.info(f"Message sent to {topic_name}: {response}") except Exception as e: - logging.error(f"Error sending message to {topic_name}: {e}") \ No newline at end of file + logging.error(f"Error sending message to {topic_name}: {e}") + + +def send_workout_reminders(): + """ + Check for scheduled workout reminders and send notifications to users + whose reminders match the current day. + """ + current_date = datetime.now().date() + current_day_name = datetime.now().strftime("%A").upper() + + reminders = ( + db_session.query(WorkoutReminder) + .filter( + WorkoutReminder.is_active == True, WorkoutReminder.days_of_week.contains([DayOfWeekEnum[current_day_name]]) + ) + .all() + ) + + for reminder in reminders: + user = db_session.query(User).filter_by(id=reminder.user_id).first() + if user and user.fcm_token: + # Format scheduled time to send in the payload + scheduled_time = f"{current_date} {reminder.reminder_time}" + payload = messaging.Message( + data={ + "title": "Workout Reminder", + "message": "Don't forget to hit the gym today!", + "scheduledTime": scheduled_time, + }, + token=user.fcm_token, + ) + + print(payload.data) + + try: + response = messaging.send(payload) + print(f"Successfully sent notification for reminder {reminder.id}, response: {response}") + except Exception as e: + print(f"Error sending notification for reminder {reminder.id}: {e}") + else: + print(f"Invalid user or no FCM token.") From 28be9d9e30127ba2916bb9ebb2002c331e4d6ce4 Mon Sep 17 00:00:00 2001 From: Sophie Strausberg Date: Mon, 31 Mar 2025 23:36:44 -0400 Subject: [PATCH 3/7] update messaging.py --- src/utils/messaging.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/utils/messaging.py b/src/utils/messaging.py index 7ec4138..e49c8cf 100644 --- a/src/utils/messaging.py +++ b/src/utils/messaging.py @@ -59,8 +59,6 @@ def send_workout_reminders(): token=user.fcm_token, ) - print(payload.data) - try: response = messaging.send(payload) print(f"Successfully sent notification for reminder {reminder.id}, response: {response}") From c97e115daa3ff27e3411805cd67268d57cd57487 Mon Sep 17 00:00:00 2001 From: Sophie Strausberg Date: Tue, 1 Apr 2025 00:30:46 -0400 Subject: [PATCH 4/7] edit workout reminders --- app_factory.py | 7 ---- schema.graphql | 2 + src/schema.py | 87 ++++++++++++++++++++++++++++++------------ src/utils/messaging.py | 41 +------------------- 4 files changed, 65 insertions(+), 72 deletions(-) diff --git a/app_factory.py b/app_factory.py index f420313..32ef13d 100644 --- a/app_factory.py +++ b/app_factory.py @@ -128,7 +128,6 @@ def setup_scrapers(app): from src.scrapers.class_scraper import fetch_classes from src.scrapers.activities_scraper import fetch_activity from src.utils.utils import create_gym_table - from src.utils.messaging import send_workout_reminders from src.models.openhours import OpenHours import os @@ -209,12 +208,6 @@ def update_hourly_avg_capacity(): except Exception as e: logging.error(f"Error updating hourly average capacity for {current_day}, hour {current_hour}: {e}") - # Send workout reminders every morning at 12:00 AM - @scheduler.task("cron", id="send_reminders", hour=0, minute=0) - def workout_reminders(): - logging.info("Sending workout reminders...") - send_workout_reminders() - # We're now handling job execution logging within each task function # Initialize scheduler diff --git a/schema.graphql b/schema.graphql index ce883ed..eb461eb 100644 --- a/schema.graphql +++ b/schema.graphql @@ -216,6 +216,7 @@ type Mutation { deleteCapacityReminder(reminderId: Int!): CapacityReminder createWorkoutReminder(daysOfWeek: [String]!, reminderTime: Time!, userId: Int!): WorkoutReminder toggleWorkoutReminder(reminderId: Int!): WorkoutReminder + editWorkoutReminder(daysOfWeek: [String], reminderId: Int!, reminderTime: Time, userId: Int): WorkoutReminder deleteWorkoutReminder(reminderId: Int!): WorkoutReminder } @@ -256,6 +257,7 @@ type Query { getWorkoutGoals(id: Int!): [String] getUserStreak(id: Int!): JSONString getHourlyAverageCapacitiesByFacilityId(facilityId: Int): [HourlyAverageCapacity] + getWorkoutRemindersByUserId(userId: Int): [WorkoutReminder] } type RefreshAccessToken { diff --git a/src/schema.py b/src/schema.py index 611afdc..e29c840 100644 --- a/src/schema.py +++ b/src/schema.py @@ -240,7 +240,7 @@ def resolve_gym(self, info): query = Gym.get_query(info).filter(GymModel.id == self.gym_id).first() return query - + # MARK: - Capacity Reminder @@ -272,13 +272,18 @@ class Query(graphene.ObjectType): get_workouts_by_id = graphene.List(Workout, id=graphene.Int(), description="Get all of a user's workouts by ID.") activities = graphene.List(Activity) get_all_reports = graphene.List(Report, description="Get all reports.") - get_workout_goals = graphene.List(graphene.String, id=graphene.Int(required=True), - description="Get the workout goals of a user by ID.") - get_user_streak = graphene.Field(graphene.JSONString, id=graphene.Int( - required=True), description="Get the current and max workout streak of a user.") + get_workout_goals = graphene.List( + graphene.String, id=graphene.Int(required=True), description="Get the workout goals of a user by ID." + ) + get_user_streak = graphene.Field( + graphene.JSONString, + id=graphene.Int(required=True), + description="Get the current and max workout streak of a user.", + ) get_hourly_average_capacities_by_facility_id = graphene.List( HourlyAverageCapacity, facility_id=graphene.Int(), description="Get all facility hourly average capacities." ) + get_workout_reminders_by_user_id = graphene.List(WorkoutReminder, user_id=graphene.Int(), description="Get the workout reminders of a user by user ID.") def resolve_get_all_gyms(self, info): query = Gym.get_query(info) @@ -382,13 +387,16 @@ def resolve_get_user_streak(self, info, id): return {"active_streak": active_streak, "max_streak": max_streak} - def resolve_get_hourly_average_capacities_by_facility_id(self, info, facility_id): valid_facility_ids = [14492437, 8500985, 7169406, 10055021, 2323580, 16099753, 15446768, 12572681] if facility_id not in valid_facility_ids: raise GraphQLError("Invalid facility ID.") query = HourlyAverageCapacity.get_query(info).filter(HourlyAverageCapacityModel.facility_id == facility_id) return query.all() + + def resolve_get_workout_reminders_by_user_id(self, info, user_id): + query = WorkoutReminder.get_query(info).filter(WorkoutReminderModel.user_id == user_id) + return query.all() # MARK: - Mutation @@ -464,10 +472,7 @@ def mutate(self, info, name, net_id, email, fcm_token, encoded_image=None): if encoded_image: upload_url = os.getenv("DIGITAL_OCEAN_URL") - payload = { - "bucket": os.getenv("BUCKET_NAME"), - "image": encoded_image # Base64-encoded image string - } + payload = {"bucket": os.getenv("BUCKET_NAME"), "image": encoded_image} # Base64-encoded image string headers = {"Content-Type": "application/json"} try: response = requests.post(upload_url, json=payload, headers=headers) @@ -485,7 +490,8 @@ def mutate(self, info, name, net_id, email, fcm_token, encoded_image=None): db_session.commit() return new_user - + + class EditUser(graphene.Mutation): class Arguments: name = graphene.String(required=False) @@ -500,7 +506,7 @@ def mutate(self, info, net_id, name=None, email=None, fcm_token=None, encoded_im existing_user = db_session.query(UserModel).filter(UserModel.net_id == net_id).first() if not existing_user: raise GraphQLError("User with given net id does not exist.") - + if name is not None: existing_user.name = name if email is not None: @@ -514,12 +520,12 @@ def mutate(self, info, net_id, name=None, email=None, fcm_token=None, encoded_im payload = { "bucket": os.getenv("BUCKET_NAME", "DEV_BUCKET"), - "image": encoded_image # Base64-encoded image string + "image": encoded_image, # Base64-encoded image string } headers = {"Content-Type": "application/json"} - + print(f"Uploading image with payload: {payload}") - + try: response = requests.post(upload_url, json=payload, headers=headers) response.raise_for_status() @@ -536,6 +542,7 @@ def mutate(self, info, net_id, name=None, email=None, fcm_token=None, encoded_im db_session.commit() return existing_user + class EnterGiveaway(graphene.Mutation): class Arguments: user_net_id = graphene.String(required=True) @@ -799,7 +806,7 @@ def mutate(self, info, reminder_id): db_session.commit() return reminder - + class CreateWorkoutReminder(graphene.Mutation): class Arguments: @@ -823,15 +830,12 @@ def mutate(self, info, user_id, reminder_time, days_of_week): except KeyError: raise GraphQLError(f"Invalid day of the week: {day}") - try: - reminder = WorkoutReminderModel( - user_id=user_id, reminder_time=reminder_time, days_of_week=validated_workout_days - ) - db_session.add(reminder) - db_session.commit() - except Exception as e: - db_session.rollback() - raise GraphQLError(f"Error creating workout reminder: {str(e)}") + reminder = WorkoutReminderModel( + user_id=user_id, reminder_time=reminder_time, days_of_week=validated_workout_days + ) + + db_session.add(reminder) + db_session.commit() return reminder @@ -853,6 +857,38 @@ def mutate(self, info, reminder_id): return reminder +class EditWorkoutReminder(graphene.Mutation): + class Arguments: + reminder_id = graphene.Int(required=True) + user_id = graphene.Int(required=False) + reminder_time = graphene.Time(required=False) + days_of_week = graphene.List(graphene.String, required=False) + + Output = WorkoutReminder + + def mutate(self, info, reminder_id, user_id=None, reminder_time=None, days_of_week=None): + reminder = db_session.query(WorkoutReminderModel).filter_by(id=reminder_id).first() + if not reminder: + raise GraphQLError("Workout reminder not found.") + + if user_id is not None: + reminder.user_id = user_id + if reminder_time is not None: + reminder.reminder_time = reminder_time + if days_of_week is not None: + validated_days = [] + for day in days_of_week: + try: + validated_days.append(DayOfWeekGraphQLEnum[day.upper()].value) + except KeyError: + raise GraphQLError(f"Invalid day of the week: {day}") + reminder.days_of_week = validated_days + + db_session.commit() + + return reminder + + class DeleteWorkoutReminder(graphene.Mutation): class Arguments: reminder_id = graphene.Int(required=True) @@ -887,6 +923,7 @@ class Mutation(graphene.ObjectType): delete_capacity_reminder = DeleteCapacityReminder.Field(description="Delete a capacity reminder") create_workout_reminder = CreateWorkoutReminder.Field(description="Create a new workout reminder.") toggle_workout_reminder = ToggleWorkoutReminder.Field(description="Toggle a workout reminder on or off.") + edit_workout_reminder = EditWorkoutReminder.Field(description="Edit a workout reminder.") delete_workout_reminder = DeleteWorkoutReminder.Field(description="Delete a workout reminder.") diff --git a/src/utils/messaging.py b/src/utils/messaging.py index e49c8cf..3810940 100644 --- a/src/utils/messaging.py +++ b/src/utils/messaging.py @@ -26,43 +26,4 @@ def send_capacity_reminder(topic_name, facility_name, current_percent): response = messaging.send(message) logging.info(f"Message sent to {topic_name}: {response}") except Exception as e: - logging.error(f"Error sending message to {topic_name}: {e}") - - -def send_workout_reminders(): - """ - Check for scheduled workout reminders and send notifications to users - whose reminders match the current day. - """ - current_date = datetime.now().date() - current_day_name = datetime.now().strftime("%A").upper() - - reminders = ( - db_session.query(WorkoutReminder) - .filter( - WorkoutReminder.is_active == True, WorkoutReminder.days_of_week.contains([DayOfWeekEnum[current_day_name]]) - ) - .all() - ) - - for reminder in reminders: - user = db_session.query(User).filter_by(id=reminder.user_id).first() - if user and user.fcm_token: - # Format scheduled time to send in the payload - scheduled_time = f"{current_date} {reminder.reminder_time}" - payload = messaging.Message( - data={ - "title": "Workout Reminder", - "message": "Don't forget to hit the gym today!", - "scheduledTime": scheduled_time, - }, - token=user.fcm_token, - ) - - try: - response = messaging.send(payload) - print(f"Successfully sent notification for reminder {reminder.id}, response: {response}") - except Exception as e: - print(f"Error sending notification for reminder {reminder.id}: {e}") - else: - print(f"Invalid user or no FCM token.") + logging.error(f"Error sending message to {topic_name}: {e}") \ No newline at end of file From aae8f22b908bc56ea336daf30ab9114917945047 Mon Sep 17 00:00:00 2001 From: Sophie Strausberg Date: Tue, 1 Apr 2025 00:32:54 -0400 Subject: [PATCH 5/7] edit --- src/utils/messaging.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/utils/messaging.py b/src/utils/messaging.py index 3810940..e23bee5 100644 --- a/src/utils/messaging.py +++ b/src/utils/messaging.py @@ -1,10 +1,5 @@ import logging -from datetime import datetime from firebase_admin import messaging -from src.database import db_session -from src.models.workout_reminder import WorkoutReminder -from src.models.user import User -from src.models.user import DayOfWeekEnum def send_capacity_reminder(topic_name, facility_name, current_percent): From 8ec276290a70686bf3555595b7f4fcd1f32b262c Mon Sep 17 00:00:00 2001 From: Sophie Strausberg Date: Tue, 1 Apr 2025 00:36:49 -0400 Subject: [PATCH 6/7] edit --- app_factory.py | 1 - 1 file changed, 1 deletion(-) diff --git a/app_factory.py b/app_factory.py index 32ef13d..a7ef49c 100644 --- a/app_factory.py +++ b/app_factory.py @@ -6,7 +6,6 @@ from flask import Flask, render_template from graphene import Schema from graphql.utils import schema_printer -from src.models.workout_reminder import WorkoutReminder from src.database import db_session, init_db from src.database import Base as db from src.database import db_url, db_user, db_password, db_name, db_host, db_port From 74662f26c99a3b27d88e56880f93687a560779a0 Mon Sep 17 00:00:00 2001 From: Sophie Strausberg Date: Tue, 1 Apr 2025 01:25:14 -0400 Subject: [PATCH 7/7] migration --- .../723cd68cf306_add_workout_reminders.py | 77 +++++++++++++++++++ 1 file changed, 77 insertions(+) create mode 100644 migrations/versions/723cd68cf306_add_workout_reminders.py diff --git a/migrations/versions/723cd68cf306_add_workout_reminders.py b/migrations/versions/723cd68cf306_add_workout_reminders.py new file mode 100644 index 0000000..827f9b4 --- /dev/null +++ b/migrations/versions/723cd68cf306_add_workout_reminders.py @@ -0,0 +1,77 @@ +"""add workout reminders + +Revision ID: 723cd68cf306 +Revises: 7a3c14648e56 +Create Date: 2025-04-01 01:06:38.476352 + +""" +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import postgresql + +# revision identifiers, used by Alembic. +revision = '723cd68cf306' +down_revision = '7a3c14648e56' +branch_labels = None +depends_on = None + +def upgrade(): + + op.execute(""" + DO $$ + BEGIN + IF NOT EXISTS ( + SELECT 1 FROM information_schema.tables WHERE table_name = 'workout_reminder' + ) THEN + CREATE TABLE workout_reminder ( + id SERIAL PRIMARY KEY, + user_id INTEGER NOT NULL REFERENCES users(id), + days_of_week dayofweekenum[] NOT NULL, + reminder_time TIME NOT NULL, + is_active BOOLEAN DEFAULT TRUE + ); + END IF; + END + $$; + """) + + op.execute(""" + DO $$ + BEGIN + IF NOT EXISTS ( + SELECT 1 FROM information_schema.columns + WHERE table_name = 'users' AND column_name = 'fcm_token' + ) THEN + ALTER TABLE users ADD COLUMN fcm_token VARCHAR NOT NULL DEFAULT 'unset'; + ALTER TABLE users ALTER COLUMN fcm_token DROP DEFAULT; + END IF; + END + $$; + """) + + +def downgrade(): + op.execute(""" + DO $$ + BEGIN + IF EXISTS ( + SELECT 1 FROM information_schema.tables WHERE table_name = 'workout_reminder' + ) THEN + DROP TABLE workout_reminder; + END IF; + END + $$; + """) + + op.execute(""" + DO $$ + BEGIN + IF EXISTS ( + SELECT 1 FROM information_schema.columns + WHERE table_name = 'users' AND column_name = 'fcm_token' + ) THEN + ALTER TABLE users DROP COLUMN fcm_token; + END IF; + END + $$; + """)