Feb 02 2026: implement remembering logged in, correct timezone
This commit is contained in:
@@ -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
@@ -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
@@ -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
@@ -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')
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -33,6 +33,11 @@
|
||||
</div>
|
||||
{% 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
|
||||
|
||||
+3
-2
@@ -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):
|
||||
|
||||
@@ -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