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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ __pycache__/
env/
.env
.ipynb_checkpoints/
*.db
10 changes: 3 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,16 +45,12 @@ Backend API for Redback Project 3 (Wearables for athletes), built with Python an
pip install -r requirements.txt
```

6. Initialize Database
```bash
python scripts/create_db.py
```

7. Run the Flask server
6. Run the Flask server
```bash
python app.py
```
The database will be automatically created and initialised on first run found in /instance.

Once running, the backend will be available at:
`http://localhost:5000`

The Environment used in the backend are available for reference in the .env.example file located inside the Backend folder.
55 changes: 55 additions & 0 deletions api/activity.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
from flask import Blueprint, request, jsonify
from models.activity import Activity, ActivityTimeSeries
from models import db
from datetime import datetime

activity_bp = Blueprint('activity', __name__, url_prefix='/api/activity')

@activity_bp.route('', methods=['POST'])
def create_activity():
data = request.get_json()

try:
activity = Activity(
user_id=data['user_id'],
begin_time=datetime.fromisoformat(data['begin_time']),
end_time=datetime.fromisoformat(data['end_time']),
activity_type=data['activity_type'],
average_speed=data['average_speed'],
max_speed=data['max_speed'],
average_heart_rate=data['average_heart_rate'],
max_heart_rate=data['max_heart_rate'],
calories=data['calories'],
duration=data['duration'],
moving_duration=data['moving_duration'],
average_moving_speed=data['average_moving_speed'],
distance=data['distance'],
elevation_gain=data['elevation_gain'],
elevation_loss=data['elevation_loss'],
max_elevation=data['max_elevation'],
min_elevation=data['min_elevation']
)

# Handle optional time series data
time_series_list = data.get('time_series', [])
for point in time_series_list:
ts = ActivityTimeSeries(
timestamp=datetime.fromisoformat(point['timestamp']),
longitude=point['longitude'],
latitude=point['latitude'],
elevation=point['elevation'],
heart_rate=point['heart_rate'],
cadence=point['cadence']
)
activity.time_series.append(ts)

db.session.add(activity)
db.session.commit()

return jsonify({"message": "Activity and time series created", "activity_id": activity.id}), 201

except Exception as e:
db.session.rollback()
return jsonify({"error": str(e)}), 500


86 changes: 86 additions & 0 deletions api/body_insight.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
#Use to POST and Get data to/from body_insight table
from flask import Blueprint, request, jsonify
from models.body_insight import BodyInsight
from models import db
from models.activity import Activity

body_insight_bp = Blueprint('body_insight', __name__, url_prefix='/api/body_insight')

@body_insight_bp.route('', methods=['POST'])
def add_body_insight():
data = request.get_json()

# Require either activity_id or user_id
activity_id = data.get('activity_id')
user_id = data.get('user_id')

if not activity_id:
if not user_id:
return jsonify({"error": "Missing required field 'activity_id' or 'user_id'"}), 400
# Find latest activity for user_id
latest_activity = (
Activity.query
.filter_by(user_id=user_id)
.order_by(Activity.begin_time.desc())
.first()
)
if not latest_activity:
return jsonify({"error": "No activities found for user"}), 404
activity_id = latest_activity.id

try:
insight = BodyInsight(
activity_id=activity_id,
vo2_max=data.get('vo2_max'),
lactate_threshold=data.get('lactate_threshold'),
race_time_prediction=data.get('race_time_prediction'),
real_time_stamina=data.get('real_time_stamina'),
functional_threshold_power=data.get('functional_threshold_power'),
power_to_weight_ratio=data.get('power_to_weight_ratio'),
critical_power=data.get('critical_power'),
threshold_heart_rate=data.get('threshold_heart_rate'),
performance_index=data.get('performance_index'),
fatigue_index=data.get('fatigue_index'),
peak_power_5s=data.get('peak_power_5s'),
peak_power_1min=data.get('peak_power_1min'),
peak_power_5min=data.get('peak_power_5min'),
peak_power_20min=data.get('peak_power_20min'),
heat_acclimation=data.get('heat_acclimation'),
altitude_acclimation=data.get('altitude_acclimation'),
training_readiness=data.get('training_readiness'),
endurance_score=data.get('endurance_score'),
)
db.session.add(insight)
db.session.commit()
return jsonify({
"message": "BodyInsight created",
"id": insight.id,
"activity_id": activity_id # Return the actual activity_id linked
}), 201
except Exception as e:
db.session.rollback()
return jsonify({"error": str(e)}), 500



