Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file modified requirements.txt
Binary file not shown.
1 change: 1 addition & 0 deletions src/models/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ class User(Base):
foreign_keys="Friendship.friend_id",
back_populates="friend")

workouts = relationship("Workout", cascade="all, delete-orphan", back_populates="user")
def add_friend(self, friend):
# Check if friendship already exists
existing = Friendship.query.filter(
Expand Down
2 changes: 2 additions & 0 deletions src/models/workout.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,5 @@ class Workout(Base):
workout_time = Column(DateTime(timezone=True), nullable=False, server_default=text("CURRENT_TIMESTAMP")) # should this be nullable?
user_id = Column(Integer, ForeignKey("users.id"), nullable=False)
facility_id = Column(Integer, ForeignKey("facility.id"), nullable=False)

user = relationship("User", back_populates='workouts')
140 changes: 95 additions & 45 deletions src/schema.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import binascii

import graphene
import base64
import os
Expand Down Expand Up @@ -32,6 +34,8 @@
import logging
from zoneinfo import ZoneInfo
from sqlalchemy import func, cast, Date
import boto3
from botocore.exceptions import ClientError

local_tz = ZoneInfo("America/New_York")

Expand Down Expand Up @@ -833,81 +837,103 @@ class Arguments:
def mutate(self, info, name, net_id, email, 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
if existing_user:
raise GraphQLError("NetID already exists.")

final_photo_url = None

if encoded_image:
upload_url = os.getenv("DIGITAL_OCEAN_URL")
if not upload_url:
raise GraphQLError("Upload URL not configured.")

headers = {"Content-Type": "application/json"}
s3 = boto3.client(
"s3",
endpoint_url=os.getenv("DIGITAL_OCEAN_URL"),
aws_access_key_id=os.getenv("DIGITAL_OCEAN_ACCESS"),
aws_secret_access_key=os.getenv("DIGITAL_OCEAN_SECRET_ACCESS")
)

image_bytes = base64.b64decode(encoded_image)
files = {"image": ("profile.png", image_bytes, "image/png")}
data = {"bucket": os.getenv("BUCKET_NAME")}
try:
response = requests.post(upload_url, files=files, data=data)
response.raise_for_status()
json_response = response.json()
final_photo_url = json_response.get("data")
if not final_photo_url:
raise GraphQLError("No URL returned from upload service.")
except requests.exceptions.RequestException as e:
print(f"Request failed: {e}")
raise GraphQLError(f"Failed to upload photo: {e}")
image_data = base64.b64decode(encoded_image, validate=True)
except (binascii.Error, ValueError) as err:
raise GraphQLError("Invalid profile image encoding.")
Comment on lines +856 to +857
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# First, let's look at the file structure and find src/schema.py
git ls-files src/schema.py

Repository: cuappdev/uplift-backend

Length of output: 80


🏁 Script executed:

# Now let's examine the specific lines mentioned in the review
sed -n '850,865p' src/schema.py

Repository: cuappdev/uplift-backend

Length of output: 706


🏁 Script executed:

# Also check lines around 916-917
sed -n '910,925p' src/schema.py

Repository: cuappdev/uplift-backend

Length of output: 719


🏁 Script executed:

# Let's check if there's any linting configuration (pylint, flake8, etc.)
fd -e "pylintrc|\.flake8|setup.cfg|pyproject.toml|tox.ini" -type f | head -20

Repository: cuappdev/uplift-backend

Length of output: 237


🏁 Script executed:

# Search for linting configuration files correctly
find . -maxdepth 2 -type f \( -name ".flake8" -o -name "pylintrc" -o -name "pyproject.toml" -o -name "setup.cfg" -o -name "tox.ini" \)

Repository: cuappdev/uplift-backend

Length of output: 93


🏁 Script executed:

# Check Python version requirement in setup.py or pyproject.toml
find . -maxdepth 2 -type f \( -name "setup.py" -o -name "pyproject.toml" -o -name "requirements.txt" \) | head -5

Repository: cuappdev/uplift-backend

Length of output: 102


🏁 Script executed:

# Search for similar exception patterns in the file to see if this is a systemic issue
rg "except.*as err:" src/schema.py -A 1

Repository: cuappdev/uplift-backend

Length of output: 322


🏁 Script executed:

# Also check broader exception handling patterns without the from clause
rg "raise GraphQLError" src/schema.py -B 2 | head -40

Repository: cuappdev/uplift-backend

Length of output: 1911


🏁 Script executed:

# Check flake8 configuration
cat .flake8

Repository: cuappdev/uplift-backend

Length of output: 178


🏁 Script executed:

# Check pyproject.toml for any Python version or linting settings
cat pyproject.toml

Repository: cuappdev/uplift-backend

Length of output: 182


🏁 Script executed:

# Let's verify the exact context around both occurrences
sed -n '854,860p' src/schema.py

Repository: cuappdev/uplift-backend

Length of output: 345


🏁 Script executed:

# And the second occurrence
sed -n '914,920p' src/schema.py

Repository: cuappdev/uplift-backend

Length of output: 344


Chain decode exceptions to fix lint and preserve root cause.

err is unused and the raised GraphQLError drops the original exception context. Add exception chaining with from err at both locations.

🛠️ Suggested fix
-            except (binascii.Error, ValueError) as err:
-                raise GraphQLError("Invalid profile image encoding.") 
+            except (binascii.Error, ValueError) as err:
+                raise GraphQLError("Invalid profile image encoding.") from err

Also applies to: 916-917

🧰 Tools
🪛 Flake8 (7.3.0)

[error] 856-856: local variable 'err' is assigned to but never used

(F841)

🪛 Ruff (0.15.6)

[warning] 857-857: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling

(B904)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/schema.py` around lines 856 - 857, In the except handlers that read
"except (binascii.Error, ValueError) as err: raise GraphQLError('Invalid profile
image encoding.')" update the raise to chain the original exception (use "raise
GraphQLError(...) from err") so the root cause is preserved; do this for the
profile image decoding handler and the other identical handler later in the file
(both currently binding the exception as err and raising GraphQLError).


try:
bucket = "appdev-upload"
path = f"uplift-dev/user-profile/{net_id}-profile.png"
region = "nyc3"

s3.put_object(
Bucket=bucket,
Key=path,
Body=image_data,
ContentType="image/png",
ACL="public-read"
)

final_photo_url = f"https://{bucket}.{region}.digitaloceanspaces.com/{path}"
except ClientError as e:
print("Upload error:", e)
raise GraphQLError("Error uploading user profile picture.")
Comment on lines +873 to +875
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# First, let's check if the file exists and its size
wc -l src/schema.py

Repository: cuappdev/uplift-backend

Length of output: 85


🏁 Script executed:

#!/bin/bash
# Examine the code at the specified lines with context
echo "=== Lines 873-875 context ===" 
sed -n '860,880p' src/schema.py | cat -n

echo -e "\n=== Lines 934-936 context ===" 
sed -n '920,945p' src/schema.py | cat -n

echo -e "\n=== Lines 1211-1213 context ===" 
sed -n '1200,1225p' src/schema.py | cat -n

Repository: cuappdev/uplift-backend

Length of output: 3470


Add exception chaining to S3 failure handlers.

Three exception handlers re-raise without chaining the original ClientError, which loses the exception context and triggers B904 warnings. Add from e to each raise statement to preserve the exception chain for better debugging.

Locations and suggested fixes

Lines 873-875:

             except ClientError as e:
                 print("Upload error:", e)
-                raise GraphQLError("Error uploading user profile picture.")        
+                raise GraphQLError("Error uploading user profile picture.") from e

Lines 934-936:

             except ClientError as e:
                 print("Upload error:", e)
-                raise GraphQLError("Error adding new user profile picture.")        
+                raise GraphQLError("Error adding new user profile picture.") from e

Lines 1211-1213:

             except ClientError as e:
                 print("Delete error:", e) 
-                raise GraphQLError("Error deleting user profile picture")
+                raise GraphQLError("Error deleting user profile picture") from e
🧰 Tools
🪛 Ruff (0.15.6)

[warning] 875-875: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling

(B904)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/schema.py` around lines 873 - 875, Three except ClientError as e handlers
in src/schema.py re-raise a GraphQLError without exception chaining; update each
raise to preserve the original exception (use "raise GraphQLError(...) from e")
so the ClientError context is retained. Specifically, modify the except
ClientError as e blocks that currently do print("Upload error:", e) / raise
GraphQLError("Error uploading user profile picture.") and the two other S3
failure handlers that raise GraphQLError to include "from e". Ensure all three
occurrences use "from e" alongside the existing GraphQLError messages to satisfy
exception chaining and eliminate B904 warnings.


new_user = UserModel(name=name, net_id=net_id, email=email, encoded_image=final_photo_url)
db_session.add(new_user)
db_session.commit()

return new_user


class EditUser(graphene.Mutation):
class EditUserById(graphene.Mutation):
class Arguments:
user_id = graphene.Int(required=True)
name = graphene.String(required=False)
net_id = graphene.String(required=True)
email = 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):
existing_user = db_session.query(UserModel).filter(UserModel.net_id == net_id).first()
@jwt_required()
def mutate(self, info, user_id, name=None, email=None, encoded_image=None):
existing_user = db_session.query(UserModel).filter(UserModel.id == user_id).first()

if not existing_user:
raise GraphQLError("User with given net id does not exist.")

raise GraphQLError("User with given id does not exist.")
if get_jwt_identity() != user_id:
raise GraphQLError("Unauthorized operation")
if name is not None:
existing_user.name = name
if email is not None:
existing_user.email = email
if encoded_image is not None:
upload_url = os.getenv("DIGITAL_OCEAN_URL") # Base URL for upload endpoint
if not upload_url:
raise GraphQLError("Upload URL not configured.")

payload = {
"bucket": os.getenv("BUCKET_NAME", "DEV_BUCKET"),
"image": encoded_image, # Base64-encoded image string
}
headers = {"Content-Type": "application/json"}

print(f"Uploading image with payload: {payload}")
final_photo_url = None
s3 = boto3.client(
"s3",
endpoint_url=os.getenv("DIGITAL_OCEAN_URL"),
aws_access_key_id=os.getenv("DIGITAL_OCEAN_ACCESS"),
aws_secret_access_key=os.getenv("DIGITAL_OCEAN_SECRET_ACCESS")
)

try:
image_data = base64.b64decode(encoded_image, validate=True)
except (binascii.Error, ValueError) as err:
raise GraphQLError("Invalid profile image encoding.")

try:
response = requests.post(upload_url, json=payload, headers=headers)
response.raise_for_status()
json_response = response.json()
print(f"Upload API response: {json_response}")
final_photo_url = json_response.get("data")
if not final_photo_url:
raise GraphQLError("No URL returned from upload service.")
bucket = "appdev-upload"
path = f"uplift-dev/user-profile/{existing_user.net_id}-profile.png"
region = "nyc3"

s3.put_object(
Bucket=bucket,
Key=path,
Body=image_data,
ContentType="image/png",
ACL="public-read"
)

final_photo_url = f"https://{bucket}.{region}.digitaloceanspaces.com/{path}"
existing_user.encoded_image = final_photo_url
except requests.exceptions.RequestException as e:
print(f"Request failed: {e}")
raise GraphQLError("Failed to upload photo.")
except ClientError as e:
print("Upload error:", e)
raise GraphQLError("Error adding new user profile picture.")

db_session.commit()
return existing_user
Expand Down Expand Up @@ -1063,6 +1089,7 @@ def mutate(self, info, user_id, workout_goal):

db_session.commit()
return user

class logWorkout(graphene.Mutation):
class Arguments:
workout_time = graphene.DateTime(required=True)
Expand Down Expand Up @@ -1157,11 +1184,34 @@ class Arguments:

Output = User

@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.")

if get_jwt_identity() != user_id:
raise GraphQLError("Unauthorized operation")

s3 = boto3.client(
"s3",
endpoint_url=os.getenv("DIGITAL_OCEAN_URL"),
aws_access_key_id=os.getenv("DIGITAL_OCEAN_ACCESS"),
aws_secret_access_key=os.getenv("DIGITAL_OCEAN_SECRET_ACCESS")
)

if user.encoded_image:
try:
s3.delete_object(
Bucket="appdev-upload",
Key=f"uplift-dev/user-profile/{user.net_id}-profile.png",
)
except ClientError as e:
print("Delete error:", e)
raise GraphQLError("Error deleting user profile picture")

db_session.delete(user)
db_session.commit()
return user
Expand Down Expand Up @@ -1440,7 +1490,7 @@ def mutate(self, info, user_id):
class Mutation(graphene.ObjectType):
create_giveaway = CreateGiveaway.Field(description="Creates a new giveaway.")
create_user = CreateUser.Field(description="Creates a new user.")
edit_user = EditUser.Field(description="Edit a new user.")
edit_user = EditUserById.Field(description="Edit a new user by id.")
enter_giveaway = EnterGiveaway.Field(description="Enters a user into a giveaway.")
set_workout_goals = SetWorkoutGoals.Field(description="Set a user's workout goals.")
log_workout = logWorkout.Field(description="Log a user's workout.")
Expand Down
Loading