Feb 02 2026: implement remembering logged in, correct timezone
This commit is contained in:
@@ -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
@@ -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
@@ -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
@@ -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')
|
||||||
|
|||||||
@@ -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),
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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')
|
||||||
|
|||||||
@@ -34,6 +34,11 @@
|
|||||||
{% 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
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
+3
-2
@@ -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):
|
||||||
|
|||||||
@@ -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)
|
||||||
Reference in New Issue
Block a user