@body_insight_bp.route('/<int:activity_id>', methods=['GET'])
def get_body_insight(activity_id):
insight = BodyInsight.query.filter_by(activity_id=activity_id).first()
if not insight:
return jsonify({"error": "BodyInsight not found for this activity_id"}), 404
return jsonify(insight.as_dict()), 200

@body_insight_bp.route('/latest', methods=['GET'])
def get_latest_body_insight():
# Get latest BodyInsight entry by most recent activity
latest = (
BodyInsight.query
.join(Activity, Activity.id == BodyInsight.activity_id)
.order_by(Activity.begin_time.desc())
.first()
)

if not latest:
return jsonify({"error": "No body insight data found"}), 404

return jsonify(latest.as_dict()), 200
2 changes: 1 addition & 1 deletion api/profile.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,4 +48,4 @@ def update_profile():

db.session.commit() # Save changes to the database

return jsonify({'message': 'Profile updated successfully'}), 200
return jsonify({'message': 'Profile updated successfully'}), 200
6 changes: 5 additions & 1 deletion app.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
from api.goals import goals_bp
from api.profile import api as profile_api
from api.dashboard import dashboard_bp
from api.body_insight import body_insight_bp
from api.activity import activity_bp
from models import db
from dotenv import load_dotenv
import os
Expand Down Expand Up @@ -35,7 +37,7 @@

# Flask config
app.secret_key = os.getenv("SECRET_KEY", "default_secret_key")
app.config['SQLALCHEMY_DATABASE_URI'] = os.getenv("DATABASE_URL", "sqlite:///goals.db")
app.config['SQLALCHEMY_DATABASE_URI'] = os.getenv("DATABASE_URL", "sqlite:///reflexionpro_backend.db")
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False

# Initialize database
Expand All @@ -51,6 +53,8 @@
app.register_blueprint(goals_bp, url_prefix='/api/goals')
app.register_blueprint(dashboard_bp, url_prefix='/api/dashboard')
app.register_blueprint(profile_api, url_prefix='/api/profile')
app.register_blueprint(body_insight_bp, url_prefix='/api/body_insight')
app.register_blueprint(activity_bp, url_prefix='/api/activity')

# Main index route (login + welcome)
@app.route('/', methods=['GET', 'POST'])
Expand Down
Binary file removed instance/goals.db
Binary file not shown.
24 changes: 24 additions & 0 deletions models/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Database Models and Local Development Guide

This directory contains the SQLAlchemy ORM model definitions used by the Flask backend. These models map to tables in the SQLite database used during development.

---

## 🛠️ Database Inspection via DB Browser for SQLite

This project uses SQLite during development. You may inspect or modify the local database file using **DB Browser for SQLite**, a free and open-source visual tool.

### ✅ Installation

- Download DB Browser from:
https://sqlitebrowser.org/dl/

- Install it according to your operating system.

---

## 📂 Locating the Database File

By default, the SQLite database is created in the backend directory /redback-fit-backend/instance/your_database_name.db

Your database name will be whatever you set DATABASE_URL to in your .env file. If no value is selected it defaults to database
9 changes: 8 additions & 1 deletion models/__init__.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,9 @@
from flask_sqlalchemy import SQLAlchemy
db = SQLAlchemy()
db = SQLAlchemy()

from .user import UserProfile
from .goal import Goal
from .activity import Activity, ActivityTimeSeries
from .activity_summary import ActivitySummary
from .metadata import ActivityMetadata
from .body_insight import BodyInsight
75 changes: 75 additions & 0 deletions models/activity.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
from models import db
from datetime import datetime

class Activity(db.Model):
__tablename__ = 'activity'

id = db.Column(db.Integer, primary_key=True)
user_id = db.Column(db.Integer, db.ForeignKey('user_profile.id'), nullable=False)

