From ebbaa972e9444f584bb5cadcec3deeb2246c3c37 Mon Sep 17 00:00:00 2001 From: nicola leonardi Date: Thu, 12 Mar 2026 12:39:28 +0100 Subject: [PATCH] UI user task assignment --- UI/dependences_ui/utils.py | 48 +- .../user_assignment_manager.py | 63 ++- UI/wcag_validator_ui.py | 475 ++++++++++++------ 3 files changed, 399 insertions(+), 187 deletions(-) diff --git a/UI/dependences_ui/utils.py b/UI/dependences_ui/utils.py index 4561038..860155d 100644 --- a/UI/dependences_ui/utils.py +++ b/UI/dependences_ui/utils.py @@ -25,13 +25,14 @@ def hash_password(password): """Hash password using SHA-256""" return hashlib.sha256(password.encode()).hexdigest() + def associate_user_with_manager(users, user_assignment_manager): - user_list=users.keys() + user_list = users.keys() print(f"registering--Associating users with manager: {list(user_list)}") user_assignment_manager.register_active_users(list(user_list)) -def register_user(username, password, confirm_password,user_assignment_manager): +def register_user(username, password, confirm_password, user_assignment_manager): """Register a new user""" if not username or not password: return "", "Username and password cannot be empty!", None @@ -53,7 +54,7 @@ def register_user(username, password, confirm_password,user_assignment_manager): try: associate_user_with_manager(users, user_assignment_manager) except Exception as e: - print(f"Error associating user with manager: {e}") + print(f"Error associating user with manager: {e}") return "", f"✅ Registration successful! You can now login.", None @@ -125,3 +126,44 @@ def protected_content(state): if state.get("logged_in"): return f"You are logged as {state.get('username')}\n" return "Please login to access this content." + + + +def get_user_assessments_done(connection_db, username): + """ + it returns: + { + "https://example.com/page1": [1, 3, 5], + "https://example.com/page2": [2, 4, 6], + } + """ + + cursor = connection_db.cursor() + username = json.dumps({"username": username}, ensure_ascii=False) + cursor.execute( + """ + SELECT page_url, json_output_data + FROM wcag_user_assessments + WHERE user = ? AND insert_type = ? + ORDER BY page_url + """, + (username, "wcag_user_llm_alttext_assessments"), + ) + + rows = cursor.fetchall() + assessment_done = {} # dict: {page_url: sorted list of image numbers} + for row in rows: + page_url = row[0] + data = json.loads(row[1]) + image_numbers = {int(item["Image #"]) for item in data} # set to deduplicate + if page_url not in assessment_done: + assessment_done[page_url] = image_numbers + else: + assessment_done[page_url].update( + image_numbers + ) # merge if url appears multiple times + + # Convert sets to sorted lists + assessment_done = {url: sorted(imgs) for url, imgs in assessment_done.items()} + + return assessment_done diff --git a/UI/user_task_assignment/user_assignment_manager.py b/UI/user_task_assignment/user_assignment_manager.py index 5fa9c5b..b1a7617 100644 --- a/UI/user_task_assignment/user_assignment_manager.py +++ b/UI/user_task_assignment/user_assignment_manager.py @@ -54,6 +54,7 @@ class UserAssignmentManager: assignments_xlsx_path: str = "alt_text_assignments_output_target_overlap.xlsx", target_overlap: int = 2, seed: int = 42, + ): """ Initialize the User Assignment Manager. @@ -86,8 +87,8 @@ class UserAssignmentManager: # Initialize database self._init_database() - # Load existing assignments from JSON if available - self._load_existing_assignments() + # Load existing assignments to db from JSON if available + #self._load_existing_assignments() def _load_sites_config(self) -> List[SiteConfig]: """Load site configuration from JSON.""" @@ -160,7 +161,7 @@ class UserAssignmentManager: conn.commit() conn.close() - def _load_existing_assignments(self, active_user_names: Optional[List[str]] = None): + def _load_existing_assignments(self, active_user_names: List[str] = []): """Load existing assignments from JSON file into database if not already there.""" if not self.assignments_json_path.exists(): return @@ -172,17 +173,15 @@ class UserAssignmentManager: conn = sqlite3.connect(self.db_path) cursor = conn.cursor() + # nb: every service restart and user registration will trigger this (ONCONFLICT ensures no duplicates) for user_id, sites_dict in assignments.items(): - for site_url, image_indices in sites_dict.items(): - # print(f"[DB] Loading assignment for user {user_id}, site {site_url}, " - # f"{image_indices} images") - try: - ''' - cursor.execute(""" - INSERT OR IGNORE INTO user_assignments - (user_id, site_url, image_indices) - VALUES (?, ?, ?) - """, (user_id, site_url, json.dumps(image_indices)))''' + try: + for site_url, image_indices in sites_dict.items(): + + print( + f"[DB] Loading assignment for user {user_id}, site {site_url}, " + f"{image_indices} images" + ) cursor.execute( """ @@ -195,28 +194,26 @@ class UserAssignmentManager: (user_id, site_url, json.dumps(image_indices)), ) - cursor.execute( # also update user_info table with user_name if active_user_names is provided and user_id starts with "user" - """ - INSERT INTO user_info (user_id, user_name) - VALUES (?, ?) - ON CONFLICT(user_id) DO UPDATE SET - user_name = excluded.user_name - """, + cursor.execute( # also update user_info table with user_name if active_user_names is provided and user_id starts with "user" + """ + INSERT INTO user_info (user_id, user_name) + VALUES (?, ?) + ON CONFLICT(user_id) DO UPDATE SET + user_name = excluded.user_name + """, + ( + user_id, ( - user_id, - ( - active_user_names[int(user_id[4:]) - 1] - if active_user_names and user_id.startswith("user") - else None - ), + active_user_names[int(user_id[4:]) - 1] + if active_user_names and user_id.startswith("user") + else None ), - ) + ), + ) - except sqlite3.IntegrityError: - print( - f"[DB] Error. Skipping existing assignment for user {user_id}, site {site_url}" - ) - pass + except sqlite3.IntegrityError: + print(f"[DB] Error. Skipping existing assignment for user {user_id}") + pass conn.commit() conn.close() @@ -243,7 +240,7 @@ class UserAssignmentManager: if from_user_name: print(f"[DB] Looking up user_id for user_name: {user_id}") - + cursor.execute( """ SELECT user_id diff --git a/UI/wcag_validator_ui.py b/UI/wcag_validator_ui.py index 159d4b6..9f1c999 100644 --- a/UI/wcag_validator_ui.py +++ b/UI/wcag_validator_ui.py @@ -18,7 +18,9 @@ from dependences.utils import ( db_persistence_startup, db_persistence_insert, return_from_env_valid, + ) + from dependences_ui.utils import * import logging import time @@ -31,18 +33,37 @@ import sqlite3 from user_task_assignment.user_assignment_manager import UserAssignmentManager +from dependences_ui.utils import load_users,get_user_assessments_done +users=load_users() +user_list=list(users.keys()) +print(f"Loaded users from simple JSON: {len(user_list)}") user_assignment_manager = UserAssignmentManager( db_path="persistence/wcag_validator_ui.db", config_json_path="user_task_assignment/sites_config.json", assignments_json_path="user_task_assignment/alt_text_assignments_output_target_overlap.json", - assignments_xlsx_path="user_task_assignment/alt_text_assignments_output_target_overlap.xlsx" + assignments_xlsx_path="user_task_assignment/alt_text_assignments_output_target_overlap.xlsx", + ) # Get current managed users -managed_users = user_assignment_manager.get_all_user_ids() -print(f"Currently managed users from db: {managed_users}") -print(f"Total managed users from db: {user_assignment_manager.get_managed_user_count()}\n") +managed_users_number = user_assignment_manager.get_managed_user_count() +print(f"Currently managed users from db: {managed_users_number}") +if managed_users_number !=len(user_list):# rigenenerate files only if some user numbers disalignmnets. Avoid only updates on new user registration process + print(f"Warning: Number of users in db ({managed_users_number}) does not match number of users loaded from JSON ({len(user_list)}). Re-init user assignments files.") + user_assignment_manager.register_active_users(user_list)#on startup register users loaded from JSON into the manager (creating also assignments .json amd .xml files) + # Get current managed users after regsitration alignment + managed_users_number = user_assignment_manager.get_managed_user_count() + print(f"Currently managed users from db after alignment: {managed_users_number}") + +# Get current managed users after regsitration alignment + +print(f"Total managed users from db: {managed_users_number}\n") +if managed_users_number !=len(user_list): + print(f"Warning: Number of users in db ({managed_users_number}) does not match number of users loaded from JSON ({len(user_list)}). Check user assignment manager initialization.") + exit(1) + + user_assignment_stats = user_assignment_manager.get_statistics() print(f"Current assignment stats:{user_assignment_stats} \n") @@ -52,9 +73,81 @@ print(f"Current assignment stats:{user_assignment_stats} \n") WCAG_VALIDATOR_RESTSERVER_HEADERS = [("Content-Type", "application/json")] -def display_user_assignment(user_state): +def maybe_close_modal(process_dataframe_output_state): + print("Checking if modal can be closed based on:",type(process_dataframe_output_state), process_dataframe_output_state) + if not process_dataframe_output_state: + print("Modal cannot be closed.") + return Modal(visible=True) # keep it open + return Modal(visible=False) # close it + +def maybe_open_modal(make_alttext_llm_assessment_api_call_output_state): + print("Checking if modal can be opened based on:",type(make_alttext_llm_assessment_api_call_output_state), make_alttext_llm_assessment_api_call_output_state) + if not make_alttext_llm_assessment_api_call_output_state: + print("Modal cannot be opened.") + return Modal(visible=False) + return Modal(visible=True) + +def render_user_assessmnet_status_table(df): + if df is None or df.empty: + return "

