11import logging
22from datetime import datetime
3+ from json import JSONDecodeError , loads
34
4- from flask import Blueprint , render_template
5+ from flask import Blueprint , render_template , url_for
56
67from application .services .dashboard_service import get_dashboard_metrics
78from application .translations import get_translation
89
910logger = logging .getLogger (__name__ )
1011dashboard_routes = Blueprint ("dashboard" , __name__ )
1112
13+ # Constants for external vendor handover and testing purposes.
14+ # These values are intentionally fixed to allow vendors to perform technical tests,
15+ # while allowing the system to identify and filter test responses.
16+ VENDOR_TEST_USER = "test"
17+ VENDOR_TEST_SURVEY_ID = "link_copy"
18+ VENDOR_TEST_LANG = "he"
19+ VENDOR_TEST_DEMO = "false"
20+
21+
22+ def _safe_json (value , default ):
23+ """Safely parse a JSON-like value (str/dict/list) with fallback."""
24+ if value is None :
25+ return default
26+ if isinstance (value , (dict , list )):
27+ return value
28+ if isinstance (value , str ):
29+ try :
30+ return loads (value )
31+ except JSONDecodeError :
32+ return default
33+ return default
34+
35+
36+ def parse_survey_data (survey ):
37+ """
38+ Parse raw survey row into a resilient dashboard console shape.
39+
40+ Rules:
41+ - status ignores `active`; survey is active only when participant_count > 0
42+ - strategy from pair_generation_config.strategy, default Unknown
43+ - dimension from subjects length
44+ - context from English story title with Hebrew fallback
45+ - date in `%b %d` format
46+ """
47+ pair_config = _safe_json (survey .get ("pair_generation_config" ), {})
48+ title = _safe_json (survey .get ("title" ), {})
49+ subjects = _safe_json (survey .get ("subjects" ), [])
50+
51+ strategy_raw = (
52+ pair_config .get ("strategy" ) if isinstance (pair_config , dict ) else None
53+ )
54+ if isinstance (strategy_raw , str ) and strategy_raw .strip ():
55+ strategy_name = strategy_raw .strip ().replace ("_" , " " ).title ()
56+ else :
57+ strategy_name = "Unknown"
58+
59+ if not isinstance (subjects , list ):
60+ subjects = []
61+
62+ if isinstance (title , dict ):
63+ context = title .get ("en" ) or title .get ("he" ) or ""
64+ else :
65+ context = str (title or "" )
66+
67+ participant_count_raw = survey .get ("participant_count" , 0 )
68+ try :
69+ participant_count = int (participant_count_raw or 0 )
70+ except (TypeError , ValueError ):
71+ participant_count = 0
72+
73+ created_at = survey .get ("created_at" )
74+ created_dt = None
75+ date_label = ""
76+ sort_date = ""
77+ if isinstance (created_at , datetime ):
78+ created_dt = created_at
79+ elif isinstance (created_at , str ):
80+ try :
81+ created_dt = datetime .fromisoformat (created_at .replace ("Z" , "+00:00" ))
82+ except ValueError :
83+ # Keep empty when date is malformed; do not break rendering.
84+ created_dt = None
85+
86+ if created_dt :
87+ date_label = created_dt .strftime ("%d %b %y" )
88+ sort_date = created_dt .strftime ("%Y-%m-%d" )
89+
90+ is_active_data = participant_count > 0
91+ dimension_count = len (subjects )
92+ dimension_label = f"{ dimension_count } D" if dimension_count > 0 else "N/A"
93+
94+ # Three-Tier Maturity Model Logic
95+ if participant_count == 0 :
96+ ui_status = "gray"
97+ ui_status_tooltip = "Inactive (N=0)"
98+ elif participant_count < 30 :
99+ ui_status = "orange"
100+ ui_status_tooltip = "Gathering Data (N<30)"
101+ else :
102+ ui_status = "green"
103+ ui_status_tooltip = "Sufficient Data (N>=30)"
104+
105+ ui_share_link = url_for (
106+ "survey.index" ,
107+ userID = VENDOR_TEST_USER ,
108+ surveyID = VENDOR_TEST_SURVEY_ID ,
109+ internalID = survey .get ("id" ),
110+ lang = VENDOR_TEST_LANG ,
111+ demo = VENDOR_TEST_DEMO ,
112+ _external = True ,
113+ )
114+
115+ return {
116+ "id" : survey .get ("id" ),
117+ "ui_date" : date_label ,
118+ "ui_status" : ui_status ,
119+ "ui_status_tooltip" : ui_status_tooltip ,
120+ "is_active_data" : is_active_data ,
121+ "ui_strategy" : strategy_name ,
122+ "ui_context" : context ,
123+ "ui_dimension" : dimension_label ,
124+ "ui_volume" : participant_count ,
125+ "sort_date" : sort_date ,
126+ "sort_identity" : strategy_name ,
127+ "sort_dim" : dimension_count ,
128+ "ui_share_link" : ui_share_link ,
129+ }
130+
12131
13132@dashboard_routes .route ("/" )
14133def view_dashboard ():
15134 """Display the dashboard overview."""
16135 try :
17136 # Get dashboard data and metrics
18137 dashboard_data = get_dashboard_metrics ()
138+ raw_surveys = dashboard_data .get ("surveys" , [])
139+ parsed_surveys = [parse_survey_data (survey ) for survey in raw_surveys ]
140+
141+ inactive_surveys = sum (1 for survey in raw_surveys if not survey .get ("active" ))
142+ logger .info (
143+ "Dashboard survey load complete: total=%s, inactive=%s" ,
144+ len (raw_surveys ),
145+ inactive_surveys ,
146+ )
19147
20148 # Get translations for dashboard content
21149 translations = {
@@ -25,9 +153,15 @@ def view_dashboard():
25153 "total_surveys_description" : get_translation (
26154 "total_surveys_description" , "dashboard"
27155 ),
28- "total_participants" : get_translation ("total_participants" , "dashboard" ),
156+ "total_participants" : get_translation (
157+ "total_participants" ,
158+ "dashboard" ,
159+ ),
29160 "excluded_users" : get_translation ("excluded_users" , "dashboard" ),
30- "all_participants" : get_translation ("all_participants" , "dashboard" ),
161+ "all_participants" : get_translation (
162+ "all_participants" ,
163+ "dashboard" ,
164+ ),
31165 "total_participants_description" : get_translation (
32166 "total_participants_description" , "dashboard"
33167 ),
@@ -40,11 +174,33 @@ def view_dashboard():
40174 "last_updated" : get_translation ("last_updated" , "dashboard" ),
41175 "view_responses" : get_translation ("view_responses" , "dashboard" ),
42176 "take_survey" : get_translation ("take_survey" , "dashboard" ),
177+ "blocked_users" : get_translation ("blocked_users" , "dashboard" ),
178+ "search_placeholder" : get_translation ("search_placeholder" , "dashboard" ),
179+ "active_data_toggle" : get_translation ("active_data_toggle" , "dashboard" ),
180+ "all_surveys_toggle" : get_translation ("all_surveys_toggle" , "dashboard" ),
181+ "copied_to_clipboard" : get_translation ("copied_to_clipboard" , "dashboard" ),
182+ "copy_link" : get_translation ("copy_link" , "dashboard" ),
183+ "col_id" : get_translation ("col_id" , "dashboard" ),
184+ "col_date" : get_translation ("col_date" , "dashboard" ),
185+ "col_status" : get_translation ("col_status" , "dashboard" ),
186+ "col_identity" : get_translation ("col_identity" , "dashboard" ),
187+ "col_dim" : get_translation ("col_dim" , "dashboard" ),
188+ "col_volume" : get_translation ("col_volume" , "dashboard" ),
189+ "col_actions" : get_translation ("col_actions" , "dashboard" ),
190+ "filter_all_strategies" : get_translation (
191+ "filter_all_strategies" , "dashboard"
192+ ),
193+ "filter_all_stories" : get_translation ("filter_all_stories" , "dashboard" ),
194+ "filter_all_dims" : get_translation ("filter_all_dims" , "dashboard" ),
195+ "col_story" : get_translation ("col_story" , "dashboard" ),
196+ "no_results_found" : get_translation ("no_results_found" , "dashboard" ),
197+ "clear_all_filters" : get_translation ("clear_all_filters" , "dashboard" ),
43198 }
44199
45200 return render_template (
46201 "dashboard/surveys_overview.html" ,
47- surveys = dashboard_data ["surveys" ],
202+ surveys = raw_surveys ,
203+ surveys_console = parsed_surveys ,
48204 total_surveys = dashboard_data ["total_surveys" ],
49205 total_participants = dashboard_data ["total_participants" ],
50206 unaware_users_count = dashboard_data ["unaware_users_count" ],
0 commit comments