04/23 Enhance app functionalities
This commit is contained in:
@@ -891,8 +891,14 @@ def get_shift_report(date_from=None, date_to=None, user_id=None, website_id=None
|
||||
|
||||
def get_unchecked_report(target_date=None, user_id=None):
|
||||
"""
|
||||
Return websites that were NOT checked on target_date (default: today)
|
||||
for the given user (or all users if omitted).
|
||||
Return websites that a user was EXPECTED to check on target_date but did not.
|
||||
|
||||
"Expected" is defined by shift membership on that day-of-week:
|
||||
- Only websites assigned to a shift the user belongs to are included.
|
||||
- Visibility rules (all / assigned) are respected.
|
||||
- When a user has no shifts, falls back to all active visible websites.
|
||||
When no target_date is supplied, defaults to today via CURDATE().
|
||||
|
||||
Columns: check_date, username, full_name, website_name, url, status
|
||||
"""
|
||||
conn = None
|
||||
@@ -902,36 +908,70 @@ def get_unchecked_report(target_date=None, user_id=None):
|
||||
|
||||
user_filter = "AND u.id = %s" if user_id else ""
|
||||
|
||||
# Use a bind param only when a specific date is provided.
|
||||
# When no date is given, embed CURDATE() directly in SQL so MySQL
|
||||
# evaluates it as a function — passing "CURDATE()" as a %s bind
|
||||
# parameter treats it as a literal string, not a SQL function, and
|
||||
# causes the NOT EXISTS filter to match nothing (returns 0 rows).
|
||||
# Embed CURDATE() directly when no date given — passing it as a %s
|
||||
# bind param would treat it as a literal string, not a SQL function.
|
||||
if target_date:
|
||||
date_val = str(target_date)
|
||||
date_expr = "%s"
|
||||
date_params = [date_val]
|
||||
# DAYOFWEEK for a specific date
|
||||
dow_expr = "DAYOFWEEK(%s)"
|
||||
dow_params = [date_val]
|
||||
else:
|
||||
date_expr = "CURDATE()"
|
||||
date_params = []
|
||||
dow_expr = "DAYOFWEEK(CURDATE())"
|
||||
dow_params = []
|
||||
|
||||
params = date_params + ([user_id] if user_id else []) + date_params
|
||||
# One user_id param slot for the user_filter inside the main query
|
||||
user_filter_params = [user_id] if user_id else []
|
||||
|
||||
# The subquery needs: dow_params, (optional user_id for shift_users join)
|
||||
# Outer query needs: date_params, user_filter_params, date_params
|
||||
# EXISTS(shift) subquery: dow_params + (user_id if filtering by user)
|
||||
# We build params carefully to match the f-string placeholders below.
|
||||
params = (
|
||||
date_params # {date_expr} in SELECT
|
||||
+ user_filter_params # {user_filter} AND u.id = %s
|
||||
+ dow_params # DAYOFWEEK(%s) in shift EXISTS
|
||||
+ (user_id and [user_id] or []) # su.user_id=%s in shift EXISTS
|
||||
+ date_params # DATE(sc.checked_at) = {date_expr}
|
||||
)
|
||||
|
||||
cur.execute(
|
||||
f"""
|
||||
SELECT
|
||||
{date_expr} AS check_date,
|
||||
{date_expr} AS check_date,
|
||||
u.username,
|
||||
COALESCE(u.full_name, u.username) AS full_name,
|
||||
w.name AS website_name,
|
||||
COALESCE(u.full_name, u.username) AS full_name,
|
||||
w.name AS website_name,
|
||||
w.url,
|
||||
'Not Checked' AS status
|
||||
FROM websites w
|
||||
CROSS JOIN users u
|
||||
WHERE w.is_active = 1
|
||||
AND u.is_active = 1
|
||||
AND u.role = 'user'
|
||||
'Not Checked' AS status
|
||||
FROM users u
|
||||
-- Only websites the user was expected to check on this date
|
||||
JOIN (
|
||||
SELECT DISTINCT sw.website_id
|
||||
FROM shift_websites sw
|
||||
JOIN shifts s ON s.id = sw.shift_id
|
||||
JOIN shift_users su ON su.shift_id = s.id
|
||||
JOIN websites w2 ON w2.id = sw.website_id
|
||||
WHERE s.is_active = 1
|
||||
AND w2.is_active = 1
|
||||
AND LOCATE(CAST({dow_expr} AS CHAR), s.days_of_week) > 0
|
||||
{"AND su.user_id = %s" if user_id else "AND su.user_id = u.id"}
|
||||
AND (
|
||||
w2.visibility = 'all'
|
||||
OR EXISTS (
|
||||
SELECT 1 FROM website_users wu
|
||||
WHERE wu.website_id = w2.id AND wu.user_id = su.user_id
|
||||
)
|
||||
)
|
||||
) expected ON 1=1
|
||||
JOIN websites w ON w.id = expected.website_id
|
||||
WHERE u.is_active = 1
|
||||
AND u.role = 'user'
|
||||
{user_filter}
|
||||
-- Exclude sites the user DID check on the target date
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM shift_checks sc
|
||||
WHERE sc.website_id = w.id
|
||||
@@ -1461,7 +1501,12 @@ def create_criterion(admin_id: int, title: str, description: str,
|
||||
|
||||
def update_criterion(admin_id: int, criterion_id: int, title: str,
|
||||
description: str, is_active: bool, sort_order: int):
|
||||
"""Update an existing AI evaluation criterion."""
|
||||
"""Update an existing AI evaluation criterion.
|
||||
|
||||
The full description text is included in the activity_log detail field so
|
||||
there is a complete audit trail of exactly what criteria wording the AI was
|
||||
evaluating against at any point in time.
|
||||
"""
|
||||
conn = None
|
||||
try:
|
||||
conn = get_connection()
|
||||
@@ -1476,8 +1521,13 @@ def update_criterion(admin_id: int, criterion_id: int, title: str,
|
||||
)
|
||||
conn.commit()
|
||||
cur.close()
|
||||
# Include full description in detail so audit log captures wording at
|
||||
# time of change — essential for reconstructing what criteria were
|
||||
# active during any historical AI analysis.
|
||||
log_action(admin_id, "UPDATE_AI_CRITERION", "ai_criteria", criterion_id,
|
||||
f"Updated criterion id={criterion_id} '{title}' active={is_active}.")
|
||||
f"Updated criterion id={criterion_id} '{title}' "
|
||||
f"active={is_active} order={sort_order}. "
|
||||
f"Description: {description[:500]}")
|
||||
logger.info(f"AI criterion id={criterion_id} updated by admin_id={admin_id}.")
|
||||
finally:
|
||||
if conn:
|
||||
@@ -1502,3 +1552,107 @@ def delete_criterion(admin_id: int, criterion_id: int):
|
||||
finally:
|
||||
if conn:
|
||||
conn.close()
|
||||
|
||||
|
||||
# ─── AI Analysis History ──────────────────────────────────────────────────────
|
||||
|
||||
def save_ai_analysis(user_id: int, file_names: str, model: str,
|
||||
verdict: str | None, criteria_snapshot: str | None,
|
||||
summary_text: str) -> int:
|
||||
"""
|
||||
Persist an AI analysis result to ai_analysis_log.
|
||||
verdict : 'PURSUE' | 'PASS' | 'UNCLEAR' | None
|
||||
criteria_snapshot: JSON/text of active criteria at time of analysis
|
||||
Returns the new row id.
|
||||
"""
|
||||
conn = None
|
||||
try:
|
||||
conn = get_connection()
|
||||
cur = conn.cursor()
|
||||
cur.execute(
|
||||
"""
|
||||
INSERT INTO ai_analysis_log
|
||||
(user_id, file_names, model, verdict, criteria_snapshot, summary_text)
|
||||
VALUES (%s, %s, %s, %s, %s, %s)
|
||||
""",
|
||||
(user_id, file_names, model, verdict, criteria_snapshot, summary_text)
|
||||
)
|
||||
conn.commit()
|
||||
new_id = cur.lastrowid
|
||||
cur.close()
|
||||
logger.info(
|
||||
f"AI analysis saved: id={new_id} user_id={user_id} "
|
||||
f"verdict={verdict} files='{file_names[:80]}'."
|
||||
)
|
||||
return new_id
|
||||
finally:
|
||||
if conn:
|
||||
conn.close()
|
||||
|
||||
|
||||
def get_ai_analysis_history(user_id: int | None = None, limit: int = 100):
|
||||
"""
|
||||
Return recent AI analysis log entries.
|
||||
When user_id is provided, filters to that user's own analyses.
|
||||
Admins pass user_id=None to see all users' analyses.
|
||||
Columns: id, username, file_names, model, verdict, analyzed_at
|
||||
(summary_text excluded for list view — fetch by id for detail).
|
||||
"""
|
||||
conn = None
|
||||
try:
|
||||
conn = get_connection()
|
||||
cur = conn.cursor(dictionary=True)
|
||||
if user_id:
|
||||
cur.execute(
|
||||
"""
|
||||
SELECT al.id, u.username, al.file_names, al.model,
|
||||
al.verdict, al.analyzed_at
|
||||
FROM ai_analysis_log al
|
||||
LEFT JOIN users u ON u.id = al.user_id
|
||||
WHERE al.user_id = %s
|
||||
ORDER BY al.analyzed_at DESC
|
||||
LIMIT %s
|
||||
""",
|
||||
(user_id, limit)
|
||||
)
|
||||
else:
|
||||
cur.execute(
|
||||
"""
|
||||
SELECT al.id, u.username, al.file_names, al.model,
|
||||
al.verdict, al.analyzed_at
|
||||
FROM ai_analysis_log al
|
||||
LEFT JOIN users u ON u.id = al.user_id
|
||||
ORDER BY al.analyzed_at DESC
|
||||
LIMIT %s
|
||||
""",
|
||||
(limit,)
|
||||
)
|
||||
rows = cur.fetchall()
|
||||
cur.close()
|
||||
return rows
|
||||
finally:
|
||||
if conn:
|
||||
conn.close()
|
||||
|
||||
|
||||
def get_ai_analysis_detail(analysis_id: int):
|
||||
"""Return a single ai_analysis_log row including summary_text and criteria_snapshot."""
|
||||
conn = None
|
||||
try:
|
||||
conn = get_connection()
|
||||
cur = conn.cursor(dictionary=True)
|
||||
cur.execute(
|
||||
"""
|
||||
SELECT al.*, u.username
|
||||
FROM ai_analysis_log al
|
||||
LEFT JOIN users u ON u.id = al.user_id
|
||||
WHERE al.id = %s
|
||||
""",
|
||||
(analysis_id,)
|
||||
)
|
||||
row = cur.fetchone()
|
||||
cur.close()
|
||||
return row
|
||||
finally:
|
||||
if conn:
|
||||
conn.close()
|
||||
|
||||
Reference in New Issue
Block a user