No assignments found.

" + + total_work_to_be_done=[] + rows = "" + for _, row in df.iterrows(): + url = row["Website URL"] + assigned = row["Assigned Image Number"] + work_done = row["Work Done on Image Number"] + work_to_be_done = [img for img in assigned if img not in work_done] + total_work_to_be_done+=work_to_be_done + + rows += f""" + + + {url} + + + {assigned} + + + {work_done} + + + {work_to_be_done} + + + """ + total_work_to_be_done_text ="" + if len(total_work_to_be_done)==0: + total_work_to_be_done_text="All assigned work is done! Great job!" + + return f""" +
+ + + + + + + + + + + {rows} + +
Website URLAssigned Image NumberWork Done on Image NumberWork Still to be Done
+
+

{total_work_to_be_done_text}

+ """ + +def display_user_assignment(db_path,user_state): if user_state and "username" in user_state: username = user_state["username"] + connection_db = sqlite3.connect(db_path) + + user_assessment_work=get_user_assessments_done(connection_db ,username) + print(f"User {username} has done assessments for {user_assessment_work} images.") + + print(f"Fetching assignment for user: {username}") assignments = user_assignment_manager.get_user_assignments(username, from_user_name=True) @@ -70,7 +163,8 @@ def display_user_assignment(user_state): data_frame.append( { "Website URL": url, - "Assigned Image Number List": assignments[url] + "Assigned Image Number": assignments[url], + "Work Done on Image Number":user_assessment_work[url] if url in user_assessment_work else [], } ) @@ -83,7 +177,7 @@ def display_user_assignment(user_state): def process_dataframe(db_path, url, updated_df, user_state={},llm_response_output={}): - print("Processing dataframe to adjust columns...type:",type(updated_df)) + print("Processing dataframe to adjust columns...type:",type(updated_df),updated_df) # accept different input forms from UI (DataFrame, JSON string, or list of dicts) try: @@ -95,23 +189,23 @@ def process_dataframe(db_path, url, updated_df, user_state={},llm_response_outpu elif isinstance(updated_df, list): updated_df = pd.DataFrame(updated_df) except Exception as e: - return f"Error parsing updated data: {str(e)}" + return f"Error parsing updated data: {str(e)}" ,False for column_rating_name in ["User Assessment for LLM Proposal 1", "User Assessment for LLM Proposal 2"]: # Get the assessment column try: updated_df[column_rating_name] = updated_df[column_rating_name].astype(int) except ValueError: - return "Error: User Assessment for LLM Proposal must be an integer" + return "Error: User Assessment for LLM Proposal must be an integer",False except KeyError: - return f"No data Saved because no image selected. Please select at least one image." + return f"No data Saved because some images are not correcly managed. Please retry." ,False except Exception as e: - return f"Error processing User Assessment for LLM Proposal: {str(e)}" + return f"Error processing User Assessment for LLM Proposal: {str(e)}" ,False if (updated_df[column_rating_name] < 1).any() or ( updated_df[column_rating_name] > 5 ).any(): - return "Error: User Assessment for LLM Proposal must be between 1 and 5" + return "Error: User Assessment for LLM Proposal must be between 1 and 5",False dataframe_json = updated_df.to_json(orient="records") connection_db = sqlite3.connect(db_path) @@ -126,7 +220,7 @@ def process_dataframe(db_path, url, updated_df, user_state={},llm_response_outpu page_url=url, user=json_user_str, llm_model="", - json_in_str=llm_response_output_str,#dataframe_json, # to improve + json_in_str=llm_response_output_str,#dataframe_json, json_out_str=dataframe_json, table="wcag_user_assessments", ) @@ -135,11 +229,27 @@ def process_dataframe(db_path, url, updated_df, user_state={},llm_response_outpu finally: if connection_db: connection_db.close() - return "User assessment saved successfully!" + print("User assessment saved to database successfully.returning:", True) + return "User assessment saved successfully!",True -def load_images_from_json(json_input): +def load_images_from_json(json_input,user_assignment_current_status_df): """Extract URLs and alt text from JSON and create HTML gallery""" + + if user_assignment_current_status_df is None or user_assignment_current_status_df.empty: + print("No user assignment status found. Displaying all images without assignment info.") + + user_assignments={} + for _, row in user_assignment_current_status_df.iterrows(): + url = row["Website URL"] + assigned = row["Assigned Image Number"] + work_done = row["Work Done on Image Number"] + user_assignments[url] = { + "assigned": assigned, + "work_done": work_done + } + #print(f"User assignments extracted for image loading: {user_assignments}") + try: data = json_input @@ -250,6 +360,17 @@ def load_images_from_json(json_input): url = img_data.get("url", "") alt_text = img_data.get("alt_text", "No description") + page_url = img_data.get("page_url", "") + assigned=user_assignments.get(page_url,{}).get("assigned",[]) + work_done=user_assignments.get(page_url,{}).get("work_done",[]) + assigned_text="" + if idx+1 in assigned: + assigned_text="-(Assigned)" + if idx+1 in work_done: + assigned_text+="->(Already managed)" + if idx+1 in assigned and idx+1 in work_done: + assigned_text+="ü" + html += f"""
{alt_text} @@ -260,9 +381,9 @@ def load_images_from_json(json_input): const panel = document.getElementById('panel-{idx}'); const checkedCount = document.querySelectorAll('.image-checkbox:checked').length; if (this.checked) {{ - if (checkedCount > 3) {{ + if (checkedCount > 6) {{ this.checked = false; - alert('Maximum 3 images can be selected!'); + alert('Maximum 6 images can be selected!'); return; }} panel.classList.add('visible'); @@ -270,7 +391,7 @@ def load_images_from_json(json_input): panel.classList.remove('visible'); }} "> - Select #{idx + 1} + Select #{idx + 1}{assigned_text}
Current alt_text: {alt_text}
@@ -287,7 +408,7 @@ def load_images_from_json(json_input): 2