Skip to content

Commit d0485d1

Browse files
authored
Merge pull request #97 from ariel-research/feature/researcher-console-ui
Feature/researcher console UI
2 parents b6457d6 + 9d4e647 commit d0485d1

File tree

14 files changed

+2367
-632
lines changed

14 files changed

+2367
-632
lines changed

application/routes/dashboard.py

Lines changed: 160 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,149 @@
11
import logging
22
from 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

67
from application.services.dashboard_service import get_dashboard_metrics
78
from application.translations import get_translation
89

910
logger = logging.getLogger(__name__)
1011
dashboard_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("/")
14133
def 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"],
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import logging
2+
from datetime import datetime
3+
4+
from flask import Blueprint, render_template
5+
6+
from application.services.dashboard_service import get_dashboard_metrics
7+
from application.translations import get_translation
8+
9+
logger = logging.getLogger(__name__)
10+
dashboard_routes = Blueprint("dashboard", __name__)
11+
12+
13+
@dashboard_routes.route("/")
14+
def view_dashboard():
15+
"""Display the dashboard overview."""
16+
try:
17+
# Get dashboard data and metrics
18+
dashboard_data = get_dashboard_metrics()
19+
20+
# Get translations for dashboard content
21+
translations = {
22+
"title": get_translation("title", "dashboard"),
23+
"subtitle": get_translation("subtitle", "dashboard"),
24+
"total_surveys": get_translation("total_surveys", "dashboard"),
25+
"total_surveys_description": get_translation(
26+
"total_surveys_description", "dashboard"
27+
),
28+
"total_participants": get_translation("total_participants", "dashboard"),
29+
"excluded_users": get_translation("excluded_users", "dashboard"),
30+
"all_participants": get_translation("all_participants", "dashboard"),
31+
"total_participants_description": get_translation(
32+
"total_participants_description", "dashboard"
33+
),
34+
"excluded_users_description": get_translation(
35+
"excluded_users_description", "dashboard"
36+
),
37+
"all_participants_description": get_translation(
38+
"all_participants_description", "dashboard"
39+
),
40+
"last_updated": get_translation("last_updated", "dashboard"),
41+
"view_responses": get_translation("view_responses", "dashboard"),
42+
"take_survey": get_translation("take_survey", "dashboard"),
43+
}
44+
45+
return render_template(
46+
"dashboard/surveys_overview.html",
47+
surveys=dashboard_data["surveys"],
48+
total_surveys=dashboard_data["total_surveys"],
49+
total_participants=dashboard_data["total_participants"],
50+
unaware_users_count=dashboard_data["unaware_users_count"],
51+
users_with_surveys=dashboard_data["users_with_surveys"],
52+
translations=translations,
53+
last_update_time=datetime.now().strftime("%Y-%m-%d %H:%M"),
54+
)
55+
56+
except Exception as e:
57+
logger.error(f"Error displaying dashboard: {str(e)}")
58+
return render_template(
59+
"error.html",
60+
message=get_translation("dashboard_error", "messages"),
61+
)

application/services/dashboard_service.py

Lines changed: 27 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@
22
from typing import Any, Dict, List
33

44
from database.queries import (
5-
get_active_surveys,
65
get_blacklisted_users,
6+
get_surveys_for_dashboard,
77
get_user_participation_overview,
88
retrieve_completed_survey_responses,
99
)
@@ -13,7 +13,8 @@
1313

1414
def process_survey_data(surveys: List[Dict]) -> List[Dict]:
1515
"""
16-
Process raw survey data to include strategy information.
16+
Process raw survey data to include strategy information while preserving
17+
rich metadata needed by the dashboard parser.
1718
1819
Args:
1920
surveys: List of survey records from database
@@ -25,23 +26,33 @@ def process_survey_data(surveys: List[Dict]) -> List[Dict]:
2526

2627
for survey in surveys:
2728
try:
29+
pair_config = survey.get("pair_generation_config") or {}
30+
strategy_name = (
31+
pair_config.get("strategy") if isinstance(pair_config, dict) else None
32+
)
2833
survey_data.append(
2934
{
35+
# Legacy fields currently used by template
3036
"id": survey["id"],
31-
"name": survey["title"],
32-
"description": (
33-
survey["description"] if survey["description"] else None
34-
),
35-
"strategy_name": (
36-
survey["pair_generation_config"].get("strategy")
37-
if survey["pair_generation_config"]
38-
else None
39-
),
40-
"story_code": survey["story_code"],
37+
"name": survey.get("title", {}),
38+
"description": survey.get("description") or None,
39+
"strategy_name": strategy_name,
40+
"story_code": survey.get("story_code"),
41+
# New fields for Researcher's Console parser
42+
"active": survey.get("active", False),
43+
"created_at": survey.get("created_at"),
44+
"pair_generation_config": pair_config,
45+
"title": survey.get("title", {}),
46+
"subjects": survey.get("subjects", []),
47+
"participant_count": int(survey.get("participant_count") or 0),
4148
}
4249
)
4350
except Exception as e:
44-
logger.error(f"Error processing survey {survey['id']}: {str(e)}")
51+
logger.error(
52+
"Error processing survey %s: %s",
53+
survey.get("id", "unknown"),
54+
str(e),
55+
)
4556
continue
4657

4758
return survey_data
@@ -50,11 +61,9 @@ def process_survey_data(surveys: List[Dict]) -> List[Dict]:
5061
def get_dashboard_metrics() -> Dict[str, Any]:
5162
"""Calculate basic metrics for the dashboard."""
5263
try:
53-
# Get and process active surveys
54-
# Note: get_active_surveys now returns pre-processed data with story information
55-
active_surveys = get_active_surveys()
56-
logger.info(f"Active surveys retrieved: {active_surveys}")
57-
processed_surveys = process_survey_data(active_surveys)
64+
# Get and process all surveys for dashboard visibility.
65+
dashboard_surveys = get_surveys_for_dashboard()
66+
processed_surveys = process_survey_data(dashboard_surveys)
5867

5968
# Get completed responses
6069
completed_responses = retrieve_completed_survey_responses()

application/static/css/_variables.css

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,11 @@
4545
--color-background-light: #ffffff;
4646
--color-background-alt: #f8fafc;
4747

48+
/* Semantic Dashboard Colors */
49+
--color-bg-page: #f8fafc;
50+
--color-bg-surface: #ffffff;
51+
--color-text-emphasis: #1e293b;
52+
4853
/* Grays and text */
4954
--color-black: #000000;
5055
--color-disabled: #708090;

0 commit comments

Comments
 (0)