From acbdad558d007d0824d99ebc3946c848e6e225c2 Mon Sep 17 00:00:00 2001 From: Sophie Strausberg Date: Wed, 19 Mar 2025 15:02:44 -0400 Subject: [PATCH 01/20] initial commit --- app_factory.py | 18 +++- schema.graphql | 30 +++++++ src/models/capacity_reminder.py | 26 ++++++ src/models/enums.py | 16 +++- src/schema.py | 130 ++++++++++++++++++++++++++++- src/scrapers/capacities_scraper.py | 64 ++++++++++++-- src/utils/messaging.py | 28 +++++++ 7 files changed, 301 insertions(+), 11 deletions(-) create mode 100644 src/models/capacity_reminder.py create mode 100644 src/utils/messaging.py diff --git a/app_factory.py b/app_factory.py index 9754b75..198a41e 100644 --- a/app_factory.py +++ b/app_factory.py @@ -1,11 +1,11 @@ import logging from datetime import timedelta, timezone from flask_jwt_extended import JWTManager +from src.utils.constants import SERVICE_ACCOUNT_PATH from datetime import datetime from flask import Flask, render_template from graphene import Schema from graphql.utils import schema_printer -from src.utils.constants import JWT_SECRET_KEY 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 @@ -14,6 +14,20 @@ from flasgger import Swagger from flask_graphql import GraphQLView from src.models.token_blacklist import TokenBlocklist +import firebase_admin +from firebase_admin import credentials + +def initialize_firebase(): + if not firebase_admin._apps: + if SERVICE_ACCOUNT_PATH: + cred = credentials.Certificate(SERVICE_ACCOUNT_PATH) + firebase_app = firebase_admin.initialize_app(cred) + else: + raise ValueError("GOOGLE_SERVICE_ACCOUNT_PATH environment variable not set.") + else: + firebase_app = firebase_admin.get_app() + logging.info("Firebase app created...") + return firebase_app # Set up logging at module level @@ -32,6 +46,7 @@ def create_app(run_migrations=False): Configured Flask application """ logger.info("Initializing application") + initialize_firebase() # Create and configure Flask app app = Flask(__name__) @@ -56,7 +71,6 @@ def create_app(run_migrations=False): schema = Schema(query=Query, mutation=Mutation) swagger = Swagger(app) - app.config["JWT_SECRET_KEY"] = JWT_SECRET_KEY app.config["JWT_ACCESS_TOKEN_EXPIRES"] = timedelta(hours=1) app.config["JWT_REFRESH_TOKEN_EXPIRES"] = timedelta(days=30) diff --git a/schema.graphql b/schema.graphql index bb22812..2ddd396 100644 --- a/schema.graphql +++ b/schema.graphql @@ -38,6 +38,23 @@ type Capacity { updated: Int! } +type CapacityReminder { + id: ID! + fcmToken: String! + gyms: [CapacityReminderGym]! + capacityThreshold: Int! + daysOfWeek: [DayOfWeekEnum]! + isActive: Boolean +} + +enum CapacityReminderGym { + TEAGLEUP + TEAGLEDOWN + HELENNEWMAN + TONIMORRISON + NOYES +} + type Class { id: ID! name: String! @@ -71,6 +88,16 @@ type CreateReport { scalar DateTime +enum DayOfWeekEnum { + MONDAY + TUESDAY + WEDNESDAY + THURSDAY + FRIDAY + SATURDAY + SUNDAY +} + enum DayOfWeekGraphQLEnum { MONDAY TUESDAY @@ -183,6 +210,9 @@ type Mutation { refreshAccessToken: RefreshAccessToken createReport(createdAt: DateTime!, description: String!, gymId: Int!, issue: String!): CreateReport deleteUser(userId: Int!): User + createCapacityReminder(capacityPercent: Int!, daysOfWeek: [String]!, fcmToken: String!, gyms: [String]!): CapacityReminder + toggleCapacityReminder(reminderId: Int!): CapacityReminder + deleteCapacityReminder(reminderId: Int!): CapacityReminder } type OpenHours { diff --git a/src/models/capacity_reminder.py b/src/models/capacity_reminder.py new file mode 100644 index 0000000..aa2046a --- /dev/null +++ b/src/models/capacity_reminder.py @@ -0,0 +1,26 @@ +from sqlalchemy import Column, Integer, ForeignKey, ARRAY, Boolean, Table, String +from sqlalchemy.orm import relationship +from sqlalchemy import Enum as SQLAEnum +from src.models.enums import DayOfWeekEnum, CapacityReminderGym +from src.database import Base + +class CapacityReminder(Base): + """ + A capacity reminder for an Uplift user. + Attributes: + - `id` The ID of the capacity reminder. + - `user_id` The ID of the user who owns this reminder. + - `gyms` The list of gyms the user wants to monitor for capacity. + - `capacity_threshold` Notify user when gym capacity dips below this percentage. + - `days_of_week` The days of the week when the reminder is active. + - `is_active` Whether the reminder is currently active (default is True). + """ + + __tablename__ = "capacity_reminder" + + id = Column(Integer, primary_key=True) + fcm_token = Column(String, nullable=False) + gyms = Column(ARRAY(SQLAEnum(CapacityReminderGym)), nullable=False) + capacity_threshold = Column(Integer, nullable=False) + days_of_week = Column(ARRAY(SQLAEnum(DayOfWeekEnum)), nullable=False) + is_active = Column(Boolean, default=True) diff --git a/src/models/enums.py b/src/models/enums.py index b88465b..6feee91 100644 --- a/src/models/enums.py +++ b/src/models/enums.py @@ -19,4 +19,18 @@ class DayOfWeekGraphQLEnum(GrapheneEnum): THURSDAY = "THURSDAY" FRIDAY = "FRIDAY" SATURDAY = "SATURDAY" - SUNDAY = "SUNDAY" \ No newline at end of file + SUNDAY = "SUNDAY" + +class CapacityReminderGym(enum.Enum): + TEAGLEUP = "TEAGLEUP" + TEAGLEDOWN = "TEAGLEDOWN" + HELENNEWMAN = "HELENNEWMAN" + TONIMORRISON = "TONIMORRISON" + NOYES = "NOYES" + +class CapacityReminderGymGraphQLEnum(GrapheneEnum): + TEAGLEUP = "TEAGLEUP" + TEAGLEDOWN = "TEAGLEDOWN" + HELENNEWMAN = "HELENNEWMAN" + TONIMORRISON = "TONIMORRISON" + NOYES = "NOYES" \ No newline at end of file diff --git a/src/schema.py b/src/schema.py index 08aa027..0cb5a28 100644 --- a/src/schema.py +++ b/src/schema.py @@ -6,6 +6,7 @@ from graphene_sqlalchemy import SQLAlchemyObjectType from graphql import GraphQLError from src.models.capacity import Capacity as CapacityModel +from src.models.capacity_reminder import CapacityReminder as CapacityReminderModel from src.models.facility import Facility as FacilityModel from src.models.gym import Gym as GymModel from src.models.openhours import OpenHours as OpenHoursModel @@ -16,13 +17,14 @@ from src.models.classes import ClassInstance as ClassInstanceModel from src.models.token_blacklist import TokenBlocklist from src.models.user import User as UserModel -from src.models.enums import DayOfWeekGraphQLEnum +from src.models.enums import DayOfWeekGraphQLEnum, CapacityReminderGymGraphQLEnum 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.report import Report as ReportModel from src.models.hourly_average_capacity import HourlyAverageCapacity as HourlyAverageCapacityModel from src.database import db_session +from firebase_admin import messaging # MARK: - Gym @@ -235,6 +237,14 @@ def resolve_gym(self, info): query = Gym.get_query(info).filter(GymModel.id == self.gym_id).first() return query + +# MARK: - Capacity Reminder + + +class CapacityReminder(SQLAlchemyObjectType): + class Meta: + model = CapacityReminderModel + # MARK: - Query @@ -595,6 +605,121 @@ def mutate(self, info, user_id): return user +class CreateCapacityReminder(graphene.Mutation): + class Arguments: + fcm_token = graphene.String(required=True) + gyms = graphene.List(graphene.String, required=True) + days_of_week = graphene.List(graphene.String, required=True) + capacity_percent = graphene.Int(required=True) + + Output = CapacityReminder # Use the renamed GraphQL type + + def mutate(self, info, fcm_token, days_of_week, gyms, capacity_percent): + if capacity_percent < 0: + raise GraphQLError("Capacity percent must be a non-negative integer.") + + # 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}") + + # Validate gym existence + valid_gyms = [] + for gym in gyms: + try: + valid_gyms.append(CapacityReminderGymGraphQLEnum[gym].value) + except KeyError: + raise GraphQLError(f"Invalid gym: {gym}") + + # Subscribe to Firebase topics for each gym and day + for gym in valid_gyms: + for day in validated_workout_days: + topic_name = f"{gym}_{day}_{capacity_percent}" + try: + messaging.subscribe_to_topic(fcm_token, topic_name) + except Exception as error: + raise GraphQLError(f"Error subscribing to topic for {topic_name}: {error}") + + reminder = CapacityReminderModel( + fcm_token=fcm_token, + gyms=valid_gyms, + capacity_threshold=capacity_percent, + days_of_week=validated_workout_days, + ) + db_session.add(reminder) + db_session.commit() + print(reminder.gyms) + + return reminder + + +class ToggleCapacityReminder(graphene.Mutation): + class Arguments: + reminder_id = graphene.Int(required=True) + + Output = CapacityReminder + + def mutate(self, info, reminder_id): + reminder = db_session.query(CapacityReminderModel).filter_by(id=reminder_id).first() + if not reminder: + raise GraphQLError("CapacityReminder not found.") + + # Prepare topics based on reminder's gym_id and days_of_week + topics = [ + f"{gym}_{day}_{reminder.capacity_threshold}" for gym in reminder.gyms for day in reminder.days_of_week + ] + + if reminder.is_active: + # Toggle to inactive and unsubscribe + for topic in topics: + try: + messaging.unsubscribe_from_topic(reminder.fcm_token, topic) + except Exception as error: + raise GraphQLError(f"Error unsubscribing from topic: {error}") + else: + # Toggle to active and resubscribe + for topic in topics: + try: + messaging.subscribe_to_topic(reminder.fcm_token, topic) + except Exception as error: + raise GraphQLError(f"Error subscribing to topic: {error}") + + reminder.is_active = not reminder.is_active + db_session.commit() + + return reminder + + +class DeleteCapacityReminder(graphene.Mutation): + class Arguments: + reminder_id = graphene.Int(required=True) + + Output = CapacityReminder + + def mutate(self, info, reminder_id): + reminder = db_session.query(CapacityReminderModel).filter_by(id=reminder_id).first() + if not reminder: + raise GraphQLError("CapacityReminder not found.") + + topics = [ + f"{gym}_{day}_{reminder.capacity_threshold}" for gym in reminder.gyms for day in reminder.days_of_week + ] + + for topic in topics: + try: + messaging.unsubscribe_from_topic(reminder.fcm_token, topic) + except Exception as error: + raise GraphQLError(f"Error unsubscribing from topic {topic}: {error}") + + db_session.delete(reminder) + db_session.commit() + + return reminder + + class Mutation(graphene.ObjectType): create_giveaway = CreateGiveaway.Field(description="Creates a new giveaway.") create_user = CreateUser.Field(description="Creates a new user.") @@ -606,6 +731,9 @@ class Mutation(graphene.ObjectType): refresh_access_token = RefreshAccessToken.Field(description="Refreshes the access token.") create_report = CreateReport.Field(description="Creates a new report.") delete_user = DeleteUserById.Field(description="Deletes a user by ID.") + 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") schema = graphene.Schema(query=Query, mutation=Mutation) diff --git a/src/scrapers/capacities_scraper.py b/src/scrapers/capacities_scraper.py index 8e58857..eaa1ba4 100644 --- a/src/scrapers/capacities_scraper.py +++ b/src/scrapers/capacities_scraper.py @@ -3,9 +3,10 @@ from collections import namedtuple from datetime import datetime from src.database import db_session +from src.utils.messaging import send_capacity_reminder from src.models.capacity import Capacity from src.models.hourly_average_capacity import HourlyAverageCapacity -from src.models.enums import DayOfWeekEnum +from src.models.enums import DayOfWeekEnum, CapacityReminderGym from src.utils.constants import ( C2C_URL, CRC_URL_NEW, @@ -52,13 +53,12 @@ def fetch_capacities_old(): # Add to sheets add_single_capacity(count, facility_id, percent, updated) + # New scraper from new API using CRC_URL_NEW def fetch_capacities(): """Fetch capacities from the new JSON API endpoint.""" try: - headers = { - "user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.9; rv:32.0) Gecko/20100101 Firefox/32.0" - } + headers = {"user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.9; rv:32.0) Gecko/20100101 Firefox/32.0"} response = requests.get(CRC_URL_NEW, headers=headers) facilities = response.json() @@ -82,6 +82,26 @@ def fetch_capacities(): percent = count / total_capacity if total_capacity > 0 else 0.0 updated = datetime.strptime(updated_str.split(".")[0], "%Y-%m-%dT%H:%M:%S") + gym_mapping = { + "HNHFITNESSCENTER": CapacityReminderGym.HELENNEWMAN, + "NOYESFITNESSCENTER": CapacityReminderGym.NOYES, + "TEAGLEDOWNFITNESSCENTER": CapacityReminderGym.TEAGLEDOWN, + "TEAGLEUPFITNESSCENTER": CapacityReminderGym.TEAGLEUP, + "MORRISONFITNESSCENTER": CapacityReminderGym.TONIMORRISON, + } + + last_percent = Capacity.query.filter_by(facility_id=facility_id).first() + if last_percent: + last_percent = last_percent.percent + else: + last_percent = 0 + + topic_name = db_name.replace(" ", "").upper() + + if topic_name in gym_mapping: + topic_enum = gym_mapping[topic_name] + check_and_send_capacity_reminders(topic_enum.name, db_name, percent, last_percent) + add_single_capacity(count, facility_id, percent, updated) except Exception as e: @@ -91,6 +111,7 @@ def fetch_capacities(): print(f"Error fetching capacities: {str(e)}") raise + def add_single_capacity(count, facility_id, percent, updated): """ Add a single capacity to the database. @@ -136,7 +157,15 @@ def update_hourly_capacity(curDay, curHour): for capacity in currentCapacities: try: - hourly_average_capacity = db_session.query(HourlyAverageCapacity).filter(HourlyAverageCapacity.facility_id == capacity.facility_id, HourlyAverageCapacity.day_of_week == DayOfWeekEnum[curDay].value, HourlyAverageCapacity.hour_of_day == curHour).first() + hourly_average_capacity = ( + db_session.query(HourlyAverageCapacity) + .filter( + HourlyAverageCapacity.facility_id == capacity.facility_id, + HourlyAverageCapacity.day_of_week == DayOfWeekEnum[curDay].value, + HourlyAverageCapacity.hour_of_day == curHour, + ) + .first() + ) if hourly_average_capacity is not None: print("updating average") @@ -148,7 +177,7 @@ def update_hourly_capacity(curDay, curHour): average_percent=capacity.percent, hour_of_day=curHour, day_of_week=DayOfWeekEnum[curDay].value, - history=[capacity.percent] + history=[capacity.percent], ) db_session.merge(hourly_average_capacity) @@ -156,4 +185,25 @@ def update_hourly_capacity(curDay, curHour): except Exception as e: print(f"Error updating hourly average: {e}") - + + +def check_and_send_capacity_reminders(facility_name, readable_name, current_percent, last_percent): + """ + Send notifications to topic if the current capacity dips below the relevant thresholds. + """ + + current_percent_int = int(current_percent * 100) # Convert to integer percentage + last_percent_int = int(last_percent * 100) + + current_day_name = datetime.now().strftime("%A").upper() + + # Define threshold levels + thresholds = [0, 10, 20, 30, 40, 50, 60, 70, 80, 90] + + # Find the lowest threshold that was crossed + crossed_thresholds = [p for p in thresholds if last_percent_int > p >= current_percent_int] + + if crossed_thresholds: + lowest_threshold = min(crossed_thresholds) # Get the lowest threshold crossed + topic_name = f"{facility_name}_{current_day_name}_{lowest_threshold}" + send_capacity_reminder(topic_name, readable_name, current_percent_int) diff --git a/src/utils/messaging.py b/src/utils/messaging.py new file mode 100644 index 0000000..72c41a9 --- /dev/null +++ b/src/utils/messaging.py @@ -0,0 +1,28 @@ +import logging +from datetime import datetime +from firebase_admin import messaging +from src.database import db_session +from src.models.user import User +from src.models.enums import DayOfWeekEnum # Ensure the DayOfWeekEnum is imported + + +def send_capacity_reminder(topic_name, facility_name, current_percent): + """ + Send a capacity reminder to the user. + Parameters: + - `topic_name`: The topic to send the notification to. + - `facility_name`: The gym facility's name. + - `current_percent`: The current capacity percentage. + """ + message = messaging.Message( + notification=messaging.Notification( + title="Gym Capacity Update", body=f"The capacity for {facility_name} is now below {current_percent}%." + ), + topic=topic_name, + ) + + try: + 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 From 447c1f91f5151b6009387c63672c533ad285ed01 Mon Sep 17 00:00:00 2001 From: Sophie Strausberg Date: Wed, 19 Mar 2025 18:09:36 -0400 Subject: [PATCH 02/20] add migration file --- ...a3c14648e56_add_capacity_reminder_model.py | 70 +++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 migrations/versions/7a3c14648e56_add_capacity_reminder_model.py diff --git a/migrations/versions/7a3c14648e56_add_capacity_reminder_model.py b/migrations/versions/7a3c14648e56_add_capacity_reminder_model.py new file mode 100644 index 0000000..f852cb1 --- /dev/null +++ b/migrations/versions/7a3c14648e56_add_capacity_reminder_model.py @@ -0,0 +1,70 @@ +"""add capacity reminder model + +Revision ID: 7a3c14648e56 +Revises: add99ce06ff5 +Create Date: 2025-03-19 17:32:02.592027 + +""" +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import postgresql + + +# revision identifiers, used by Alembic. +revision = '7a3c14648e56' +down_revision = 'add99ce06ff5' +branch_labels = None +depends_on = None + +capacity_reminder_gym_enum = postgresql.ENUM( + 'TEAGLEUP', 'TEAGLEDOWN', 'HELENNEWMAN', 'TONIMORRISON', 'NOYES', + name='capacityremindergym', create_type=False +) + +day_of_week_enum = postgresql.ENUM( + 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday', + name='dayofweekenum', create_type=False +) + +def upgrade(): + # ### Ensure the table does not exist before creating ### + op.execute(""" + DO $$ + BEGIN + IF NOT EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'capacity_reminder') THEN + CREATE TABLE capacity_reminder ( + id SERIAL PRIMARY KEY, + fcm_token VARCHAR NOT NULL, + gyms capacityremindergym[] NOT NULL, + capacity_threshold INTEGER NOT NULL, + days_of_week dayofweekenum[] NOT NULL, + is_active BOOLEAN DEFAULT TRUE NOT NULL + ); + END IF; + END $$; + """) + + +def downgrade(): + # ### Drop the table safely if it exists ### + op.execute(""" + DO $$ + BEGIN + IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'capacity_reminder') THEN + DROP TABLE capacity_reminder; + END IF; + END $$; + """) + + op.execute(""" + DO $$ + BEGIN + IF EXISTS (SELECT 1 FROM pg_type WHERE typname = 'capacityremindergym') + AND NOT EXISTS ( + SELECT 1 FROM pg_attribute + WHERE atttypid = (SELECT oid FROM pg_type WHERE typname = 'capacityremindergym') + ) THEN + DROP TYPE capacityremindergym CASCADE; + END IF; + END $$ LANGUAGE plpgsql; + """) From d5c58545a3b7f433f62b84567fb5c3932ca61542 Mon Sep 17 00:00:00 2001 From: Joshua Dirga Date: Sun, 23 Mar 2025 13:52:16 -0400 Subject: [PATCH 03/20] Added friends storing --- schema.graphql | 5 ++++ src/models/user.py | 40 +++++++++++++++++++++++++++-- src/schema.py | 64 ++++++++++++++++++++++++++++++++++++++++++---- 3 files changed, 102 insertions(+), 7 deletions(-) diff --git a/schema.graphql b/schema.graphql index a4aeaea..055192d 100644 --- a/schema.graphql +++ b/schema.graphql @@ -184,6 +184,8 @@ type Mutation { refreshAccessToken: RefreshAccessToken createReport(createdAt: DateTime!, description: String!, gymId: Int!, issue: String!): CreateReport deleteUser(userId: Int!): User + addFriend(friendNetId: String!, userNetId: String!): User + removeFriend(friendNetId: String!, userNetId: String!): User } type OpenHours { @@ -215,6 +217,7 @@ enum PriceType { type Query { getAllGyms: [Gym] getUserByNetId(netId: String): [User] + getUsersFriends(id: Int): [User] getUsersByGiveawayId(id: Int): [User] getWeeklyWorkoutDays(id: Int): [String] getWorkoutsById(id: Int): [Workout] @@ -256,6 +259,8 @@ type User { workoutGoal: [DayOfWeekGraphQLEnum] encodedImage: String giveaways: [Giveaway] + friends: [User] + friendedBy: [User] } type Workout { diff --git a/src/models/user.py b/src/models/user.py index 9e608a7..20b703c 100644 --- a/src/models/user.py +++ b/src/models/user.py @@ -1,4 +1,4 @@ -from sqlalchemy import Column, Integer, String, ARRAY, Enum +from sqlalchemy import Column, Integer, String, ARRAY, Enum, Table, ForeignKey from sqlalchemy.orm import backref, relationship from src.database import Base from src.models.enums import DayOfWeekEnum @@ -22,6 +22,13 @@ class User(Base): __tablename__ = "users" + friendship = Table( + "friends", + Base.metadata, + Column("user_id", Integer, ForeignKey("users.id"), primary_key=True), + Column("friend_id", Integer, ForeignKey("users.id"), primary_key=True), + ) + id = Column(Integer, primary_key=True) email = Column(String, nullable=True) giveaways = relationship("Giveaway", secondary="giveaway_instance", back_populates="users") @@ -30,4 +37,33 @@ 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) + + friends = relationship( + 'User', + secondary=friendship, + primaryjoin=(friendship.c.user_id == id), + secondaryjoin=(friendship.c.friend_id == id), + backref=backref('friended_by', lazy='dynamic'), + lazy='dynamic' + ) + + def add_friend(self, friend): + if friend not in self.friends: + self.friends.append(friend) + + def remove_friend(self, friend): + if friend in self.friends: + self.friends.remove(friend) + + def get_friends(self): + # Get direct friends (users this user has added) + direct_friends = self.friends.all() + + # Get users who have added this user as a friend + reverse_friends = self.friended_by.all() + + # Combine both lists and remove duplicates using set + all_friends = list(set(direct_friends + reverse_friends)) + + return all_friends diff --git a/src/schema.py b/src/schema.py index e3d27bb..2b7becb 100644 --- a/src/schema.py +++ b/src/schema.py @@ -244,6 +244,7 @@ def resolve_gym(self, info): class Query(graphene.ObjectType): get_all_gyms = graphene.List(Gym, description="Get all gyms.") get_user_by_net_id = graphene.List(User, net_id=graphene.String(), description="Get user by Net ID.") + get_users_friends = graphene.List(User, id=graphene.Int(), description="Get all friends of a user by ID.") get_users_by_giveaway_id = graphene.List(User, id=graphene.Int(), description="Get all users given a giveaway ID.") get_weekly_workout_days = graphene.List( graphene.String, id=graphene.Int(), description="Get the days a user worked out for the current week." @@ -273,6 +274,13 @@ def resolve_get_user_by_net_id(self, info, net_id): raise GraphQLError("User with the given Net ID does not exist.") return user + def resolve_get_users_friends(self, info, id): + user = User.get_query(info).filter(UserModel.id == id).first() + if not user: + raise GraphQLError("User with the given ID does not exist.") + friends = user.get_friends() + return friends + def resolve_get_users_by_giveaway_id(self, info, id): entries = GiveawayInstance.get_query(info).filter(GiveawayInstanceModel.giveaway_id == id).all() users = [User.get_query(info).filter(UserModel.id == entry.user_id).first() for entry in entries] @@ -361,7 +369,6 @@ 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: @@ -463,7 +470,7 @@ def mutate(self, info, name, net_id, email, encoded_image=None): db_session.commit() return new_user - + class EditUser(graphene.Mutation): class Arguments: name = graphene.String(required=False) @@ -477,7 +484,7 @@ def mutate(self, info, net_id, name=None, email=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.") - + if name is not None: existing_user.name = name if email is not None: @@ -492,9 +499,9 @@ def mutate(self, info, net_id, name=None, email=None, encoded_image=None): "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() @@ -560,6 +567,51 @@ def mutate(self, info, name): db_session.commit() return giveaway +class AddFriend(graphene.Mutation): + class Arguments: + user_net_id = graphene.String(required=True, description="The Net ID of the user.") + friend_net_id = graphene.String(required=True, description="The Net ID of the friend to add.") + + Output = User + + def mutate(self, info, user_net_id, friend_net_id): + user = User.get_query(info).filter(UserModel.net_id == user_net_id).first() + if not user: + raise GraphQLError("User with given NetID does not exist.") + + friend = User.get_query(info).filter(UserModel.net_id == friend_net_id).first() + if not friend: + raise GraphQLError("Friend with given NetID does not exist.") + + # Add friend + if friend not in user.friends: + user.add_friend(friend) + + db_session.commit() + return user + +class RemoveFriend(graphene.Mutation): + class Arguments: + user_net_id = graphene.String(required=True, description="The Net ID of the user.") + friend_net_id = graphene.String(required=True, description="The Net ID of the friend to remove.") + + Output = User + + def mutate(self, info, user_net_id, friend_net_id): + user = User.get_query(info).filter(UserModel.net_id == user_net_id).first() + if not user: + raise GraphQLError("User with given NetID does not exist.") + + friend = User.get_query(info).filter(UserModel.net_id == friend_net_id).first() + if not friend: + raise GraphQLError("Friend with given NetID does not exist.") + + # Remove friend + if friend in user.friends: + user.remove_friend(friend) + + db_session.commit() + return user class SetWorkoutGoals(graphene.Mutation): class Arguments: @@ -675,6 +727,8 @@ class Mutation(graphene.ObjectType): refresh_access_token = RefreshAccessToken.Field(description="Refreshes the access token.") create_report = CreateReport.Field(description="Creates a new report.") delete_user = DeleteUserById.Field(description="Deletes a user by ID.") + add_friend = AddFriend.Field(description="Adds a friend to a user.") + remove_friend = RemoveFriend.Field(description="Removes a friend from a user.") schema = graphene.Schema(query=Query, mutation=Mutation) From ce5b41543ddec4c7ea3ae2b8507efb5853cb9ea8 Mon Sep 17 00:00:00 2001 From: Sophie Strausberg Date: Thu, 27 Mar 2025 21:37:16 -0400 Subject: [PATCH 04/20] updates --- ...a3c14648e56_add_capacity_reminder_model.py | 2 - src/schema.py | 10 ++-- src/scrapers/capacities_scraper.py | 49 +++++++++++-------- src/utils/messaging.py | 4 -- 4 files changed, 33 insertions(+), 32 deletions(-) diff --git a/migrations/versions/7a3c14648e56_add_capacity_reminder_model.py b/migrations/versions/7a3c14648e56_add_capacity_reminder_model.py index f852cb1..2370e9a 100644 --- a/migrations/versions/7a3c14648e56_add_capacity_reminder_model.py +++ b/migrations/versions/7a3c14648e56_add_capacity_reminder_model.py @@ -27,7 +27,6 @@ ) def upgrade(): - # ### Ensure the table does not exist before creating ### op.execute(""" DO $$ BEGIN @@ -46,7 +45,6 @@ def upgrade(): def downgrade(): - # ### Drop the table safely if it exists ### op.execute(""" DO $$ BEGIN diff --git a/src/schema.py b/src/schema.py index 68b52c3..b4752b4 100644 --- a/src/schema.py +++ b/src/schema.py @@ -680,11 +680,11 @@ class Arguments: days_of_week = graphene.List(graphene.String, required=True) capacity_percent = graphene.Int(required=True) - Output = CapacityReminder # Use the renamed GraphQL type + Output = CapacityReminder def mutate(self, info, fcm_token, days_of_week, gyms, capacity_percent): - if capacity_percent < 0: - raise GraphQLError("Capacity percent must be a non-negative integer.") + if capacity_percent not in range(0, 91, 10): + raise GraphQLError("Capacity percent must be an interval of 10 from 0-90.") # Validate days of the week validated_workout_days = [] @@ -694,7 +694,7 @@ def mutate(self, info, fcm_token, days_of_week, gyms, capacity_percent): except KeyError: raise GraphQLError(f"Invalid day of the week: {day}") - # Validate gym existence + # Validate gyma valid_gyms = [] for gym in gyms: try: @@ -719,7 +719,6 @@ def mutate(self, info, fcm_token, days_of_week, gyms, capacity_percent): ) db_session.add(reminder) db_session.commit() - print(reminder.gyms) return reminder @@ -735,7 +734,6 @@ def mutate(self, info, reminder_id): if not reminder: raise GraphQLError("CapacityReminder not found.") - # Prepare topics based on reminder's gym_id and days_of_week topics = [ f"{gym}_{day}_{reminder.capacity_threshold}" for gym in reminder.gyms for day in reminder.days_of_week ] diff --git a/src/scrapers/capacities_scraper.py b/src/scrapers/capacities_scraper.py index eaa1ba4..7e6157b 100644 --- a/src/scrapers/capacities_scraper.py +++ b/src/scrapers/capacities_scraper.py @@ -1,4 +1,5 @@ import requests +import time from bs4 import BeautifulSoup from collections import namedtuple from datetime import datetime @@ -6,6 +7,7 @@ from src.utils.messaging import send_capacity_reminder from src.models.capacity import Capacity from src.models.hourly_average_capacity import HourlyAverageCapacity +from src.models.facility import Facility from src.models.enums import DayOfWeekEnum, CapacityReminderGym from src.utils.constants import ( C2C_URL, @@ -83,24 +85,31 @@ def fetch_capacities(): updated = datetime.strptime(updated_str.split(".")[0], "%Y-%m-%dT%H:%M:%S") gym_mapping = { - "HNHFITNESSCENTER": CapacityReminderGym.HELENNEWMAN, - "NOYESFITNESSCENTER": CapacityReminderGym.NOYES, - "TEAGLEDOWNFITNESSCENTER": CapacityReminderGym.TEAGLEDOWN, - "TEAGLEUPFITNESSCENTER": CapacityReminderGym.TEAGLEUP, - "MORRISONFITNESSCENTER": CapacityReminderGym.TONIMORRISON, + "HNH Fitness Center": CapacityReminderGym.HELENNEWMAN, + "Noyes Fitness Center": CapacityReminderGym.NOYES, + "Teagle Down Fitness Center": CapacityReminderGym.TEAGLEDOWN, + "Teagle Up Fitness Center": CapacityReminderGym.TEAGLEUP, + "Morrison Fitness Center": CapacityReminderGym.TONIMORRISON, } - last_percent = Capacity.query.filter_by(facility_id=facility_id).first() - if last_percent: - last_percent = last_percent.percent - else: - last_percent = 0 + last_capacity = Capacity.query.filter_by(facility_id=facility_id).first() + last_percent = last_capacity.percent if last_capacity else 0 - topic_name = db_name.replace(" ", "").upper() + if db_name in gym_mapping: + # Check if facility is closed + facility = Facility.query.filter_by(id=facility_id).first() - if topic_name in gym_mapping: - topic_enum = gym_mapping[topic_name] - check_and_send_capacity_reminders(topic_enum.name, db_name, percent, last_percent) + if not facility or not facility.hours: + print(f"Warning: No hours found for facility ID {facility_id}") + continue + + current_time = int(time.time()) + + 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] + check_and_send_capacity_reminders(topic_enum.name, db_name, percent, last_percent) add_single_capacity(count, facility_id, percent, updated) @@ -189,7 +198,7 @@ def update_hourly_capacity(curDay, curHour): def check_and_send_capacity_reminders(facility_name, readable_name, current_percent, last_percent): """ - Send notifications to topic if the current capacity dips below the relevant thresholds. + Send notifications to topic if the current capacity dips below relevant thresholds. """ current_percent_int = int(current_percent * 100) # Convert to integer percentage @@ -200,10 +209,10 @@ def check_and_send_capacity_reminders(facility_name, readable_name, current_perc # Define threshold levels thresholds = [0, 10, 20, 30, 40, 50, 60, 70, 80, 90] - # Find the lowest threshold that was crossed + # Find the thresholds that were crossed crossed_thresholds = [p for p in thresholds if last_percent_int > p >= current_percent_int] - if crossed_thresholds: - lowest_threshold = min(crossed_thresholds) # Get the lowest threshold crossed - topic_name = f"{facility_name}_{current_day_name}_{lowest_threshold}" - send_capacity_reminder(topic_name, readable_name, current_percent_int) + for threshold in crossed_thresholds: + topic_name = f"{facility_name}_{current_day_name}_{threshold}" + print(f"Sending message to devices subscribed to {topic_name}") + send_capacity_reminder(topic_name, readable_name, threshold) diff --git a/src/utils/messaging.py b/src/utils/messaging.py index 72c41a9..e23bee5 100644 --- a/src/utils/messaging.py +++ b/src/utils/messaging.py @@ -1,9 +1,5 @@ import logging -from datetime import datetime from firebase_admin import messaging -from src.database import db_session -from src.models.user import User -from src.models.enums import DayOfWeekEnum # Ensure the DayOfWeekEnum is imported def send_capacity_reminder(topic_name, facility_name, current_percent): From a9849a48d848e5c8a3c37cad455da6d45b165404 Mon Sep 17 00:00:00 2001 From: sophiestrausberg <68089631+sophiestrausberg@users.noreply.github.com> Date: Thu, 27 Mar 2025 21:51:02 -0400 Subject: [PATCH 05/20] typo --- src/schema.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/schema.py b/src/schema.py index b4752b4..9d4526d 100644 --- a/src/schema.py +++ b/src/schema.py @@ -694,7 +694,7 @@ def mutate(self, info, fcm_token, days_of_week, gyms, capacity_percent): except KeyError: raise GraphQLError(f"Invalid day of the week: {day}") - # Validate gyma + # Validate gyms valid_gyms = [] for gym in gyms: try: From fad12b436d0b30d8ff5acc81f5718d25124c994a Mon Sep 17 00:00:00 2001 From: sophiestrausberg <68089631+sophiestrausberg@users.noreply.github.com> Date: Fri, 28 Mar 2025 16:53:30 -0400 Subject: [PATCH 06/20] add comment --- src/models/capacity_reminder.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/models/capacity_reminder.py b/src/models/capacity_reminder.py index aa2046a..a7c9f59 100644 --- a/src/models/capacity_reminder.py +++ b/src/models/capacity_reminder.py @@ -9,6 +9,7 @@ class CapacityReminder(Base): A capacity reminder for an Uplift user. Attributes: - `id` The ID of the capacity reminder. + - `fcm_token` FCM token used to send notifications to the user's device. - `user_id` The ID of the user who owns this reminder. - `gyms` The list of gyms the user wants to monitor for capacity. - `capacity_threshold` Notify user when gym capacity dips below this percentage. From 5d401d0eac0acf72046d328bbf25e4f5e0f03b06 Mon Sep 17 00:00:00 2001 From: sophiestrausberg <68089631+sophiestrausberg@users.noreply.github.com> Date: Fri, 28 Mar 2025 17:23:05 -0400 Subject: [PATCH 07/20] Update requirements.txt --- requirements.txt | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index 3d4cc15..c6219de 100644 --- a/requirements.txt +++ b/requirements.txt @@ -24,7 +24,7 @@ Flask-Script==2.0.5 Flask-SQLAlchemy==2.3.1 Flask-RESTful==0.3.10 flasgger==0.9.7.1 -google-auth==1.12.0 +google-auth==2.14.1 graphene==2.1.3 graphene-sqlalchemy==2.3.0 graphql-core==2.1 @@ -79,4 +79,5 @@ wcwidth==0.2.6 Werkzeug==2.2.2 zipp==3.15.0 sentry-sdk==2.13.0 -flask_jwt_extended==4.7.1 \ No newline at end of file +flask_jwt_extended==4.7.1 +firebase-admin==6.4.0 From e0c6f8fbc898c01a3fd661f0cdfe85191d9d0539 Mon Sep 17 00:00:00 2001 From: Joshua Dirga Date: Sat, 29 Mar 2025 01:05:17 -0400 Subject: [PATCH 08/20] Solved merge conflicts --- .gitignore | 3 +- app.py | 2 +- .../01234abcdef1_add_friends_table.py | 36 ++++ .../versions/3c406131c004_merge_branches.py | 24 +++ .../versions/add_friends_table_migration.py | 36 ++++ schema.graphql | 28 ++- src/models/friends.py | 35 ++++ src/models/user.py | 52 +++-- src/schema.py | 185 ++++++++++++++++++ 9 files changed, 380 insertions(+), 21 deletions(-) create mode 100644 migrations/versions/01234abcdef1_add_friends_table.py create mode 100644 migrations/versions/3c406131c004_merge_branches.py create mode 100644 migrations/versions/add_friends_table_migration.py create mode 100644 src/models/friends.py diff --git a/.gitignore b/.gitignore index e78072a..2edd957 100644 --- a/.gitignore +++ b/.gitignore @@ -16,4 +16,5 @@ build/ Archive scripts *.sqlite3 -service-account-key.json \ No newline at end of file +service-account-key.json +docker-compose.yml \ No newline at end of file diff --git a/app.py b/app.py index 70a7c30..3c1f713 100644 --- a/app.py +++ b/app.py @@ -11,7 +11,7 @@ ) # Create Flask app with scrapers enabled -app = create_app(run_migrations=False) +app = create_app(run_migrations=True) if __name__ == "__main__": app.run(host="0.0.0.0", port=5000) diff --git a/migrations/versions/01234abcdef1_add_friends_table.py b/migrations/versions/01234abcdef1_add_friends_table.py new file mode 100644 index 0000000..dbc7389 --- /dev/null +++ b/migrations/versions/01234abcdef1_add_friends_table.py @@ -0,0 +1,36 @@ +"""Add friends table + +Revision ID: 01234abcdef1 +Revises: previous_revision_id_here +Create Date: 2025-03-29 00:45:00.000000 + +""" +from alembic import op +import sqlalchemy as sa +from datetime import datetime + + +# revision identifiers, used by Alembic. +revision = '01234abcdef1' +down_revision = 'add99ce06ff5' +branch_labels = None +depends_on = None + + +def upgrade(): + # Create friends table + op.create_table('friends', + sa.Column('user_id', sa.Integer(), nullable=False), + sa.Column('friend_id', sa.Integer(), nullable=False), + sa.Column('created_at', sa.DateTime(), nullable=True, default=datetime.utcnow), + sa.Column('is_accepted', sa.Boolean(), nullable=True, default=False), + sa.Column('accepted_at', sa.DateTime(), nullable=True), + sa.ForeignKeyConstraint(['user_id'], ['users.id'], ondelete='CASCADE'), + sa.ForeignKeyConstraint(['friend_id'], ['users.id'], ondelete='CASCADE'), + sa.PrimaryKeyConstraint('user_id', 'friend_id') + ) + + +def downgrade(): + # Drop friends table + op.drop_table('friends') diff --git a/migrations/versions/3c406131c004_merge_branches.py b/migrations/versions/3c406131c004_merge_branches.py new file mode 100644 index 0000000..74a002b --- /dev/null +++ b/migrations/versions/3c406131c004_merge_branches.py @@ -0,0 +1,24 @@ +"""merge branches + +Revision ID: 3c406131c004 +Revises: 01234abcdef1, 7a3c14648e56 +Create Date: 2025-03-29 00:36:22.980924 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '3c406131c004' +down_revision = ('01234abcdef1', '7a3c14648e56') +branch_labels = None +depends_on = None + + +def upgrade(): + pass + + +def downgrade(): + pass diff --git a/migrations/versions/add_friends_table_migration.py b/migrations/versions/add_friends_table_migration.py new file mode 100644 index 0000000..57e5a4e --- /dev/null +++ b/migrations/versions/add_friends_table_migration.py @@ -0,0 +1,36 @@ +"""Add friends table + +Revision ID: add_friends_table +Revises: 3c406131c004 +Create Date: 2025-03-29 00:55:00.000000 + +""" +from alembic import op +import sqlalchemy as sa +from datetime import datetime + + +# revision identifiers, used by Alembic. +revision = 'add_friends_table' +down_revision = '3c406131c004' +branch_labels = None +depends_on = None + + +def upgrade(): + # Create friends table + op.create_table('friends', + sa.Column('user_id', sa.Integer(), nullable=False), + sa.Column('friend_id', sa.Integer(), nullable=False), + sa.Column('created_at', sa.DateTime(), nullable=True, server_default=sa.text('CURRENT_TIMESTAMP')), + sa.Column('is_accepted', sa.Boolean(), nullable=True, server_default=sa.text('false')), + sa.Column('accepted_at', sa.DateTime(), nullable=True), + sa.ForeignKeyConstraint(['user_id'], ['users.id'], ondelete='CASCADE'), + sa.ForeignKeyConstraint(['friend_id'], ['users.id'], ondelete='CASCADE'), + sa.PrimaryKeyConstraint('user_id', 'friend_id') + ) + + +def downgrade(): + # Drop friends table + op.drop_table('friends') diff --git a/schema.graphql b/schema.graphql index e85ff94..b94f693 100644 --- a/schema.graphql +++ b/schema.graphql @@ -136,6 +136,21 @@ enum FacilityType { COURT } +type Friendship { + id: ID! + userId: Int + friendId: Int + createdAt: DateTime + isAccepted: Boolean + acceptedAt: DateTime + user: User + friend: User +} + +type GetPendingFriendRequests { + pendingRequests: [Friendship] +} + type Giveaway { id: ID! name: String! @@ -236,6 +251,10 @@ type Mutation { ): CapacityReminder toggleCapacityReminder(reminderId: Int!): CapacityReminder deleteCapacityReminder(reminderId: Int!): CapacityReminder + addFriend(friendId: Int!, userId: Int!): Friendship + acceptFriendRequest(friendshipId: Int!): Friendship + removeFriend(friendId: Int!, userId: Int!): RemoveFriend + getPendingFriendRequests(userId: Int!): GetPendingFriendRequests } type OpenHours { @@ -278,12 +297,17 @@ type Query { getHourlyAverageCapacitiesByFacilityId( facilityId: Int ): [HourlyAverageCapacity] + getUserFriends(userId: Int!): [User] } type RefreshAccessToken { newAccessToken: String } +type RemoveFriend { + success: Boolean +} + type Report { id: ID! createdAt: DateTime! @@ -311,8 +335,10 @@ type User { workoutGoal: [DayOfWeekGraphQLEnum] encodedImage: String giveaways: [Giveaway] + friendRequestsSent: [Friendship] + friendRequestsReceived: [Friendship] + friendships: [Friendship] friends: [User] - friendedBy: [User] } type Workout { diff --git a/src/models/friends.py b/src/models/friends.py new file mode 100644 index 0000000..a18493f --- /dev/null +++ b/src/models/friends.py @@ -0,0 +1,35 @@ +from sqlalchemy import Column, Integer, ForeignKey, DateTime, Boolean +from sqlalchemy.orm import relationship +from src.database import Base +from datetime import datetime + +class Friendship(Base): + """ + A friendship relationship between two users. + + Attributes: + - `id` The ID of the friendship. + - `user_id` The ID of the user who initiated the friendship. + - `friend_id` The ID of the user who received the friendship request. + - `created_at` When the friendship was created. + - `is_accepted` Whether the friendship has been accepted. + - `accepted_at` When the friendship was accepted. + """ + + __tablename__ = "friends" + + id = Column(Integer, primary_key=True, autoincrement=True) + user_id = Column(Integer, ForeignKey("users.id")) + friend_id = Column(Integer, ForeignKey("users.id")) + + created_at = Column(DateTime, default=datetime.utcnow) + is_accepted = Column(Boolean, default=False) + accepted_at = Column(DateTime, nullable=True) + + user = relationship("User", foreign_keys=[user_id], back_populates="friend_requests_sent") + friend = relationship("User", foreign_keys=[friend_id], back_populates="friend_requests_received") + + def accept(self): + """Accept a friendship request.""" + self.is_accepted = True + self.accepted_at = datetime.utcnow() diff --git a/src/models/user.py b/src/models/user.py index 20b703c..52de390 100644 --- a/src/models/user.py +++ b/src/models/user.py @@ -1,7 +1,8 @@ -from sqlalchemy import Column, Integer, String, ARRAY, Enum, Table, ForeignKey -from sqlalchemy.orm import backref, relationship +from sqlalchemy import Column, Integer, String, ARRAY, Enum, ForeignKey +from sqlalchemy.orm import relationship from src.database import Base from src.models.enums import DayOfWeekEnum +from src.models.friends import Friendship class User(Base): """ @@ -39,31 +40,46 @@ class User(Base): workout_goal = Column(ARRAY(Enum(DayOfWeekEnum)), nullable=True) encoded_image = Column(String, nullable=True) - friends = relationship( - 'User', - secondary=friendship, - primaryjoin=(friendship.c.user_id == id), - secondaryjoin=(friendship.c.friend_id == id), - backref=backref('friended_by', lazy='dynamic'), - lazy='dynamic' - ) + friend_requests_sent = relationship("Friendship", + foreign_keys="Friendship.user_id", + back_populates="user") + + friend_requests_received = relationship("Friendship", + foreign_keys="Friendship.friend_id", + back_populates="friend") def add_friend(self, friend): - if friend not in self.friends: - self.friends.append(friend) + # Check if friendship already exists + existing = Friendship.query.filter( + (Friendship.user_id == self.id) & + (Friendship.friend_id == friend.id) + ).first() + + if not existing: + new_friendship = Friendship(user_id=self.id, friend_id=friend.id) + # Add to database session here or return for external handling + return new_friendship def remove_friend(self, friend): - if friend in self.friends: - self.friends.remove(friend) + friendship = Friendship.query.filter( + (Friendship.user_id == self.id) & + (Friendship.friend_id == friend.id) + ).first() + + if friendship: + # Delete from database session here or return for external handling + return friendship def get_friends(self): - # Get direct friends (users this user has added) - direct_friends = self.friends.all() + # Get users this user has added as friends + direct_friends_query = Friendship.query.filter_by(user_id=self.id) + direct_friends = [friendship.friend for friendship in direct_friends_query] # Get users who have added this user as a friend - reverse_friends = self.friended_by.all() + reverse_friends_query = Friendship.query.filter_by(friend_id=self.id) + reverse_friends = [friendship.user for friendship in reverse_friends_query] - # Combine both lists and remove duplicates using set + # Combine both lists and remove duplicates all_friends = list(set(direct_friends + reverse_friends)) return all_friends diff --git a/src/schema.py b/src/schema.py index 5dd56f3..3af636b 100644 --- a/src/schema.py +++ b/src/schema.py @@ -17,6 +17,7 @@ from src.models.classes import ClassInstance as ClassInstanceModel from src.models.token_blacklist import TokenBlocklist from src.models.user import User as UserModel +from src.models.friends import Friendship as FriendshipModel from src.models.enums import DayOfWeekGraphQLEnum, CapacityReminderGymGraphQLEnum from src.models.giveaway import Giveaway as GiveawayModel from src.models.giveaway import GiveawayInstance as GiveawayInstanceModel @@ -195,6 +196,34 @@ class Meta: model = UserModel workout_goal = graphene.List(DayOfWeekGraphQLEnum) + friendships = graphene.List(lambda: Friendship) + friends = graphene.List(lambda: User) + + def resolve_friendships(self, info): + # Return all friendship relationships for this user + query = Friendship.get_query(info).filter( + (FriendshipModel.user_id == self.id) | (FriendshipModel.friend_id == self.id) + ) + return query.all() + + def resolve_friends(self, info): + # Return all friend users for this user + direct_friendships = Friendship.get_query(info).filter(FriendshipModel.user_id == self.id).all() + reverse_friendships = Friendship.get_query(info).filter(FriendshipModel.friend_id == self.id).all() + + friend_ids = set() + # Add friend_ids from direct friendships + for friendship in direct_friendships: + if friendship.is_accepted: # Only include accepted friendships + friend_ids.add(friendship.friend_id) + + # Add user_ids from reverse friendships + for friendship in reverse_friendships: + if friendship.is_accepted: # Only include accepted friendships + friend_ids.add(friendship.user_id) + + # Query for all the users at once + return User.get_query(info).filter(UserModel.id.in_(friend_ids)).all() class UserInput(graphene.InputObjectType): @@ -202,6 +231,23 @@ class UserInput(graphene.InputObjectType): giveaway_id = graphene.Int(required=True) +# MARK: - Friendship + +class Friendship(SQLAlchemyObjectType): + class Meta: + model = FriendshipModel + + user = graphene.Field(lambda: User) + friend = graphene.Field(lambda: User) + + def resolve_user(self, info): + query = User.get_query(info).filter(UserModel.id == self.user_id).first() + return query + + def resolve_friend(self, info): + query = User.get_query(info).filter(UserModel.id == self.friend_id).first() + return query + # MARK: - Giveaway @@ -269,6 +315,11 @@ class Query(graphene.ObjectType): get_hourly_average_capacities_by_facility_id = graphene.List( HourlyAverageCapacity, facility_id=graphene.Int(), description="Get all facility hourly average capacities." ) + get_user_friends = graphene.List( + User, + user_id=graphene.Int(required=True), + description="Get all friends for a user." + ) def resolve_get_all_gyms(self, info): query = Gym.get_query(info) @@ -386,6 +437,32 @@ def resolve_get_hourly_average_capacities_by_facility_id(self, info, facility_id query = HourlyAverageCapacity.get_query(info).filter(HourlyAverageCapacityModel.facility_id == facility_id) return query.all() + @jwt_required() + def resolve_get_user_friends(self, info, user_id): + user = User.get_query(info).filter(UserModel.id == user_id).first() + if not user: + raise GraphQLError("User with the given ID does not exist.") + + # Direct friendships where user is the initiator + direct_friendships = Friendship.get_query(info).filter( + (FriendshipModel.user_id == user_id) & (FriendshipModel.is_accepted == True) + ).all() + + # Reverse friendships where user is the recipient + reverse_friendships = Friendship.get_query(info).filter( + (FriendshipModel.friend_id == user_id) & (FriendshipModel.is_accepted == True) + ).all() + + friend_ids = set() + for friendship in direct_friendships: + friend_ids.add(friendship.friend_id) + + for friendship in reverse_friendships: + friend_ids.add(friendship.user_id) + + # Query for all friends at once + return User.get_query(info).filter(UserModel.id.in_(friend_ids)).all() + # MARK: - Mutation @@ -837,6 +914,109 @@ def mutate(self, info, reminder_id): return reminder +class AddFriend(graphene.Mutation): + class Arguments: + user_id = graphene.Int(required=True) + friend_id = graphene.Int(required=True) + + Output = Friendship + + @jwt_required() + def mutate(self, info, user_id, friend_id): + # Check if users exist + user = User.get_query(info).filter(UserModel.id == user_id).first() + if not user: + raise GraphQLError("User with given ID does not exist.") + + friend = User.get_query(info).filter(UserModel.id == friend_id).first() + if not friend: + raise GraphQLError("Friend with given ID does not exist.") + + # Check if friendship already exists + existing = Friendship.get_query(info).filter( + ((FriendshipModel.user_id == user_id) & (FriendshipModel.friend_id == friend_id)) | + ((FriendshipModel.user_id == friend_id) & (FriendshipModel.friend_id == user_id)) + ).first() + + if existing: + raise GraphQLError("Friendship already exists.") + + # Create new friendship (not automatically accepted) + friendship = FriendshipModel(user_id=user_id, friend_id=friend_id) + db_session.add(friendship) + db_session.commit() + + return friendship + +class AcceptFriendRequest(graphene.Mutation): + class Arguments: + friendship_id = graphene.Int(required=True) + + Output = Friendship + + @jwt_required() + def mutate(self, info, friendship_id): + # Find the friendship + friendship = Friendship.get_query(info).filter(FriendshipModel.id == friendship_id).first() + if not friendship: + raise GraphQLError("Friendship not found.") + + # Check if already accepted + if friendship.is_accepted: + raise GraphQLError("Friendship already accepted.") + + # Accept friendship + friendship.is_accepted = True + friendship.accepted_at = datetime.utcnow() + db_session.commit() + + return friendship + +class RemoveFriend(graphene.Mutation): + class Arguments: + user_id = graphene.Int(required=True) + friend_id = graphene.Int(required=True) + + success = graphene.Boolean() + + @jwt_required() + def mutate(self, info, user_id, friend_id): + # Find the friendship + friendship = Friendship.get_query(info).filter( + ((FriendshipModel.user_id == user_id) & (FriendshipModel.friend_id == friend_id)) | + ((FriendshipModel.user_id == friend_id) & (FriendshipModel.friend_id == user_id)) + ).first() + + if not friendship: + raise GraphQLError("Friendship not found.") + + # Delete friendship + db_session.delete(friendship) + db_session.commit() + + return RemoveFriend(success=True) + +class GetPendingFriendRequests(graphene.Mutation): + class Arguments: + user_id = graphene.Int(required=True) + + pending_requests = graphene.List(Friendship) + + @jwt_required() + def mutate(self, info, user_id): + # Check if user exists + user = User.get_query(info).filter(UserModel.id == user_id).first() + if not user: + raise GraphQLError("User with given ID does not exist.") + + # Get pending friend requests (where this user is the friend) + pending = Friendship.get_query(info).filter( + (FriendshipModel.friend_id == user_id) & + (FriendshipModel.is_accepted == False) + ).all() + + return GetPendingFriendRequests(pending_requests=pending) + class Mutation(graphene.ObjectType): create_giveaway = CreateGiveaway.Field(description="Creates a new giveaway.") @@ -855,6 +1035,11 @@ 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") + add_friend = AddFriend.Field(description="Send a friend request to another user.") + accept_friend_request = AcceptFriendRequest.Field(description="Accept a friend request.") + remove_friend = RemoveFriend.Field(description="Remove a friendship.") + get_pending_friend_requests = GetPendingFriendRequests.Field( + description="Get all pending friend requests for a user.") schema = graphene.Schema(query=Query, mutation=Mutation) From 347205e7f13df5436d7287b10d66aeb2af8b67a4 Mon Sep 17 00:00:00 2001 From: Joshua Dirga Date: Sat, 29 Mar 2025 01:06:50 -0400 Subject: [PATCH 09/20] Changed back run migrations to be false --- app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app.py b/app.py index 3c1f713..70a7c30 100644 --- a/app.py +++ b/app.py @@ -11,7 +11,7 @@ ) # Create Flask app with scrapers enabled -app = create_app(run_migrations=True) +app = create_app(run_migrations=False) if __name__ == "__main__": app.run(host="0.0.0.0", port=5000) From dd10ac182833ead6c577a084a2e60ae38a50aa3d Mon Sep 17 00:00:00 2001 From: Sophie Strausberg Date: Sat, 12 Apr 2025 17:46:26 -0400 Subject: [PATCH 10/20] initial commit --- schema.graphql | 2 +- src/schema.py | 92 +++++++++++++++++++++++++++++++------------------- 2 files changed, 59 insertions(+), 35 deletions(-) diff --git a/schema.graphql b/schema.graphql index 10289db..c044644 100644 --- a/schema.graphql +++ b/schema.graphql @@ -212,7 +212,7 @@ type Mutation { createReport(createdAt: DateTime!, description: String!, gymId: Int!, issue: String!): CreateReport deleteUser(userId: Int!): User createCapacityReminder(capacityPercent: Int!, daysOfWeek: [String]!, fcmToken: String!, gyms: [String]!): CapacityReminder - toggleCapacityReminder(reminderId: Int!): CapacityReminder + editCapacityReminder(capacityPercent: Int!, daysOfWeek: [String]!, gyms: [String]!, reminderId: Int!): CapacityReminder deleteCapacityReminder(reminderId: Int!): CapacityReminder } diff --git a/src/schema.py b/src/schema.py index 9d4526d..50c02a5 100644 --- a/src/schema.py +++ b/src/schema.py @@ -239,7 +239,7 @@ def resolve_gym(self, info): query = Gym.get_query(info).filter(GymModel.id == self.gym_id).first() return query - + # MARK: - Capacity Reminder @@ -261,10 +261,14 @@ 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." ) @@ -371,7 +375,6 @@ 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: @@ -452,10 +455,7 @@ def mutate(self, info, name, net_id, email, 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) @@ -473,7 +473,8 @@ def mutate(self, info, name, net_id, email, encoded_image=None): db_session.commit() return new_user - + + class EditUser(graphene.Mutation): class Arguments: name = graphene.String(required=False) @@ -487,7 +488,7 @@ def mutate(self, info, net_id, name=None, email=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.") - + if name is not None: existing_user.name = name if email is not None: @@ -499,12 +500,12 @@ def mutate(self, info, net_id, name=None, email=None, encoded_image=None): 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() @@ -521,6 +522,7 @@ def mutate(self, info, net_id, name=None, email=None, encoded_image=None): db_session.commit() return existing_user + class EnterGiveaway(graphene.Mutation): class Arguments: user_net_id = graphene.String(required=True) @@ -723,39 +725,61 @@ def mutate(self, info, fcm_token, days_of_week, gyms, capacity_percent): return reminder -class ToggleCapacityReminder(graphene.Mutation): +class EditCapacityReminder(graphene.Mutation): class Arguments: reminder_id = graphene.Int(required=True) + gyms = graphene.List(graphene.String, required=True) + days_of_week = graphene.List(graphene.String, required=True) + capacity_percent = graphene.Int(required=True) Output = CapacityReminder - def mutate(self, info, reminder_id): + def mutate(self, info, reminder_id, gyms, days_of_week, capacity_percent): reminder = db_session.query(CapacityReminderModel).filter_by(id=reminder_id).first() if not reminder: raise GraphQLError("CapacityReminder 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}") + + # Validate gyms + valid_gyms = [] + for gym in gyms: + try: + valid_gyms.append(CapacityReminderGymGraphQLEnum[gym].value) + except KeyError: + raise GraphQLError(f"Invalid gym: {gym}") + + # Unsubscribe from old reminders topics = [ f"{gym}_{day}_{reminder.capacity_threshold}" for gym in reminder.gyms for day in reminder.days_of_week ] - if reminder.is_active: - # Toggle to inactive and unsubscribe - for topic in topics: - try: - messaging.unsubscribe_from_topic(reminder.fcm_token, topic) - except Exception as error: - raise GraphQLError(f"Error unsubscribing from topic: {error}") - else: - # Toggle to active and resubscribe - for topic in topics: - try: - messaging.subscribe_to_topic(reminder.fcm_token, topic) - except Exception as error: - raise GraphQLError(f"Error subscribing to topic: {error}") + for topic in topics: + try: + messaging.unsubscribe_from_topic(reminder.fcm_token, topic) + except Exception as error: + raise GraphQLError(f"Error subscribing to topic: {error}") - reminder.is_active = not reminder.is_active - db_session.commit() + # Subscribe to new reminders + topics = [f"{gym}_{day}_{reminder.capacity_threshold}" for gym in valid_gyms for day in validated_workout_days] + for topic in topics: + try: + messaging.subscribe_to_topic(reminder.fcm_token, topic) + except Exception as error: + raise GraphQLError(f"Error subscribing to topic: {error}") + + reminder.gyms = valid_gyms + reminder.days_of_week = validated_workout_days + reminder.capacity_threshold = capacity_percent + + db_session.commit() return reminder @@ -799,7 +823,7 @@ class Mutation(graphene.ObjectType): create_report = CreateReport.Field(description="Creates a new report.") delete_user = DeleteUserById.Field(description="Deletes a user by ID.") create_capacity_reminder = CreateCapacityReminder.Field(description="Create a new capacity reminder.") - toggle_capacity_reminder = ToggleCapacityReminder.Field(description="Toggle a capacity reminder on or off.") + edit_capacity_reminder = EditCapacityReminder.Field(description="Edit capacity reminder.") delete_capacity_reminder = DeleteCapacityReminder.Field(description="Delete a capacity reminder") From e93839632c5b5f7110e08589149a888662895422 Mon Sep 17 00:00:00 2001 From: Joshua Dirga Date: Wed, 16 Apr 2025 17:26:29 -0400 Subject: [PATCH 11/20] Finally added friends --- app_factory.py | 3 +- .../01234abcdef1_add_friends_table.py | 3 +- schema.graphql | 38 ++++--------------- src/models/friends.py | 9 +++-- src/models/user.py | 7 ---- 5 files changed, 16 insertions(+), 44 deletions(-) diff --git a/app_factory.py b/app_factory.py index 198a41e..57fdcd7 100644 --- a/app_factory.py +++ b/app_factory.py @@ -1,7 +1,7 @@ import logging from datetime import timedelta, timezone from flask_jwt_extended import JWTManager -from src.utils.constants import SERVICE_ACCOUNT_PATH +from src.utils.constants import SERVICE_ACCOUNT_PATH, JWT_SECRET_KEY from datetime import datetime from flask import Flask, render_template from graphene import Schema @@ -71,6 +71,7 @@ def create_app(run_migrations=False): schema = Schema(query=Query, mutation=Mutation) swagger = Swagger(app) + app.config['JWT_SECRET_KEY'] = JWT_SECRET_KEY app.config["JWT_ACCESS_TOKEN_EXPIRES"] = timedelta(hours=1) app.config["JWT_REFRESH_TOKEN_EXPIRES"] = timedelta(days=30) diff --git a/migrations/versions/01234abcdef1_add_friends_table.py b/migrations/versions/01234abcdef1_add_friends_table.py index dbc7389..411edeb 100644 --- a/migrations/versions/01234abcdef1_add_friends_table.py +++ b/migrations/versions/01234abcdef1_add_friends_table.py @@ -20,6 +20,7 @@ def upgrade(): # Create friends table op.create_table('friends', + sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), sa.Column('user_id', sa.Integer(), nullable=False), sa.Column('friend_id', sa.Integer(), nullable=False), sa.Column('created_at', sa.DateTime(), nullable=True, default=datetime.utcnow), @@ -27,7 +28,7 @@ def upgrade(): sa.Column('accepted_at', sa.DateTime(), nullable=True), sa.ForeignKeyConstraint(['user_id'], ['users.id'], ondelete='CASCADE'), sa.ForeignKeyConstraint(['friend_id'], ['users.id'], ondelete='CASCADE'), - sa.PrimaryKeyConstraint('user_id', 'friend_id') + sa.PrimaryKeyConstraint('id') ) diff --git a/schema.graphql b/schema.graphql index b94f693..b3054a4 100644 --- a/schema.graphql +++ b/schema.graphql @@ -138,8 +138,8 @@ enum FacilityType { type Friendship { id: ID! - userId: Int - friendId: Int + userId: Int! + friendId: Int! createdAt: DateTime isAccepted: Boolean acceptedAt: DateTime @@ -216,39 +216,17 @@ 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, name: String!, netId: String!): User + editUser(email: String, encodedImage: 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 loginUser(netId: String!): LoginUser logoutUser: LogoutUser refreshAccessToken: RefreshAccessToken - createReport( - createdAt: DateTime! - description: String! - gymId: Int! - issue: String! - ): CreateReport + createReport(createdAt: DateTime!, description: String!, gymId: Int!, issue: String!): CreateReport deleteUser(userId: Int!): User - addFriend(friendNetId: String!, userNetId: String!): User - removeFriend(friendNetId: String!, userNetId: String!): User - createCapacityReminder( - capacityPercent: Int! - daysOfWeek: [String]! - fcmToken: String! - gyms: [String]! - ): CapacityReminder + createCapacityReminder(capacityPercent: Int!, daysOfWeek: [String]!, fcmToken: String!, gyms: [String]!): CapacityReminder toggleCapacityReminder(reminderId: Int!): CapacityReminder deleteCapacityReminder(reminderId: Int!): CapacityReminder addFriend(friendId: Int!, userId: Int!): Friendship @@ -294,9 +272,7 @@ type Query { getAllReports: [Report] getWorkoutGoals(id: Int!): [String] getUserStreak(id: Int!): JSONString - getHourlyAverageCapacitiesByFacilityId( - facilityId: Int - ): [HourlyAverageCapacity] + getHourlyAverageCapacitiesByFacilityId(facilityId: Int): [HourlyAverageCapacity] getUserFriends(userId: Int!): [User] } diff --git a/src/models/friends.py b/src/models/friends.py index a18493f..db3b525 100644 --- a/src/models/friends.py +++ b/src/models/friends.py @@ -1,4 +1,4 @@ -from sqlalchemy import Column, Integer, ForeignKey, DateTime, Boolean +from sqlalchemy import Column, Integer, ForeignKey, DateTime, Boolean, UniqueConstraint from sqlalchemy.orm import relationship from src.database import Base from datetime import datetime @@ -8,7 +8,7 @@ class Friendship(Base): A friendship relationship between two users. Attributes: - - `id` The ID of the friendship. + - `id` The ID of the friendship. - `user_id` The ID of the user who initiated the friendship. - `friend_id` The ID of the user who received the friendship request. - `created_at` When the friendship was created. @@ -19,8 +19,9 @@ class Friendship(Base): __tablename__ = "friends" id = Column(Integer, primary_key=True, autoincrement=True) - user_id = Column(Integer, ForeignKey("users.id")) - friend_id = Column(Integer, ForeignKey("users.id")) + user_id = Column(Integer, ForeignKey("users.id"), nullable=False) + friend_id = Column(Integer, ForeignKey("users.id"), nullable=False) + __table_args__ = (UniqueConstraint('user_id', 'friend_id', name='unique_friendship'),) created_at = Column(DateTime, default=datetime.utcnow) is_accepted = Column(Boolean, default=False) diff --git a/src/models/user.py b/src/models/user.py index 52de390..98ffb39 100644 --- a/src/models/user.py +++ b/src/models/user.py @@ -23,13 +23,6 @@ class User(Base): __tablename__ = "users" - friendship = Table( - "friends", - Base.metadata, - Column("user_id", Integer, ForeignKey("users.id"), primary_key=True), - Column("friend_id", Integer, ForeignKey("users.id"), primary_key=True), - ) - id = Column(Integer, primary_key=True) email = Column(String, nullable=True) giveaways = relationship("Giveaway", secondary="giveaway_instance", back_populates="users") From 91481301deff4599adb388fd716a19a930a0e087 Mon Sep 17 00:00:00 2001 From: Joshua Dirga Date: Wed, 16 Apr 2025 17:33:21 -0400 Subject: [PATCH 12/20] Removed random migration file --- .../versions/3c406131c004_merge_branches.py | 24 ------------------- 1 file changed, 24 deletions(-) delete mode 100644 migrations/versions/3c406131c004_merge_branches.py diff --git a/migrations/versions/3c406131c004_merge_branches.py b/migrations/versions/3c406131c004_merge_branches.py deleted file mode 100644 index 74a002b..0000000 --- a/migrations/versions/3c406131c004_merge_branches.py +++ /dev/null @@ -1,24 +0,0 @@ -"""merge branches - -Revision ID: 3c406131c004 -Revises: 01234abcdef1, 7a3c14648e56 -Create Date: 2025-03-29 00:36:22.980924 - -""" -from alembic import op -import sqlalchemy as sa - - -# revision identifiers, used by Alembic. -revision = '3c406131c004' -down_revision = ('01234abcdef1', '7a3c14648e56') -branch_labels = None -depends_on = None - - -def upgrade(): - pass - - -def downgrade(): - pass From a8d4bf0b6c9b3ce5f5cb105c1edb64e35c1b3799 Mon Sep 17 00:00:00 2001 From: Joshua Dirga Date: Wed, 16 Apr 2025 17:34:41 -0400 Subject: [PATCH 13/20] Removed space --- src/schema.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/schema.py b/src/schema.py index 3af636b..cdccc37 100644 --- a/src/schema.py +++ b/src/schema.py @@ -557,7 +557,6 @@ def mutate(self, info, name, net_id, email, encoded_image=None): db_session.commit() return new_user - class EditUser(graphene.Mutation): class Arguments: name = graphene.String(required=False) From df17198d5b90c3412c35bf6b4bb2484e78f1f6d8 Mon Sep 17 00:00:00 2001 From: Joshua Dirga Date: Wed, 16 Apr 2025 17:35:54 -0400 Subject: [PATCH 14/20] Added space --- src/schema.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/schema.py b/src/schema.py index cdccc37..466cb44 100644 --- a/src/schema.py +++ b/src/schema.py @@ -557,6 +557,8 @@ def mutate(self, info, name, net_id, email, encoded_image=None): db_session.commit() return new_user + + class EditUser(graphene.Mutation): class Arguments: name = graphene.String(required=False) From 159a0d20041fc410ff11ce2aa8b1eda1ca437dc3 Mon Sep 17 00:00:00 2001 From: Joshua Dirga Date: Wed, 16 Apr 2025 17:37:11 -0400 Subject: [PATCH 15/20] Removed tabs --- src/schema.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/schema.py b/src/schema.py index 466cb44..93d1a4b 100644 --- a/src/schema.py +++ b/src/schema.py @@ -557,8 +557,8 @@ def mutate(self, info, name, net_id, email, encoded_image=None): db_session.commit() return new_user - - + + class EditUser(graphene.Mutation): class Arguments: name = graphene.String(required=False) From 275d5d5f81b2d454949c7897dddbd14182fc0c7d Mon Sep 17 00:00:00 2001 From: Joshua Dirga Date: Wed, 16 Apr 2025 17:50:42 -0400 Subject: [PATCH 16/20] Added merging migration --- .../versions/3c406131c004_merge_branches.py | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 migrations/versions/3c406131c004_merge_branches.py diff --git a/migrations/versions/3c406131c004_merge_branches.py b/migrations/versions/3c406131c004_merge_branches.py new file mode 100644 index 0000000..74a002b --- /dev/null +++ b/migrations/versions/3c406131c004_merge_branches.py @@ -0,0 +1,24 @@ +"""merge branches + +Revision ID: 3c406131c004 +Revises: 01234abcdef1, 7a3c14648e56 +Create Date: 2025-03-29 00:36:22.980924 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '3c406131c004' +down_revision = ('01234abcdef1', '7a3c14648e56') +branch_labels = None +depends_on = None + + +def upgrade(): + pass + + +def downgrade(): + pass From dda388484c6e0e21e22e542d91e7c1a944d1ba2b Mon Sep 17 00:00:00 2001 From: Joshua Dirga Date: Wed, 16 Apr 2025 18:15:31 -0400 Subject: [PATCH 17/20] Removed duplicate file and fixed merge migration --- .../01234abcdef1_add_friends_table.py | 37 ------------------- .../versions/3c406131c004_merge_branches.py | 4 +- 2 files changed, 2 insertions(+), 39 deletions(-) delete mode 100644 migrations/versions/01234abcdef1_add_friends_table.py diff --git a/migrations/versions/01234abcdef1_add_friends_table.py b/migrations/versions/01234abcdef1_add_friends_table.py deleted file mode 100644 index 411edeb..0000000 --- a/migrations/versions/01234abcdef1_add_friends_table.py +++ /dev/null @@ -1,37 +0,0 @@ -"""Add friends table - -Revision ID: 01234abcdef1 -Revises: previous_revision_id_here -Create Date: 2025-03-29 00:45:00.000000 - -""" -from alembic import op -import sqlalchemy as sa -from datetime import datetime - - -# revision identifiers, used by Alembic. -revision = '01234abcdef1' -down_revision = 'add99ce06ff5' -branch_labels = None -depends_on = None - - -def upgrade(): - # Create friends table - op.create_table('friends', - sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), - sa.Column('user_id', sa.Integer(), nullable=False), - sa.Column('friend_id', sa.Integer(), nullable=False), - sa.Column('created_at', sa.DateTime(), nullable=True, default=datetime.utcnow), - sa.Column('is_accepted', sa.Boolean(), nullable=True, default=False), - sa.Column('accepted_at', sa.DateTime(), nullable=True), - sa.ForeignKeyConstraint(['user_id'], ['users.id'], ondelete='CASCADE'), - sa.ForeignKeyConstraint(['friend_id'], ['users.id'], ondelete='CASCADE'), - sa.PrimaryKeyConstraint('id') - ) - - -def downgrade(): - # Drop friends table - op.drop_table('friends') diff --git a/migrations/versions/3c406131c004_merge_branches.py b/migrations/versions/3c406131c004_merge_branches.py index 74a002b..bbeca01 100644 --- a/migrations/versions/3c406131c004_merge_branches.py +++ b/migrations/versions/3c406131c004_merge_branches.py @@ -1,7 +1,7 @@ """merge branches Revision ID: 3c406131c004 -Revises: 01234abcdef1, 7a3c14648e56 +Revises: 7a3c14648e56 Create Date: 2025-03-29 00:36:22.980924 """ @@ -11,7 +11,7 @@ # revision identifiers, used by Alembic. revision = '3c406131c004' -down_revision = ('01234abcdef1', '7a3c14648e56') +down_revision = '7a3c14648e56' branch_labels = None depends_on = None From 724006593b536d316fcf46653329f01f141bc418 Mon Sep 17 00:00:00 2001 From: Joshua Dirga Date: Wed, 23 Apr 2025 00:19:36 -0400 Subject: [PATCH 18/20] Fixed special hours bug (9am-9am) --- src/scrapers/sp_hours_scraper.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/scrapers/sp_hours_scraper.py b/src/scrapers/sp_hours_scraper.py index 01e2af0..645c981 100644 --- a/src/scrapers/sp_hours_scraper.py +++ b/src/scrapers/sp_hours_scraper.py @@ -102,6 +102,10 @@ def add_special_facility_hours(start_time, end_time, facility_id, court_type=Non # Convert datetime objects to Unix start_unix = unix_time(start_time) end_unix = unix_time(end_time) + + if start_unix == end_unix: + print(f"Skipping special hours because times are equal: start_unix={start_unix}, end_unix={end_unix}, facility_id={facility_id}") + return print(f"Adding special hours: start_unix={start_unix}, end_unix={end_unix}, facility_id={facility_id}, is_special=True") From a2ee9ec60ea099873c95ec283927534ae7c27197 Mon Sep 17 00:00:00 2001 From: Joshua Dirga Date: Wed, 23 Apr 2025 00:40:02 -0400 Subject: [PATCH 19/20] Added unchecked get/get all queries --- src/schema.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/src/schema.py b/src/schema.py index 8913741..ed003d2 100644 --- a/src/schema.py +++ b/src/schema.py @@ -324,6 +324,15 @@ class Query(graphene.ObjectType): user_id=graphene.Int(required=True), description="Get all friends for a user." ) + get_capacity_reminder_by_id = graphene.Field( + CapacityReminder, + id=graphene.Int(required=True), + description="Get a specific capacity reminder by its ID." + ) + get_all_capacity_reminders = graphene.List( + CapacityReminder, + description="Get all capacity reminders." + ) def resolve_get_all_gyms(self, info): query = Gym.get_query(info) @@ -466,6 +475,20 @@ def resolve_get_user_friends(self, info, user_id): # Query for all friends at once return User.get_query(info).filter(UserModel.id.in_(friend_ids)).all() + + @jwt_required() + def resolve_get_capacity_reminder_by_id(self, info, id): + reminder = CapacityReminder.get_query(info).filter(CapacityReminderModel.id == id).first() + + if not reminder: + raise GraphQLError("Capacity reminder with the given ID does not exist.") + + return reminder + + @jwt_required() + def resolve_get_all_capacity_reminders(self, info): + query = CapacityReminder.get_query(info) + return query.all() # MARK: - Mutation From 81eb8d8311b4e1114b6e6befdc66305bc9bb883f Mon Sep 17 00:00:00 2001 From: Joshua Dirga Date: Wed, 23 Apr 2025 19:13:32 -0400 Subject: [PATCH 20/20] Exclude fcm token from schema --- src/schema.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/schema.py b/src/schema.py index ed003d2..21dfdc0 100644 --- a/src/schema.py +++ b/src/schema.py @@ -292,6 +292,7 @@ def resolve_gym(self, info): class CapacityReminder(SQLAlchemyObjectType): class Meta: model = CapacityReminderModel + exclude_fields = ("fcm_token",) # MARK: - Query