# Summary data
begin_time = db.Column(db.DateTime, nullable=False)
end_time = db.Column(db.DateTime, nullable=False)
activity_type = db.Column(db.String(50), nullable=False)
average_speed = db.Column(db.Float, nullable=False)
max_speed = db.Column(db.Float, nullable=False)
average_heart_rate = db.Column(db.Integer, nullable=False)
max_heart_rate = db.Column(db.Integer, nullable=False)
calories = db.Column(db.Float, nullable=False)
duration = db.Column(db.String(20), nullable=False)
moving_duration = db.Column(db.String(20), nullable=False)
average_moving_speed = db.Column(db.Float, nullable=False)
distance = db.Column(db.Float, nullable=False)
elevation_gain = db.Column(db.Float, nullable=False)
elevation_loss = db.Column(db.Float, nullable=False)
max_elevation = db.Column(db.Float, nullable=False)
min_elevation = db.Column(db.Float, nullable=False)

time_series = db.relationship('ActivityTimeSeries', backref='activity', cascade="all, delete-orphan")

def as_dict(self):
return {
"id": self.id,
"user_id": self.user_id,
"begin_time": self.begin_time.isoformat(),
"end_time": self.end_time.isoformat(),
"activity_type": self.activity_type,
"average_speed": self.average_speed,
"max_speed": self.max_speed,
"average_heart_rate": self.average_heart_rate,
"max_heart_rate": self.max_heart_rate,
"calories": self.calories,
"duration": self.duration,
"moving_duration": self.moving_duration,
"average_moving_speed": self.average_moving_speed,
"distance": self.distance,
"elevation_gain": self.elevation_gain,
"elevation_loss": self.elevation_loss,
"max_elevation": self.max_elevation,
"min_elevation": self.min_elevation,
"time_series": [ts.as_dict() for ts in self.time_series]
}


class ActivityTimeSeries(db.Model):
__tablename__ = 'activity_time_series'

id = db.Column(db.Integer, primary_key=True)
activity_id = db.Column(db.Integer, db.ForeignKey('activity.id'), nullable=False)

timestamp = db.Column(db.DateTime, nullable=False)
longitude = db.Column(db.Float, nullable=False)
latitude = db.Column(db.Float, nullable=False)
elevation = db.Column(db.Float, nullable=False)
heart_rate = db.Column(db.Integer, nullable=False)
cadence = db.Column(db.Float, nullable=False)

def as_dict(self):
return {
"timestamp": self.timestamp.isoformat(),
"longitude": self.longitude,
"latitude": self.latitude,
"elevation": self.elevation,
"heart_rate": self.heart_rate,
"cadence": self.cadence
}
50 changes: 50 additions & 0 deletions models/activity_summary.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
from models import db
import json

class ActivitySummary(db.Model):
__tablename__ = 'activity_summary'

id = db.Column(db.Integer, primary_key=True)
activity_id = db.Column(db.Integer, db.ForeignKey('activity.id'), nullable=False)

training_status = db.Column(db.String(50), nullable=True)
body_battery = db.Column(db.Float, nullable=True)
recovery_time = db.Column(db.Float, nullable=True)
training_load = db.Column(db.Float, nullable=True)
training_load_focus = db.Column(db.String(50), nullable=True)

training_effect_aerobic = db.Column(db.Float, nullable=True)
training_effect_anaerobic = db.Column(db.Float, nullable=True)

hrv = db.Column(db.Text, nullable=True) # Stored as JSON string
hr_zones = db.Column(db.Text, nullable=True) # Stored as JSON string

hydration_status = db.Column(db.Float, nullable=True)
epoc = db.Column(db.Float, nullable=True)

def as_dict(self):
return {
"id": self.id,
"activity_id": self.activity_id,
"trainingStatus": self.training_status,
"bodyBattery": self.body_battery,
"recoveryTime": self.recovery_time,
"trainingLoad": self.training_load,
"trainingLoadFocus": self.training_load_focus,
"trainingEffect": {
"aerobic": self.training_effect_aerobic,
"anaerobic": self.training_effect_anaerobic
},
"hrv": json.loads(self.hrv) if self.hrv else [],
"hrZones": json.loads(self.hr_zones) if self.hr_zones else {},
"hydrationStatus": self.hydration_status,
"epoc": self.epoc
}

def set_hrv(self, hrv_data):
"""Accepts a list of HRV dicts and serialises them to JSON text."""
self.hrv = json.dumps(hrv_data)

def set_hr_zones(self, zones_data):
"""Accepts a dict of HR zones and serialises them to JSON text."""
self.hr_zones = json.dumps(zones_data)
Loading