Skip to content

Commit

Permalink
Refactor - Remove streams from activities DB table
Browse files Browse the repository at this point in the history
- Removed waypoints column on activities DB table
- Added strava_gear_id column to activities DB table in order to prepare
DB schema to store this value
- Added new DB table activities_stream
- Adapted code to support new DB schema
  • Loading branch information
joaovitoriasilva committed Jan 8, 2024
1 parent 1b3ebab commit 0f08cc2
Show file tree
Hide file tree
Showing 13 changed files with 591 additions and 71 deletions.
2 changes: 1 addition & 1 deletion backend/constants.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# Constant related to version
API_VERSION="v0.1.2"
API_VERSION="v0.1.3"

# Constants related to user access types
ADMIN_ACCESS = 2
Expand Down
61 changes: 56 additions & 5 deletions backend/controllers/activityController.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,6 @@ class ActivityBase(BaseModel):
- city (Optional[str]): The city where the activity took place (optional).
- town (Optional[str]): The town where the activity took place (optional).
- country (Optional[str]): The country where the activity took place (optional).
- waypoints (List[dict]): List of waypoints for the activity.
- elevation_gain (int): The elevation gain during the activity.
- elevation_loss (int): The elevation loss during the activity.
- pace (float): The pace of the activity.
Expand All @@ -101,12 +100,12 @@ class ActivityBase(BaseModel):
city: Optional[str]
town: Optional[str]
country: Optional[str]
waypoints: List[dict]
elevation_gain: int
elevation_loss: int
pace: float
average_speed: float
average_power: int
strava_gear_id: Optional[int]
strava_activity_id: Optional[int]


