Get project working locally
This commit is contained in:
40
app/__init__.py
Normal file
40
app/__init__.py
Normal file
@@ -0,0 +1,40 @@
|
||||
from flask import Flask
|
||||
from flask_sqlalchemy import SQLAlchemy
|
||||
from flask_migrate import Migrate
|
||||
from flask_bcrypt import Bcrypt
|
||||
from flask_login import LoginManager
|
||||
|
||||
# Initialize Flask extensions
|
||||
db = SQLAlchemy()
|
||||
migrate = Migrate()
|
||||
bcrypt = Bcrypt()
|
||||
login_manager = LoginManager()
|
||||
|
||||
login_manager.login_view = 'auth.login'
|
||||
login_manager.login_message_category = 'info'
|
||||
|
||||
def create_app(config_class='app.config.DevelopmentConfig'):
|
||||
app = Flask(__name__)
|
||||
app.config.from_object(config_class)
|
||||
|
||||
# Initialize extensions
|
||||
db.init_app(app)
|
||||
migrate.init_app(app, db)
|
||||
bcrypt.init_app(app)
|
||||
login_manager.init_app(app)
|
||||
|
||||
# Import models here to avoid circular imports
|
||||
from app.models import User # Import the User model
|
||||
|
||||
# Set up the user_loader function
|
||||
@login_manager.user_loader
|
||||
def load_user(user_id):
|
||||
return User.query.get(int(user_id)) # Query the User model by ID
|
||||
|
||||
# Register blueprints
|
||||
from app.routes import main, auth, user
|
||||
app.register_blueprint(main, url_prefix='/')
|
||||
app.register_blueprint(auth, url_prefix='/auth')
|
||||
app.register_blueprint(user, url_prefix='/user')
|
||||
|
||||
return app
|
||||
31
app/config.py
Normal file
31
app/config.py
Normal file
@@ -0,0 +1,31 @@
|
||||
import os
|
||||
|
||||
class Config:
|
||||
"""Base configuration."""
|
||||
SECRET_KEY = os.environ.get('SECRET_KEY', '234234sdfsdfsdfsdf345345')
|
||||
SQLALCHEMY_TRACK_MODIFICATIONS = False
|
||||
|
||||
class DevelopmentConfig(Config):
|
||||
"""Development configuration."""
|
||||
DEBUG = True
|
||||
|
||||
uri = os.environ.get('DATABASE_URL', 'postgresql://postgres:59fff56880e1bbb42e753d2a82ac21b6@peterstockings.com:15389/bloodpressure_db')
|
||||
if uri and uri.startswith("postgres://"):
|
||||
uri = uri.replace("postgres://", "postgresql://", 1)
|
||||
|
||||
SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL', 'postgresql://postgres:59fff56880e1bbb42e753d2a82ac21b6@peterstockings.com:15389/bloodpressure_db')
|
||||
|
||||
class ProductionConfig(Config):
|
||||
"""Production configuration."""
|
||||
DEBUG = False
|
||||
|
||||
uri = os.environ.get('DATABASE_URL', 'postgresql://postgres:59fff56880e1bbb42e753d2a82ac21b6@peterstockings.com:15389/bloodpressure_db')
|
||||
if uri and uri.startswith("postgres://"):
|
||||
uri = uri.replace("postgres://", "postgresql://", 1)
|
||||
|
||||
SQLALCHEMY_DATABASE_URI = uri
|
||||
|
||||
class TestingConfig(Config):
|
||||
"""Testing configuration."""
|
||||
TESTING = True
|
||||
SQLALCHEMY_DATABASE_URI = 'sqlite:///test.db'
|
||||
100
app/forms.py
Normal file
100
app/forms.py
Normal file
@@ -0,0 +1,100 @@
|
||||
from typing import Optional
|
||||
from flask_wtf import FlaskForm
|
||||
from wtforms import BooleanField, FileField, StringField, PasswordField, SubmitField, IntegerField, DateTimeLocalField
|
||||
from wtforms.validators import DataRequired, Length, EqualTo, ValidationError, Email, Optional, NumberRange
|
||||
from app.models import User
|
||||
from datetime import datetime
|
||||
|
||||
class SignupForm(FlaskForm):
|
||||
username = StringField(
|
||||
'Username',
|
||||
validators=[
|
||||
DataRequired(),
|
||||
Length(min=4, max=20, message="Username must be between 4 and 20 characters.")
|
||||
]
|
||||
)
|
||||
password = PasswordField(
|
||||
'Password',
|
||||
validators=[
|
||||
DataRequired(),
|
||||
Length(min=6, message="Password must be at least 6 characters long.")
|
||||
]
|
||||
)
|
||||
confirm_password = PasswordField(
|
||||
'Confirm Password',
|
||||
validators=[
|
||||
DataRequired(),
|
||||
EqualTo('password', message="Passwords must match.")
|
||||
]
|
||||
)
|
||||
submit = SubmitField('Sign Up')
|
||||
|
||||
# Custom validator to check if username is already taken
|
||||
def validate_username(self, username):
|
||||
user = User.query.filter_by(username=username.data).first()
|
||||
if user:
|
||||
raise ValidationError("Username is already taken. Please choose a different one.")
|
||||
|
||||
|
||||
class LoginForm(FlaskForm):
|
||||
username = StringField(
|
||||
'Username',
|
||||
validators=[DataRequired(message="Username is required.")]
|
||||
)
|
||||
password = PasswordField(
|
||||
'Password',
|
||||
validators=[DataRequired(message="Password is required.")]
|
||||
)
|
||||
submit = SubmitField('Login')
|
||||
|
||||
|
||||
class ReadingForm(FlaskForm):
|
||||
timestamp = DateTimeLocalField(
|
||||
'Timestamp',
|
||||
format='%Y-%m-%dT%H:%M',
|
||||
default=datetime.now, # Set default to current time
|
||||
validators=[DataRequired(message="Timestamp is required.")]
|
||||
)
|
||||
systolic = IntegerField(
|
||||
'Systolic (mmHg)',
|
||||
validators=[
|
||||
DataRequired(),
|
||||
NumberRange(min=50, max=250, message="Systolic pressure must be between 50 and 250 mmHg.")
|
||||
]
|
||||
)
|
||||
diastolic = IntegerField(
|
||||
'Diastolic (mmHg)',
|
||||
validators=[
|
||||
DataRequired(),
|
||||
NumberRange(min=30, max=150, message="Diastolic pressure must be between 30 and 150 mmHg.")
|
||||
]
|
||||
)
|
||||
heart_rate = IntegerField(
|
||||
'Heart Rate (bpm)',
|
||||
validators=[
|
||||
DataRequired(),
|
||||
NumberRange(min=30, max=200, message="Heart rate must be between 30 and 200 bpm.")
|
||||
]
|
||||
)
|
||||
submit = SubmitField('Save Reading')
|
||||
|
||||
class DeleteForm(FlaskForm):
|
||||
submit = SubmitField('Delete')
|
||||
|
||||
class ProfileForm(FlaskForm):
|
||||
name = StringField('Name', validators=[Optional()])
|
||||
email = StringField('Email', validators=[Optional(), Email()])
|
||||
profile_pic = FileField('Profile Picture (optional)')
|
||||
systolic_threshold = IntegerField(
|
||||
'Systolic Threshold (mmHg)',
|
||||
validators=[Optional(), NumberRange(min=90, max=200)]
|
||||
)
|
||||
diastolic_threshold = IntegerField(
|
||||
'Diastolic Threshold (mmHg)',
|
||||
validators=[Optional(), NumberRange(min=60, max=120)]
|
||||
)
|
||||
dark_mode = BooleanField('Enable Dark Mode')
|
||||
submit = SubmitField('Save Settings')
|
||||
|
||||
|
||||
|
||||
29
app/models.py
Normal file
29
app/models.py
Normal file
@@ -0,0 +1,29 @@
|
||||
from flask_sqlalchemy import SQLAlchemy
|
||||
from flask_login import UserMixin
|
||||
|
||||
from app import db
|
||||
|
||||
class User(UserMixin, db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
username = db.Column(db.String(255), nullable=False, unique=True)
|
||||
password_hash = db.Column(db.Text, nullable=False)
|
||||
profile = db.relationship('Profile', backref='user', uselist=False)
|
||||
|
||||
class Profile(db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
|
||||
name = db.Column(db.String(100))
|
||||
email = db.Column(db.String(150), unique=True)
|
||||
profile_pic = db.Column(db.Text) # Store image as a base64 string
|
||||
systolic_threshold = db.Column(db.Integer, default=140)
|
||||
diastolic_threshold = db.Column(db.Integer, default=90)
|
||||
dark_mode = db.Column(db.Boolean, default=False)
|
||||
|
||||
class Reading(db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
|
||||
timestamp = db.Column(db.DateTime, nullable=False)
|
||||
systolic = db.Column(db.Integer, nullable=False)
|
||||
diastolic = db.Column(db.Integer, nullable=False)
|
||||
heart_rate = db.Column(db.Integer, nullable=False)
|
||||
user = db.relationship('User', backref='readings')
|
||||
176
app/routes.py
Normal file
176
app/routes.py
Normal file
@@ -0,0 +1,176 @@
|
||||
from flask import Blueprint, render_template, redirect, request, url_for, flash
|
||||
from werkzeug.security import generate_password_hash, check_password_hash
|
||||
from app.models import Profile, Reading, db, User
|
||||
from app.forms import DeleteForm, LoginForm, ProfileForm, ReadingForm, SignupForm
|
||||
from flask_login import login_user, login_required, current_user, logout_user
|
||||
import base64
|
||||
|
||||
main = Blueprint('main', __name__)
|
||||
auth = Blueprint('auth', __name__)
|
||||
user = Blueprint('user', __name__)
|
||||
|
||||
|
||||
@auth.route('/signup', methods=['GET', 'POST'])
|
||||
def signup():
|
||||
form = SignupForm()
|
||||
if form.validate_on_submit():
|
||||
hashed_password = generate_password_hash(form.password.data)
|
||||
new_user = User(username=form.username.data, password_hash=hashed_password)
|
||||
db.session.add(new_user)
|
||||
db.session.commit()
|
||||
flash("Account created successfully. Please log in.", "success")
|
||||
return redirect(url_for('auth.login'))
|
||||
return render_template('signup.html', form=form)
|
||||
|
||||
@auth.route('/login', methods=['GET', 'POST'])
|
||||
def login():
|
||||
form = LoginForm()
|
||||
if form.validate_on_submit():
|
||||
user = User.query.filter_by(username=form.username.data).first()
|
||||
if user and check_password_hash(user.password_hash, form.password.data):
|
||||
login_user(user)
|
||||
flash("Logged in successfully.", "success")
|
||||
return redirect(url_for('main.dashboard'))
|
||||
flash("Invalid username or password.", "danger")
|
||||
return render_template('login.html', form=form)
|
||||
|
||||
@auth.route('/logout')
|
||||
@login_required
|
||||
def logout():
|
||||
logout_user() # Logs out the current user
|
||||
flash('You have been logged out.', 'success')
|
||||
return redirect(url_for('auth.login')) # Redirect to login page or home page
|
||||
|
||||
|
||||
@main.route('/')
|
||||
@login_required
|
||||
def dashboard():
|
||||
from datetime import datetime, timedelta
|
||||
profile = current_user.profile
|
||||
|
||||
# Get all readings for the user
|
||||
readings = Reading.query.filter_by(user_id=current_user.id).order_by(Reading.timestamp.desc()).all()
|
||||
|
||||
# Weekly summary (last 7 days)
|
||||
one_week_ago = datetime.now() - timedelta(days=7)
|
||||
weekly_readings = [r for r in readings if r.timestamp >= one_week_ago]
|
||||
systolic_avg = round(sum(r.systolic for r in weekly_readings) / len(weekly_readings), 1) if weekly_readings else 0
|
||||
diastolic_avg = round(sum(r.diastolic for r in weekly_readings) / len(weekly_readings), 1) if weekly_readings else 0
|
||||
heart_rate_avg = round(sum(r.heart_rate for r in weekly_readings) / len(weekly_readings), 1) if weekly_readings else 0
|
||||
|
||||
# Progress badges
|
||||
badges = []
|
||||
if len(readings) >= 10:
|
||||
badges.append("10 Readings Logged")
|
||||
if len(readings) >= 100:
|
||||
badges.append("100 Readings Milestone")
|
||||
if len(weekly_readings) >= 7:
|
||||
badges.append("Logged Readings for 7 Days")
|
||||
|
||||
# Pass the delete form to the template
|
||||
delete_form = DeleteForm()
|
||||
|
||||
return render_template('dashboard.html', readings=readings, profile=profile, badges=badges,
|
||||
systolic_avg=systolic_avg, diastolic_avg=diastolic_avg, heart_rate_avg=heart_rate_avg,
|
||||
delete_form=delete_form)
|
||||
|
||||
@main.route('/dashboard/filter', methods=['POST'])
|
||||
@login_required
|
||||
def filter_dashboard():
|
||||
from datetime import datetime
|
||||
start_date = request.form.get('start_date')
|
||||
end_date = request.form.get('end_date')
|
||||
|
||||
# Parse dates and filter readings
|
||||
readings = Reading.query.filter(
|
||||
Reading.user_id == current_user.id,
|
||||
Reading.timestamp >= datetime.strptime(start_date, '%Y-%m-%d'),
|
||||
Reading.timestamp <= datetime.strptime(end_date, '%Y-%m-%d')
|
||||
).order_by(Reading.timestamp.desc()).all()
|
||||
|
||||
# Pass the delete form to the template
|
||||
delete_form = DeleteForm()
|
||||
|
||||
return render_template('dashboard.html', readings=readings, delete_form=delete_form)
|
||||
|
||||
@main.route('/add-reading', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
def add_reading():
|
||||
form = ReadingForm()
|
||||
if form.validate_on_submit():
|
||||
new_reading = Reading(
|
||||
user_id=current_user.id,
|
||||
timestamp=form.timestamp.data,
|
||||
systolic=form.systolic.data,
|
||||
diastolic=form.diastolic.data,
|
||||
heart_rate=form.heart_rate.data
|
||||
)
|
||||
db.session.add(new_reading)
|
||||
db.session.commit()
|
||||
flash("Reading added successfully.", "success")
|
||||
return redirect(url_for('main.dashboard'))
|
||||
return render_template('add_reading.html', form=form)
|
||||
|
||||
@main.route('/reading/<int:reading_id>/edit', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
def edit_reading(reading_id):
|
||||
reading = Reading.query.get_or_404(reading_id)
|
||||
|
||||
# Ensure the reading belongs to the logged-in user
|
||||
if reading.user_id != current_user.id:
|
||||
flash('You are not authorized to edit this reading.', 'danger')
|
||||
return redirect(url_for('main.dashboard'))
|
||||
|
||||
form = ReadingForm(obj=reading) # Populate form with existing reading data
|
||||
if form.validate_on_submit():
|
||||
reading.timestamp = form.timestamp.data
|
||||
reading.systolic = form.systolic.data
|
||||
reading.diastolic = form.diastolic.data
|
||||
reading.heart_rate = form.heart_rate.data
|
||||
db.session.commit()
|
||||
flash('Reading updated successfully!', 'success')
|
||||
return redirect(url_for('main.dashboard'))
|
||||
|
||||
return render_template('edit_reading.html', form=form, reading=reading)
|
||||
|
||||
@main.route('/reading/<int:reading_id>/delete', methods=['POST'])
|
||||
@login_required
|
||||
def delete_reading(reading_id):
|
||||
reading = Reading.query.get_or_404(reading_id)
|
||||
|
||||
# Ensure the reading belongs to the logged-in user
|
||||
if reading.user_id != current_user.id:
|
||||
flash('You are not authorized to delete this reading.', 'danger')
|
||||
return redirect(url_for('main.dashboard'))
|
||||
|
||||
db.session.delete(reading)
|
||||
db.session.commit()
|
||||
flash('Reading deleted successfully!', 'success')
|
||||
return redirect(url_for('main.dashboard'))
|
||||
|
||||
|
||||
@user.route('/profile', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
def profile():
|
||||
profile = current_user.profile or Profile(user_id=current_user.id)
|
||||
form = ProfileForm(obj=profile)
|
||||
|
||||
if form.validate_on_submit():
|
||||
# Update profile fields
|
||||
profile.name = form.name.data
|
||||
profile.email = form.email.data
|
||||
profile.systolic_threshold = form.systolic_threshold.data or profile.systolic_threshold
|
||||
profile.diastolic_threshold = form.diastolic_threshold.data or profile.diastolic_threshold
|
||||
profile.dark_mode = form.dark_mode.data
|
||||
|
||||
# Handle profile picture upload
|
||||
if form.profile_pic.data:
|
||||
file_data = form.profile_pic.data.read()
|
||||
profile.profile_pic = base64.b64encode(file_data).decode('utf-8')
|
||||
|
||||
db.session.add(profile)
|
||||
db.session.commit()
|
||||
flash('Profile updated successfully!', 'success')
|
||||
return redirect(url_for('user.profile'))
|
||||
|
||||
return render_template('profile.html', form=form, profile=profile)
|
||||
63
app/static/js/tailwindcss@3.2.4.js
Normal file
63
app/static/js/tailwindcss@3.2.4.js
Normal file
File diff suppressed because one or more lines are too long
58
app/templates/_layout.html
Normal file
58
app/templates/_layout.html
Normal file
@@ -0,0 +1,58 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{% block title %}BP Tracker{% endblock %}</title>
|
||||
<script src="/static/js/tailwindcss@3.2.4.js"></script>
|
||||
</head>
|
||||
|
||||
<body class="bg-gray-100 text-gray-800">
|
||||
<!-- Navbar -->
|
||||
<nav class="bg-blue-600 text-white p-4">
|
||||
<div class="container mx-auto flex justify-between items-center">
|
||||
<a href="/" class="text-2xl font-bold">BP Tracker</a>
|
||||
<div>
|
||||
{% if current_user.is_authenticated %}
|
||||
<a href="{{ url_for('main.dashboard') }}" class="px-4 py-2 hover:underline">Dashboard</a>
|
||||
<a href="{{ url_for('user.profile') }}" class="px-4 py-2 hover:underline">Profile</a>
|
||||
<a href="{{ url_for('auth.logout') }}" class="px-4 py-2 bg-red-500 rounded hover:bg-red-600">Logout</a>
|
||||
{% else %}
|
||||
<a href="{{ url_for('auth.login') }}" class="px-4 py-2 hover:underline">Login</a>
|
||||
<a href="{{ url_for('auth.signup') }}"
|
||||
class="px-4 py-2 bg-white text-blue-600 rounded hover:bg-gray-200">Signup</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
{% with messages = get_flashed_messages(with_categories=True) %}
|
||||
{% if messages %}
|
||||
<div class="container mx-auto mt-4">
|
||||
{% for category, message in messages %}
|
||||
<div class="p-4 mb-4 rounded text-white bg-{{ 'red' if category == 'danger' else 'green' }}-500">
|
||||
{{ message }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
|
||||
<!-- Main Content -->
|
||||
<main class="container mx-auto mt-6">
|
||||
{% block content %}
|
||||
<!-- Content goes here -->
|
||||
{% endblock %}
|
||||
</main>
|
||||
|
||||
<!-- Footer -->
|
||||
<footer class="bg-gray-800 text-white py-4 mt-10">
|
||||
<div class="container mx-auto text-center">
|
||||
<p>© 2024 BP Tracker. All rights reserved.</p>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
||||
55
app/templates/add_reading.html
Normal file
55
app/templates/add_reading.html
Normal file
@@ -0,0 +1,55 @@
|
||||
{% extends "_layout.html" %}
|
||||
{% block content %}
|
||||
<div class="max-w-lg mx-auto bg-white p-8 rounded-lg shadow-md">
|
||||
<h1 class="text-3xl font-bold text-center text-gray-800 mb-6">Add Reading</h1>
|
||||
<form method="POST" action="{{ url_for('main.add_reading') }}" novalidate class="space-y-6">
|
||||
{{ form.hidden_tag() }}
|
||||
|
||||
<!-- Timestamp Field -->
|
||||
<div>
|
||||
{{ form.timestamp.label(class="block text-sm font-medium text-gray-700 mb-2") }}
|
||||
{{ form.timestamp(class="w-full p-3 border border-gray-300 rounded-lg shadow-sm focus:outline-none
|
||||
focus:ring-2 focus:ring-blue-500 focus:border-blue-500") }}
|
||||
{% for error in form.timestamp.errors %}
|
||||
<p class="text-sm text-red-600 mt-1">{{ error }}</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<!-- Systolic Field -->
|
||||
<div>
|
||||
{{ form.systolic.label(class="block text-sm font-medium text-gray-700 mb-2") }}
|
||||
{{ form.systolic(class="w-full p-3 border border-gray-300 rounded-lg shadow-sm focus:outline-none
|
||||
focus:ring-2 focus:ring-blue-500 focus:border-blue-500") }}
|
||||
{% for error in form.systolic.errors %}
|
||||
<p class="text-sm text-red-600 mt-1">{{ error }}</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<!-- Diastolic Field -->
|
||||
<div>
|
||||
{{ form.diastolic.label(class="block text-sm font-medium text-gray-700 mb-2") }}
|
||||
{{ form.diastolic(class="w-full p-3 border border-gray-300 rounded-lg shadow-sm focus:outline-none
|
||||
focus:ring-2 focus:ring-blue-500 focus:border-blue-500") }}
|
||||
{% for error in form.diastolic.errors %}
|
||||
<p class="text-sm text-red-600 mt-1">{{ error }}</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<!-- Heart Rate Field -->
|
||||
<div>
|
||||
{{ form.heart_rate.label(class="block text-sm font-medium text-gray-700 mb-2") }}
|
||||
{{ form.heart_rate(class="w-full p-3 border border-gray-300 rounded-lg shadow-sm focus:outline-none
|
||||
focus:ring-2 focus:ring-blue-500 focus:border-blue-500") }}
|
||||
{% for error in form.heart_rate.errors %}
|
||||
<p class="text-sm text-red-600 mt-1">{{ error }}</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<!-- Submit Button -->
|
||||
<div>
|
||||
{{ form.submit(class="w-full bg-gradient-to-r from-blue-500 to-blue-700 text-white py-3 rounded-lg
|
||||
font-semibold hover:from-blue-600 hover:to-blue-800 shadow-lg") }}
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
||||
106
app/templates/dashboard.html
Normal file
106
app/templates/dashboard.html
Normal file
@@ -0,0 +1,106 @@
|
||||
{% extends "_layout.html" %}
|
||||
{% block content %}
|
||||
<div class="max-w-5xl mx-auto p-4 space-y-6">
|
||||
|
||||
<!-- Header Section with "Add New Reading" Button -->
|
||||
<div class="flex justify-between items-center">
|
||||
<h1 class="text-2xl font-bold text-gray-800">Dashboard</h1>
|
||||
<a href="{{ url_for('main.add_reading') }}"
|
||||
class="bg-blue-600 text-white px-4 py-2 rounded shadow hover:bg-blue-700">
|
||||
+ Add New Reading
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Weekly Summary -->
|
||||
<div class="bg-gradient-to-r from-blue-500 to-blue-700 text-white p-6 rounded-lg shadow-md">
|
||||
<h3 class="text-lg font-bold">Weekly Summary</h3>
|
||||
<div class="flex justify-between mt-4">
|
||||
<div>
|
||||
<p class="text-sm font-semibold">Systolic Average</p>
|
||||
<p class="text-2xl">{{ systolic_avg }} <span class="text-base">mmHg</span></p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-sm font-semibold">Diastolic Average</p>
|
||||
<p class="text-2xl">{{ diastolic_avg }} <span class="text-base">mmHg</span></p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-sm font-semibold">Heart Rate Average</p>
|
||||
<p class="text-2xl">{{ heart_rate_avg }} <span class="text-base">bpm</span></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Progress Badges -->
|
||||
<div>
|
||||
<h3 class="text-lg font-bold text-gray-800 mb-2">Progress Badges</h3>
|
||||
<div class="flex flex-wrap gap-4">
|
||||
{% for badge in badges %}
|
||||
<div class="bg-green-100 text-green-800 px-4 py-2 rounded shadow text-sm font-medium">
|
||||
{{ badge }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Filter Form -->
|
||||
<form method="POST" action="{{ url_for('main.filter_dashboard') }}" class="p-4 bg-white rounded-lg shadow-md">
|
||||
<h3 class="text-lg font-bold text-gray-800 mb-4">Filter Readings</h3>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label for="start_date" class="block text-sm font-medium text-gray-700">Start Date</label>
|
||||
<input type="date" name="start_date" id="start_date" class="w-full p-2 border rounded">
|
||||
</div>
|
||||
<div>
|
||||
<label for="end_date" class="block text-sm font-medium text-gray-700">End Date</label>
|
||||
<input type="date" name="end_date" id="end_date" class="w-full p-2 border rounded">
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-4">
|
||||
<button type="submit" class="w-full md:w-auto bg-blue-600 text-white px-6 py-2 rounded hover:bg-blue-700">
|
||||
Apply Filters
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<!-- Readings Table -->
|
||||
<div class="bg-white rounded-lg shadow-md overflow-hidden">
|
||||
<table class="w-full bg-white rounded shadow border-collapse">
|
||||
<thead class="bg-gray-200">
|
||||
<tr>
|
||||
<th class="p-2 text-left">Timestamp</th>
|
||||
<th class="p-2 text-left">Systolic</th>
|
||||
<th class="p-2 text-left">Diastolic</th>
|
||||
<th class="p-2 text-left">Heart Rate</th>
|
||||
<th class="p-2 text-left">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for reading in readings %}
|
||||
<tr class="border-t hover:bg-gray-100">
|
||||
<td class="p-2">{{ reading.timestamp }}</td>
|
||||
<td class="p-2">{{ reading.systolic }}</td>
|
||||
<td class="p-2">{{ reading.diastolic }}</td>
|
||||
<td class="p-2">{{ reading.heart_rate }}</td>
|
||||
<td class="p-2">
|
||||
<a href="{{ url_for('main.edit_reading', reading_id=reading.id) }}"
|
||||
class="text-blue-600 hover:underline">Edit</a>
|
||||
<form method="POST" action="{{ url_for('main.delete_reading', reading_id=reading.id) }}"
|
||||
class="inline">
|
||||
{{ delete_form.hidden_tag() }}
|
||||
<button type="submit" class="text-red-600 hover:underline ml-2">
|
||||
Delete
|
||||
</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr>
|
||||
<td colspan="5" class="p-2 text-center text-gray-500">No readings found.</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
56
app/templates/edit_reading.html
Normal file
56
app/templates/edit_reading.html
Normal file
@@ -0,0 +1,56 @@
|
||||
{% extends "_layout.html" %}
|
||||
{% block content %}
|
||||
<div class="max-w-lg mx-auto bg-white p-8 rounded-lg shadow-md">
|
||||
<h1 class="text-3xl font-bold text-center text-gray-800 mb-6">Edit Reading</h1>
|
||||
<form method="POST" action="{{ url_for('main.edit_reading', reading_id=reading.id) }}" novalidate>
|
||||
{{ form.hidden_tag() }}
|
||||
|
||||
<!-- Timestamp Field -->
|
||||
<div class="mb-4">
|
||||
{{ form.timestamp.label(class="block text-sm font-medium text-gray-700") }}
|
||||
{{ form.timestamp(class="w-full p-3 border rounded-lg shadow-sm focus:outline-none focus:ring-2
|
||||
focus:ring-blue-500 focus:border-blue-500") }}
|
||||
{% for error in form.timestamp.errors %}
|
||||
<p class="text-sm text-red-600 mt-1">{{ error }}</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<!-- Systolic Field -->
|
||||
<div class="mb-4">
|
||||
{{ form.systolic.label(class="block text-sm font-medium text-gray-700") }}
|
||||
{{ form.systolic(class="w-full p-3 border rounded-lg shadow-sm focus:outline-none focus:ring-2
|
||||
focus:ring-blue-500 focus:border-blue-500") }}
|
||||
{% for error in form.systolic.errors %}
|
||||
<p class="text-sm text-red-600 mt-1">{{ error }}</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<!-- Diastolic Field -->
|
||||
<div class="mb-4">
|
||||
{{ form.diastolic.label(class="block text-sm font-medium text-gray-700") }}
|
||||
{{ form.diastolic(class="w-full p-3 border rounded-lg shadow-sm focus:outline-none focus:ring-2
|
||||
focus:ring-blue-500 focus:border-blue-500") }}
|
||||
{% for error in form.diastolic.errors %}
|
||||
<p class="text-sm text-red-600 mt-1">{{ error }}</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<!-- Heart Rate Field -->
|
||||
<div class="mb-4">
|
||||
{{ form.heart_rate.label(class="block text-sm font-medium text-gray-700") }}
|
||||
{{ form.heart_rate(class="w-full p-3 border rounded-lg shadow-sm focus:outline-none focus:ring-2
|
||||
focus:ring-blue-500 focus:border-blue-500") }}
|
||||
{% for error in form.heart_rate.errors %}
|
||||
<p class="text-sm text-red-600 mt-1">{{ error }}</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<!-- Submit Button -->
|
||||
<div>
|
||||
<button type="submit" class="w-full bg-blue-600 text-white py-3 rounded-lg font-semibold hover:bg-blue-700">
|
||||
Save Changes
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
||||
32
app/templates/login.html
Normal file
32
app/templates/login.html
Normal file
@@ -0,0 +1,32 @@
|
||||
{% extends "_layout.html" %}
|
||||
{% block content %}
|
||||
<div class="max-w-md mx-auto bg-white p-6 rounded-lg shadow-md">
|
||||
<h1 class="text-2xl font-bold text-center mb-4">Login</h1>
|
||||
<form method="POST" action="{{ url_for('auth.login') }}" novalidate>
|
||||
{{ form.hidden_tag() }}
|
||||
|
||||
<!-- Username Field -->
|
||||
<div class="mb-4">
|
||||
{{ form.username.label(class="block text-sm font-medium text-gray-700") }}
|
||||
{{ form.username(class="w-full p-2 border rounded focus:outline-none focus:ring-2 focus:ring-blue-500") }}
|
||||
{% for error in form.username.errors %}
|
||||
<p class="text-sm text-red-600 mt-1">{{ error }}</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<!-- Password Field -->
|
||||
<div class="mb-4">
|
||||
{{ form.password.label(class="block text-sm font-medium text-gray-700") }}
|
||||
{{ form.password(class="w-full p-2 border rounded focus:outline-none focus:ring-2 focus:ring-blue-500") }}
|
||||
{% for error in form.password.errors %}
|
||||
<p class="text-sm text-red-600 mt-1">{{ error }}</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<!-- Submit Button -->
|
||||
<div>
|
||||
{{ form.submit(class="w-full bg-blue-600 text-white py-2 rounded hover:bg-blue-700") }}
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
||||
58
app/templates/profile.html
Normal file
58
app/templates/profile.html
Normal file
@@ -0,0 +1,58 @@
|
||||
{% extends "_layout.html" %}
|
||||
{% block content %}
|
||||
<div class="max-w-2xl mx-auto bg-white p-6 rounded-lg shadow-md">
|
||||
<h1 class="text-2xl font-bold text-center mb-6">Profile Settings</h1>
|
||||
|
||||
<div class="flex items-center justify-center mb-4">
|
||||
{% if profile.profile_pic %}
|
||||
<img src="data:image/jpeg;base64,{{ profile.profile_pic }}" alt="Profile Picture"
|
||||
class="w-24 h-24 rounded-full border">
|
||||
{% else %}
|
||||
<img src="{{ url_for('static', filename='default.png') }}" alt="Default Profile Picture"
|
||||
class="w-24 h-24 rounded-full border">
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
|
||||
<form method="POST" enctype="multipart/form-data">
|
||||
{{ form.hidden_tag() }}
|
||||
|
||||
<div class="mb-4">
|
||||
{{ form.name.label(class="block text-sm font-medium text-gray-700") }}
|
||||
{{ form.name(class="w-full p-2 border rounded focus:outline-none focus:ring-2 focus:ring-blue-500") }}
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
{{ form.email.label(class="block text-sm font-medium text-gray-700") }}
|
||||
{{ form.email(class="w-full p-2 border rounded focus:outline-none focus:ring-2 focus:ring-blue-500") }}
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
{{ form.profile_pic.label(class="block text-sm font-medium text-gray-700") }}
|
||||
{{ form.profile_pic(class="w-full p-2 border rounded focus:outline-none focus:ring-2 focus:ring-blue-500")
|
||||
}}
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
{{ form.systolic_threshold.label(class="block text-sm font-medium text-gray-700") }}
|
||||
{{ form.systolic_threshold(class="w-full p-2 border rounded focus:outline-none focus:ring-2
|
||||
focus:ring-blue-500") }}
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
{{ form.diastolic_threshold.label(class="block text-sm font-medium text-gray-700") }}
|
||||
{{ form.diastolic_threshold(class="w-full p-2 border rounded focus:outline-none focus:ring-2
|
||||
focus:ring-blue-500") }}
|
||||
</div>
|
||||
|
||||
<div class="mb-4 flex items-center">
|
||||
{{ form.dark_mode }}
|
||||
{{ form.dark_mode.label(class="ml-2 text-sm font-medium text-gray-700") }}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
{{ form.submit(class="w-full bg-blue-600 text-white py-2 rounded hover:bg-blue-700") }}
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
||||
42
app/templates/signup.html
Normal file
42
app/templates/signup.html
Normal file
@@ -0,0 +1,42 @@
|
||||
{% extends "_layout.html" %}
|
||||
{% block content %}
|
||||
<div class="max-w-md mx-auto bg-white p-6 rounded-lg shadow-md">
|
||||
<h1 class="text-2xl font-bold text-center mb-4">Sign Up</h1>
|
||||
<form method="POST" action="{{ url_for('auth.signup') }}" novalidate>
|
||||
{{ form.hidden_tag() }}
|
||||
|
||||
<!-- Username Field -->
|
||||
<div class="mb-4">
|
||||
{{ form.username.label(class="block text-sm font-medium text-gray-700") }}
|
||||
{{ form.username(class="w-full p-2 border rounded focus:outline-none focus:ring-2 focus:ring-blue-500") }}
|
||||
{% for error in form.username.errors %}
|
||||
<p class="text-sm text-red-600 mt-1">{{ error }}</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<!-- Password Field -->
|
||||
<div class="mb-4">
|
||||
{{ form.password.label(class="block text-sm font-medium text-gray-700") }}
|
||||
{{ form.password(class="w-full p-2 border rounded focus:outline-none focus:ring-2 focus:ring-blue-500") }}
|
||||
{% for error in form.password.errors %}
|
||||
<p class="text-sm text-red-600 mt-1">{{ error }}</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<!-- Confirm Password Field -->
|
||||
<div class="mb-4">
|
||||
{{ form.confirm_password.label(class="block text-sm font-medium text-gray-700") }}
|
||||
{{ form.confirm_password(class="w-full p-2 border rounded focus:outline-none focus:ring-2
|
||||
focus:ring-blue-500") }}
|
||||
{% for error in form.confirm_password.errors %}
|
||||
<p class="text-sm text-red-600 mt-1">{{ error }}</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<!-- Submit Button -->
|
||||
<div>
|
||||
{{ form.submit(class="w-full bg-blue-600 text-white py-2 rounded hover:bg-blue-700") }}
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
||||
18
manage.py
Normal file
18
manage.py
Normal file
@@ -0,0 +1,18 @@
|
||||
from app import create_app, db
|
||||
from flask_migrate import Migrate
|
||||
|
||||
from app.models import Profile, Reading, User
|
||||
|
||||
app = create_app()
|
||||
|
||||
# Initialize Flask-Migrate
|
||||
migrate = Migrate(app, db)
|
||||
|
||||
# Add shell context for Flask CLI
|
||||
@app.shell_context_processor
|
||||
def make_shell_context():
|
||||
return {'db': db, 'User': User, 'Profile': Profile, 'Reading': Reading}
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
app.run()
|
||||
1
migrations/README
Normal file
1
migrations/README
Normal file
@@ -0,0 +1 @@
|
||||
Single-database configuration for Flask.
|
||||
50
migrations/alembic.ini
Normal file
50
migrations/alembic.ini
Normal file
@@ -0,0 +1,50 @@
|
||||
# A generic, single database configuration.
|
||||
|
||||
[alembic]
|
||||
# template used to generate migration files
|
||||
# file_template = %%(rev)s_%%(slug)s
|
||||
|
||||
# set to 'true' to run the environment during
|
||||
# the 'revision' command, regardless of autogenerate
|
||||
# revision_environment = false
|
||||
|
||||
|
||||
# Logging configuration
|
||||
[loggers]
|
||||
keys = root,sqlalchemy,alembic,flask_migrate
|
||||
|
||||
[handlers]
|
||||
keys = console
|
||||
|
||||
[formatters]
|
||||
keys = generic
|
||||
|
||||
[logger_root]
|
||||
level = WARN
|
||||
handlers = console
|
||||
qualname =
|
||||
|
||||
[logger_sqlalchemy]
|
||||
level = WARN
|
||||
handlers =
|
||||
qualname = sqlalchemy.engine
|
||||
|
||||
[logger_alembic]
|
||||
level = INFO
|
||||
handlers =
|
||||
qualname = alembic
|
||||
|
||||
[logger_flask_migrate]
|
||||
level = INFO
|
||||
handlers =
|
||||
qualname = flask_migrate
|
||||
|
||||
[handler_console]
|
||||
class = StreamHandler
|
||||
args = (sys.stderr,)
|
||||
level = NOTSET
|
||||
formatter = generic
|
||||
|
||||
[formatter_generic]
|
||||
format = %(levelname)-5.5s [%(name)s] %(message)s
|
||||
datefmt = %H:%M:%S
|
||||
113
migrations/env.py
Normal file
113
migrations/env.py
Normal file
@@ -0,0 +1,113 @@
|
||||
import logging
|
||||
from logging.config import fileConfig
|
||||
|
||||
from flask import current_app
|
||||
|
||||
from alembic import context
|
||||
|
||||
# this is the Alembic Config object, which provides
|
||||
# access to the values within the .ini file in use.
|
||||
config = context.config
|
||||
|
||||
# Interpret the config file for Python logging.
|
||||
# This line sets up loggers basically.
|
||||
fileConfig(config.config_file_name)
|
||||
logger = logging.getLogger('alembic.env')
|
||||
|
||||
|
||||
def get_engine():
|
||||
try:
|
||||
# this works with Flask-SQLAlchemy<3 and Alchemical
|
||||
return current_app.extensions['migrate'].db.get_engine()
|
||||
except (TypeError, AttributeError):
|
||||
# this works with Flask-SQLAlchemy>=3
|
||||
return current_app.extensions['migrate'].db.engine
|
||||
|
||||
|
||||
def get_engine_url():
|
||||
try:
|
||||
return get_engine().url.render_as_string(hide_password=False).replace(
|
||||
'%', '%%')
|
||||
except AttributeError:
|
||||
return str(get_engine().url).replace('%', '%%')
|
||||
|
||||
|
||||
# add your model's MetaData object here
|
||||
# for 'autogenerate' support
|
||||
# from myapp import mymodel
|
||||
# target_metadata = mymodel.Base.metadata
|
||||
config.set_main_option('sqlalchemy.url', get_engine_url())
|
||||
target_db = current_app.extensions['migrate'].db
|
||||
|
||||
# other values from the config, defined by the needs of env.py,
|
||||
# can be acquired:
|
||||
# my_important_option = config.get_main_option("my_important_option")
|
||||
# ... etc.
|
||||
|
||||
|
||||
def get_metadata():
|
||||
if hasattr(target_db, 'metadatas'):
|
||||
return target_db.metadatas[None]
|
||||
return target_db.metadata
|
||||
|
||||
|
||||
def run_migrations_offline():
|
||||
"""Run migrations in 'offline' mode.
|
||||
|
||||
This configures the context with just a URL
|
||||
and not an Engine, though an Engine is acceptable
|
||||
here as well. By skipping the Engine creation
|
||||
we don't even need a DBAPI to be available.
|
||||
|
||||
Calls to context.execute() here emit the given string to the
|
||||
script output.
|
||||
|
||||
"""
|
||||
url = config.get_main_option("sqlalchemy.url")
|
||||
context.configure(
|
||||
url=url, target_metadata=get_metadata(), literal_binds=True
|
||||
)
|
||||
|
||||
with context.begin_transaction():
|
||||
context.run_migrations()
|
||||
|
||||
|
||||
def run_migrations_online():
|
||||
"""Run migrations in 'online' mode.
|
||||
|
||||
In this scenario we need to create an Engine
|
||||
and associate a connection with the context.
|
||||
|
||||
"""
|
||||
|
||||
# this callback is used to prevent an auto-migration from being generated
|
||||
# when there are no changes to the schema
|
||||
# reference: http://alembic.zzzcomputing.com/en/latest/cookbook.html
|
||||
def process_revision_directives(context, revision, directives):
|
||||
if getattr(config.cmd_opts, 'autogenerate', False):
|
||||
script = directives[0]
|
||||
if script.upgrade_ops.is_empty():
|
||||
directives[:] = []
|
||||
logger.info('No changes in schema detected.')
|
||||
|
||||
conf_args = current_app.extensions['migrate'].configure_args
|
||||
if conf_args.get("process_revision_directives") is None:
|
||||
conf_args["process_revision_directives"] = process_revision_directives
|
||||
|
||||
connectable = get_engine()
|
||||
|
||||
with connectable.connect() as connection:
|
||||
context.configure(
|
||||
connection=connection,
|
||||
target_metadata=get_metadata(),
|
||||
**conf_args
|
||||
)
|
||||
|
||||
with context.begin_transaction():
|
||||
context.run_migrations()
|
||||
|
||||
|
||||
if context.is_offline_mode():
|
||||
run_migrations_offline()
|
||||
else:
|
||||
run_migrations_online()
|
||||
24
migrations/script.py.mako
Normal file
24
migrations/script.py.mako
Normal file
@@ -0,0 +1,24 @@
|
||||
"""${message}
|
||||
|
||||
Revision ID: ${up_revision}
|
||||
Revises: ${down_revision | comma,n}
|
||||
Create Date: ${create_date}
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
${imports if imports else ""}
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = ${repr(up_revision)}
|
||||
down_revision = ${repr(down_revision)}
|
||||
branch_labels = ${repr(branch_labels)}
|
||||
depends_on = ${repr(depends_on)}
|
||||
|
||||
|
||||
def upgrade():
|
||||
${upgrades if upgrades else "pass"}
|
||||
|
||||
|
||||
def downgrade():
|
||||
${downgrades if downgrades else "pass"}
|
||||
@@ -0,0 +1,38 @@
|
||||
"""Change Password Hash column to text0
|
||||
|
||||
Revision ID: 59097bee8942
|
||||
Revises: b409c5bae0bb
|
||||
Create Date: 2024-12-24 00:35:11.199266
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '59097bee8942'
|
||||
down_revision = 'b409c5bae0bb'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
with op.batch_alter_table('user', schema=None) as batch_op:
|
||||
batch_op.alter_column('password_hash',
|
||||
existing_type=sa.VARCHAR(length=150),
|
||||
type_=sa.Text(),
|
||||
existing_nullable=False)
|
||||
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
with op.batch_alter_table('user', schema=None) as batch_op:
|
||||
batch_op.alter_column('password_hash',
|
||||
existing_type=sa.Text(),
|
||||
type_=sa.VARCHAR(length=150),
|
||||
existing_nullable=False)
|
||||
|
||||
# ### end Alembic commands ###
|
||||
@@ -0,0 +1,38 @@
|
||||
"""Increase length of password_hash column
|
||||
|
||||
Revision ID: b409c5bae0bb
|
||||
Revises: e276f9e056a2
|
||||
Create Date: 2024-12-24 00:33:58.419416
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = 'b409c5bae0bb'
|
||||
down_revision = 'e276f9e056a2'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
with op.batch_alter_table('user', schema=None) as batch_op:
|
||||
batch_op.alter_column('username',
|
||||
existing_type=sa.VARCHAR(length=150),
|
||||
type_=sa.String(length=255),
|
||||
existing_nullable=False)
|
||||
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
with op.batch_alter_table('user', schema=None) as batch_op:
|
||||
batch_op.alter_column('username',
|
||||
existing_type=sa.String(length=255),
|
||||
type_=sa.VARCHAR(length=150),
|
||||
existing_nullable=False)
|
||||
|
||||
# ### end Alembic commands ###
|
||||
59
migrations/versions/e276f9e056a2_add_tables.py
Normal file
59
migrations/versions/e276f9e056a2_add_tables.py
Normal file
@@ -0,0 +1,59 @@
|
||||
"""Add tables
|
||||
|
||||
Revision ID: e276f9e056a2
|
||||
Revises:
|
||||
Create Date: 2024-12-24 00:28:49.674352
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = 'e276f9e056a2'
|
||||
down_revision = None
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.create_table('user',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('username', sa.String(length=150), nullable=False),
|
||||
sa.Column('password_hash', sa.String(length=150), nullable=False),
|
||||
sa.PrimaryKeyConstraint('id'),
|
||||
sa.UniqueConstraint('username')
|
||||
)
|
||||
op.create_table('profile',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('user_id', sa.Integer(), nullable=False),
|
||||
sa.Column('name', sa.String(length=100), nullable=True),
|
||||
sa.Column('email', sa.String(length=150), nullable=True),
|
||||
sa.Column('profile_pic', sa.Text(), nullable=True),
|
||||
sa.Column('systolic_threshold', sa.Integer(), nullable=True),
|
||||
sa.Column('diastolic_threshold', sa.Integer(), nullable=True),
|
||||
sa.Column('dark_mode', sa.Boolean(), nullable=True),
|
||||
sa.ForeignKeyConstraint(['user_id'], ['user.id'], ),
|
||||
sa.PrimaryKeyConstraint('id'),
|
||||
sa.UniqueConstraint('email')
|
||||
)
|
||||
op.create_table('reading',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('user_id', sa.Integer(), nullable=False),
|
||||
sa.Column('timestamp', sa.DateTime(), nullable=False),
|
||||
sa.Column('systolic', sa.Integer(), nullable=False),
|
||||
sa.Column('diastolic', sa.Integer(), nullable=False),
|
||||
sa.Column('heart_rate', sa.Integer(), nullable=False),
|
||||
sa.ForeignKeyConstraint(['user_id'], ['user.id'], ),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_table('reading')
|
||||
op.drop_table('profile')
|
||||
op.drop_table('user')
|
||||
# ### end Alembic commands ###
|
||||
@@ -1,14 +1,27 @@
|
||||
Flask==2.2.2
|
||||
gunicorn==19.7.1
|
||||
Jinja2==3.1.0
|
||||
jinja-partials==0.1.1
|
||||
psycopg2-binary==2.9.3
|
||||
flask-htmx==0.2.0
|
||||
python-dateutil==2.8.2
|
||||
minify-html==0.10.3
|
||||
jinja2-fragments==0.3.0
|
||||
Werkzeug==2.2.2
|
||||
numpy==1.19.5
|
||||
pandas==1.3.1
|
||||
python-dotenv==1.0.1
|
||||
plotly==5.24.1
|
||||
alembic==1.14.0
|
||||
bcrypt==4.2.1
|
||||
blinker==1.9.0
|
||||
click==8.1.8
|
||||
colorama==0.4.6
|
||||
dnspython==2.7.0
|
||||
email-validator==2.2.0
|
||||
flask==3.1.0
|
||||
Flask-Bcrypt==1.0.1
|
||||
Flask-Login==0.6.3
|
||||
Flask-Migrate==4.0.7
|
||||
flask-sqlalchemy==3.1.1
|
||||
flask-wtf==1.2.2
|
||||
greenlet==3.1.1
|
||||
idna==3.10
|
||||
importlib-metadata==8.5.0
|
||||
itsdangerous==2.2.0
|
||||
jinja2==3.1.5
|
||||
Mako==1.3.8
|
||||
MarkupSafe==3.0.2
|
||||
psycopg2==2.9.10
|
||||
psycopg2-binary==2.9.10
|
||||
SQLAlchemy==2.0.36
|
||||
typing-extensions==4.12.2
|
||||
werkzeug==3.1.3
|
||||
wtforms==3.2.1
|
||||
zipp==3.21.0
|
||||
|
||||
Reference in New Issue
Block a user