Add support for setting user timezone
This commit is contained in:
14
Readme.md
14
Readme.md
@@ -30,6 +30,20 @@
|
||||
|
||||
`docker run -p 5000:5000 -e DATABASE_URL=postgres://postgres:59fff56880e1bbb42e753d2a82ac21b6@peterstockings.com:15389/bloodpressure_db bloodpressure`
|
||||
|
||||
# Model updates
|
||||
|
||||
Create migration
|
||||
|
||||
```
|
||||
flask db migrate -m "Add timezone to Profile model"
|
||||
```
|
||||
|
||||
Apply migration
|
||||
|
||||
```
|
||||
flask db upgrade
|
||||
```
|
||||
|
||||
# Fix deployment issues
|
||||
|
||||
Because I was originally using a Heroku buildpack to build/host this app prior to switching to a Dockerfile it sets the ports to:
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from typing import Optional
|
||||
from flask_wtf import FlaskForm
|
||||
from wtforms import BooleanField, FileField, StringField, PasswordField, SubmitField, IntegerField, DateTimeLocalField
|
||||
from pytz import all_timezones
|
||||
from wtforms import BooleanField, FileField, SelectField, 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
|
||||
@@ -85,6 +86,7 @@ class ProfileForm(FlaskForm):
|
||||
name = StringField('Name', validators=[Optional()])
|
||||
email = StringField('Email', validators=[Optional(), Email()])
|
||||
profile_pic = FileField('Profile Picture (optional)')
|
||||
timezone = SelectField('Timezone', choices=[(tz, tz) for tz in all_timezones])
|
||||
systolic_threshold = IntegerField(
|
||||
'Systolic Threshold (mmHg)',
|
||||
validators=[Optional(), NumberRange(min=90, max=200)]
|
||||
|
||||
@@ -18,6 +18,7 @@ class Profile(db.Model):
|
||||
systolic_threshold = db.Column(db.Integer, default=140)
|
||||
diastolic_threshold = db.Column(db.Integer, default=90)
|
||||
dark_mode = db.Column(db.Boolean, default=False)
|
||||
timezone = db.Column(db.String(50), default='UTC') # e.g., 'Australia/Sydney'
|
||||
|
||||
class Reading(db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
|
||||
@@ -4,6 +4,7 @@ from io import StringIO
|
||||
import io
|
||||
from flask import Blueprint, Response, make_response, render_template, redirect, request, send_file, url_for, flash
|
||||
import humanize
|
||||
from pytz import timezone, utc
|
||||
from sqlalchemy import func
|
||||
from werkzeug.security import generate_password_hash, check_password_hash
|
||||
from werkzeug.http import http_date
|
||||
@@ -177,10 +178,15 @@ def dashboard():
|
||||
# Fetch readings
|
||||
readings = readings_query.order_by(Reading.timestamp.desc()).all()
|
||||
|
||||
# Add relative timestamps to readings
|
||||
# Fetch the user's timezone (default to 'UTC' if none is set)
|
||||
user_timezone = current_user.profile.timezone if current_user.profile and current_user.profile.timezone else 'UTC'
|
||||
local_tz = timezone(user_timezone)
|
||||
|
||||
# Add relative & local timestamps to readings
|
||||
now = datetime.utcnow()
|
||||
for reading in readings:
|
||||
reading.relative_timestamp = humanize.naturaltime(now - reading.timestamp)
|
||||
reading.local_timestamp = utc.localize(reading.timestamp).astimezone(local_tz)
|
||||
|
||||
# Calculate weekly summary and progress badges
|
||||
systolic_avg, diastolic_avg, heart_rate_avg = calculate_weekly_summary(readings)
|
||||
@@ -248,10 +254,20 @@ def edit_reading(reading_id):
|
||||
if reading.user_id != current_user.id:
|
||||
flash('You are not authorized to edit this reading.', 'danger')
|
||||
return redirect(url_for('main.dashboard'))
|
||||
|
||||
# Fetch the user's timezone (default to 'UTC' if none is set)
|
||||
user_timezone = current_user.profile.timezone if current_user.profile and current_user.profile.timezone else 'UTC'
|
||||
local_tz = timezone(user_timezone)
|
||||
|
||||
reading.local_timestamp = utc.localize(reading.timestamp).astimezone(local_tz)
|
||||
|
||||
form = ReadingForm(obj=reading) # Populate form with existing reading data
|
||||
form.timestamp.data = reading.local_timestamp
|
||||
if form.validate_on_submit():
|
||||
reading.timestamp = form.timestamp.data
|
||||
# Convert the local timestamp back to UTC for saving
|
||||
local_timestamp = form.timestamp.data
|
||||
reading.timestamp = user_timezone.localize(local_timestamp).astimezone(utc)
|
||||
|
||||
reading.systolic = form.systolic.data
|
||||
reading.diastolic = form.diastolic.data
|
||||
reading.heart_rate = form.heart_rate.data
|
||||
@@ -306,6 +322,7 @@ def profile():
|
||||
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
|
||||
profile.timezone = form.timezone.data
|
||||
|
||||
# Handle profile picture upload
|
||||
if form.profile_pic.data:
|
||||
|
||||
@@ -608,58 +608,42 @@ video {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.inset-0 {
|
||||
inset: 0px;
|
||||
}
|
||||
|
||||
.top-0 {
|
||||
top: 0px;
|
||||
}
|
||||
|
||||
.right-2 {
|
||||
right: 0.5rem;
|
||||
}
|
||||
|
||||
.top-2 {
|
||||
top: 0.5rem;
|
||||
}
|
||||
|
||||
.bottom-2 {
|
||||
bottom: 0.5rem;
|
||||
.bottom-0 {
|
||||
bottom: 0px;
|
||||
}
|
||||
|
||||
.left-4 {
|
||||
left: 1rem;
|
||||
}
|
||||
|
||||
.right-0 {
|
||||
right: 0px;
|
||||
}
|
||||
|
||||
.right-2 {
|
||||
right: 0.5rem;
|
||||
}
|
||||
|
||||
.right-4 {
|
||||
right: 1rem;
|
||||
}
|
||||
|
||||
.top-0 {
|
||||
top: 0px;
|
||||
}
|
||||
|
||||
.top-2 {
|
||||
top: 0.5rem;
|
||||
}
|
||||
|
||||
.top-4 {
|
||||
top: 1rem;
|
||||
}
|
||||
|
||||
.top-6 {
|
||||
top: 1.5rem;
|
||||
}
|
||||
|
||||
.top-5 {
|
||||
top: 1.25rem;
|
||||
}
|
||||
|
||||
.bottom-0 {
|
||||
bottom: 0px;
|
||||
}
|
||||
|
||||
.left-0 {
|
||||
left: 0px;
|
||||
}
|
||||
|
||||
.right-0 {
|
||||
right: 0px;
|
||||
}
|
||||
|
||||
.z-10 {
|
||||
z-index: 10;
|
||||
}
|
||||
@@ -673,16 +657,6 @@ video {
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.my-2 {
|
||||
margin-top: 0.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.my-3 {
|
||||
margin-top: 0.75rem;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.mb-2 {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
@@ -755,14 +729,6 @@ video {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.inline-flex {
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
.table {
|
||||
display: table;
|
||||
}
|
||||
|
||||
.grid {
|
||||
display: grid;
|
||||
}
|
||||
@@ -780,14 +746,14 @@ video {
|
||||
height: 4rem;
|
||||
}
|
||||
|
||||
.h-24 {
|
||||
height: 6rem;
|
||||
}
|
||||
|
||||
.h-4 {
|
||||
height: 1rem;
|
||||
}
|
||||
|
||||
.h-44 {
|
||||
height: 11rem;
|
||||
}
|
||||
|
||||
.h-5 {
|
||||
height: 1.25rem;
|
||||
}
|
||||
@@ -800,34 +766,18 @@ video {
|
||||
height: 2rem;
|
||||
}
|
||||
|
||||
.h-3 {
|
||||
height: 0.75rem;
|
||||
}
|
||||
|
||||
.h-28 {
|
||||
height: 7rem;
|
||||
}
|
||||
|
||||
.h-32 {
|
||||
height: 8rem;
|
||||
}
|
||||
|
||||
.h-44 {
|
||||
height: 11rem;
|
||||
}
|
||||
|
||||
.w-16 {
|
||||
width: 4rem;
|
||||
}
|
||||
|
||||
.w-24 {
|
||||
width: 6rem;
|
||||
}
|
||||
|
||||
.w-4 {
|
||||
width: 1rem;
|
||||
}
|
||||
|
||||
.w-44 {
|
||||
width: 11rem;
|
||||
}
|
||||
|
||||
.w-5 {
|
||||
width: 1.25rem;
|
||||
}
|
||||
@@ -844,26 +794,6 @@ video {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.w-3 {
|
||||
width: 0.75rem;
|
||||
}
|
||||
|
||||
.w-28 {
|
||||
width: 7rem;
|
||||
}
|
||||
|
||||
.w-32 {
|
||||
width: 8rem;
|
||||
}
|
||||
|
||||
.w-44 {
|
||||
width: 11rem;
|
||||
}
|
||||
|
||||
.max-w-2xl {
|
||||
max-width: 42rem;
|
||||
}
|
||||
|
||||
.max-w-4xl {
|
||||
max-width: 56rem;
|
||||
}
|
||||
@@ -892,10 +822,6 @@ video {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.border-collapse {
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
.transform {
|
||||
transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
|
||||
}
|
||||
@@ -940,17 +866,12 @@ video {
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.gap-8 {
|
||||
gap: 2rem;
|
||||
}
|
||||
|
||||
.gap-6 {
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.gap-x-4 {
|
||||
-moz-column-gap: 1rem;
|
||||
column-gap: 1rem;
|
||||
.gap-8 {
|
||||
gap: 2rem;
|
||||
}
|
||||
|
||||
.space-x-2 > :not([hidden]) ~ :not([hidden]) {
|
||||
@@ -983,24 +904,6 @@ video {
|
||||
margin-bottom: calc(1.5rem * var(--tw-space-y-reverse));
|
||||
}
|
||||
|
||||
.overflow-hidden {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.overflow-x-auto {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.truncate {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.whitespace-nowrap {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.rounded {
|
||||
border-radius: 0.25rem;
|
||||
}
|
||||
@@ -1013,10 +916,6 @@ video {
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
|
||||
.rounded-md {
|
||||
border-radius: 0.375rem;
|
||||
}
|
||||
|
||||
.border {
|
||||
border-width: 1px;
|
||||
}
|
||||
@@ -1033,6 +932,11 @@ video {
|
||||
border-bottom-width: 2px;
|
||||
}
|
||||
|
||||
.border-blue-600 {
|
||||
--tw-border-opacity: 1;
|
||||
border-color: rgb(37 99 235 / var(--tw-border-opacity, 1));
|
||||
}
|
||||
|
||||
.border-gray-300 {
|
||||
--tw-border-opacity: 1;
|
||||
border-color: rgb(209 213 219 / var(--tw-border-opacity, 1));
|
||||
@@ -1043,16 +947,6 @@ video {
|
||||
border-color: rgb(255 255 255 / var(--tw-border-opacity, 1));
|
||||
}
|
||||
|
||||
.border-blue-600 {
|
||||
--tw-border-opacity: 1;
|
||||
border-color: rgb(37 99 235 / var(--tw-border-opacity, 1));
|
||||
}
|
||||
|
||||
.border-green-200 {
|
||||
--tw-border-opacity: 1;
|
||||
border-color: rgb(187 247 208 / var(--tw-border-opacity, 1));
|
||||
}
|
||||
|
||||
.bg-blue-600 {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(37 99 235 / var(--tw-bg-opacity, 1));
|
||||
@@ -1108,16 +1002,6 @@ video {
|
||||
background-color: rgb(255 255 255 / var(--tw-bg-opacity, 1));
|
||||
}
|
||||
|
||||
.bg-blue-100 {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(219 234 254 / var(--tw-bg-opacity, 1));
|
||||
}
|
||||
|
||||
.bg-green-50 {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(240 253 244 / var(--tw-bg-opacity, 1));
|
||||
}
|
||||
|
||||
.bg-gradient-to-r {
|
||||
background-image: linear-gradient(to right, var(--tw-gradient-stops));
|
||||
}
|
||||
@@ -1141,6 +1025,14 @@ video {
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.p-0 {
|
||||
padding: 0px;
|
||||
}
|
||||
|
||||
.p-1 {
|
||||
padding: 0.25rem;
|
||||
}
|
||||
|
||||
.p-2 {
|
||||
padding: 0.5rem;
|
||||
}
|
||||
@@ -1161,14 +1053,6 @@ video {
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.p-1 {
|
||||
padding: 0.25rem;
|
||||
}
|
||||
|
||||
.p-0 {
|
||||
padding: 0px;
|
||||
}
|
||||
|
||||
.px-2 {
|
||||
padding-left: 0.5rem;
|
||||
padding-right: 0.5rem;
|
||||
@@ -1189,11 +1073,6 @@ video {
|
||||
padding-right: 2rem;
|
||||
}
|
||||
|
||||
.py-1 {
|
||||
padding-top: 0.25rem;
|
||||
padding-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.py-16 {
|
||||
padding-top: 4rem;
|
||||
padding-bottom: 4rem;
|
||||
@@ -1219,11 +1098,6 @@ video {
|
||||
padding-bottom: 1rem;
|
||||
}
|
||||
|
||||
.px-3 {
|
||||
padding-left: 0.75rem;
|
||||
padding-right: 0.75rem;
|
||||
}
|
||||
|
||||
.pl-2 {
|
||||
padding-left: 0.5rem;
|
||||
}
|
||||
@@ -1232,10 +1106,6 @@ video {
|
||||
padding-top: 1.5rem;
|
||||
}
|
||||
|
||||
.pt-2 {
|
||||
padding-top: 0.5rem;
|
||||
}
|
||||
|
||||
.text-left {
|
||||
text-align: left;
|
||||
}
|
||||
@@ -1296,15 +1166,16 @@ video {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.uppercase {
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.text-blue-600 {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(37 99 235 / var(--tw-text-opacity, 1));
|
||||
}
|
||||
|
||||
.text-gray-400 {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(156 163 175 / var(--tw-text-opacity, 1));
|
||||
}
|
||||
|
||||
.text-gray-500 {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(107 114 128 / var(--tw-text-opacity, 1));
|
||||
@@ -1325,11 +1196,21 @@ video {
|
||||
color: rgb(31 41 55 / var(--tw-text-opacity, 1));
|
||||
}
|
||||
|
||||
.text-green-600 {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(22 163 74 / var(--tw-text-opacity, 1));
|
||||
}
|
||||
|
||||
.text-green-800 {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(22 101 52 / var(--tw-text-opacity, 1));
|
||||
}
|
||||
|
||||
.text-red-500 {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(239 68 68 / var(--tw-text-opacity, 1));
|
||||
}
|
||||
|
||||
.text-red-600 {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(220 38 38 / var(--tw-text-opacity, 1));
|
||||
@@ -1340,31 +1221,6 @@ video {
|
||||
color: rgb(255 255 255 / var(--tw-text-opacity, 1));
|
||||
}
|
||||
|
||||
.text-gray-400 {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(156 163 175 / var(--tw-text-opacity, 1));
|
||||
}
|
||||
|
||||
.text-green-600 {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(22 163 74 / var(--tw-text-opacity, 1));
|
||||
}
|
||||
|
||||
.text-red-500 {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(239 68 68 / var(--tw-text-opacity, 1));
|
||||
}
|
||||
|
||||
.text-blue-800 {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(30 64 175 / var(--tw-text-opacity, 1));
|
||||
}
|
||||
|
||||
.text-green-700 {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(21 128 61 / var(--tw-text-opacity, 1));
|
||||
}
|
||||
|
||||
.no-underline {
|
||||
text-decoration-line: none;
|
||||
}
|
||||
@@ -1401,12 +1257,6 @@ video {
|
||||
transition-duration: 150ms;
|
||||
}
|
||||
|
||||
.transition-all {
|
||||
transition-property: all;
|
||||
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
||||
transition-duration: 150ms;
|
||||
}
|
||||
|
||||
.transition-shadow {
|
||||
transition-property: box-shadow;
|
||||
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
||||
@@ -1433,9 +1283,9 @@ video {
|
||||
background-color: rgb(156 163 175 / var(--tw-bg-opacity, 1));
|
||||
}
|
||||
|
||||
.hover\:bg-gray-50:hover {
|
||||
.hover\:bg-green-200:hover {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(249 250 251 / var(--tw-bg-opacity, 1));
|
||||
background-color: rgb(187 247 208 / var(--tw-bg-opacity, 1));
|
||||
}
|
||||
|
||||
.hover\:bg-green-700:hover {
|
||||
@@ -1448,41 +1298,11 @@ video {
|
||||
background-color: rgb(185 28 28 / var(--tw-bg-opacity, 1));
|
||||
}
|
||||
|
||||
.hover\:bg-green-200:hover {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(187 247 208 / var(--tw-bg-opacity, 1));
|
||||
}
|
||||
|
||||
.hover\:bg-green-100:hover {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(220 252 231 / var(--tw-bg-opacity, 1));
|
||||
}
|
||||
|
||||
.hover\:text-blue-800:hover {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(30 64 175 / var(--tw-text-opacity, 1));
|
||||
}
|
||||
|
||||
.hover\:text-gray-200:hover {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(229 231 235 / var(--tw-text-opacity, 1));
|
||||
}
|
||||
|
||||
.hover\:text-gray-900:hover {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(17 24 39 / var(--tw-text-opacity, 1));
|
||||
}
|
||||
|
||||
.hover\:text-red-800:hover {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(153 27 27 / var(--tw-text-opacity, 1));
|
||||
}
|
||||
|
||||
.hover\:text-white:hover {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(255 255 255 / var(--tw-text-opacity, 1));
|
||||
}
|
||||
|
||||
.hover\:text-gray-600:hover {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(75 85 99 / var(--tw-text-opacity, 1));
|
||||
@@ -1493,11 +1313,21 @@ video {
|
||||
color: rgb(31 41 55 / var(--tw-text-opacity, 1));
|
||||
}
|
||||
|
||||
.hover\:text-gray-900:hover {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(17 24 39 / var(--tw-text-opacity, 1));
|
||||
}
|
||||
|
||||
.hover\:text-red-700:hover {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(185 28 28 / var(--tw-text-opacity, 1));
|
||||
}
|
||||
|
||||
.hover\:text-white:hover {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(255 255 255 / var(--tw-text-opacity, 1));
|
||||
}
|
||||
|
||||
.hover\:underline:hover {
|
||||
text-decoration-line: underline;
|
||||
}
|
||||
@@ -1517,16 +1347,6 @@ video {
|
||||
border-color: rgb(59 130 246 / var(--tw-border-opacity, 1));
|
||||
}
|
||||
|
||||
.focus\:border-gray-400:focus {
|
||||
--tw-border-opacity: 1;
|
||||
border-color: rgb(156 163 175 / var(--tw-border-opacity, 1));
|
||||
}
|
||||
|
||||
.focus\:border-red-500:focus {
|
||||
--tw-border-opacity: 1;
|
||||
border-color: rgb(239 68 68 / var(--tw-border-opacity, 1));
|
||||
}
|
||||
|
||||
.focus\:text-white:focus {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(255 255 255 / var(--tw-text-opacity, 1));
|
||||
@@ -1548,21 +1368,11 @@ video {
|
||||
--tw-ring-color: rgb(59 130 246 / var(--tw-ring-opacity, 1));
|
||||
}
|
||||
|
||||
.focus\:ring-gray-400:focus {
|
||||
--tw-ring-opacity: 1;
|
||||
--tw-ring-color: rgb(156 163 175 / var(--tw-ring-opacity, 1));
|
||||
}
|
||||
|
||||
.focus\:ring-green-500:focus {
|
||||
--tw-ring-opacity: 1;
|
||||
--tw-ring-color: rgb(34 197 94 / var(--tw-ring-opacity, 1));
|
||||
}
|
||||
|
||||
.focus\:ring-red-500:focus {
|
||||
--tw-ring-opacity: 1;
|
||||
--tw-ring-color: rgb(239 68 68 / var(--tw-ring-opacity, 1));
|
||||
}
|
||||
|
||||
.group:hover .group-hover\:scale-105 {
|
||||
--tw-scale-x: 1.05;
|
||||
--tw-scale-y: 1.05;
|
||||
@@ -1573,12 +1383,6 @@ video {
|
||||
text-decoration-line: underline;
|
||||
}
|
||||
|
||||
@media (min-width: 640px) {
|
||||
.sm\:p-0 {
|
||||
padding: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.md\:w-auto {
|
||||
width: auto;
|
||||
|
||||
@@ -107,7 +107,7 @@
|
||||
<path stroke-linecap="round" stroke-linejoin="round"
|
||||
d="M12 8v4l3 3m9-3a9 9 0 1 1-18 0 9 9 0 0 1 18 0z" />
|
||||
</svg>
|
||||
<span title="{{ reading.timestamp.strftime('%d %b %Y, %I:%M %p') }}">
|
||||
<span title="{{ reading.local_timestamp.strftime('%d %b %Y, %I:%M %p') }}">
|
||||
{{ reading.relative_timestamp }}
|
||||
</span>
|
||||
</div>
|
||||
@@ -163,7 +163,7 @@
|
||||
<p class="text-xs text-gray-600 mt-1">{{ reading.heart_rate }} bpm</p>
|
||||
<!-- Timestamp -->
|
||||
<div class="text-xs text-gray-500 mt-1">
|
||||
{{ reading.timestamp.strftime('%I:%M %p') }}
|
||||
{{ reading.local_timestamp.strftime('%I:%M %p') }}
|
||||
</div>
|
||||
</a>
|
||||
{% endfor %}
|
||||
@@ -222,7 +222,7 @@
|
||||
<p class="text-xs text-gray-600 mt-1">{{ reading.heart_rate }} bpm</p>
|
||||
<!-- Timestamp -->
|
||||
<div class="text-xs text-gray-500 mt-1">
|
||||
{{ reading.timestamp.strftime('%I:%M %p') }}
|
||||
{{ reading.local_timestamp.strftime('%I:%M %p') }}
|
||||
</div>
|
||||
</a>
|
||||
{% endfor %}
|
||||
|
||||
@@ -57,6 +57,12 @@
|
||||
focus:ring-blue-500") }}
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
{{ form.timezone.label(class="block text-sm font-medium text-gray-700") }}
|
||||
{{ form.timezone(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") }}
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
"""Add timezone to Profile model
|
||||
|
||||
Revision ID: 5e5a1b78b966
|
||||
Revises: 59097bee8942
|
||||
Create Date: 2024-12-28 00:46:52.941616
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '5e5a1b78b966'
|
||||
down_revision = '59097bee8942'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
with op.batch_alter_table('profile', schema=None) as batch_op:
|
||||
batch_op.add_column(sa.Column('timezone', sa.String(length=50), nullable=True))
|
||||
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
with op.batch_alter_table('profile', schema=None) as batch_op:
|
||||
batch_op.drop_column('timezone')
|
||||
|
||||
# ### end Alembic commands ###
|
||||
@@ -24,6 +24,7 @@ packaging==24.2
|
||||
pillow==11.0.0
|
||||
psycopg2==2.9.10
|
||||
psycopg2-binary==2.9.10
|
||||
pytz==2024.2
|
||||
SQLAlchemy==2.0.36
|
||||
typing-extensions==4.12.2
|
||||
werkzeug==3.1.3
|
||||
|
||||
Reference in New Issue
Block a user