Redesign BP tracker with lightweight views and unobtrusive save confirmations
This commit is contained in:
+97
-77
@@ -1,77 +1,97 @@
|
||||
import csv
|
||||
from io import StringIO
|
||||
from flask import Blueprint, render_template, redirect, request, send_file, url_for, flash
|
||||
from app.models import Reading, db
|
||||
from flask_login import login_required, current_user
|
||||
from datetime import datetime
|
||||
|
||||
data = Blueprint('data', __name__)
|
||||
|
||||
@data.route('/', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
def manage_data():
|
||||
if request.method == 'POST':
|
||||
# Handle CSV file upload
|
||||
file = request.files.get('file')
|
||||
if file and file.filename.endswith('.csv'):
|
||||
try:
|
||||
csv_data = csv.reader(StringIO(file.read().decode('utf-8')))
|
||||
next(csv_data) # Skip the header row
|
||||
readings_to_add = []
|
||||
for row in csv_data:
|
||||
timestamp, systolic, diastolic, heart_rate = row
|
||||
readings_to_add.append(Reading(
|
||||
user_id=current_user.id,
|
||||
timestamp=datetime.strptime(timestamp, '%Y-%m-%d %H:%M:%S'),
|
||||
systolic=int(systolic),
|
||||
diastolic=int(diastolic),
|
||||
heart_rate=int(heart_rate),
|
||||
))
|
||||
db.session.bulk_save_objects(readings_to_add)
|
||||
db.session.commit()
|
||||
flash('Data imported successfully!', 'success')
|
||||
except Exception as e:
|
||||
flash(f'Error importing data: {str(e)}', 'danger')
|
||||
else:
|
||||
flash('Please upload a valid CSV file.', 'danger')
|
||||
return redirect(url_for('data.manage_data'))
|
||||
|
||||
return render_template('data.html')
|
||||
|
||||
@data.route('/export', methods=['GET'])
|
||||
@login_required
|
||||
def export_data():
|
||||
import io
|
||||
from flask import Response
|
||||
|
||||
def generate_csv():
|
||||
"""Stream CSV rows to avoid loading all readings into memory."""
|
||||
# Write header
|
||||
output = io.StringIO()
|
||||
writer = csv.writer(output)
|
||||
writer.writerow(['Timestamp', 'Systolic', 'Diastolic', 'Heart Rate'])
|
||||
yield output.getvalue()
|
||||
output.seek(0)
|
||||
output.truncate(0)
|
||||
|
||||
# Stream readings in chunks using yield_per
|
||||
readings = Reading.query.filter_by(user_id=current_user.id).order_by(
|
||||
Reading.timestamp
|
||||
).yield_per(500)
|
||||
|
||||
for reading in readings:
|
||||
writer.writerow([
|
||||
reading.timestamp.strftime('%Y-%m-%d %H:%M:%S'),
|
||||
reading.systolic,
|
||||
reading.diastolic,
|
||||
reading.heart_rate,
|
||||
])
|
||||
yield output.getvalue()
|
||||
output.seek(0)
|
||||
output.truncate(0)
|
||||
|
||||
return Response(
|
||||
generate_csv(),
|
||||
mimetype='text/csv',
|
||||
headers={'Content-Disposition': 'attachment; filename=readings.csv'}
|
||||
)
|
||||
import csv
|
||||
from io import StringIO
|
||||
from flask import Blueprint, render_template, redirect, request, url_for, flash, stream_with_context
|
||||
from app.models import Reading, db
|
||||
from flask_login import login_required, current_user
|
||||
from datetime import datetime
|
||||
from app.forms import DeleteForm
|
||||
from app.insights import parse_range, readings_query, user_timezone
|
||||
|
||||
data = Blueprint('data', __name__)
|
||||
|
||||
@data.route('/', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
def manage_data():
|
||||
form = DeleteForm()
|
||||
if request.method == 'POST':
|
||||
if not form.validate_on_submit():
|
||||
flash('Your form expired. Please try again.', 'danger')
|
||||
return redirect(url_for('data.manage_data'))
|
||||
# Handle CSV file upload
|
||||
file = request.files.get('file')
|
||||
if file and file.filename.endswith('.csv'):
|
||||
try:
|
||||
payload = file.read(2_000_001)
|
||||
if len(payload) > 2_000_000:
|
||||
raise ValueError('CSV must be 2 MB or smaller.')
|
||||
csv_data = csv.reader(StringIO(payload.decode('utf-8-sig')))
|
||||
if next(csv_data, None) != ['Timestamp', 'Systolic', 'Diastolic', 'Heart Rate']:
|
||||
raise ValueError('Use the column headings from a BP Tracker export.')
|
||||
readings_to_add = []
|
||||
for line, row in enumerate(csv_data, 2):
|
||||
timestamp, systolic, diastolic, heart_rate = row
|
||||
if not (50 <= int(systolic) <= 250 and 30 <= int(diastolic) <= 150 and 30 <= int(heart_rate) <= 200):
|
||||
raise ValueError(f'Check the measurement values on row {line}.')
|
||||
readings_to_add.append(Reading(
|
||||
user_id=current_user.id,
|
||||
timestamp=datetime.strptime(timestamp, '%Y-%m-%d %H:%M:%S'),
|
||||
systolic=int(systolic),
|
||||
diastolic=int(diastolic),
|
||||
heart_rate=int(heart_rate),
|
||||
))
|
||||
db.session.bulk_save_objects(readings_to_add)
|
||||
db.session.commit()
|
||||
flash('Data imported successfully!', 'success')
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
flash(f'Error importing data: {str(e)}', 'danger')
|
||||
else:
|
||||
flash('Please upload a valid CSV file.', 'danger')
|
||||
return redirect(url_for('data.manage_data'))
|
||||
|
||||
return render_template('data.html', form=form)
|
||||
|
||||
@data.route('/export', methods=['GET'])
|
||||
@login_required
|
||||
def export_data():
|
||||
import io
|
||||
from flask import Response
|
||||
user_id = current_user.id
|
||||
selected = None
|
||||
if any(request.args.get(key) for key in ('start_date', 'end_date', 'days')):
|
||||
try:
|
||||
selected = parse_range(request.args, user_timezone(), 30)
|
||||
except ValueError as error:
|
||||
return render_template('error.html', message=str(error)), 400
|
||||
|
||||
def generate_csv():
|
||||
"""Stream CSV rows to avoid loading all readings into memory."""
|
||||
# Write header
|
||||
output = io.StringIO()
|
||||
writer = csv.writer(output)
|
||||
writer.writerow(['Timestamp', 'Systolic', 'Diastolic', 'Heart Rate'])
|
||||
yield output.getvalue()
|
||||
output.seek(0)
|
||||
output.truncate(0)
|
||||
|
||||
# Stream readings in chunks using yield_per
|
||||
readings = readings_query(user_id, selected).order_by(
|
||||
Reading.timestamp, Reading.id
|
||||
).yield_per(500)
|
||||
|
||||
for reading in readings:
|
||||
writer.writerow([
|
||||
reading.timestamp.strftime('%Y-%m-%d %H:%M:%S'),
|
||||
reading.systolic,
|
||||
reading.diastolic,
|
||||
reading.heart_rate,
|
||||
])
|
||||
yield output.getvalue()
|
||||
output.seek(0)
|
||||
output.truncate(0)
|
||||
|
||||
return Response(
|
||||
stream_with_context(generate_csv()),
|
||||
mimetype='text/csv',
|
||||
headers={'Content-Disposition': 'attachment; filename=readings.csv'}
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user