Expand Down Expand Up @@ -148,7 +147,6 @@ def activity_record_to_dict(record: Activity) -> dict:
"town": record.town,
"country": record.country,
"created_at": record.created_at.strftime("%Y-%m-%dT%H:%M:%S"),
"waypoints": record.waypoints,
"elevation_gain": record.elevation_gain,
"elevation_loss": record.elevation_loss,
"pace": str(record.pace),
Expand Down Expand Up @@ -1143,8 +1141,10 @@ async def create_activity(

auxType = 10 # Default value
type_mapping = {
"Run": 1,
"running": 1,
"trail running": 2,
"TrailRun": 2,
"VirtualRun": 3,
"cycling": 4,
"Ride": 4,
Expand All @@ -1154,7 +1154,58 @@ async def create_activity(
"virtual_ride": 7,
"swimming": 8,
"open_water_swimming": 8,
"Walk": 9,
}
# "AlpineSki",
# "BackcountrySki",
# "Badminton",
# "Canoeing",
# "Crossfit",
# "EBikeRide",
# "Elliptical",
# "EMountainBikeRide",
# "Golf",
# "GravelRide",
# "Handcycle",
# "HighIntensityIntervalTraining",
# "Hike",
# "IceSkate",
# "InlineSkate",
# "Kayaking",
# "Kitesurf",
# "MountainBikeRide",
# "NordicSki",
# "Pickleball",
# "Pilates",
# "Racquetball",
# "Ride",
# "RockClimbing",
# "RollerSki",
# "Rowing",
# "Run",
# "Sail",
# "Skateboard",
# "Snowboard",
# "Snowshoe",
# "Soccer",
# "Squash",
# "StairStepper",
# "StandUpPaddling",
# "Surfing",
# "Swim",
# "TableTennis",
# "Tennis",
# "TrailRun",
# "Velomobile",
# "VirtualRide",
# "VirtualRow",
# "VirtualRun",
# "Walk",
# "WeightTraining",
# "Wheelchair",
# "Windsurf",
# "Workout",
# "Yoga"
auxType = type_mapping.get(activity_data.activity_type, 10)

# Create a new Activity record
Expand All @@ -1169,12 +1220,12 @@ async def create_activity(
town=activity_data.town,
country=activity_data.country,
created_at=func.now(), # Use func.now() to set 'created_at' to the current timestamp
waypoints=activity_data.waypoints,
elevation_gain=activity_data.elevation_gain,
elevation_loss=activity_data.elevation_loss,
pace=activity_data.pace,
average_speed=activity_data.average_speed,
average_power=activity_data.average_power,
strava_gear_id=activity_data.strava_gear_id,
strava_activity_id=activity_data.strava_activity_id,
)

Expand All @@ -1185,7 +1236,7 @@ async def create_activity(

# Return a JSONResponse indicating the success of the activity creation
return JSONResponse(
content={"message": "Activity created successfully"},
content={"message": "Activity created successfully", "activity_id": activity.id},
status_code=201,
)

Expand Down
212 changes: 212 additions & 0 deletions backend/controllers/activity_streamsController.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
"""
API Router for managing activity stream information.
This module defines FastAPI routes for performing CRUD operations on activity stream records.
It includes endpoints for retrieving and creating activity stream records.
The routes handle user authentication, database interactions using SQLAlchemy,
and provide JSON responses with appropriate metadata.
Endpoints:
- GET /activities/streams/activity_id/{activity_id}/all: Retrieve all activity streams for a specific activity.
- POST /activities/streams/create: Create a new activity stream record.
Dependencies:
- OAuth2PasswordBearer: FastAPI security scheme for handling OAuth2 password bearer tokens.
- get_db_session: Dependency function to get a database session.
- create_error_response: Function to create a standardized error response.
Models:
- ActivityStreamBase: Pydantic model for representing activity stream attributes.
- ActivityStreamCreateRequest: Pydantic model for creating activity stream records.
Functions:
- activity_streams_records_to_dict: Convert ActivityStreams SQLAlchemy objects to dictionaries.
Logger:
- Logger named "myLogger" for logging errors and exceptions.
Routes:
- read_activities_streams_for_activity_all: Retrieve all activity streams for a specific activity.
- create_activity_stream: Create a new activity stream record.
"""
import logging
from fastapi import APIRouter, Depends
from fastapi.security import OAuth2PasswordBearer
from typing import List, Optional
from db.db import ActivityStreams
from jose import JWTError
from pydantic import BaseModel
from . import sessionController
from dependencies import get_db_session, create_error_response
from sqlalchemy.orm import Session
from fastapi.responses import JSONResponse
from constants import API_VERSION

# Define the API router
router = APIRouter()

# Define a loggger created on main.py
logger = logging.getLogger("myLogger")

# Define the OAuth2 scheme for handling bearer tokens
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")


class ActivityStreamBase(BaseModel):
"""
Pydantic model for representing activity attributes.
Attributes:
- activity_id (int): The activity this activity stream belongs.
- stream_type (str): The stream type.
- stream_waypoints (List[dict]): List of waypoints for the activity stream, typically contains datetime and specify stream like HR, Power, etc.
- strava_activity_stream_id (Optional[int]): The ID of the activity stream on Strava (optional).
"""
activity_id: int
stream_type: str
stream_waypoints: List[dict]
strava_activity_stream_id: Optional[int]


class ActivityStreamCreateRequest(ActivityStreamBase):
"""
Pydantic model for creating activity stream records.
Inherits from ActivityStreamBase, which defines the base attributes for activity stream.
This class extends the ActivityStreamBase Pydantic model and is specifically tailored for
creating new activity stream records.
"""
pass


# Define a function to convert Activity SQLAlchemy objects to dictionaries
def activity_streams_records_to_dict(record: ActivityStreams) -> dict:
"""
Converts an ActivityStreams SQLAlchemy object to a dictionary.
Parameters:
- record (ActivityStreams): The SQLAlchemy object representing an activity stream record.
Returns:
dict: A dictionary representation of the ActivityStreams object.
This function is used to convert an SQLAlchemy ActivityStreams object into a dictionary format for easier serialization and response handling.
"""
return {
"id": record.id,
"activity_id": record.activity_id,
"stream_type": record.stream_type,
"stream_waypoints": record.stream_waypoints,
"strava_activity_stream_id": record.strava_activity_stream_id,
}


@router.get("/activities/streams/activity_id/{activity_id}/all", response_model=List[dict])
async def read_activities_streams_for_activity_all(
activity_id=int,
token: str = Depends(oauth2_scheme),
db_session: Session = Depends(get_db_session),
):
"""
Retrieve all activity streams for a specific activity.
Parameters:
- activity_id (int): The ID of the activity for which to retrieve streams.
- token (str): OAuth2 bearer token for user authentication.
- db_session (Session): SQLAlchemy database session.
Returns:
- JSONResponse: JSON response containing metadata and activity stream records.
Raises:
- JWTError: If the user is not authenticated.
- Exception: For other unexpected errors.
"""
try:
# Validate the token
sessionController.validate_token(db_session, token)

# Query the activities streams records using SQLAlchemy
activity_streams_records = (
db_session.query(ActivityStreams)
.filter(ActivityStreams.activity_id == activity_id)
.all()
)

# Use the activity_record_to_dict function to convert SQLAlchemy objects to dictionaries
activity_streams_records_dict = [
activity_streams_records_to_dict(record)
for record in activity_streams_records
]

# Include metadata in the response
metadata = {
"total_records": len(activity_streams_records),
"api_version": API_VERSION,
}

# Return the queried values using JSONResponse
return JSONResponse(
content={"metadata": metadata, "content": activity_streams_records_dict}
)

except JWTError:
# Return an error response if the user is not authenticated
return create_error_response("UNAUTHORIZED", "Unauthorized", 401)
except Exception as err:
# Log the error and return an error response
logger.error(f"Error in read_activities_streams_for_activity_all: {err}", exc_info=True)
return create_error_response(
"INTERNAL_SERVER_ERROR", "Internal Server Error", 500
)

@router.post("/activities/streams/create")
async def create_activity_stream(
activity_stream_data: ActivityStreamCreateRequest,
db_session: Session = Depends(get_db_session),
):
"""
Create a new activity stream record.
Parameters:
- activity_stream_data (ActivityStreamCreateRequest): Pydantic model representing the data for creating a new activity stream.
- db_session (Session): SQLAlchemy database session.
Returns:
- JSONResponse: JSON response indicating the success of the activity stream creation.
Raises:
- JWTError: If the user is not authenticated.
- Exception: For other unexpected errors.
"""
try:
# Create a new Activity record
activity_stream = ActivityStreams(
activity_id=activity_stream_data.activity_id,
stream_type=activity_stream_data.stream_type,
stream_waypoints=activity_stream_data.stream_waypoints,
strava_activity_stream_id=activity_stream_data.strava_activity_stream_id,
)

# Store the Activity record in the database
db_session.add(activity_stream)
db_session.commit()
db_session.refresh(activity_stream)

# Return a JSONResponse indicating the success of the activity creation
return JSONResponse(
content={"message": "Activity stream created successfully"},
status_code=201,
)

except JWTError:
# Return an error response if the user is not authenticated
return create_error_response("UNAUTHORIZED", "Unauthorized", 401)
except Exception as err:
# Log the error, rollback the transaction, and return an error response
db_session.rollback()
logger.error(f"Error in create_activity_stream: {err}", exc_info=True)
return create_error_response(
"INTERNAL_SERVER_ERROR", "Internal Server Error", 500
)
Loading

0 comments on commit 0f08cc2

Please sign in to comment.