diff --git a/.github/prompts/update-user-messages.prompt.md b/.github/prompts/update-user-messages.prompt.md index 66f9e464f3c..37eee04951b 100644 --- a/.github/prompts/update-user-messages.prompt.md +++ b/.github/prompts/update-user-messages.prompt.md @@ -18,7 +18,7 @@ error_msg = user_message("Operation failed. Please try again later.") ## Guidelines for Updating User Messages -When modifying user messages, follow these rules: +When modifying user messages, follow **as close as possible** these rules: 1. **Version Tracking**: Every modification to a user message must include an incremented `_version` parameter: @@ -43,7 +43,7 @@ When modifying user messages, follow these rules: user_message("Unable to load project.", _version=1) ``` -3. **Message Style**: Follow *strictly* the guidelines in `${workspaceFolder}/docs/user-messages-guidelines.md` +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. @@ -56,6 +56,8 @@ When modifying user messages, follow these rules: # After user_message("Your session has expired. Please log in again.", _version=3) ``` +6. **Replace 'Study' by 'Project'**: If the message contains the word 'Study', replace it with 'Project' to align with our terminology. + ## Examples @@ -63,10 +65,10 @@ When modifying user messages, follow these rules: ```python # Before -error_dialog(user_message("Failed to save changes.")) +error_dialog(user_message("Failed to save changes in this study.")) # After -error_dialog(user_message("Unable to save your changes. Please try again.", _version=1)) +error_dialog(user_message("Unable to save your changes in this project.", _version=1)) ``` ### Example 2: F-string Message Update diff --git a/packages/service-library/src/servicelib/aiohttp/rest_middlewares.py b/packages/service-library/src/servicelib/aiohttp/rest_middlewares.py index 393c0500694..58616a3d335 100644 --- a/packages/service-library/src/servicelib/aiohttp/rest_middlewares.py +++ b/packages/service-library/src/servicelib/aiohttp/rest_middlewares.py @@ -36,7 +36,8 @@ DEFAULT_API_VERSION = "v0" _FMSG_INTERNAL_ERROR_USER_FRIENDLY = user_message( "We apologize for the inconvenience. " - "The issue has been recorded, please report it if it persists." + "The issue has been recorded, please report it if it persists.", + _version=1, ) diff --git a/services/web/server/src/simcore_service_webserver/catalog/_constants.py b/services/web/server/src/simcore_service_webserver/catalog/_constants.py index 5fe94022060..ba805923947 100644 --- a/services/web/server/src/simcore_service_webserver/catalog/_constants.py +++ b/services/web/server/src/simcore_service_webserver/catalog/_constants.py @@ -1,11 +1,14 @@ from typing import Final +from common_library.user_messages import user_message + from ..constants import MSG_TRY_AGAIN_OR_SUPPORT -MSG_CATALOG_SERVICE_UNAVAILABLE: Final[str] = ( +MSG_CATALOG_SERVICE_UNAVAILABLE: Final[str] = 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 + "The catalog service is currently unavailable. This issue has been logged and will be investigated. " + + MSG_TRY_AGAIN_OR_SUPPORT, + _version=1, ) diff --git a/services/web/server/src/simcore_service_webserver/constants.py b/services/web/server/src/simcore_service_webserver/constants.py index ef963529d53..2962f5a3b0d 100644 --- a/services/web/server/src/simcore_service_webserver/constants.py +++ b/services/web/server/src/simcore_service_webserver/constants.py @@ -1,5 +1,6 @@ # pylint:disable=unused-import +from sys import version from typing import Final from common_library.user_messages import user_message @@ -39,7 +40,7 @@ # main index route name = front-end INDEX_RESOURCE_NAME: Final[str] = "get_cached_frontend_index" -MSG_UNDER_DEVELOPMENT: Final[str] = ( +MSG_UNDER_DEVELOPMENT: Final[str] = user_message( "Under development. Use WEBSERVER_DEV_FEATURES_ENABLED=1 to enable current implementation" ) @@ -48,7 +49,7 @@ MSG_TRY_AGAIN_OR_SUPPORT: Final[str] = user_message( - "Please try again shortly. If the issue persists, contact support." + "Please try again shortly. If the issue persists, contact support.", _version=1 ) __all__: tuple[str, ...] = ( 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 ac1958870a4..809ec47b6a8 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 @@ -87,11 +87,17 @@ async def _handler_director_service_error_as_503_or_4xx( _TO_HTTP_ERROR_MAP: ExceptionToHttpErrorMap = { UserDefaultWalletNotFoundError: HttpErrorInfo( status.HTTP_404_NOT_FOUND, - user_message("Default wallet not found but necessary for computations"), + user_message( + "A default wallet is required for running computations but could not be found.", + _version=1, + ), ), WalletNotEnoughCreditsError: HttpErrorInfo( status.HTTP_402_PAYMENT_REQUIRED, - user_message("Wallet does not have enough credits for computations. {reason}"), + user_message( + "Your wallet does not have sufficient credits to run this computation. {reason}", + _version=1, + ), ), } 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 875f2790f5d..52f2658ac7c 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 @@ -32,49 +32,56 @@ _TO_HTTP_ERROR_MAP: ExceptionToHttpErrorMap = { FolderNotFoundError: HttpErrorInfo( status.HTTP_404_NOT_FOUND, - user_message("Folder was not found"), + user_message("The requested folder could not be found.", _version=1), ), WorkspaceNotFoundError: HttpErrorInfo( status.HTTP_404_NOT_FOUND, - user_message("Workspace was not found"), + user_message("The requested workspace could not be found.", _version=1), ), FolderAccessForbiddenError: HttpErrorInfo( status.HTTP_403_FORBIDDEN, - user_message("Does not have access to this folder"), + user_message("You do not have permission to access this folder.", _version=1), ), WorkspaceAccessForbiddenError: HttpErrorInfo( status.HTTP_403_FORBIDDEN, - user_message("Does not have access to this workspace"), + user_message( + "You do not have permission to access this workspace.", _version=1 + ), ), WorkspaceFolderInconsistencyError: HttpErrorInfo( status.HTTP_403_FORBIDDEN, - user_message("This folder does not exist in this workspace"), + user_message( + "This folder is not available in the selected workspace.", _version=1 + ), ), FolderValueNotPermittedError: HttpErrorInfo( status.HTTP_409_CONFLICT, - user_message("Provided folder value is not permitted: {reason}"), + user_message("The folder operation cannot be completed: {reason}", _version=1), ), FoldersValueError: HttpErrorInfo( status.HTTP_409_CONFLICT, - user_message("Invalid folder value set: {reason}"), + user_message("The folder configuration is invalid: {reason}", _version=1), ), ProjectInvalidRightsError: HttpErrorInfo( status.HTTP_403_FORBIDDEN, 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." + "You do not have permission to move the project with UUID: {project_uuid}. To locate this project, copy and paste the UUID into the search bar.", + _version=1, ), ), # Trashing ProjectRunningConflictError: HttpErrorInfo( status.HTTP_409_CONFLICT, user_message( - "One or more studies in this folder are in use and cannot be trashed. Please stop all services first and try again" + "Cannot move folder to trash because it contains projects that are currently running. Please stop all running services first and try again.", + _version=2, ), ), ProjectStoppingError: HttpErrorInfo( status.HTTP_503_SERVICE_UNAVAILABLE, user_message( - "Something went wrong while stopping running services in studies within this folder before trashing. Aborting trash." + "Something went wrong while stopping running services in projects within this folder before trashing. Aborting trash.", + _version=2, ), ), } 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 c6ffa9e7092..c0237b2c45f 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 @@ -24,32 +24,40 @@ _TO_HTTP_ERROR_MAP: ExceptionToHttpErrorMap = { UserNotFoundError: HttpErrorInfo( status.HTTP_404_NOT_FOUND, - user_message("User {uid} or {email} not found"), + user_message( + "The user with ID {uid} or email {email} could not be found.", _version=1 + ), ), GroupNotFoundError: HttpErrorInfo( status.HTTP_404_NOT_FOUND, - user_message("Group {gid} not found"), + user_message("The group with ID {gid} could not be found.", _version=1), ), UserInGroupNotFoundError: HttpErrorInfo( status.HTTP_404_NOT_FOUND, - user_message("User not found in group {gid}"), + user_message("The user is not a member of group {gid}.", _version=1), ), UserAlreadyInGroupError: HttpErrorInfo( status.HTTP_409_CONFLICT, - user_message("User is already in group {gid}"), + user_message("The user is already a member of group {gid}.", _version=1), ), UserInsufficientRightsError: HttpErrorInfo( status.HTTP_403_FORBIDDEN, - user_message("Insufficient rights for {permission} access to group {gid}"), + user_message( + "You do not have sufficient rights for {permission} access to group {gid}.", + _version=1, + ), ), # scicrunch InvalidRRIDError: HttpErrorInfo( status.HTTP_409_CONFLICT, - user_message("Invalid RRID {rrid}"), + user_message("The RRID {rrid} is not valid.", _version=1), ), ScicrunchError: HttpErrorInfo( status.HTTP_409_CONFLICT, - user_message("Cannot get RRID since scicrunch.org service is not reachable."), + user_message( + "Unable to retrieve RRID information because the scicrunch.org service is currently unavailable.", + _version=1, + ), ), } diff --git a/services/web/server/src/simcore_service_webserver/invitations/errors.py b/services/web/server/src/simcore_service_webserver/invitations/errors.py index 881b62c6df9..0375c9b6685 100644 --- a/services/web/server/src/simcore_service_webserver/invitations/errors.py +++ b/services/web/server/src/simcore_service_webserver/invitations/errors.py @@ -1,16 +1,20 @@ """ - API plugin errors +API plugin errors """ +from common_library.user_messages import user_message from ..errors import WebServerBaseError -MSG_INVALID_INVITATION_URL = "Link seems corrupted or incomplete" -MSG_INVITATION_ALREADY_USED = "This invitation was already used" +MSG_INVALID_INVITATION_URL = user_message( + "The invitation link appears to be corrupted or incomplete.", _version=1 +) +MSG_INVITATION_ALREADY_USED = user_message( + "This invitation has already been used and cannot be used again.", _version=1 +) -class InvitationsError(WebServerBaseError, ValueError): - ... +class InvitationsError(WebServerBaseError, ValueError): ... class InvalidInvitationError(InvitationsError): 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 0fc0c82f5a5..d7358587530 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 @@ -18,20 +18,30 @@ _TO_HTTP_ERROR_MAP: ExceptionToHttpErrorMap = { LicensedItemNotFoundError: HttpErrorInfo( status.HTTP_404_NOT_FOUND, - user_message("Market item {licensed_item_id} not found."), + user_message( + "The requested market item '{licensed_item_id}' could not be found.", + _version=1, + ), ), WalletAccessForbiddenError: HttpErrorInfo( status.HTTP_403_FORBIDDEN, - user_message("Credit account {wallet_id} forbidden."), + user_message( + "You do not have permission to access credit account '{wallet_id}'.", + _version=1, + ), ), WalletNotEnoughCreditsError: HttpErrorInfo( status.HTTP_402_PAYMENT_REQUIRED, - user_message("Not enough credits in the credit account."), + user_message( + "Your credit account does not have sufficient funds to complete this purchase.", + _version=1, + ), ), LicensedItemPricingPlanMatchError: HttpErrorInfo( status.HTTP_400_BAD_REQUEST, user_message( - "The provided pricing plan does not match the one associated with the licensed item." + "The selected pricing plan is not valid for this licensed item. Please choose a different plan.", + _version=1, ), ), } diff --git a/services/web/server/src/simcore_service_webserver/login/_constants.py b/services/web/server/src/simcore_service_webserver/login/_constants.py index cc10d6ed340..2ee9e5f874f 100644 --- a/services/web/server/src/simcore_service_webserver/login/_constants.py +++ b/services/web/server/src/simcore_service_webserver/login/_constants.py @@ -1,79 +1,118 @@ from typing import Final -MSG_2FA_CODE_SENT: Final[str] = "A code was sent by SMS to {phone_number}." -MSG_2FA_UNAVAILABLE: Final[str] = "Two-factor authentication is temporarily unavailable" -MSG_ACTIVATED: Final[str] = "Your account has been activated." -MSG_ACTIVATION_REQUIRED: Final[str] = ( - "Please activate your account via the email we sent before logging in." +from common_library.user_messages import user_message + +MSG_2FA_CODE_SENT: Final[str] = user_message( + "A verification code has been sent via SMS to {phone_number}.", _version=1 +) +MSG_2FA_UNAVAILABLE: Final[str] = user_message( + "Two-factor authentication is temporarily unavailable. Please try again later.", + _version=1, +) +MSG_ACTIVATED: Final[str] = user_message( + "Your account has been successfully activated.", _version=1 +) +MSG_ACTIVATION_REQUIRED: Final[str] = user_message( + "Please activate your account using the activation link we sent to your email before logging in.", + _version=1, +) +MSG_AUTH_FAILED: Final[str] = user_message( + "We couldn't sign you in with those credentials. Please check your email and password and try again.", + _version=1, +) +MSG_CANT_SEND_MAIL: Final[str] = user_message( + "We're unable to send emails at this time. Please try again later.", _version=1 ) -MSG_AUTH_FAILED: Final[str] = ( - "Authorization was not successful. Please check your credentials and try again." +MSG_CHANGE_EMAIL_REQUESTED: Final[str] = user_message( + "Please check your new email address and click the verification link we sent you.", + _version=1, ) -MSG_CANT_SEND_MAIL: Final[str] = ( - "Unable to send email at this time. Please try again later." +MSG_EMAIL_CHANGED: Final[str] = user_message( + "Your email address has been successfully updated.", _version=1 ) -MSG_CHANGE_EMAIL_REQUESTED: Final[str] = ( - "Please click the verification link sent to your new email address." +MSG_EMAIL_ALREADY_REGISTERED: Final[str] = user_message( + "This email address is already associated with an account. Please sign in or use a different email address.", + _version=1, ) -MSG_EMAIL_CHANGED: Final[str] = "Your email address has been updated." -MSG_EMAIL_ALREADY_REGISTERED: Final[str] = ( - "This email address is already registered. Try logging in or use a different address." +MSG_EMAIL_SENT: Final[str] = user_message( + "We've sent an email to {email} with further instructions.", _version=1 ) -MSG_EMAIL_SENT: Final[str] = "An email was sent to {email} with further instructions." -MSG_LOGGED_IN: Final[str] = "You have successfully logged in." -MSG_LOGGED_OUT: Final[str] = "You have successfully logged out." -MSG_OFTEN_RESET_PASSWORD: Final[str] = ( - "You've requested a password reset recently. Please use the link we sent you or wait before requesting again." +MSG_LOGGED_IN: Final[str] = user_message("You have successfully signed in.", _version=1) +MSG_LOGGED_OUT: Final[str] = user_message( + "You have successfully signed out.", _version=1 ) -MSG_PASSWORD_CHANGE_NOT_ALLOWED: Final[str] = ( +MSG_OFTEN_RESET_PASSWORD: Final[str] = user_message( + "You've recently requested a password reset. Please check your email for the reset link or wait before requesting another one.", + _version=1, +) +MSG_PASSWORD_CHANGE_NOT_ALLOWED: Final[str] = user_message( "Unable to reset password. Permissions may have expired or been removed. " - "Please try again, or contact support if the problem continues: {support_email}" + "Please try again, or contact support if the problem continues: {support_email}", + _version=1, +) +MSG_PASSWORD_CHANGED: Final[str] = user_message( + "Your password has been updated.", _version=1 +) +MSG_PASSWORD_MISMATCH: Final[str] = user_message( + "Password and confirmation do not match. Please try again.", _version=1 +) +MSG_PHONE_MISSING: Final[str] = user_message( + "No phone number is associated with this account.", _version=1 +) +MSG_UNAUTHORIZED_CODE_RESEND_2FA: Final[str] = user_message( + "You can no longer resend the verification code. Please restart the verification process.", + _version=2, ) -MSG_PASSWORD_CHANGED: Final[str] = "Your password has been updated." -MSG_PASSWORD_MISMATCH: Final[str] = ( - "Password and confirmation do not match. Please try again." +MSG_UNAUTHORIZED_LOGIN_2FA: Final[str] = user_message( + "You can no longer submit a verification code. Please restart the login process.", + _version=2, ) -MSG_PHONE_MISSING: Final[str] = "No phone number is associated with this account." -MSG_UNAUTHORIZED_CODE_RESEND_2FA: Final[str] = ( - "You can no longer resend the code. Please restart the verification process." +MSG_UNAUTHORIZED_REGISTER_PHONE: Final[str] = user_message( + "Phone registration is no longer allowed. Please restart the registration process.", + _version=1, ) -MSG_UNAUTHORIZED_LOGIN_2FA: Final[str] = ( - "You can no longer submit a code. Please restart the login process." +MSG_UNAUTHORIZED_PHONE_CONFIRMATION: Final[str] = user_message( + "You can no longer submit a verification code. Please restart the confirmation process.", + _version=2, ) -MSG_UNAUTHORIZED_REGISTER_PHONE: Final[str] = ( - "Phone registration is no longer allowed. Please restart the registration process." +MSG_UNKNOWN_EMAIL: Final[str] = user_message( + "This email address is not registered.", _version=1 ) -MSG_UNAUTHORIZED_PHONE_CONFIRMATION: Final[str] = ( - "You can no longer submit a code. Please restart the confirmation process." +MSG_USER_DELETED: Final[str] = user_message( + "This account is scheduled for deletion. To reactivate it or for more information, please contact support: {support_email}", + _version=1, ) -MSG_UNKNOWN_EMAIL: Final[str] = "This email address is not registered." -MSG_USER_DELETED: Final[str] = ( - "This account is scheduled for deletion. To reactivate it or for more information, please contact support: {support_email}" +MSG_USER_BANNED: Final[str] = user_message( + "Access to this account is no longer available. Please contact support for more information: {support_email}", + _version=1, ) -MSG_USER_BANNED: Final[str] = ( - "Access to this account is no longer available. Please contact support for more information: {support_email}" +MSG_USER_EXPIRED: Final[str] = user_message( + "This account has expired and access is no longer available. Please contact support for assistance: {support_email}", + _version=1, ) -MSG_USER_EXPIRED: Final[str] = ( - "This account has expired and access is no longer available. Please contact support for assistance: {support_email}" +MSG_USER_DISABLED: Final[str] = user_message( + "This account has been disabled and cannot be registered again. Please contact support for details: {support_email}", + _version=1, ) -MSG_USER_DISABLED: Final[str] = ( - "This account has been disabled and cannot be registered again. Please contact support for details: {support_email}" +MSG_WRONG_2FA_CODE__INVALID: Final[str] = user_message( + "The verification code entered is not valid. Please enter a valid verification code or generate a new one.", + _version=2, ) -MSG_WRONG_2FA_CODE__INVALID: Final[str] = ( - "The code entered is not valid. Please enter a valid code or generate a new one." +MSG_WRONG_2FA_CODE__EXPIRED: Final[str] = user_message( + "The verification code is either incorrect or has expired. Please request a new verification code and try again.", + _version=3, ) -MSG_WRONG_2FA_CODE__EXPIRED: Final[str] = ( - "The code has expired. Please generate a new code." +MSG_WRONG_CAPTCHA__INVALID: Final[str] = user_message( + "The CAPTCHA entered is incorrect. Please try again.", _version=1 ) -MSG_WRONG_CAPTCHA__INVALID: Final[str] = ( - "The CAPTCHA entered is incorrect. Please try again." +MSG_WRONG_PASSWORD: Final[str] = user_message( + "The password is incorrect. Please try again.", _version=1 ) -MSG_WRONG_PASSWORD: Final[str] = "The password is incorrect. Please try again." -MSG_WEAK_PASSWORD: Final[str] = ( - "Password must be at least {LOGIN_PASSWORD_MIN_LENGTH} characters long." +MSG_WEAK_PASSWORD: Final[str] = user_message( + "Password must be at least {LOGIN_PASSWORD_MIN_LENGTH} characters long.", _version=1 ) -MSG_INVITATIONS_CONTACT_SUFFIX: Final[str] = ( - "Please contact our support team to request a new invitation." +MSG_INVITATIONS_CONTACT_SUFFIX: Final[str] = user_message( + "Please contact our support team to request a new invitation.", _version=1 ) # Login Accepted Response Codes: diff --git a/services/web/server/src/simcore_service_webserver/payments/_onetime_api.py b/services/web/server/src/simcore_service_webserver/payments/_onetime_api.py index 488189a81a5..ef74187c5df 100644 --- a/services/web/server/src/simcore_service_webserver/payments/_onetime_api.py +++ b/services/web/server/src/simcore_service_webserver/payments/_onetime_api.py @@ -5,6 +5,7 @@ import arrow from aiohttp import web +from common_library.user_messages import user_message from models_library.api_schemas_webserver.wallets import ( PaymentID, PaymentMethodID, @@ -36,7 +37,11 @@ _logger = logging.getLogger(__name__) -MSG_WALLET_NO_ACCESS_ERROR = "User {user_id} does not have necessary permissions to do a payment into wallet {wallet_id}" +MSG_WALLET_NO_ACCESS_ERROR = user_message( + "You do not have the necessary permissions to make payments to wallet {wallet_id}.", + _version=1, +) + _FAKE_PAYMENT_TRANSACTION_ID_PREFIX = "fpt" 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 9402486e27e..78635508bd9 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 @@ -13,13 +13,16 @@ _TO_HTTP_ERROR_MAP: ExceptionToHttpErrorMap = { ProductNotFoundError: HttpErrorInfo( status.HTTP_404_NOT_FOUND, - user_message("{product_name} was not found"), + user_message( + "This product could not be found." + MSG_TRY_AGAIN_OR_SUPPORT, _version=1 + ), ), MissingStripeConfigError: HttpErrorInfo( status.HTTP_503_SERVICE_UNAVAILABLE, user_message( - "{product_name} service is currently unavailable." - + MSG_TRY_AGAIN_OR_SUPPORT + "This service is temporarily unavailable due to a configuration issue. " + + MSG_TRY_AGAIN_OR_SUPPORT, + _version=1, ), ), } 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 ee6ffe52408..e703b0ec865 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 @@ -55,11 +55,11 @@ _FOLDER_ERRORS: ExceptionToHttpErrorMap = { FolderAccessForbiddenError: HttpErrorInfo( status.HTTP_403_FORBIDDEN, - user_message("Access to folder forbidden"), + user_message("Access to this folder is forbidden.", _version=1), ), FolderNotFoundError: HttpErrorInfo( status.HTTP_404_NOT_FOUND, - user_message("Folder not found: {reason}"), + user_message("The requested folder could not be found: {reason}", _version=1), ), } @@ -67,15 +67,19 @@ _NODE_ERRORS: ExceptionToHttpErrorMap = { NodeNotFoundError: HttpErrorInfo( status.HTTP_404_NOT_FOUND, - user_message("Node '{node_uuid}' not found in project '{project_uuid}'"), + user_message( + "Node '{node_uuid}' was not found in project '{project_uuid}'.", _version=1 + ), ), ParentNodeNotFoundError: HttpErrorInfo( status.HTTP_404_NOT_FOUND, - user_message("Parent node '{node_uuid}' not found"), + user_message("Parent node '{node_uuid}' was not found.", _version=1), ), ProjectNodeRequiredInputsNotSetError: HttpErrorInfo( status.HTTP_409_CONFLICT, - user_message("Project node is required but input is not set"), + user_message( + "Required input values for this project node have not been set.", _version=1 + ), ), } @@ -83,59 +87,91 @@ _PROJECT_ERRORS: ExceptionToHttpErrorMap = { ProjectDeleteError: HttpErrorInfo( status.HTTP_409_CONFLICT, - "Failed to complete deletion of '{project_uuid}': {reason}", + user_message( + "Unable to complete deletion of project '{project_uuid}': {reason}", + _version=1, + ), ), ProjectGroupNotFoundError: HttpErrorInfo( status.HTTP_404_NOT_FOUND, - "Project group not found: {reason}", + user_message( + "The requested project group could not be found: {reason}", _version=1 + ), ), ProjectInvalidRightsError: HttpErrorInfo( status.HTTP_403_FORBIDDEN, - "Do not have sufficient access rights on project {project_uuid} for this action", + user_message( + "You do not have sufficient access rights to perform this action on project {project_uuid}.", + _version=1, + ), ), InsufficientRoleForProjectTemplateTypeUpdateError: HttpErrorInfo( status.HTTP_403_FORBIDDEN, - "Do not have sufficient access rights on updating project template type", + user_message( + "You do not have sufficient permissions to update the project template type.", + _version=1, + ), ), ProjectInvalidUsageError: HttpErrorInfo( status.HTTP_422_UNPROCESSABLE_ENTITY, - "Invalid usage for project", + user_message("The project cannot be used in this way.", _version=1), ), ProjectNotFoundError: HttpErrorInfo( status.HTTP_404_NOT_FOUND, - "Project {project_uuid} not found", + user_message("Project {project_uuid} could not be found.", _version=1), ), ProjectOwnerNotFoundInTheProjectAccessRightsError: HttpErrorInfo( status.HTTP_400_BAD_REQUEST, - "Project owner identifier was not found in the project's access-rights field", + user_message( + "The project owner could not be found in the project's access rights.", + _version=1, + ), ), ProjectTooManyProjectOpenedError: HttpErrorInfo( status.HTTP_409_CONFLICT, - "You cannot open more than {max_num_projects} study/ies at once. Please close another study and retry.", + user_message( + "You cannot open more than {max_num_projects} project/s at once. Please close another project and retry.", + _version=2, + ), ), ProjectStartsTooManyDynamicNodesError: HttpErrorInfo( status.HTTP_409_CONFLICT, - "The maximal amount of concurrently running dynamic services was reached. Please manually stop a service and retry.", + user_message( + "The maximum number of concurrently running dynamic services has been reached. Please manually stop a service and retry.", + _version=1, + ), ), ProjectWalletPendingTransactionError: HttpErrorInfo( status.HTTP_409_CONFLICT, - "Project has currently pending transactions. It is forbidden to change wallet.", + user_message( + "This project has pending transactions. Changing the wallet is currently not allowed.", + _version=1, + ), ), ProjectInDebtCanNotChangeWalletError: HttpErrorInfo( status.HTTP_402_PAYMENT_REQUIRED, - "Unable to change the credit account linked to the project. The project is embargoed because the last transaction of {debt_amount} resulted in the credit account going negative.", + user_message( + "Unable to change the credit account linked to the project. The project is embargoed because the last transaction of {debt_amount} resulted in the credit account going negative.", + _version=1, + ), ), ProjectInDebtCanNotOpenError: HttpErrorInfo( status.HTTP_402_PAYMENT_REQUIRED, - "Unable to open the project. The project is embargoed because the last transaction of {debt_amount} resulted in the credit account going negative.", + user_message( + "Unable to open the project. The project is embargoed because the last transaction of {debt_amount} resulted in the credit account going negative.", + _version=1, + ), ), WrongTagIdsInQueryError: HttpErrorInfo( status.HTTP_400_BAD_REQUEST, - "Wrong tag IDs in query", + user_message("Invalid tag IDs were provided in the request.", _version=1), ), ProjectTypeAndTemplateIncompatibilityError: HttpErrorInfo( status.HTTP_400_BAD_REQUEST, - "Wrong project type and template type combination: {reason}", + user_message( + "The project type and template type combination is not valid: {reason}", + _version=1, + ), ), } @@ -143,11 +179,13 @@ _WORKSPACE_ERRORS: ExceptionToHttpErrorMap = { WorkspaceAccessForbiddenError: HttpErrorInfo( status.HTTP_403_FORBIDDEN, - "Access to workspace forbidden: {reason}", + user_message("Access to this workspace is forbidden: {reason}", _version=1), ), WorkspaceNotFoundError: HttpErrorInfo( status.HTTP_404_NOT_FOUND, - "Workspace not found: {reason}", + user_message( + "The requested workspace could not be found: {reason}", _version=1 + ), ), } @@ -155,15 +193,21 @@ _WALLET_ERRORS: ExceptionToHttpErrorMap = { UserDefaultWalletNotFoundError: HttpErrorInfo( status.HTTP_404_NOT_FOUND, - "Wallet not found: {reason}", + user_message("The requested wallet could not be found: {reason}", _version=1), ), WalletAccessForbiddenError: HttpErrorInfo( status.HTTP_403_FORBIDDEN, - "Payment required, but the user lacks access to the project's linked wallet: Wallet access forbidden. {reason}", + user_message( + "Payment is required, but you do not have access to the project's linked wallet: {reason}", + _version=1, + ), ), WalletNotEnoughCreditsError: HttpErrorInfo( status.HTTP_402_PAYMENT_REQUIRED, - "Wallet does not have enough credits. {reason}", + user_message( + "The wallet does not have enough credits to complete this operation: {reason}", + _version=1, + ), ), } @@ -171,11 +215,11 @@ _PRICING_ERRORS: ExceptionToHttpErrorMap = { DefaultPricingPlanNotFoundError: HttpErrorInfo( status.HTTP_404_NOT_FOUND, - "Default pricing plan not found", + user_message("The default pricing plan could not be found.", _version=1), ), DefaultPricingUnitNotFoundError: HttpErrorInfo( status.HTTP_404_NOT_FOUND, - "Default pricing unit not found", + user_message("The default pricing unit could not be found.", _version=1), ), } @@ -183,11 +227,13 @@ _CONVERSATION_ERRORS: ExceptionToHttpErrorMap = { ConversationErrorNotFoundError: HttpErrorInfo( status.HTTP_404_NOT_FOUND, - "Conversation not found", + user_message("The requested conversation could not be found.", _version=1), ), ConversationMessageErrorNotFoundError: HttpErrorInfo( status.HTTP_404_NOT_FOUND, - "Conversation message not found", + user_message( + "The requested conversation message could not be found.", _version=1 + ), ), } @@ -195,18 +241,24 @@ _OTHER_ERRORS: ExceptionToHttpErrorMap = { CatalogNotAvailableError: HttpErrorInfo( status.HTTP_503_SERVICE_UNAVAILABLE, - "This service is currently not available", + user_message("The catalog service is currently unavailable.", _version=1), ), ClustersKeeperNotAvailableError: HttpErrorInfo( status.HTTP_503_SERVICE_UNAVAILABLE, - "Clusters-keeper service is not available", + user_message( + "The clusters-keeper service is currently unavailable.", _version=1 + ), ), CatalogForbiddenError: HttpErrorInfo( status.HTTP_403_FORBIDDEN, - "Catalog forbidden: Insufficient access rights for {name}", + user_message( + "Access denied: You do not have sufficient permissions for {name}.", + _version=1, + ), ), CatalogItemNotFoundError: HttpErrorInfo( - status.HTTP_404_NOT_FOUND, "{name} was not found" + status.HTTP_404_NOT_FOUND, + user_message("The requested item '{name}' was not found.", _version=1), ), } diff --git a/services/web/server/src/simcore_service_webserver/resource_usage/_constants.py b/services/web/server/src/simcore_service_webserver/resource_usage/_constants.py index 64586ad4c49..5b4741478d6 100644 --- a/services/web/server/src/simcore_service_webserver/resource_usage/_constants.py +++ b/services/web/server/src/simcore_service_webserver/resource_usage/_constants.py @@ -1,9 +1,14 @@ from typing import Final +from common_library.user_messages import user_message + APP_RABBITMQ_CONSUMERS_KEY: Final[str] = f"{__name__}.rabbit_consumers" -MSG_RESOURCE_USAGE_TRACKER_SERVICE_UNAVAILABLE: Final[ - str -] = "Currently resource usage tracker service is unavailable, please try again later" +MSG_RESOURCE_USAGE_TRACKER_SERVICE_UNAVAILABLE: Final[str] = user_message( + "The resource usage tracking service is temporarily unavailable. Please try again in a few moments.", + _version=1, +) -MSG_RESOURCE_USAGE_TRACKER_NOT_FOUND: Final[str] = "Not Found" +MSG_RESOURCE_USAGE_TRACKER_NOT_FOUND: Final[str] = user_message( + "The requested resource usage information could not be found.", _version=1 +) diff --git a/services/web/server/src/simcore_service_webserver/security/_constants.py b/services/web/server/src/simcore_service_webserver/security/_constants.py index 62974bb6adb..b10848ee522 100644 --- a/services/web/server/src/simcore_service_webserver/security/_constants.py +++ b/services/web/server/src/simcore_service_webserver/security/_constants.py @@ -1,5 +1,10 @@ from typing import Final -MSG_AUTH_NOT_AVAILABLE: Final[str] = "Authentication service is temporary unavailable" -MSG_UNAUTHORIZED: Final[str] = "Unauthorized" +from common_library.user_messages import user_message + +MSG_UNAUTHORIZED: Final[str] = user_message("Unauthorized") +MSG_AUTH_NOT_AVAILABLE: Final[str] = user_message( + "Authentication service is temporary unavailable" +) + PERMISSION_PRODUCT_LOGIN_KEY: Final[str] = "product.login" diff --git a/services/web/server/src/simcore_service_webserver/studies_dispatcher/_constants.py b/services/web/server/src/simcore_service_webserver/studies_dispatcher/_constants.py index b36820434b0..3a2409cf4e9 100644 --- a/services/web/server/src/simcore_service_webserver/studies_dispatcher/_constants.py +++ b/services/web/server/src/simcore_service_webserver/studies_dispatcher/_constants.py @@ -1,34 +1,44 @@ from typing import Final +from common_library.user_messages import user_message + # # NOTE: MSG_$(ERROR_CODE_NAME) strings MUST be human readable messages # Please keep alphabetical order # -MSG_PROJECT_NOT_FOUND: Final[str] = "Cannot find any study with ID '{project_id}'" +MSG_PROJECT_NOT_FOUND: Final[str] = user_message( + "The project with ID '{project_id}' could not be found.", _version=1 +) -# This error happens when the linked study ID does not exists OR is not shared with everyone -MSG_PROJECT_NOT_PUBLISHED: Final[str] = "Cannot find any study with ID '{project_id}'" +# This error happens when the linked project ID does not exists OR is not shared with everyone +MSG_PROJECT_NOT_PUBLISHED: Final[str] = user_message( + "The project with ID '{project_id}' is not available or not shared.", _version=1 +) -# This error happens when the linked study ID does not exists OR is not shared with everyone OR is NOT public -MSG_PUBLIC_PROJECT_NOT_PUBLISHED: Final[str] = ( - "Only available for registered users.

