diff --git a/backend/models/dtos/user_dto.py b/backend/models/dtos/user_dto.py index cbbe50432c..6c44e736cc 100644 --- a/backend/models/dtos/user_dto.py +++ b/backend/models/dtos/user_dto.py @@ -54,6 +54,10 @@ class UserDTO(BaseModel): ) projects_notifications: bool = Field(None, alias="projectsNotifications") tasks_notifications: bool = Field(None, alias="tasksNotifications") + task_validation_notification: bool = Field(None, alias="taskValidationNotification") + task_invalidation_notification: bool = Field( + None, alias="taskInvalidationNotification" + ) tasks_comments_notifications: bool = Field(None, alias="taskCommentsNotifications") teams_announcement_notifications: bool = Field( None, alias="teamsAnnouncementNotifications" diff --git a/backend/models/postgis/user.py b/backend/models/postgis/user.py index 11ae8d3fdc..c61303e86f 100644 --- a/backend/models/postgis/user.py +++ b/backend/models/postgis/user.py @@ -77,6 +77,8 @@ class User(Base): projects_comments_notifications = Column(Boolean, default=False, nullable=False) projects_notifications = Column(Boolean, default=True, nullable=False) tasks_notifications = Column(Boolean, default=True, nullable=False) + task_validation_notification = Column(Boolean, default=True, nullable=False) + task_invalidation_notification = Column(Boolean, default=True, nullable=False) tasks_comments_notifications = Column(Boolean, default=False, nullable=False) teams_announcement_notifications = Column(Boolean, default=True, nullable=False) date_registered = Column(DateTime, default=timestamp) @@ -503,6 +505,8 @@ async def as_dto(self, logged_in_username: str, db: Database) -> UserDTO: user_dto.projects_notifications = self.projects_notifications user_dto.projects_comments_notifications = self.projects_comments_notifications user_dto.tasks_notifications = self.tasks_notifications + user_dto.task_validation_notification = self.task_validation_notification + user_dto.task_invalidation_notification = self.task_invalidation_notification user_dto.tasks_comments_notifications = self.tasks_comments_notifications user_dto.teams_announcement_notifications = ( self.teams_announcement_notifications diff --git a/backend/services/messaging/message_service.py b/backend/services/messaging/message_service.py index fb355d647e..dc1919225b 100644 --- a/backend/services/messaging/message_service.py +++ b/backend/services/messaging/message_service.py @@ -206,12 +206,16 @@ async def _push_messages(messages: list, db: Database): and obj.message_type == MessageType.TASK_COMMENT_NOTIFICATION.value ): continue + if ( + user.task_validation_notification is False + and obj.message_type == MessageType.VALIDATION_NOTIFICATION.value + ): + continue - if user.tasks_notifications is False and obj.message_type in ( - MessageType.VALIDATION_NOTIFICATION.value, - MessageType.INVALIDATION_NOTIFICATION.value, + if ( + user.task_invalidation_notification is False + and obj.message_type == MessageType.INVALIDATION_NOTIFICATION.value ): - messages_objs.append(obj) continue # If the notification is enabled, send an email messages_objs.append(obj) diff --git a/backend/services/users/user_service.py b/backend/services/users/user_service.py index 9eccc9e4e3..60992a63e6 100644 --- a/backend/services/users/user_service.py +++ b/backend/services/users/user_service.py @@ -251,6 +251,8 @@ async def register_user(osm_id, username, changeset_count, picture_url, email, d "projects_comments_notifications": False, "projects_notifications": True, "tasks_notifications": True, + "task_validation_notification": True, + "task_invalidation_notification": True, "tasks_comments_notifications": False, "teams_announcement_notifications": True, "date_registered": datetime.datetime.utcnow(), diff --git a/frontend/src/components/user/forms/notifications.js b/frontend/src/components/user/forms/notifications.js index da94d3d2a9..88666e23ad 100644 --- a/frontend/src/components/user/forms/notifications.js +++ b/frontend/src/components/user/forms/notifications.js @@ -19,9 +19,15 @@ export function UserNotificationsForm(props) { default: false, }, { - labelId: 'taskUpdates', - descriptionId: 'taskUpdatesDescription', - fieldName: 'tasksNotifications', + labelId: 'taskValidationUpdates', + descriptionId: 'taskValidationUpdatesDescription', + fieldName: 'taskValidationNotification', + default: true, + }, + { + labelId: 'taskInvalidationUpdates', + descriptionId: 'taskInvalidationUpdatesDescription', + fieldName: 'taskInvalidationNotification', default: true, }, { diff --git a/frontend/src/components/user/messages.js b/frontend/src/components/user/messages.js index d4a28d5a1f..83fea05056 100644 --- a/frontend/src/components/user/messages.js +++ b/frontend/src/components/user/messages.js @@ -146,9 +146,13 @@ export default defineMessages({ id: 'user.notifications.projects', defaultMessage: 'Project updates', }, - taskUpdates: { - id: 'user.notifications.tasks', - defaultMessage: 'Tasks validation emails', + taskValidationUpdates: { + id: 'user.notifications.tasks.validation', + defaultMessage: 'Task validation emails', + }, + taskInvalidationUpdates: { + id: 'user.notifications.tasks.invalidation', + defaultMessage: 'Task invalidation emails', }, required: { id: 'user.settings.required', @@ -158,10 +162,14 @@ export default defineMessages({ id: 'user.notifications.projects.description', defaultMessage: 'You get a notification when a project you have contributed to makes progress.', }, - taskUpdatesDescription: { - id: 'user.notifications.task.description', + taskValidationUpdatesDescription: { + id: 'user.notifications.task.validation.description', defaultMessage: 'Receive an email when a task you have contributed to is validated.', }, + taskInvalidationUpdatesDescription: { + id: 'user.notifications.task.invalidation.description', + defaultMessage: 'Receive an email when a task you have contributed to is invalidated.', + }, questionsAndComments: { id: 'user.notifications.questionsAndComments', defaultMessage: 'Questions and comments', diff --git a/frontend/src/network/tests/mockData/userList.js b/frontend/src/network/tests/mockData/userList.js index 740ecc9428..87f3d20ff8 100644 --- a/frontend/src/network/tests/mockData/userList.js +++ b/frontend/src/network/tests/mockData/userList.js @@ -56,7 +56,8 @@ export const userQueryDetails = { mentionsNotifications: true, questionsAndCommentsNotifications: true, projectsNotifications: true, - tasksNotifications: true, + taskValidationNotification: true, + taskInvalidationNotification: true, taskCommentsNotifications: true, teamsAnnouncementNotifications: false, gender: 'MALE', diff --git a/migrations/versions/a1b2c3d4e5f6_.py b/migrations/versions/a1b2c3d4e5f6_.py new file mode 100644 index 0000000000..80a2d14aaf --- /dev/null +++ b/migrations/versions/a1b2c3d4e5f6_.py @@ -0,0 +1,42 @@ +"""Split tasks_notifications into task_validation_notification and task_invalidation_notification + +Revision ID: a1b2c3d4e5f6 +Revises: b720f42ce3e8 +Create Date: 2026-03-17 10:25:00.000000 + +""" + +import sqlalchemy as sa +from alembic import op + +# revision identifiers, used by Alembic. +revision = "a1b2c3d4e5f6" +down_revision = "b720f42ce3e8" +branch_labels = None +depends_on = None + + +def upgrade(): + op.add_column( + "users", + sa.Column( + "task_validation_notification", + sa.Boolean(), + nullable=False, + server_default=sa.text("true"), + ), + ) + op.add_column( + "users", + sa.Column( + "task_invalidation_notification", + sa.Boolean(), + nullable=False, + server_default=sa.text("true"), + ), + ) + + +def downgrade(): + op.drop_column("users", "task_validation_notification") + op.drop_column("users", "task_invalidation_notification") diff --git a/tests/api/helpers/test_helpers.py b/tests/api/helpers/test_helpers.py index f5adfcefb7..ab9ec890ff 100644 --- a/tests/api/helpers/test_helpers.py +++ b/tests/api/helpers/test_helpers.py @@ -254,6 +254,8 @@ async def return_canned_user(db, username=TEST_USERNAME, id=TEST_USER_ID) -> Use test_user.projects_comments_notifications = False test_user.projects_notifications = True test_user.tasks_notifications = True + test_user.task_validation_notification = True + test_user.task_invalidation_notification = True test_user.tasks_comments_notifications = False test_user.teams_announcement_notifications = True test_user.date_registered = datetime.utcnow() @@ -281,6 +283,7 @@ async def create_canned_user(db, test_user=None): id, username, role, mapping_level, tasks_mapped, tasks_validated, tasks_invalidated, email_address, is_email_verified, is_expert, default_editor, mentions_notifications, projects_comments_notifications, projects_notifications, tasks_notifications, + task_validation_notification, task_invalidation_notification, tasks_comments_notifications, teams_announcement_notifications, date_registered, last_validation_date ) @@ -288,6 +291,7 @@ async def create_canned_user(db, test_user=None): :id, :username, :role, :mapping_level, :tasks_mapped, :tasks_validated, :tasks_invalidated, :email_address, :is_email_verified, :is_expert, :default_editor, :mentions_notifications, :projects_comments_notifications, :projects_notifications, :tasks_notifications, + :task_validation_notification, :task_invalidation_notification, :tasks_comments_notifications, :teams_announcement_notifications, :date_registered, :last_validation_date ) @@ -308,6 +312,8 @@ async def create_canned_user(db, test_user=None): "projects_comments_notifications": test_user.projects_comments_notifications, "projects_notifications": test_user.projects_notifications, "tasks_notifications": test_user.tasks_notifications, + "task_validation_notification": test_user.task_validation_notification, + "task_invalidation_notification": test_user.task_invalidation_notification, "tasks_comments_notifications": test_user.tasks_comments_notifications, "teams_announcement_notifications": test_user.teams_announcement_notifications, "date_registered": test_user.date_registered, diff --git a/tests/api/unit/models/postgis/test_user.py b/tests/api/unit/models/postgis/test_user.py index 5acb2d0bea..0dae0ed516 100644 --- a/tests/api/unit/models/postgis/test_user.py +++ b/tests/api/unit/models/postgis/test_user.py @@ -31,6 +31,8 @@ async def setup_test_data(self, db_connection_fixture, request): "projects_comments_notifications": False, "projects_notifications": True, "tasks_notifications": True, + "task_validation_notification": True, + "task_invalidation_notification": True, "tasks_comments_notifications": False, "teams_announcement_notifications": True, } @@ -42,6 +44,7 @@ async def setup_test_data(self, db_connection_fixture, request): is_email_verified, is_expert, default_editor, mentions_notifications, projects_comments_notifications, projects_notifications, tasks_notifications, + task_validation_notification, task_invalidation_notification, tasks_comments_notifications, teams_announcement_notifications ) VALUES ( @@ -50,6 +53,7 @@ async def setup_test_data(self, db_connection_fixture, request): :is_email_verified, :is_expert, :default_editor, :mentions_notifications, :projects_comments_notifications, :projects_notifications, :tasks_notifications, + :task_validation_notification, :task_invalidation_notification, :tasks_comments_notifications, :teams_announcement_notifications ) ON CONFLICT (id) DO NOTHING