Feb 02 2026: implement remembering logged in, correct timezone

This commit is contained in:
2026-02-28 12:37:20 -05:00
parent 998569d402
commit ab2d5546b4
11 changed files with 53 additions and 20 deletions
+3 -3
View File
@@ -1,5 +1,5 @@
from app import db
from datetime import datetime
from app.utils.time_utils import now_eastern
import json
@@ -11,7 +11,7 @@ class InspectionTemplate(db.Model):
description = db.Column(db.Text)
frequency = db.Column(db.Enum('daily', 'weekly', 'monthly', 'quarterly'))
created_by = db.Column(db.Integer, db.ForeignKey('users.id'))
created_at = db.Column(db.DateTime, default=datetime.utcnow)
created_at = db.Column(db.DateTime, default=now_eastern)
form_schema = db.Column(db.JSON, nullable=True)
checklist_items = db.relationship('ChecklistItem', backref='template', lazy='dynamic', cascade='all, delete-orphan')
@@ -57,7 +57,7 @@ class Inspection(db.Model):
facility_id = db.Column(db.Integer, db.ForeignKey('facilities.id'), nullable=False)
area_id = db.Column(db.Integer, db.ForeignKey('areas.id'))
inspector_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
inspection_date = db.Column(db.DateTime, nullable=False, default=datetime.utcnow)
inspection_date = db.Column(db.DateTime, nullable=False, default=now_eastern)
overall_score = db.Column(db.Numeric(5, 2))
status = db.Column(db.Enum('in_progress', 'completed', 'flagged'), default='in_progress')
notes = db.Column(db.Text) # inspector free-text notes
+3 -3
View File
@@ -1,5 +1,5 @@
from app import db
from datetime import datetime
from app.utils.time_utils import now_eastern
class IssueComment(db.Model):
@@ -10,7 +10,7 @@ class IssueComment(db.Model):
user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
status_at_time = db.Column(db.String(20)) # snapshot of issue status when comment was made
body = db.Column(db.Text, nullable=False)
created_at = db.Column(db.DateTime, default=datetime.utcnow, nullable=False)
created_at = db.Column(db.DateTime, default=now_eastern, nullable=False)
# Relationships
author = db.relationship('User', foreign_keys=[user_id])
@@ -30,7 +30,7 @@ class Issue(db.Model):
photo_path = db.Column(db.String(255))
status = db.Column(db.Enum('open', 'in_progress', 'resolved'), default='open')
assigned_to = db.Column(db.Integer, db.ForeignKey('users.id'))
reported_at = db.Column(db.DateTime, default=datetime.utcnow)
reported_at = db.Column(db.DateTime, default=now_eastern)
resolved_at = db.Column(db.DateTime)
result_notes = db.Column(db.Text)
result_photos = db.Column(db.JSON) # list of relative paths e.g. ["uploads/issue_photos/abc.jpg"]
+2 -2
View File
@@ -1,7 +1,7 @@
from app import db, login_manager
from flask_login import UserMixin
from werkzeug.security import generate_password_hash, check_password_hash
from datetime import datetime
from app.utils.time_utils import now_eastern
@login_manager.user_loader
def load_user(user_id):
@@ -15,7 +15,7 @@ class User(UserMixin, db.Model):
email = db.Column(db.String(255), unique=True, nullable=False, index=True)
password_hash = db.Column(db.String(255), nullable=False)
role = db.Column(db.Enum('admin', 'supervisor', 'inspector'), nullable=False)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
created_at = db.Column(db.DateTime, default=now_eastern)
# Relationships
inspections = db.relationship('Inspection', backref='inspector', lazy='dynamic')
+1 -1
View File
@@ -35,7 +35,7 @@ def login():
user = User.query.filter_by(username=form.username.data).first()
if user and user.check_password(form.password.data):
login_user(user)
login_user(user, remember=form.remember_me.data)
# Use validated next URL — never redirect blindly to request.args['next']
next_page = _safe_next(request.args.get('next'))
flash(f'Welcome back, {user.username}!', 'success')
+3 -2
View File
@@ -7,6 +7,7 @@ from app.models.issue import Issue
from app.models.user import User
from sqlalchemy import func
from datetime import datetime, timedelta
from app.utils.time_utils import now_eastern
bp = Blueprint('dashboard', __name__)
@@ -15,7 +16,7 @@ bp = Blueprint('dashboard', __name__)
@login_required
def index():
# Get today's date range
today_start = datetime.utcnow().replace(hour=0, minute=0, second=0, microsecond=0)
today_start = now_eastern().replace(hour=0, minute=0, second=0, microsecond=0)
today_end = today_start + timedelta(days=1)
# Statistics for today
@@ -55,7 +56,7 @@ def index():
).count()
# Calculate average score (last 30 days)
thirty_days_ago = datetime.utcnow() - timedelta(days=30)
thirty_days_ago = now_eastern() - timedelta(days=30)
avg_score_query = Inspection.query.filter(
Inspection.status == 'completed',
Inspection.overall_score.isnot(None),
+4 -3
View File
@@ -2,6 +2,7 @@ import os
import json
import uuid
from datetime import datetime
from app.utils.time_utils import now_eastern
from flask import (Blueprint, render_template, redirect, url_for,
flash, request, current_app, jsonify)
from flask_login import login_required, current_user
@@ -223,7 +224,7 @@ def start():
facility_id = form.facility_id.data,
area_id = form.area_id.data or None,
inspector_id = current_user.id,
inspection_date = datetime.utcnow(),
inspection_date = now_eastern(),
status = 'in_progress',
notes = form.notes.data or None,
)
@@ -300,7 +301,7 @@ def execute(inspection_id):
score = _compute_score_from_form(form_fields, responses)
inspection.overall_score = score
inspection.status = 'completed'
inspection.completed_at = datetime.utcnow()
inspection.completed_at = now_eastern()
# Persist the final form data alongside any inspector notes
_save_responses(inspection, responses)
@@ -401,7 +402,7 @@ def flag_issue(inspection_id):
photo_path = photo_path,
status = 'open',
assigned_to = form.assigned_to.data or None,
reported_at = datetime.utcnow(),
reported_at = now_eastern(),
)
db.session.add(issue)
+3 -3
View File
@@ -1,4 +1,4 @@
from datetime import datetime
from app.utils.time_utils import now_eastern
from flask import (Blueprint, render_template, redirect, url_for,
flash, request, current_app)
from flask_login import login_required, current_user
@@ -66,7 +66,7 @@ def view(issue_id):
issue.assigned_to = form.assigned_to.data or None
if form.status.data == 'resolved' and not issue.resolved_at:
issue.resolved_at = datetime.utcnow()
issue.resolved_at = now_eastern()
elif form.status.data != 'resolved':
issue.resolved_at = None
@@ -130,7 +130,7 @@ def create():
photo_path = photo_path,
status = 'open',
assigned_to = form.assigned_to.data or None,
reported_at = datetime.utcnow(),
reported_at = now_eastern(),
)
db.session.add(issue)
db.session.commit()
+2 -1
View File
@@ -1,6 +1,7 @@
import csv
import io
from datetime import datetime, timedelta
from app.utils.time_utils import now_eastern
from flask import (Blueprint, render_template, request,
Response, stream_with_context)
from flask_login import login_required, current_user
@@ -17,7 +18,7 @@ bp = Blueprint('reports', __name__, url_prefix='/reports')
def _date_range():
"""Parse ?start= and ?end= query params; default to last 30 days."""
end_default = datetime.utcnow()
end_default = now_eastern()
start_default = end_default - timedelta(days=30)
try:
start = datetime.strptime(request.args.get('start', ''), '%Y-%m-%d')
+5
View File
@@ -34,6 +34,11 @@
{% endif %}
</div>
<div class="mb-3 form-check">
{{ form.remember_me(class="form-check-input") }}
{{ form.remember_me.label(class="form-check-label text-muted") }}
</div>
<button type="submit" class="btn btn-primary btn-lg w-100">
<i class="bi bi-box-arrow-in-right"></i> Login
</button>
+3 -2
View File
@@ -11,8 +11,9 @@ from app.models.user import User
# ── Auth ─────────────────────────────────────────────────────────────────────
class LoginForm(FlaskForm):
username = StringField('Username', validators=[DataRequired(), Length(min=3, max=100)])
password = PasswordField('Password', validators=[DataRequired()])
username = StringField('Username', validators=[DataRequired(), Length(min=3, max=100)])
password = PasswordField('Password', validators=[DataRequired()])
remember_me = BooleanField('Keep me logged in')
class UserForm(FlaskForm):
+24
View File
@@ -0,0 +1,24 @@
"""
time_utils.py
-------------
Centralised time helpers for the JQC application.
All timestamps are stored as Eastern Time (America/New_York) so that
displayed dates reflect the local business timezone without any
conversion layer in templates or reports.
"""
from datetime import datetime
import pytz
EASTERN = pytz.timezone('America/New_York')
def now_eastern() -> datetime:
"""Return the current wall-clock time in US/Eastern (naive datetime).
Stored as a naive datetime in the database so that existing DateTime
columns require no schema change. The value is always Eastern local
time (auto-adjusts for EDT / EST).
"""
return datetime.now(tz=EASTERN).replace(tzinfo=None)