From 0f08cc2bc9b687ec0c7afd6c893d1ef8876504d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Vit=C3=B3ria=20Silva?= Date: Mon, 8 Jan 2024 16:28:37 +0000 Subject: [PATCH] Refactor - Remove streams from activities DB table - 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 --- backend/constants.py | 2 +- backend/controllers/activityController.py | 61 ++++- .../controllers/activity_streamsController.py | 212 ++++++++++++++++++ backend/db/db.py | 32 ++- backend/main.py | 4 +- frontend/activities/activity.php | 96 +++++--- frontend/inc/Template-Bottom.php | 2 +- frontend/inc/func/activities-funcs.php | 125 +++++++++-- .../inc/func/activities-streams-funcs.php | 39 ++++ frontend/inc/sqlFunctions.php | 1 + frontend/index.php | 76 ++++++- frontend/lang/index/en.php | 1 + frontend/users/user.php | 11 +- 13 files changed, 591 insertions(+), 71 deletions(-) create mode 100644 backend/controllers/activity_streamsController.py create mode 100644 frontend/inc/func/activities-streams-funcs.php diff --git a/backend/constants.py b/backend/constants.py index 13c84d4a..171e4dd3 100644 --- a/backend/constants.py +++ b/backend/constants.py @@ -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 diff --git a/backend/controllers/activityController.py b/backend/controllers/activityController.py index 38319329..0a9a83b5 100644 --- a/backend/controllers/activityController.py +++ b/backend/controllers/activityController.py @@ -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. @@ -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] @@ -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), @@ -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, @@ -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 @@ -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, ) @@ -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, ) diff --git a/backend/controllers/activity_streamsController.py b/backend/controllers/activity_streamsController.py new file mode 100644 index 00000000..7fcde7de --- /dev/null +++ b/backend/controllers/activity_streamsController.py @@ -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 + ) \ No newline at end of file diff --git a/backend/db/db.py b/backend/db/db.py index 0e9d8d32..7659106c 100644 --- a/backend/db/db.py +++ b/backend/db/db.py @@ -294,7 +294,6 @@ class Activity(Base): created_at = Column( DateTime, nullable=False, comment="Activity creation date (datetime)" ) - waypoints = Column(JSON, nullable=True, doc="Store waypoints data") elevation_gain = Column(Integer, nullable=False, comment="Elevation gain in meters") elevation_loss = Column(Integer, nullable=False, comment="Elevation loss in meters") pace = Column( @@ -321,6 +320,7 @@ class Activity(Base): index=True, comment="Gear ID associated with this activity", ) + strava_gear_id = Column(BigInteger, nullable=True, comment="Strava gear ID") strava_activity_id = Column(BigInteger, nullable=True, comment="Strava activity ID") # Define a relationship to the User model @@ -329,6 +329,34 @@ class Activity(Base): # Define a relationship to the Gear model gear = relationship("Gear", back_populates="activities") + # Establish a one-to-many relationship with 'activities_streams' + activities_streams = relationship("ActivityStreams", back_populates="activity") + + +class ActivityStreams(Base): + __tablename__ = "activities_streams" + + id = Column(Integer, primary_key=True, autoincrement=True) + activity_id = Column( + Integer, + ForeignKey("activities.id", ondelete="CASCADE"), + nullable=False, + index=True, + comment="Activity ID that the activity stream belongs", + ) + stream_type = Column( + Integer, + nullable=False, + comment="Stream type (1 - HR, 2 - Power, 3 - Cadence, 4 - Elevation, 5 - Velocity, 6 - Pace, 7 - lat/lon)", + ) + stream_waypoints = Column(JSON, nullable=False, doc="Store waypoints data") + strava_activity_stream_id = Column( + BigInteger, nullable=True, comment="Strava activity stream ID" + ) + + # Define a relationship to the User model + activity = relationship("Activity", back_populates="activities_streams") + def create_database_tables(): # Create tables @@ -346,7 +374,7 @@ def create_database_tables(): sha256_hash = hashlib.sha256() # Update the hash object with the bytes of the input string - sha256_hash.update("admin".encode('utf-8')) + sha256_hash.update("admin".encode("utf-8")) # Get the hexadecimal representation of the hash hashed_string = sha256_hash.hexdigest() diff --git a/backend/main.py b/backend/main.py index 675e514b..a0507874 100644 --- a/backend/main.py +++ b/backend/main.py @@ -22,7 +22,7 @@ - JAEGER_PROTOCOL, JAEGER_HOST, JAGGER_PORT: Jaeger exporter configuration. Routes: -- Session, User, Gear, Activity, Follower, and Strava controllers are included as routers. +- Session, User, Gear, Activity, ActivityStreams, Follower, and Strava controllers are included as routers. Event Handlers: - "startup": Triggers the creation of database tables during application startup. @@ -42,6 +42,7 @@ userController, gearController, activityController, + activity_streamsController, followerController, stravaController, ) @@ -224,6 +225,7 @@ def get_strava_activities_job(): app.include_router(userController.router) app.include_router(gearController.router) app.include_router(activityController.router) +app.include_router(activity_streamsController.router) app.include_router(followerController.router) app.include_router(stravaController.router) diff --git a/frontend/activities/activity.php b/frontend/activities/activity.php index 1223aac8..8c6b5ae1 100755 --- a/frontend/activities/activity.php +++ b/frontend/activities/activity.php @@ -65,31 +65,59 @@ header("Location: ../index.php?invalidActivity=1"); } -// HR data for activity -$hrData = []; -$cadData = []; -$powerData = []; -$eleData = []; -$velData = []; -$paceData = []; -foreach ($activity[0]['waypoints'] as $waypoint) { - $hrData[] = $waypoint['hr']; - $cadData[] = $waypoint['cad']; - $powerData[] = $waypoint['power']; - $eleData[] = $waypoint['ele']; - $velData[] = (float) number_format(($waypoint['vel'] * 3.6), 0); +$activityStreams = getActivityActivitiesStream($activity[0]["id"]); +$hrStream = []; +$cadStream = []; +$powerStream = []; +$eleStream = []; +$velStream = []; +$paceStream = []; +foreach ($activityStreams as $stream) { + if($stream["stream_type"] == 1){ + $hrStream = $stream["stream_waypoints"]; + } + if($stream["stream_type"] == 2){ + $powerStream = $stream["stream_waypoints"]; + } + if($stream["stream_type"] == 3){ + $cadStream = $stream["stream_waypoints"]; + } + if($stream["stream_type"] == 4){ + $eleStream = $stream["stream_waypoints"]; + } + if($stream["stream_type"] == 5){ + #$velStream = $stream["stream_waypoints"]; + foreach($stream["stream_waypoints"] as $velData){ + $velStream[] = (float) number_format(($velData['vel'] * 3.6), 0); + } + } + #if($stream["stream_type"] == 6){ + # $paceStream = $stream["stream_waypoints"]; + #} + if($stream["stream_type"] == 7){ + $latlonStream = $stream["stream_waypoints"]; + } + #$velStream[] = (float) number_format(($waypoint['vel'] * 3.6), 0); if ($activity[0]["activity_type"] == 1 || $activity[0]["activity_type"] == 2 || $activity[0]["activity_type"] == 3) { - if ($waypoint['pace'] == 0 || $waypoint['pace'] == null) { - $paceData[] = 0; - } else { - $paceData[] = ($waypoint["pace"] * 1000) / 60; + if($stream["stream_type"] == 6){ + foreach($stream["stream_waypoints"] as $paceData){ + if ($paceData['pace'] == 0 || $paceData['pace'] == null) { + $paceStream[] = 0; + } else { + $paceStream[] = ($paceData["pace"] * 1000) / 60; + } + } } } else { if ($activity[0]["activity_type"] == 9) { - if ($waypoint['pace'] == 0 || $waypoint['pace'] == null) { - $paceData[] = 0; - } else { - $paceData[] = ($waypoint["pace"] * 100) / 60; + if($stream["stream_type"] == 6){ + foreach($stream["stream_waypoints"] as $paceData){ + if ($paceData['pace'] == 0 || $paceData['pace'] == null) { + $paceStream[] = 0; + } else { + $paceStream[] = ($paceData["pace"] * 100) / 60; + } + } } } } @@ -377,14 +405,14 @@
- +