diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 7223b71aee6..a1293f80e9d 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -51,5 +51,5 @@ This document provides guidelines and best practices for using GitHub Copilot in - [Python Coding Conventions](../docs/coding-conventions.md) - [Environment Variables Guide](../docs/env-vars.md) -- [Steps to Upgrade Python](../docs/steps-to-upgrade-python.md) - [Pydantic Annotated fields](../docs/llm-prompts/pydantic-annotated-fields.md) +- [Steps to Upgrade Python](../docs/steps-to-upgrade-python.md) diff --git a/.github/prompts/update-user-messages.prompt.md b/.github/prompts/update-user-messages.prompt.md new file mode 100644 index 00000000000..66f9e464f3c --- /dev/null +++ b/.github/prompts/update-user-messages.prompt.md @@ -0,0 +1,92 @@ +--- +mode: 'edit' +description: 'Update user messages' +--- + +This prompt guide is for updating user-facing messages in ${file} or ${selection} + +## What is a User Message? + +A user message is any string that will be displayed to end-users of the application. +In our codebase, these messages are marked with the `user_message` function: + +```python +from common_library.user_messages import user_message + +error_msg = user_message("Operation failed. Please try again later.") +``` + +## Guidelines for Updating User Messages + +When modifying user messages, follow these rules: + +1. **Version Tracking**: Every modification to a user message must include an incremented `_version` parameter: + + ```python + # Before modification + user_message("Error: Unable to connect to the server.") + + # After modification, add _version or increment it if it already exists + user_message("Currently unable to establish connection to the server.", _version=1) + ``` + +2. **F-String Preservation**: When modifying messages that use f-strings, preserve all parameters and their formatting: + + ```python + # Before + user_message(f"Project {project_name} could not be loaded.") + + # After (correct) + user_message(f"Unable to load project {project_name}.", _version=1) + + # After (incorrect - lost the parameter) + user_message("Unable to load project.", _version=1) + ``` + +3. **Message Style**: Follow *strictly* the guidelines in `${workspaceFolder}/docs/user-messages-guidelines.md` + +4. **Preserve Context**: Ensure the modified message conveys the same meaning and context as the original. + +5. **Incremental Versioning**: If a message already has a version, increment it by 1: + + ```python + # Before + user_message("Session expired.", _version=2) + + # After + user_message("Your session has expired. Please log in again.", _version=3) + ``` + +## Examples + +### Example 1: Simple Message Update + +```python +# Before +error_dialog(user_message("Failed to save changes.")) + +# After +error_dialog(user_message("Unable to save your changes. Please try again.", _version=1)) +``` + +### Example 2: F-string Message Update + +```python +# Before +raise ValueError(user_message(f"Invalid input parameter: {param_name}")) + +# After +raise ValueError(user_message(f"The parameter '{param_name}' contains a value that is not allowed.", _version=1)) +``` + +### Example 3: Already Versioned Message + +```python +# Before +return HttpErrorInfo(status.HTTP_404_NOT_FOUND, user_message("User not found.", _version=1)) + +# After +return HttpErrorInfo(status.HTTP_404_NOT_FOUND, user_message("The requested user could not be found.", _version=2)) +``` + +Remember: The goal is to improve clarity and helpfulness for end-users while maintaining accurate versioning for tracking changes. diff --git a/docs/messages-guidelines.md b/docs/user-messages-guidelines.md similarity index 96% rename from docs/messages-guidelines.md rename to docs/user-messages-guidelines.md index cc07d2c2d1f..dbd87e8cd60 100644 --- a/docs/messages-guidelines.md +++ b/docs/user-messages-guidelines.md @@ -1,6 +1,6 @@ -# Error and Warning Message Guidelines +# User Message Guidelines -These guidelines ensure that messages are user-friendly, clear, and helpful while maintaining a professional tone. 🚀 +These guidelines ensure that error and warnings user-facing messages are user-friendly, clear, and helpful while maintaining a professional tone. 🚀 Some details: diff --git a/packages/common-library/src/common_library/user_messages.py b/packages/common-library/src/common_library/user_messages.py new file mode 100644 index 00000000000..4c6e22cdc2e --- /dev/null +++ b/packages/common-library/src/common_library/user_messages.py @@ -0,0 +1,11 @@ +def user_message(msg: str, *, _version: int | None = None) -> str: + """Marks a message as user-facing + + Arguments: + msg -- human-friendly string that follows docs/user-messages-guidelines.md + _version -- version number to track changes to messages; increment when modifying an existing message + + Returns: + The original message string, allowing it to be used inline in code + """ + return msg diff --git a/packages/common-library/tests/test_user_messages.py b/packages/common-library/tests/test_user_messages.py new file mode 100644 index 00000000000..e5629700f42 --- /dev/null +++ b/packages/common-library/tests/test_user_messages.py @@ -0,0 +1,6 @@ +from common_library.user_messages import user_message + + +def test_user_message() -> None: + + assert user_message("This is a user message") == "This is a user message" diff --git a/packages/service-library/src/servicelib/aiohttp/rest_middlewares.py b/packages/service-library/src/servicelib/aiohttp/rest_middlewares.py index c8c731a2b8c..b874a57be0c 100644 --- a/packages/service-library/src/servicelib/aiohttp/rest_middlewares.py +++ b/packages/service-library/src/servicelib/aiohttp/rest_middlewares.py @@ -13,6 +13,7 @@ from aiohttp.web_response import StreamResponse from common_library.error_codes import create_error_code from common_library.json_serialization import json_dumps, json_loads +from common_library.user_messages import user_message from models_library.rest_error import ErrorGet, ErrorItemType, LogMessageType from ..logging_errors import create_troubleshotting_log_kwargs @@ -31,7 +32,7 @@ from .web_exceptions_extension import get_http_error_class_or_none DEFAULT_API_VERSION = "v0" -_FMSG_INTERNAL_ERROR_USER_FRIENDLY = ( +_FMSG_INTERNAL_ERROR_USER_FRIENDLY = user_message( "We apologize for the inconvenience. " "The issue has been recorded, please report it if it persists." ) diff --git a/services/web/server/src/simcore_service_webserver/api_keys/_controller/rest_exceptions.py b/services/web/server/src/simcore_service_webserver/api_keys/_controller/rest_exceptions.py index f5888adc99b..e65fe8bfdc3 100644 --- a/services/web/server/src/simcore_service_webserver/api_keys/_controller/rest_exceptions.py +++ b/services/web/server/src/simcore_service_webserver/api_keys/_controller/rest_exceptions.py @@ -1,3 +1,4 @@ +from common_library.user_messages import user_message from servicelib.aiohttp import status from ...exception_handling import ( @@ -11,11 +12,11 @@ _TO_HTTP_ERROR_MAP: ExceptionToHttpErrorMap = { ApiKeyDuplicatedDisplayNameError: HttpErrorInfo( status.HTTP_409_CONFLICT, - "API key display name duplicated", + user_message("An API key with this display name already exists", _version=1), ), ApiKeyNotFoundError: HttpErrorInfo( status.HTTP_404_NOT_FOUND, - "API key was not found", + user_message("The requested API key could not be found", _version=1), ), } diff --git a/services/web/server/src/simcore_service_webserver/catalog/_controller_rest_exceptions.py b/services/web/server/src/simcore_service_webserver/catalog/_controller_rest_exceptions.py index 98603af76c1..244cb5a34a0 100644 --- a/services/web/server/src/simcore_service_webserver/catalog/_controller_rest_exceptions.py +++ b/services/web/server/src/simcore_service_webserver/catalog/_controller_rest_exceptions.py @@ -4,6 +4,7 @@ from aiohttp import web from common_library.error_codes import create_error_code +from common_library.user_messages import user_message from models_library.rest_error import ErrorGet from servicelib.aiohttp import status from servicelib.logging_errors import create_troubleshotting_log_kwargs @@ -85,22 +86,35 @@ async def _handler_catalog_client_errors( _TO_HTTP_ERROR_MAP: ExceptionToHttpErrorMap = { RemoteMethodNotRegisteredError: HttpErrorInfo( status.HTTP_503_SERVICE_UNAVAILABLE, - MSG_CATALOG_SERVICE_UNAVAILABLE, + user_message( + "The catalog service is temporarily unavailable. Please try again later.", + _version=2, + ), ), CatalogForbiddenError: HttpErrorInfo( status.HTTP_403_FORBIDDEN, - "Forbidden catalog access", + user_message( + "Access denied: You don't have permission to view this catalog item.", + _version=2, + ), ), CatalogItemNotFoundError: HttpErrorInfo( status.HTTP_404_NOT_FOUND, - "Catalog item not found", + user_message( + "This catalog item does not exist or has been removed.", _version=2 + ), ), DefaultPricingPlanNotFoundError: HttpErrorInfo( status.HTTP_404_NOT_FOUND, - "Default pricing plan not found", + user_message( + "No default pricing plan is available for this operation.", _version=2 + ), ), DefaultPricingUnitForServiceNotFoundError: HttpErrorInfo( - status.HTTP_404_NOT_FOUND, "Default pricing unit not found" + status.HTTP_404_NOT_FOUND, + user_message( + "No default pricing unit is defined for this service.", _version=2 + ), ), } diff --git a/services/web/server/src/simcore_service_webserver/constants.py b/services/web/server/src/simcore_service_webserver/constants.py index 6c0dae060da..ef963529d53 100644 --- a/services/web/server/src/simcore_service_webserver/constants.py +++ b/services/web/server/src/simcore_service_webserver/constants.py @@ -2,6 +2,7 @@ from typing import Final +from common_library.user_messages import user_message from servicelib.aiohttp.application_keys import ( APP_AIOPG_ENGINE_KEY, APP_CONFIG_KEY, @@ -42,12 +43,11 @@ "Under development. Use WEBSERVER_DEV_FEATURES_ENABLED=1 to enable current implementation" ) - # Request storage keys RQ_PRODUCT_KEY: Final[str] = f"{__name__}.RQ_PRODUCT_KEY" -MSG_TRY_AGAIN_OR_SUPPORT: Final[str] = ( +MSG_TRY_AGAIN_OR_SUPPORT: Final[str] = user_message( "Please try again shortly. If the issue persists, contact support." ) diff --git a/services/web/server/src/simcore_service_webserver/director_v2/_controller/_rest_exceptions.py b/services/web/server/src/simcore_service_webserver/director_v2/_controller/_rest_exceptions.py index b5bb7a6cd7f..2d866c48b38 100644 --- a/services/web/server/src/simcore_service_webserver/director_v2/_controller/_rest_exceptions.py +++ b/services/web/server/src/simcore_service_webserver/director_v2/_controller/_rest_exceptions.py @@ -2,6 +2,7 @@ from aiohttp import web from common_library.error_codes import create_error_code +from common_library.user_messages import user_message from models_library.rest_error import ErrorGet from servicelib import status_codes_utils from servicelib.aiohttp import status @@ -43,10 +44,11 @@ async def _handler_director_service_error_as_503_or_4xx( if status_codes_utils.is_5xx_server_error(exception.status): # NOTE: All directorv2 5XX are mapped to 503 status_code = status.HTTP_503_SERVICE_UNAVAILABLE - user_msg = ( + user_msg = user_message( # Most likely the director service is down or misconfigured so the user is asked to try again later. - "This service is temporarily unavailable. The incident was logged and will be investigated. " - + MSG_TRY_AGAIN_OR_SUPPORT + "This service is temporarily unavailable. The incident has been logged and will be investigated. " + + MSG_TRY_AGAIN_OR_SUPPORT, + _version=1, ) # Log for further investigation @@ -85,11 +87,11 @@ async def _handler_director_service_error_as_503_or_4xx( _TO_HTTP_ERROR_MAP: ExceptionToHttpErrorMap = { UserDefaultWalletNotFoundError: HttpErrorInfo( status.HTTP_404_NOT_FOUND, - "Default wallet not found but necessary for computations", + user_message("Default wallet not found but necessary for computations"), ), WalletNotEnoughCreditsError: HttpErrorInfo( status.HTTP_402_PAYMENT_REQUIRED, - "Wallet does not have enough credits for computations. {reason}", + user_message("Wallet does not have enough credits for computations. {reason}"), ), } diff --git a/services/web/server/src/simcore_service_webserver/folders/_common/exceptions_handlers.py b/services/web/server/src/simcore_service_webserver/folders/_common/exceptions_handlers.py index d117b870c97..875f2790f5d 100644 --- a/services/web/server/src/simcore_service_webserver/folders/_common/exceptions_handlers.py +++ b/services/web/server/src/simcore_service_webserver/folders/_common/exceptions_handlers.py @@ -1,5 +1,6 @@ import logging +from common_library.user_messages import user_message from servicelib.aiohttp import status from ...exception_handling import ( @@ -31,44 +32,50 @@ _TO_HTTP_ERROR_MAP: ExceptionToHttpErrorMap = { FolderNotFoundError: HttpErrorInfo( status.HTTP_404_NOT_FOUND, - "Folder was not found", + user_message("Folder was not found"), ), WorkspaceNotFoundError: HttpErrorInfo( status.HTTP_404_NOT_FOUND, - "Workspace was not found", + user_message("Workspace was not found"), ), FolderAccessForbiddenError: HttpErrorInfo( status.HTTP_403_FORBIDDEN, - "Does not have access to this folder", + user_message("Does not have access to this folder"), ), WorkspaceAccessForbiddenError: HttpErrorInfo( status.HTTP_403_FORBIDDEN, - "Does not have access to this workspace", + user_message("Does not have access to this workspace"), ), WorkspaceFolderInconsistencyError: HttpErrorInfo( status.HTTP_403_FORBIDDEN, - "This folder does not exist in this workspace", + user_message("This folder does not exist in this workspace"), ), FolderValueNotPermittedError: HttpErrorInfo( status.HTTP_409_CONFLICT, - "Provided folder value is not permitted: {reason}", + user_message("Provided folder value is not permitted: {reason}"), ), FoldersValueError: HttpErrorInfo( status.HTTP_409_CONFLICT, - "Invalid folder value set: {reason}", + user_message("Invalid folder value set: {reason}"), ), ProjectInvalidRightsError: HttpErrorInfo( status.HTTP_403_FORBIDDEN, - "Access Denied: You do not have permission to move the project with UUID: {project_uuid}. Tip: Copy and paste the UUID into the search bar to locate the project.", + user_message( + "Access Denied: You do not have permission to move the project with UUID: {project_uuid}. Tip: Copy and paste the UUID into the search bar to locate the project." + ), ), # Trashing ProjectRunningConflictError: HttpErrorInfo( status.HTTP_409_CONFLICT, - "One or more studies in this folder are in use and cannot be trashed. Please stop all services first and try again", + user_message( + "One or more studies in this folder are in use and cannot be trashed. Please stop all services first and try again" + ), ), ProjectStoppingError: HttpErrorInfo( status.HTTP_503_SERVICE_UNAVAILABLE, - "Something went wrong while stopping running services in studies within this folder before trashing. Aborting trash.", + user_message( + "Something went wrong while stopping running services in studies within this folder before trashing. Aborting trash." + ), ), } diff --git a/services/web/server/src/simcore_service_webserver/groups/_common/exceptions_handlers.py b/services/web/server/src/simcore_service_webserver/groups/_common/exceptions_handlers.py index f0b9242fb70..c6ffa9e7092 100644 --- a/services/web/server/src/simcore_service_webserver/groups/_common/exceptions_handlers.py +++ b/services/web/server/src/simcore_service_webserver/groups/_common/exceptions_handlers.py @@ -1,5 +1,6 @@ import logging +from common_library.user_messages import user_message from servicelib.aiohttp import status from ...exception_handling import ( @@ -23,32 +24,32 @@ _TO_HTTP_ERROR_MAP: ExceptionToHttpErrorMap = { UserNotFoundError: HttpErrorInfo( status.HTTP_404_NOT_FOUND, - "User {uid} or {email} not found", + user_message("User {uid} or {email} not found"), ), GroupNotFoundError: HttpErrorInfo( status.HTTP_404_NOT_FOUND, - "Group {gid} not found", + user_message("Group {gid} not found"), ), UserInGroupNotFoundError: HttpErrorInfo( status.HTTP_404_NOT_FOUND, - "User not found in group {gid}", + user_message("User not found in group {gid}"), ), UserAlreadyInGroupError: HttpErrorInfo( status.HTTP_409_CONFLICT, - "User is already in group {gid}", + user_message("User is already in group {gid}"), ), UserInsufficientRightsError: HttpErrorInfo( status.HTTP_403_FORBIDDEN, - "Insufficient rights for {permission} access to group {gid}", + user_message("Insufficient rights for {permission} access to group {gid}"), ), # scicrunch InvalidRRIDError: HttpErrorInfo( status.HTTP_409_CONFLICT, - "Invalid RRID {rrid}", + user_message("Invalid RRID {rrid}"), ), ScicrunchError: HttpErrorInfo( status.HTTP_409_CONFLICT, - "Cannot get RRID since scicrunch.org service is not reachable.", + user_message("Cannot get RRID since scicrunch.org service is not reachable."), ), } diff --git a/services/web/server/src/simcore_service_webserver/licenses/_common/exceptions_handlers.py b/services/web/server/src/simcore_service_webserver/licenses/_common/exceptions_handlers.py index e5586a5e76a..0fc0c82f5a5 100644 --- a/services/web/server/src/simcore_service_webserver/licenses/_common/exceptions_handlers.py +++ b/services/web/server/src/simcore_service_webserver/licenses/_common/exceptions_handlers.py @@ -1,5 +1,6 @@ import logging +from common_library.user_messages import user_message from servicelib.aiohttp import status from ...exception_handling import ( @@ -17,19 +18,21 @@ _TO_HTTP_ERROR_MAP: ExceptionToHttpErrorMap = { LicensedItemNotFoundError: HttpErrorInfo( status.HTTP_404_NOT_FOUND, - "Market item {licensed_item_id} not found.", + user_message("Market item {licensed_item_id} not found."), ), WalletAccessForbiddenError: HttpErrorInfo( status.HTTP_403_FORBIDDEN, - "Credit account {wallet_id} forbidden.", + user_message("Credit account {wallet_id} forbidden."), ), WalletNotEnoughCreditsError: HttpErrorInfo( status.HTTP_402_PAYMENT_REQUIRED, - "Not enough credits in the credit account.", + user_message("Not enough credits in the credit account."), ), LicensedItemPricingPlanMatchError: HttpErrorInfo( status.HTTP_400_BAD_REQUEST, - "The provided pricing plan does not match the one associated with the licensed item.", + user_message( + "The provided pricing plan does not match the one associated with the licensed item." + ), ), } diff --git a/services/web/server/src/simcore_service_webserver/products/_controller/rest_exceptions.py b/services/web/server/src/simcore_service_webserver/products/_controller/rest_exceptions.py index a9e8cb13f00..9402486e27e 100644 --- a/services/web/server/src/simcore_service_webserver/products/_controller/rest_exceptions.py +++ b/services/web/server/src/simcore_service_webserver/products/_controller/rest_exceptions.py @@ -1,3 +1,4 @@ +from common_library.user_messages import user_message from servicelib.aiohttp import status from ...constants import MSG_TRY_AGAIN_OR_SUPPORT @@ -12,11 +13,14 @@ _TO_HTTP_ERROR_MAP: ExceptionToHttpErrorMap = { ProductNotFoundError: HttpErrorInfo( status.HTTP_404_NOT_FOUND, - "{product_name} was not found", + user_message("{product_name} was not found"), ), MissingStripeConfigError: HttpErrorInfo( status.HTTP_503_SERVICE_UNAVAILABLE, - "{product_name} service is currently unavailable." + MSG_TRY_AGAIN_OR_SUPPORT, + user_message( + "{product_name} service is currently unavailable." + + MSG_TRY_AGAIN_OR_SUPPORT + ), ), } diff --git a/services/web/server/src/simcore_service_webserver/projects/_controller/_rest_exceptions.py b/services/web/server/src/simcore_service_webserver/projects/_controller/_rest_exceptions.py index 4185089f2dc..ee6ffe52408 100644 --- a/services/web/server/src/simcore_service_webserver/projects/_controller/_rest_exceptions.py +++ b/services/web/server/src/simcore_service_webserver/projects/_controller/_rest_exceptions.py @@ -2,6 +2,7 @@ import logging from collections import Counter +from common_library.user_messages import user_message from servicelib.aiohttp import status from servicelib.rabbitmq.rpc_interfaces.catalog.errors import ( CatalogForbiddenError, @@ -54,11 +55,11 @@ _FOLDER_ERRORS: ExceptionToHttpErrorMap = { FolderAccessForbiddenError: HttpErrorInfo( status.HTTP_403_FORBIDDEN, - "Access to folder forbidden", + user_message("Access to folder forbidden"), ), FolderNotFoundError: HttpErrorInfo( status.HTTP_404_NOT_FOUND, - "Folder not found: {reason}", + user_message("Folder not found: {reason}"), ), } @@ -66,15 +67,15 @@ _NODE_ERRORS: ExceptionToHttpErrorMap = { NodeNotFoundError: HttpErrorInfo( status.HTTP_404_NOT_FOUND, - "Node '{node_uuid}' not found in project '{project_uuid}'", + user_message("Node '{node_uuid}' not found in project '{project_uuid}'"), ), ParentNodeNotFoundError: HttpErrorInfo( status.HTTP_404_NOT_FOUND, - "Parent node '{node_uuid}' not found", + user_message("Parent node '{node_uuid}' not found"), ), ProjectNodeRequiredInputsNotSetError: HttpErrorInfo( status.HTTP_409_CONFLICT, - "Project node is required but input is not set", + user_message("Project node is required but input is not set"), ), } diff --git a/services/web/server/src/simcore_service_webserver/projects/_controller/trash_rest.py b/services/web/server/src/simcore_service_webserver/projects/_controller/trash_rest.py index f1cae188a7d..f7e488e92d3 100644 --- a/services/web/server/src/simcore_service_webserver/projects/_controller/trash_rest.py +++ b/services/web/server/src/simcore_service_webserver/projects/_controller/trash_rest.py @@ -1,6 +1,7 @@ import logging from aiohttp import web +from common_library.user_messages import user_message from servicelib.aiohttp import status from servicelib.aiohttp.requests_validation import ( parse_request_path_parameters_as, @@ -28,11 +29,15 @@ _TRASH_ERRORS: ExceptionToHttpErrorMap = { ProjectRunningConflictError: HttpErrorInfo( status.HTTP_409_CONFLICT, - "Current study is in use and cannot be trashed [project_id={project_uuid}]. Please stop all services first and try again", + user_message( + "Current study is in use and cannot be trashed [project_id={project_uuid}]. Please stop all services first and try again" + ), ), ProjectStoppingError: HttpErrorInfo( status.HTTP_503_SERVICE_UNAVAILABLE, - "Something went wrong while stopping services before trashing. Aborting trash.", + user_message( + "Something went wrong while stopping services before trashing. Aborting trash." + ), ), } diff --git a/services/web/server/src/simcore_service_webserver/tags/_rest.py b/services/web/server/src/simcore_service_webserver/tags/_rest.py index ea39edd6c2a..10d4c86a422 100644 --- a/services/web/server/src/simcore_service_webserver/tags/_rest.py +++ b/services/web/server/src/simcore_service_webserver/tags/_rest.py @@ -1,4 +1,5 @@ from aiohttp import web +from common_library.user_messages import user_message from servicelib.aiohttp import status from servicelib.aiohttp.requests_validation import ( parse_request_body_as, @@ -38,23 +39,27 @@ _TO_HTTP_ERROR_MAP: ExceptionToHttpErrorMap = { TagNotFoundError: HttpErrorInfo( status.HTTP_404_NOT_FOUND, - "Tag {tag_id} not found: either no access or does not exists", + user_message("Tag {tag_id} not found: either no access or does not exists"), ), TagOperationNotAllowedError: HttpErrorInfo( status.HTTP_403_FORBIDDEN, - "Could not {operation} tag {tag_id}. Not found or insuficient access.", + user_message( + "Could not {operation} tag {tag_id}. Not found or insuficient access." + ), ), ShareTagWithEveryoneNotAllowedError: HttpErrorInfo( status.HTTP_403_FORBIDDEN, - "Sharing with everyone is not permitted.", + user_message("Sharing with everyone is not permitted."), ), ShareTagWithProductGroupNotAllowedError: HttpErrorInfo( status.HTTP_403_FORBIDDEN, - "Sharing with all users is only permitted to admin users (e.g. testers, POs, ...).", + user_message( + "Sharing with all users is only permitted to admin users (e.g. testers, POs, ...)." + ), ), InsufficientTagShareAccessError: HttpErrorInfo( status.HTTP_403_FORBIDDEN, - "Insufficient access rightst to share (or unshare) tag {tag_id}.", + user_message("Insufficient access rightst to share (or unshare) tag {tag_id}."), ), } diff --git a/services/web/server/src/simcore_service_webserver/tasks/_exception_handlers.py b/services/web/server/src/simcore_service_webserver/tasks/_exception_handlers.py index b0b9d8754c5..81b9806c57a 100644 --- a/services/web/server/src/simcore_service_webserver/tasks/_exception_handlers.py +++ b/services/web/server/src/simcore_service_webserver/tasks/_exception_handlers.py @@ -1,3 +1,4 @@ +from common_library.user_messages import user_message from models_library.api_schemas_rpc_async_jobs.exceptions import ( JobAbortedError, JobError, @@ -22,35 +23,48 @@ _TO_HTTP_ERROR_MAP: ExceptionToHttpErrorMap = { InvalidFileIdentifierError: HttpErrorInfo( status.HTTP_404_NOT_FOUND, - "Could not find file {file_id}", + user_message( + "The file with identifier {file_id} could not be found", _version=2 + ), ), AccessRightError: HttpErrorInfo( status.HTTP_403_FORBIDDEN, - "Accessright error: user {user_id} does not have access to file {file_id} with location {location_id}", + user_message( + "Permission denied: You (user {user_id}) don't have the necessary rights to access file {file_id} in location {location_id}", + _version=2, + ), ), JobAbortedError: HttpErrorInfo( status.HTTP_410_GONE, - "Task {job_id} is aborted", + user_message("Task {job_id} was terminated before completion", _version=2), ), JobError: HttpErrorInfo( status.HTTP_500_INTERNAL_SERVER_ERROR, - "Task '{job_id}' failed with exception type '{exc_type}' and message: {exc_msg}", + user_message( + "Task '{job_id}' encountered an error: {exc_msg} (error type: '{exc_type}')", + _version=2, + ), ), JobNotDoneError: HttpErrorInfo( status.HTTP_404_NOT_FOUND, - "task {job_id} is not done yet", + user_message( + "Task {job_id} is still running and has not completed yet", _version=2 + ), ), JobMissingError: HttpErrorInfo( status.HTTP_404_NOT_FOUND, - "No task with id: {job_id}", + user_message("The requested task with ID {job_id} does not exist", _version=2), ), JobSchedulerError: HttpErrorInfo( status.HTTP_500_INTERNAL_SERVER_ERROR, - "Encountered an error with the task scheduling system", + user_message( + "The task scheduling system encountered an error. Please try again later", + _version=2, + ), ), JobStatusError: HttpErrorInfo( status.HTTP_500_INTERNAL_SERVER_ERROR, - "Encountered an error while getting the status of task {job_id}", + user_message("Unable to get the current status for task {job_id}", _version=2), ), } diff --git a/services/web/server/src/simcore_service_webserver/trash/_rest.py b/services/web/server/src/simcore_service_webserver/trash/_rest.py index d6971984086..728aca09b64 100644 --- a/services/web/server/src/simcore_service_webserver/trash/_rest.py +++ b/services/web/server/src/simcore_service_webserver/trash/_rest.py @@ -2,6 +2,7 @@ import logging from aiohttp import web +from common_library.user_messages import user_message from servicelib.aiohttp import status from servicelib.aiohttp.application_keys import APP_FIRE_AND_FORGET_TASKS_KEY from servicelib.utils import fire_and_forget_task @@ -25,11 +26,15 @@ _TO_HTTP_ERROR_MAP: ExceptionToHttpErrorMap = { ProjectRunningConflictError: HttpErrorInfo( status.HTTP_409_CONFLICT, - "Current study is in use and cannot be trashed [project_id={project_uuid}]. Please stop all services first and try again", + user_message( + "Current study is in use and cannot be trashed [project_id={project_uuid}]. Please stop all services first and try again" + ), ), ProjectStoppingError: HttpErrorInfo( status.HTTP_503_SERVICE_UNAVAILABLE, - "Something went wrong while stopping services before trashing. Aborting trash.", + user_message( + "Something went wrong while stopping services before trashing. Aborting trash." + ), ), } diff --git a/services/web/server/src/simcore_service_webserver/users/_users_rest.py b/services/web/server/src/simcore_service_webserver/users/_users_rest.py index 66bfa26e3fd..7590c7fb17f 100644 --- a/services/web/server/src/simcore_service_webserver/users/_users_rest.py +++ b/services/web/server/src/simcore_service_webserver/users/_users_rest.py @@ -3,6 +3,7 @@ from typing import Any from aiohttp import web +from common_library.user_messages import user_message from common_library.users_enums import AccountRequestStatus from models_library.api_schemas_webserver.users import ( MyProfileGet, @@ -54,25 +55,33 @@ _TO_HTTP_ERROR_MAP: ExceptionToHttpErrorMap = { PendingPreRegistrationNotFoundError: HttpErrorInfo( status.HTTP_400_BAD_REQUEST, - PendingPreRegistrationNotFoundError.msg_template, + user_message(PendingPreRegistrationNotFoundError.msg_template), ), UserNotFoundError: HttpErrorInfo( status.HTTP_404_NOT_FOUND, - "This user cannot be found. Either it is not registered or has enabled privacy settings.", + user_message( + "This user cannot be found. Either it is not registered or has enabled privacy settings." + ), ), UserNameDuplicateError: HttpErrorInfo( status.HTTP_409_CONFLICT, - "Username '{user_name}' is already taken. " - "Consider '{alternative_user_name}' instead.", + user_message( + "Username '{user_name}' is already taken. " + "Consider '{alternative_user_name}' instead." + ), ), AlreadyPreRegisteredError: HttpErrorInfo( status.HTTP_409_CONFLICT, - "Found {num_found} matches for '{email}'. Cannot pre-register existing user", + user_message( + "Found {num_found} matches for '{email}'. Cannot pre-register existing user" + ), ), MissingGroupExtraPropertiesForProductError: HttpErrorInfo( status.HTTP_503_SERVICE_UNAVAILABLE, - "The product is not ready for use until the configuration is fully completed. " - "Please wait and try again. ", + user_message( + "The product is not ready for use until the configuration is fully completed. " + "Please wait and try again. " + ), ), } diff --git a/services/web/server/src/simcore_service_webserver/workspaces/_common/exceptions_handlers.py b/services/web/server/src/simcore_service_webserver/workspaces/_common/exceptions_handlers.py index 32bb81224a7..4a4b77a5cca 100644 --- a/services/web/server/src/simcore_service_webserver/workspaces/_common/exceptions_handlers.py +++ b/services/web/server/src/simcore_service_webserver/workspaces/_common/exceptions_handlers.py @@ -1,5 +1,6 @@ import logging +from common_library.user_messages import user_message from servicelib.aiohttp import status from ...exception_handling import ( @@ -21,24 +22,28 @@ _TO_HTTP_ERROR_MAP: ExceptionToHttpErrorMap = { WorkspaceGroupNotFoundError: HttpErrorInfo( status.HTTP_404_NOT_FOUND, - "Workspace {workspace_id} group {group_id} not found.", + user_message("Workspace {workspace_id} group {group_id} not found."), ), WorkspaceAccessForbiddenError: HttpErrorInfo( status.HTTP_403_FORBIDDEN, - "Does not have access to this workspace", + user_message("Does not have access to this workspace"), ), WorkspaceNotFoundError: HttpErrorInfo( status.HTTP_404_NOT_FOUND, - "Workspace not found. {reason}", + user_message("Workspace not found. {reason}"), ), # Trashing ProjectRunningConflictError: HttpErrorInfo( status.HTTP_409_CONFLICT, - "One or more studies in this workspace are in use and cannot be trashed. Please stop all services first and try again", + user_message( + "One or more studies in this workspace are in use and cannot be trashed. Please stop all services first and try again" + ), ), ProjectStoppingError: HttpErrorInfo( status.HTTP_503_SERVICE_UNAVAILABLE, - "Something went wrong while stopping running services in studies within this workspace before trashing. Aborting trash.", + user_message( + "Something went wrong while stopping running services in studies within this workspace before trashing. Aborting trash." + ), ), }