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 app import db
from datetime import datetime from app.utils.time_utils import now_eastern
import json import json
@@ -11,7 +11,7 @@ class InspectionTemplate(db.Model):
description = db.Column(db.Text) description = db.Column(db.Text)
frequency = db.Column(db.Enum('daily', 'weekly', 'monthly', 'quarterly')) frequency = db.Column(db.Enum('daily', 'weekly', 'monthly', 'quarterly'))
created_by = db.Column(db.Integer, db.ForeignKey('users.id')) 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) form_schema = db.Column(db.JSON, nullable=True)
checklist_items = db.relationship('ChecklistItem', backref='template', lazy='dynamic', cascade='all, delete-orphan') 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) facility_id = db.Column(db.Integer, db.ForeignKey('facilities.id'), nullable=False)
area_id = db.Column(db.Integer, db.ForeignKey('areas.id')) area_id = db.Column(db.Integer, db.ForeignKey('areas.id'))
inspector_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False) 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)) overall_score = db.Column(db.Numeric(5, 2))
status = db.Column(db.Enum('in_progress', 'completed', 'flagged'), default='in_progress') status = db.Column(db.Enum('in_progress', 'completed', 'flagged'), default='in_progress')
notes = db.Column(db.Text) # inspector free-text notes notes = db.Column(db.Text) # inspector free-text notes
+3 -3
View File
@@ -1,5 +1,5 @@
from app import db from app import db
from datetime import datetime from app.utils.time_utils import now_eastern
class IssueComment(db.Model): class IssueComment(db.Model):
@@ -10,7 +10,7 @@ class IssueComment(db.Model):
user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False) 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 status_at_time = db.Column(db.String(20)) # snapshot of issue status when comment was made
body = db.Column(db.Text, nullable=False) 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 # Relationships
author = db.relationship('User', foreign_keys=[user_id]) author = db.relationship('User', foreign_keys=[user_id])
@@ -30,7 +30,7 @@ class Issue(db.Model):
photo_path = db.Column(db.String(255)) photo_path = db.Column(db.String(255))
status = db.Column(db.Enum('open', 'in_progress', 'resolved'), default='open') status = db.Column(db.Enum('open', 'in_progress', 'resolved'), default='open')
assigned_to = db.Column(db.Integer, db.ForeignKey('users.id')) 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) resolved_at = db.Column(db.DateTime)
result_notes = db.Column(db.Text) result_notes = db.Column(db.Text)
result_photos = db.Column(db.JSON) # list of relative paths e.g. ["uploads/issue_photos/abc.jpg"] 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 app import db, login_manager
from flask_login import UserMixin from flask_login import UserMixin
from werkzeug.security import generate_password_hash, check_password_hash 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 @login_manager.user_loader
def load_user(user_id): 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) email = db.Column(db.String(255), unique=True, nullable=False, index=True)
password_hash = db.Column(db.String(255), nullable=False) password_hash = db.Column(db.String(255), nullable=False)
role = db.Column(db.Enum('admin', 'supervisor', 'inspector'), 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 # Relationships
inspections = db.relationship('Inspection', backref='inspector', lazy='dynamic') 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() user = User.query.filter_by(username=form.username.data).first()
if user and user.check_password(form.password.data): 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'] # Use validated next URL — never redirect blindly to request.args['next']
next_page = _safe_next(request.args.get('next')) next_page = _safe_next(request.args.get('next'))
flash(f'Welcome back, {user.username}!', 'success') 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 app.models.user import User
from sqlalchemy import func from sqlalchemy import func
from datetime import datetime, timedelta from datetime import datetime, timedelta
from app.utils.time_utils import now_eastern
bp = Blueprint('dashboard', __name__) bp = Blueprint('dashboard', __name__)
@@ -15,7 +16,7 @@ bp = Blueprint('dashboard', __name__)
@login_required @login_required
def index(): def index():
# Get today's date range # 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) today_end = today_start + timedelta(days=1)
# Statistics for today # Statistics for today
@@ -55,7 +56,7 @@ def index():
).count() ).count()
# Calculate average score (last 30 days) # 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( avg_score_query = Inspection.query.filter(
Inspection.status == 'completed', Inspection.status == 'completed',
Inspection.overall_score.isnot(None), Inspection.overall_score.isnot(None),
+4 -3
View File
@@ -2,6 +2,7 @@ import os
import json import json
import uuid import uuid
from datetime import datetime from datetime import datetime
from app.utils.time_utils import now_eastern
from flask import (Blueprint, render_template, redirect, url_for, from flask import (Blueprint, render_template, redirect, url_for,
flash, request, current_app, jsonify) flash, request, current_app, jsonify)
from flask_login import login_required, current_user from flask_login import login_required, current_user
@@ -223,7 +224,7 @@ def start():
facility_id = form.facility_id.data, facility_id = form.facility_id.data,
area_id = form.area_id.data or None, area_id = form.area_id.data or None,
inspector_id = current_user.id, inspector_id = current_user.id,
inspection_date = datetime.utcnow(), inspection_date = now_eastern(),
status = 'in_progress', status = 'in_progress',
notes = form.notes.data or None, notes = form.notes.data or None,
) )
@@ -300,7 +301,7 @@ def execute(inspection_id):
score = _compute_score_from_form(form_fields, responses) score = _compute_score_from_form(form_fields, responses)
inspection.overall_score = score inspection.overall_score = score
inspection.status = 'completed' inspection.status = 'completed'
inspection.completed_at = datetime.utcnow() inspection.completed_at = now_eastern()
# Persist the final form data alongside any inspector notes # Persist the final form data alongside any inspector notes
_save_responses(inspection, responses) _save_responses(inspection, responses)
@@ -401,7 +402,7 @@ def flag_issue(inspection_id):
photo_path = photo_path, photo_path = photo_path,
status = 'open', status = 'open',
assigned_to = form.assigned_to.data or None, assigned_to = form.assigned_to.data or None,
reported_at = datetime.utcnow(), reported_at = now_eastern(),
) )
db.session.add(issue) 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, from flask import (Blueprint, render_template, redirect, url_for,
flash, request, current_app) flash, request, current_app)
from flask_login import login_required, current_user 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 issue.assigned_to = form.assigned_to.data or None
if form.status.data == 'resolved' and not issue.resolved_at: 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': elif form.status.data != 'resolved':
issue.resolved_at = None issue.resolved_at = None
@@ -130,7 +130,7 @@ def create():
photo_path = photo_path, photo_path = photo_path,
status = 'open', status = 'open',
assigned_to = form.assigned_to.data or None, assigned_to = form.assigned_to.data or None,
reported_at = datetime.utcnow(), reported_at = now_eastern(),
) )
db.session.add(issue) db.session.add(issue)
db.session.commit() db.session.commit()
+2 -1
View File
@@ -1,6 +1,7 @@
import csv import csv
import io import io
from datetime import datetime, timedelta from datetime import datetime, timedelta
from app.utils.time_utils import now_eastern
from flask import (Blueprint, render_template, request, from flask import (Blueprint, render_template, request,
Response, stream_with_context) Response, stream_with_context)
from flask_login import login_required, current_user from flask_login import login_required, current_user
@@ -17,7 +18,7 @@ bp = Blueprint('reports', __name__, url_prefix='/reports')
def _date_range(): def _date_range():
"""Parse ?start= and ?end= query params; default to last 30 days.""" """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) start_default = end_default - timedelta(days=30)
try: try:
start = datetime.strptime(request.args.get('start', ''), '%Y-%m-%d') start = datetime.strptime(request.args.get('start', ''), '%Y-%m-%d')
+5
View File
@@ -33,6 +33,11 @@
</div> </div>
{% endif %} {% endif %}
</div> </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"> <button type="submit" class="btn btn-primary btn-lg w-100">
<i class="bi bi-box-arrow-in-right"></i> Login <i class="bi bi-box-arrow-in-right"></i> Login
+3 -2
View File
@@ -11,8 +11,9 @@ from app.models.user import User
# ── Auth ───────────────────────────────────────────────────────────────────── # ── Auth ─────────────────────────────────────────────────────────────────────
class LoginForm(FlaskForm): class LoginForm(FlaskForm):
username = StringField('Username', validators=[DataRequired(), Length(min=3, max=100)]) username = StringField('Username', validators=[DataRequired(), Length(min=3, max=100)])
password = PasswordField('Password', validators=[DataRequired()]) password = PasswordField('Password', validators=[DataRequired()])
remember_me = BooleanField('Keep me logged in')
class UserForm(FlaskForm): 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)