" - "Please login and try again.

" - "If you don't have an account, please request one at {support_email}

" +# This error happens when the linked project ID does not exists OR is not shared with everyone OR is NOT public +MSG_PUBLIC_PROJECT_NOT_PUBLISHED: Final[str] = user_message( + "This project is only available for registered users.

" + "Please log in and try again.

" + "If you don't have an account, please request one at {support_email}.

", + _version=1, ) -MSG_GUESTS_NOT_ALLOWED: Final[str] = ( - "Access restricted to registered users.

" - "If you don't have an account, please email to support and request one

" +MSG_GUESTS_NOT_ALLOWED: Final[str] = user_message( + "Access is restricted to registered users.

" + "If you don't have an account, please contact support to request one.

", + _version=1, ) -MSG_TOO_MANY_GUESTS: Final[str] = ( - "We have reached the maximum of anonymous users allowed the platform. " - "Please try later or login with a registered account." +MSG_TOO_MANY_GUESTS: Final[str] = user_message( + "We have reached the maximum number of anonymous users allowed on the platform. " + "Please try again later or log in with a registered account.", + _version=1, ) -MSG_UNEXPECTED_DISPATCH_ERROR: Final[str] = ( - "Sorry, but looks like something unexpected went wrong!" - "We track these errors automatically, but if the problem persists feel free to contact us." - "In the meantime, try refreshing." +MSG_UNEXPECTED_DISPATCH_ERROR: Final[str] = user_message( + "Sorry, something unexpected went wrong! " + "We track these errors automatically, but if the problem persists please contact us. " + "In the meantime, try refreshing the page.", + _version=1, ) 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 10d4c86a422..21dc5e97c6e 100644 --- a/services/web/server/src/simcore_service_webserver/tags/_rest.py +++ b/services/web/server/src/simcore_service_webserver/tags/_rest.py @@ -39,27 +39,35 @@ _TO_HTTP_ERROR_MAP: ExceptionToHttpErrorMap = { TagNotFoundError: HttpErrorInfo( status.HTTP_404_NOT_FOUND, - user_message("Tag {tag_id} not found: either no access or does not exists"), + user_message( + "The tag '{tag_id}' could not be found or you don't have access to it.", + _version=1, + ), ), TagOperationNotAllowedError: HttpErrorInfo( status.HTTP_403_FORBIDDEN, user_message( - "Could not {operation} tag {tag_id}. Not found or insuficient access." + "Unable to {operation} tag '{tag_id}'. The tag was not found or you don't have sufficient access.", + _version=1, ), ), ShareTagWithEveryoneNotAllowedError: HttpErrorInfo( status.HTTP_403_FORBIDDEN, - user_message("Sharing with everyone is not permitted."), + user_message("Sharing with everyone is not allowed.", _version=1), ), ShareTagWithProductGroupNotAllowedError: HttpErrorInfo( status.HTTP_403_FORBIDDEN, user_message( - "Sharing with all users is only permitted to admin users (e.g. testers, POs, ...)." + "Sharing with all users is only allowed for admin users (e.g. testers, POs, ...).", + _version=1, ), ), InsufficientTagShareAccessError: HttpErrorInfo( status.HTTP_403_FORBIDDEN, - user_message("Insufficient access rightst to share (or unshare) tag {tag_id}."), + user_message( + "You don't have sufficient access rights to share (or unshare) tag '{tag_id}'.", + _version=1, + ), ), } 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 728aca09b64..3fc2cf234d5 100644 --- a/services/web/server/src/simcore_service_webserver/trash/_rest.py +++ b/services/web/server/src/simcore_service_webserver/trash/_rest.py @@ -27,13 +27,15 @@ ProjectRunningConflictError: HttpErrorInfo( status.HTTP_409_CONFLICT, user_message( - "Current study is in use and cannot be trashed [project_id={project_uuid}]. Please stop all services first and try again" + "The project is currently in use and cannot be moved to trash. Please stop all running services first and try again.", + _version=1, ), ), ProjectStoppingError: HttpErrorInfo( status.HTTP_503_SERVICE_UNAVAILABLE, user_message( - "Something went wrong while stopping services before trashing. Aborting trash." + "An error occurred while stopping services before moving to trash. The operation has been cancelled.", + _version=1, ), ), } 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 7590c7fb17f..abb8ff7365d 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 @@ -55,32 +55,40 @@ _TO_HTTP_ERROR_MAP: ExceptionToHttpErrorMap = { PendingPreRegistrationNotFoundError: HttpErrorInfo( status.HTTP_400_BAD_REQUEST, - user_message(PendingPreRegistrationNotFoundError.msg_template), + user_message( + "No pending registration request found for email {email} in {product_name}.", + _version=2, + ), ), UserNotFoundError: HttpErrorInfo( status.HTTP_404_NOT_FOUND, user_message( - "This user cannot be found. Either it is not registered or has enabled privacy settings." + "The requested user could not be found. " + "This may be because the user is not registered or has privacy settings enabled.", + _version=1, ), ), UserNameDuplicateError: HttpErrorInfo( status.HTTP_409_CONFLICT, user_message( - "Username '{user_name}' is already taken. " - "Consider '{alternative_user_name}' instead." + "The username '{user_name}' is already in use. " + "Please try '{alternative_user_name}' instead.", + _version=1, ), ), AlreadyPreRegisteredError: HttpErrorInfo( status.HTTP_409_CONFLICT, user_message( - "Found {num_found} matches for '{email}'. Cannot pre-register existing user" + "Found {num_found} existing account(s) for '{email}'. Unable to pre-register an existing user.", + _version=1, ), ), MissingGroupExtraPropertiesForProductError: HttpErrorInfo( status.HTTP_503_SERVICE_UNAVAILABLE, user_message( - "The product is not ready for use until the configuration is fully completed. " - "Please wait and try again. " + "This product is currently being configured and is not yet ready for use. " + "Please try again later.", + _version=1, ), ), } diff --git a/services/web/server/src/simcore_service_webserver/utils_rate_limiting.py b/services/web/server/src/simcore_service_webserver/utils_rate_limiting.py index 2266170c5ac..5311231a29b 100644 --- a/services/web/server/src/simcore_service_webserver/utils_rate_limiting.py +++ b/services/web/server/src/simcore_service_webserver/utils_rate_limiting.py @@ -6,6 +6,7 @@ from typing import Final, NamedTuple from aiohttp.web_exceptions import HTTPTooManyRequests +from common_library.user_messages import user_message from models_library.rest_error import EnvelopedError, ErrorGet from servicelib.aiohttp import status @@ -15,7 +16,7 @@ class RateLimitSetup(NamedTuple): interval_seconds: float -MSG_TOO_MANY_REQUESTS: Final[str] = ( +MSG_TOO_MANY_REQUESTS: Final[str] = user_message( "Requests are being made too frequently. Please wait a moment before trying again." ) diff --git a/services/web/server/src/simcore_service_webserver/wallets/_constants.py b/services/web/server/src/simcore_service_webserver/wallets/_constants.py index 579c403ef28..3cf69f28779 100644 --- a/services/web/server/src/simcore_service_webserver/wallets/_constants.py +++ b/services/web/server/src/simcore_service_webserver/wallets/_constants.py @@ -3,10 +3,12 @@ from common_library.user_messages import user_message MSG_PRICE_NOT_DEFINED_ERROR: Final[str] = user_message( - "No payments are accepted until this product has a price" + "Payments are not currently available for this product as pricing has not been configured.", + _version=1, ) MSG_BILLING_DETAILS_NOT_DEFINED_ERROR: Final[str] = user_message( - "Payments cannot be processed: Required billing details (e.g. country for tax) are missing from your account." - "Please contact support to resolve this configuration issue." + "Unable to process payment because required billing information (such as country for tax purposes) is missing from your account. " + "Please contact support to complete your billing setup.", + _version=1, ) 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 4a4b77a5cca..aa11ad4afe4 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 @@ -22,27 +22,36 @@ _TO_HTTP_ERROR_MAP: ExceptionToHttpErrorMap = { WorkspaceGroupNotFoundError: HttpErrorInfo( status.HTTP_404_NOT_FOUND, - user_message("Workspace {workspace_id} group {group_id} not found."), + user_message( + "The requested workspace {workspace_id} group {group_id} could not be found.", + _version=1, + ), ), WorkspaceAccessForbiddenError: HttpErrorInfo( status.HTTP_403_FORBIDDEN, - user_message("Does not have access to this workspace"), + user_message( + "You do not have permission to access this workspace.", _version=1 + ), ), WorkspaceNotFoundError: HttpErrorInfo( status.HTTP_404_NOT_FOUND, - user_message("Workspace not found. {reason}"), + user_message( + "The requested workspace could not be found. {reason}", _version=1 + ), ), # Trashing ProjectRunningConflictError: HttpErrorInfo( status.HTTP_409_CONFLICT, user_message( - "One or more studies in this workspace are in use and cannot be trashed. Please stop all services first and try again" + "Unable to delete workspace because one or more projects are currently running. Please stop all running services and try again.", + _version=1, ), ), ProjectStoppingError: HttpErrorInfo( status.HTTP_503_SERVICE_UNAVAILABLE, user_message( - "Something went wrong while stopping running services in studies within this workspace before trashing. Aborting trash." + "Something went wrong while stopping running services in projects within this workspace before trashing. Aborting trash.", + _version=1, ), ), } diff --git a/services/web/server/tests/unit/with_dbs/03/login/test_login_twofa.py b/services/web/server/tests/unit/with_dbs/03/login/test_login_twofa.py index 3bd849f086b..c84bc7ab5ac 100644 --- a/services/web/server/tests/unit/with_dbs/03/login/test_login_twofa.py +++ b/services/web/server/tests/unit/with_dbs/03/login/test_login_twofa.py @@ -24,6 +24,7 @@ from simcore_service_webserver.login._constants import ( CODE_2FA_SMS_CODE_REQUIRED, MSG_2FA_UNAVAILABLE, + MSG_LOGGED_IN, ) from simcore_service_webserver.login._login_repository_legacy import AsyncpgStorage from simcore_service_webserver.login._twofa_service import ( @@ -307,7 +308,7 @@ def _get_confirmation_link_from_email(): }, ) data, _ = await assert_status(response, status.HTTP_200_OK) - assert "logged in" in data["message"] + assert MSG_LOGGED_IN in data["message"] async def test_can_register_same_phone_in_different_accounts(