04/24 Fixed running report issues

This commit is contained in:
2026-04-24 12:44:51 -04:00
parent 07bae2a724
commit 5f2a4e4749
+60 -34
View File
@@ -953,15 +953,26 @@ def get_unchecked_report(target_date=None, user_id=None):
# 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.
# Fix for Bug #1: MySQL does not allow a derived table (subquery in FROM/JOIN)
# to reference outer-query aliases (e.g. u.id). The previous approach used
# "AND su.user_id = u.id" inside a derived table when user_id=None, which
# MySQL rejects with "Unknown column 'u.id' in 'where clause'".
#
# Solution: replace the derived-table JOIN with an EXISTS correlated subquery
# in the WHERE clause. Correlated subqueries CAN reference outer aliases,
# so u.id is always in scope. This works identically for both the
# single-user and all-users cases.
#
# Param order:
# date_params → {date_expr} in SELECT
# user_filter_params → {user_filter} AND u.id = %s
# dow_params → DAYOFWEEK(%s) in EXISTS
# user_filter_params → su.user_id = u.id or %s in EXISTS (always u.id now)
# date_params → DATE(sc.checked_at) = {date_expr}
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}
)
@@ -975,29 +986,27 @@ def get_unchecked_report(target_date=None, user_id=None):
w.url,
'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
JOIN websites w ON w.is_active = 1
WHERE u.is_active = 1
AND u.role = 'user'
{user_filter}
-- Only include websites the user was expected to check via their shifts
AND EXISTS (
SELECT 1
FROM shift_websites sw
JOIN shifts s ON s.id = sw.shift_id
JOIN shift_users su ON su.shift_id = s.id AND su.user_id = u.id
WHERE sw.website_id = w.id
AND s.is_active = 1
AND LOCATE(CAST({dow_expr} AS CHAR), s.days_of_week) > 0
AND (
w.visibility = 'all'
OR EXISTS (
SELECT 1 FROM website_users wu
WHERE wu.website_id = w.id AND wu.user_id = u.id
)
)
)
-- Exclude sites the user DID check on the target date
AND NOT EXISTS (
SELECT 1 FROM shift_checks sc
@@ -1049,13 +1058,32 @@ def get_summary_report(date_from=None, date_to=None):
where_clause = ("WHERE " + " AND ".join(conditions)) if conditions else ""
# Fix for Bug #2: MySQL only_full_group_by rejects referencing sc.checked_at
# (the full timestamp) inside correlated subqueries when only DATE(sc.checked_at)
# appears in the GROUP BY clause. Even though sc.checked_at is functionally
# determined by DATE(sc.checked_at) in intent, MySQL strict mode does not
# infer that relationship automatically.
#
# Solution: pre-aggregate in a CTE (agg) that produces a single, unambiguous
# check_date (DATE) and user_id per group. The outer SELECT then references
# agg.check_date — a fully grouped column — inside the correlated subqueries,
# satisfying only_full_group_by completely.
cur.execute(
f"""
WITH agg AS (
SELECT
DATE(sc.checked_at) AS check_date,
sc.user_id,
COUNT(DISTINCT sc.website_id) AS checked_count
FROM shift_checks sc
{where_clause}
GROUP BY DATE(sc.checked_at), sc.user_id
)
SELECT
agg.check_date,
u.username,
COALESCE(u.full_name, u.username) AS full_name,
COUNT(DISTINCT sc.website_id) AS checked_count,
agg.checked_count,
(
SELECT COUNT(DISTINCT sw2.website_id)
FROM shift_websites sw2
@@ -1064,12 +1092,12 @@ def get_summary_report(date_from=None, date_to=None):
AND su2.user_id = u.id
WHERE s2.is_active = 1
AND LOCATE(
CAST(DAYOFWEEK(DATE(sc.checked_at)) AS CHAR),
CAST(DAYOFWEEK(agg.check_date) AS CHAR),
s2.days_of_week
) > 0
) AS total_sites,
ROUND(
COUNT(DISTINCT sc.website_id) * 100.0 /
agg.checked_count * 100.0 /
NULLIF((
SELECT COUNT(DISTINCT sw2.website_id)
FROM shift_websites sw2
@@ -1078,17 +1106,15 @@ def get_summary_report(date_from=None, date_to=None):
AND su2.user_id = u.id
WHERE s2.is_active = 1
AND LOCATE(
CAST(DAYOFWEEK(DATE(sc.checked_at)) AS CHAR),
CAST(DAYOFWEEK(agg.check_date) AS CHAR),
s2.days_of_week
) > 0
), 0),
1
) AS pct_complete
FROM shift_checks sc
JOIN users u ON u.id = sc.user_id
{where_clause}
GROUP BY DATE(sc.checked_at), sc.user_id
ORDER BY check_date DESC, u.username
FROM agg
JOIN users u ON u.id = agg.user_id
ORDER BY agg.check_date DESC, u.username
""",
params
)