05/24 Enhance functionalities
This commit is contained in:
@@ -1480,3 +1480,191 @@ def purge_app_log(older_than_days: int = 30):
|
||||
finally:
|
||||
if conn:
|
||||
conn.close()
|
||||
|
||||
|
||||
# ─── Missed Shifts ────────────────────────────────────────────────────────────
|
||||
|
||||
def get_missed_shifts_today() -> list:
|
||||
"""Return shift+user pairs scheduled today where the user has checked 0 sites."""
|
||||
conn = None
|
||||
try:
|
||||
conn = get_connection()
|
||||
cur = conn.cursor(dictionary=True)
|
||||
cur.execute(
|
||||
"""
|
||||
SELECT s.name AS shift_name,
|
||||
u.id AS user_id, u.username,
|
||||
COALESCE(u.full_name, u.username) AS full_name,
|
||||
COUNT(DISTINCT sw.website_id) AS total_sites,
|
||||
COUNT(DISTINCT sc.website_id) AS checked_sites
|
||||
FROM shifts s
|
||||
JOIN shift_users su ON su.shift_id = s.id
|
||||
JOIN users u ON u.id = su.user_id AND u.is_active = 1
|
||||
JOIN shift_websites sw ON sw.shift_id = s.id
|
||||
LEFT JOIN shift_checks sc
|
||||
ON sc.user_id = su.user_id
|
||||
AND sc.website_id = sw.website_id
|
||||
AND DATE(sc.checked_at) = CURDATE()
|
||||
WHERE s.is_active = 1
|
||||
AND LOCATE(CAST(DAYOFWEEK(CURDATE()) AS CHAR), s.days_of_week) > 0
|
||||
GROUP BY s.id, u.id
|
||||
HAVING total_sites > 0 AND checked_sites = 0
|
||||
ORDER BY s.name, u.username
|
||||
"""
|
||||
)
|
||||
rows = cur.fetchall()
|
||||
cur.close()
|
||||
return rows
|
||||
finally:
|
||||
if conn:
|
||||
conn.close()
|
||||
|
||||
|
||||
# ─── Bid Reminders ────────────────────────────────────────────────────────────
|
||||
|
||||
def get_bids_due_soon(days: int = 7) -> list:
|
||||
"""Return open/monitoring bids with due_date within the next `days` days."""
|
||||
conn = None
|
||||
try:
|
||||
conn = get_connection()
|
||||
cur = conn.cursor(dictionary=True)
|
||||
cur.execute(
|
||||
"SELECT b.*, u.username AS added_by_username, u.full_name AS added_by_full_name"
|
||||
" FROM bid_tracker b"
|
||||
" LEFT JOIN users u ON u.id = b.added_by"
|
||||
" WHERE b.status IN ('open','monitoring')"
|
||||
" AND b.due_date IS NOT NULL"
|
||||
" AND b.due_date BETWEEN CURDATE() AND DATE_ADD(CURDATE(), INTERVAL %s DAY)"
|
||||
" ORDER BY b.due_date ASC",
|
||||
(days,),
|
||||
)
|
||||
rows = cur.fetchall()
|
||||
cur.close()
|
||||
return rows
|
||||
finally:
|
||||
if conn:
|
||||
conn.close()
|
||||
|
||||
|
||||
def get_admin_emails() -> list:
|
||||
"""Return email addresses of all active admin users that have one set."""
|
||||
conn = None
|
||||
try:
|
||||
conn = get_connection()
|
||||
cur = conn.cursor(dictionary=True)
|
||||
cur.execute(
|
||||
"SELECT email FROM users"
|
||||
" WHERE role='admin' AND is_active=1 AND email IS NOT NULL AND email != ''"
|
||||
)
|
||||
rows = cur.fetchall()
|
||||
cur.close()
|
||||
return [r["email"] for r in rows]
|
||||
finally:
|
||||
if conn:
|
||||
conn.close()
|
||||
|
||||
|
||||
# ─── Server-side Health Check ─────────────────────────────────────────────────
|
||||
|
||||
def get_website_url(website_id: int):
|
||||
"""Return the URL of an active website by ID, or None if not found."""
|
||||
conn = None
|
||||
try:
|
||||
conn = get_connection()
|
||||
cur = conn.cursor()
|
||||
cur.execute("SELECT url FROM websites WHERE id=%s AND is_active=1", (website_id,))
|
||||
row = cur.fetchone()
|
||||
cur.close()
|
||||
return row[0] if row else None
|
||||
finally:
|
||||
if conn:
|
||||
conn.close()
|
||||
|
||||
|
||||
# ─── Password Reset ───────────────────────────────────────────────────────────
|
||||
|
||||
def get_user_by_email(email: str):
|
||||
"""Return the active user matching the given email (case-insensitive), or None."""
|
||||
conn = None
|
||||
try:
|
||||
conn = get_connection()
|
||||
cur = conn.cursor(dictionary=True)
|
||||
cur.execute(
|
||||
"SELECT * FROM users WHERE LOWER(email)=%s AND is_active=1 LIMIT 1",
|
||||
(email.lower(),),
|
||||
)
|
||||
row = cur.fetchone()
|
||||
cur.close()
|
||||
return row
|
||||
finally:
|
||||
if conn:
|
||||
conn.close()
|
||||
|
||||
|
||||
def create_password_reset_token(user_id: int) -> str:
|
||||
"""Generate a secure 1-hour reset token; invalidates any prior token for the user."""
|
||||
import secrets
|
||||
token = secrets.token_urlsafe(48)
|
||||
conn = None
|
||||
try:
|
||||
conn = get_connection()
|
||||
cur = conn.cursor()
|
||||
cur.execute("DELETE FROM password_reset_tokens WHERE user_id=%s", (user_id,))
|
||||
cur.execute(
|
||||
"INSERT INTO password_reset_tokens (user_id, token, expires_at)"
|
||||
" VALUES (%s, %s, DATE_ADD(NOW(), INTERVAL 1 HOUR))",
|
||||
(user_id, token),
|
||||
)
|
||||
conn.commit()
|
||||
cur.close()
|
||||
return token
|
||||
finally:
|
||||
if conn:
|
||||
conn.close()
|
||||
|
||||
|
||||
def get_password_reset_user(token: str):
|
||||
"""Return the user row for a valid, unexpired reset token, or None."""
|
||||
conn = None
|
||||
try:
|
||||
conn = get_connection()
|
||||
cur = conn.cursor(dictionary=True)
|
||||
cur.execute(
|
||||
"SELECT u.* FROM password_reset_tokens t"
|
||||
" JOIN users u ON u.id = t.user_id"
|
||||
" WHERE t.token=%s AND t.expires_at > NOW()",
|
||||
(token,),
|
||||
)
|
||||
row = cur.fetchone()
|
||||
cur.close()
|
||||
return row
|
||||
finally:
|
||||
if conn:
|
||||
conn.close()
|
||||
|
||||
|
||||
def consume_password_reset_token(token: str, new_password: str) -> bool:
|
||||
"""Validate token, update the user's password, and delete the token. Returns True on success."""
|
||||
conn = None
|
||||
try:
|
||||
conn = get_connection()
|
||||
cur = conn.cursor(dictionary=True)
|
||||
cur.execute(
|
||||
"SELECT user_id FROM password_reset_tokens WHERE token=%s AND expires_at > NOW()",
|
||||
(token,),
|
||||
)
|
||||
row = cur.fetchone()
|
||||
if not row:
|
||||
cur.close()
|
||||
return False
|
||||
user_id = row["user_id"]
|
||||
new_hash = _hash_password(new_password)
|
||||
cur = conn.cursor()
|
||||
cur.execute("UPDATE users SET password=%s WHERE id=%s", (new_hash, user_id))
|
||||
cur.execute("DELETE FROM password_reset_tokens WHERE token=%s", (token,))
|
||||
conn.commit()
|
||||
cur.close()
|
||||
return True
|
||||
finally:
|
||||
if conn:
|
||||
conn.close()
|
||||
|
||||
Reference in New Issue
Block a user