diff --git a/fittrackee/administration/models.py b/fittrackee/administration/models.py index 4fb1f3f73..aef5e596c 100644 --- a/fittrackee/administration/models.py +++ b/fittrackee/administration/models.py @@ -9,7 +9,7 @@ from sqlalchemy.orm.session import Session from fittrackee import BaseModel, db -from fittrackee.users.models import User +from fittrackee.users.models import Notification, User from fittrackee.utils import encode_uuid from .exceptions import ( @@ -311,8 +311,33 @@ def serialize(self, current_user: User) -> Dict: return appeal +@listens_for(AdminAction, 'after_insert') +def on_admin_insert( + mapper: Mapper, connection: Connection, new_action: AdminAction +) -> None: + @listens_for(db.Session, 'after_flush', once=True) + def receive_after_flush(session: Session, context: Connection) -> None: + from fittrackee.administration.models import ( + COMMENT_ACTION_TYPES, + WORKOUT_ACTION_TYPES, + ) + + if ( + new_action.action_type + in COMMENT_ACTION_TYPES + WORKOUT_ACTION_TYPES + ["user_warning"] + ): + notification = Notification( + from_user_id=new_action.admin_user_id, + to_user_id=new_action.user_id, + created_at=new_action.created_at, + event_type=new_action.action_type, + event_object_id=new_action.id, + ) + session.add(notification) + + @listens_for(AdminActionAppeal, 'after_insert') -def on_report_insert( +def on_admin_action_appeal_insert( mapper: Mapper, connection: Connection, new_appeal: AdminActionAppeal ) -> None: @listens_for(db.Session, 'after_flush', once=True) diff --git a/fittrackee/tests/mixins.py b/fittrackee/tests/mixins.py index f68a1ee09..00916e96e 100644 --- a/fittrackee/tests/mixins.py +++ b/fittrackee/tests/mixins.py @@ -402,16 +402,37 @@ def create_admin_action( self, admin_user: User, user: User, + *, action_type: Optional[str] = None, report_id: Optional[int] = None, + comment_id: Optional[int] = None, + workout_id: Optional[int] = None, ) -> AdminAction: if action_type in REPORT_ACTION_TYPES and not report_id: report_id = self.create_report(admin_user, user).id admin_action = AdminAction( admin_user_id=admin_user.id, action_type=action_type if action_type else "user_suspension", + comment_id=( + comment_id + if ( + comment_id + and action_type + and action_type.startswith("comment_") + ) + else None + ), report_id=report_id, user_id=user.id, + workout_id=( + workout_id + if ( + workout_id + and action_type + and action_type.startswith("workout_") + ) + else None + ), ) db.session.add(admin_action) db.session.commit() @@ -426,7 +447,7 @@ def create_user_suspension_action( if not report_id: report_id = self.create_user_report(admin, user).id admin_action = self.create_admin_action( - admin, user, "user_suspension", report_id + admin, user, action_type="user_suspension", report_id=report_id ) user.suspended_at = datetime.utcnow() db.session.commit() diff --git a/fittrackee/tests/reports/test_reports_api.py b/fittrackee/tests/reports/test_reports_api.py index ca685f550..959fda97f 100644 --- a/fittrackee/tests/reports/test_reports_api.py +++ b/fittrackee/tests/reports/test_reports_api.py @@ -2038,7 +2038,10 @@ def test_it_returns_400_when_when_user_already_warned( app, user_1_admin.email ) self.create_admin_action( - user_1_admin, user_2, "user_warning", report.id + user_1_admin, + user_2, + action_type="user_warning", + report_id=report.id, ) db.session.commit() diff --git a/fittrackee/tests/users/test_users_notifications_model.py b/fittrackee/tests/users/test_users_notifications_model.py index 475b22c0f..abecd827c 100644 --- a/fittrackee/tests/users/test_users_notifications_model.py +++ b/fittrackee/tests/users/test_users_notifications_model.py @@ -5,6 +5,10 @@ from flask import Flask from fittrackee import db +from fittrackee.administration.models import ( + COMMENT_ACTION_TYPES, + WORKOUT_ACTION_TYPES, +) from fittrackee.comments.models import Comment, CommentLike, Mention from fittrackee.privacy_levels import PrivacyLevel from fittrackee.reports.models import Report @@ -192,7 +196,7 @@ def test_it_deletes_notification_on_follow_request_delete( ).first() assert notification is None - def test_serialize_follow_request_notification( + def test_it_serializes_follow_request_notification( self, app: Flask, user_1: User, user_2: User ) -> None: user_1.send_follow_request_to(user_2) @@ -212,8 +216,12 @@ def test_serialize_follow_request_notification( assert serialized_notification["id"] == notification.id assert serialized_notification["marked_as_read"] is False assert serialized_notification["type"] == "follow_request" + assert "admin_action" not in serialized_notification + assert "comment" not in serialized_notification + assert "report" not in serialized_notification + assert "workout" not in serialized_notification - def test_serialize_follow_notification( + def test_it_serializes_follow_notification( self, app: Flask, user_1: User, user_2: User ) -> None: user_2.manually_approves_followers = False @@ -234,6 +242,10 @@ def test_serialize_follow_notification( assert serialized_notification["id"] == notification.id assert serialized_notification["marked_as_read"] is False assert serialized_notification["type"] == "follow" + assert "admin_action" not in serialized_notification + assert "comment" not in serialized_notification + assert "report" not in serialized_notification + assert "workout" not in serialized_notification class TestNotificationForWorkoutLike(NotificationTestCase): @@ -309,7 +321,7 @@ def test_it_does_not_raise_error_when_user_unlike_his_own_workout( db.session.delete(like) - def test_serialize_workout_like_notification( + def test_it_serializes_workout_like_notification( self, app: Flask, user_1: User, @@ -336,6 +348,9 @@ def test_serialize_workout_like_notification( assert serialized_notification[ "workout" ] == workout_cycling_user_1.serialize(user_1) + assert "admin_action" not in serialized_notification + assert "comment" not in serialized_notification + assert "report" not in serialized_notification class TestNotificationForWorkoutComment(NotificationTestCase): @@ -444,7 +459,7 @@ def test_it_does_not_raise_error_when_user_unlike_his_own_workout( db.session.delete(comment) - def test_serialize_workout_comment_notification( + def test_it_serializes_workout_comment_notification( self, app: Flask, user_1: User, @@ -469,6 +484,84 @@ def test_serialize_workout_comment_notification( assert serialized_notification["id"] == notification.id assert serialized_notification["marked_as_read"] is False assert serialized_notification["type"] == "workout_comment" + assert "admin_action" not in serialized_notification + assert "report" not in serialized_notification + assert "workout" not in serialized_notification + + +class TestNotificationForWorkoutAdminAction( + NotificationTestCase, UserModerationMixin +): + @pytest.mark.parametrize("input_admin_action", WORKOUT_ACTION_TYPES) + def test_it_creates_notification_on_comment_admin_action( + self, + app: Flask, + user_1_admin: User, + user_2: User, + user_3: User, + sport_1_cycling: Sport, + workout_cycling_user_2: Workout, + input_admin_action: str, + ) -> None: + report = self.create_report(user_3, workout_cycling_user_2) + + admin_action = self.create_admin_action( + user_1_admin, + user_2, + action_type=input_admin_action, + report_id=report.id, + workout_id=workout_cycling_user_2.id, + ) + + notification = Notification.query.filter_by( + from_user_id=user_1_admin.id, + to_user_id=user_2.id, + event_object_id=workout_cycling_user_2.id, + ).first() + assert notification.created_at == admin_action.created_at + assert notification.marked_as_read is False + assert notification.event_type == input_admin_action + + @pytest.mark.parametrize("input_admin_action", WORKOUT_ACTION_TYPES) + def test_it_serializes_comment_action_notification( + self, + app: Flask, + user_1_admin: User, + user_2: User, + user_3: User, + sport_1_cycling: Sport, + workout_cycling_user_2: Workout, + input_admin_action: str, + ) -> None: + report = self.create_report(user_3, workout_cycling_user_2) + admin_action = self.create_admin_action( + user_1_admin, + user_2, + action_type=input_admin_action, + report_id=report.id, + workout_id=workout_cycling_user_2.id, + ) + notification = Notification.query.filter_by( + from_user_id=user_1_admin.id, + to_user_id=user_2.id, + event_object_id=workout_cycling_user_2.id, + ).first() + + serialized_notification = notification.serialize() + + assert serialized_notification[ + "admin_action" + ] == admin_action.serialize(user_2) + assert serialized_notification["created_at"] == notification.created_at + assert serialized_notification["from"] is None + assert serialized_notification["id"] == notification.id + assert serialized_notification["marked_as_read"] is False + assert serialized_notification["type"] == input_admin_action + assert serialized_notification[ + "workout" + ] == workout_cycling_user_2.serialize(user_2) + assert "comment" not in serialized_notification + assert "report" not in serialized_notification class TestNotificationForCommentReply(NotificationTestCase): @@ -554,7 +647,7 @@ def test_it_does_not_raise_error_when_user_deletes_reply_on_his_own_comment( # db.session.delete(reply) - def test_serialize_comment_reply_notification( + def test_it_serializes_comment_reply_notification( self, app: Flask, user_1: User, @@ -581,6 +674,9 @@ def test_serialize_comment_reply_notification( assert serialized_notification["id"] == notification.id assert serialized_notification["marked_as_read"] is False assert serialized_notification["type"] == "comment_reply" + assert "admin_action" not in serialized_notification + assert "report" not in serialized_notification + assert "workout" not in serialized_notification class TestNotificationForCommentLike(NotificationTestCase): @@ -657,7 +753,7 @@ def test_it_does_not_raise_error_when_user_unlikes_on_his_own_comment( db.session.delete(like) - def test_serialize_comment_like_notification( + def test_it_serializes_comment_like_notification( self, app: Flask, user_1: User, @@ -682,6 +778,84 @@ def test_serialize_comment_like_notification( assert serialized_notification["id"] == notification.id assert serialized_notification["marked_as_read"] is False assert serialized_notification["type"] == "comment_like" + assert "admin_action" not in serialized_notification + assert "report" not in serialized_notification + assert "workout" not in serialized_notification + + +class TestNotificationForCommentAdminAction( + NotificationTestCase, UserModerationMixin +): + @pytest.mark.parametrize("input_admin_action", COMMENT_ACTION_TYPES) + def test_it_creates_notification_on_comment_admin_action( + self, + app: Flask, + user_1_admin: User, + user_2: User, + user_3: User, + sport_1_cycling: Sport, + workout_cycling_user_2: Workout, + input_admin_action: str, + ) -> None: + comment = self.comment_workout(user_3, workout_cycling_user_2) + report = self.create_report(user_2, comment) + + admin_action = self.create_admin_action( + user_1_admin, + user_3, + action_type=input_admin_action, + report_id=report.id, + comment_id=comment.id, + ) + + notification = Notification.query.filter_by( + from_user_id=user_1_admin.id, + to_user_id=user_3.id, + event_object_id=comment.id, + ).first() + assert notification.created_at == admin_action.created_at + assert notification.marked_as_read is False + assert notification.event_type == input_admin_action + + @pytest.mark.parametrize("input_admin_action", COMMENT_ACTION_TYPES) + def test_it_serializes_comment_action_notification( + self, + app: Flask, + user_1_admin: User, + user_2: User, + user_3: User, + sport_1_cycling: Sport, + workout_cycling_user_2: Workout, + input_admin_action: str, + ) -> None: + comment = self.comment_workout(user_3, workout_cycling_user_2) + report = self.create_report(user_2, comment) + admin_action = self.create_admin_action( + user_1_admin, + user_3, + action_type=input_admin_action, + report_id=report.id, + comment_id=comment.id, + ) + notification = Notification.query.filter_by( + from_user_id=user_1_admin.id, + to_user_id=user_3.id, + event_object_id=comment.id, + ).first() + + serialized_notification = notification.serialize() + + assert serialized_notification[ + "admin_action" + ] == admin_action.serialize(user_3) + assert serialized_notification["created_at"] == notification.created_at + assert serialized_notification["comment"] == comment.serialize(user_3) + assert serialized_notification["from"] is None + assert serialized_notification["id"] == notification.id + assert serialized_notification["marked_as_read"] is False + assert serialized_notification["type"] == input_admin_action + assert "report" not in serialized_notification + assert "workout" not in serialized_notification class TestNotificationForMention(NotificationTestCase): @@ -811,7 +985,7 @@ def test_it_does_not_create_notification_on_own_mention( ).first() assert notification is None - def test_serialize_mention_notification( + def test_it_serializes_mention_notification( self, app: Flask, user_1: User, @@ -841,6 +1015,9 @@ def test_serialize_mention_notification( assert serialized_notification["id"] == notification.id assert serialized_notification["marked_as_read"] is False assert serialized_notification["type"] == "mention" + assert "admin_action" not in serialized_notification + assert "report" not in serialized_notification + assert "workout" not in serialized_notification class TestMultipleNotificationsForComment(NotificationTestCase): @@ -1151,3 +1328,196 @@ def test_it_serializes_suspension_appeal_notification( user_1_admin ) assert serialized_notification["type"] == "suspension_appeal" + + +class TestNotificationForUserWarning( + NotificationTestCase, UserModerationMixin +): + def test_it_creates_notification_on_user_warning_on_user_report( + self, + app: Flask, + user_1_admin: User, + user_2: User, + user_3: User, + sport_1_cycling: Sport, + workout_cycling_user_2: Workout, + ) -> None: + report = self.create_report(user_2, user_3) + + admin_action = self.create_admin_action( + user_1_admin, + user_3, + action_type="user_warning", + report_id=report.id, + ) + + notification = Notification.query.filter_by( + from_user_id=user_1_admin.id, + to_user_id=user_3.id, + ).first() + assert notification.created_at == admin_action.created_at + assert notification.marked_as_read is False + assert notification.event_type == "user_warning" + + def test_it_serializes_user_warning_notification_on_user_report( + self, + app: Flask, + user_1_admin: User, + user_2: User, + user_3: User, + sport_1_cycling: Sport, + workout_cycling_user_2: Workout, + ) -> None: + report = self.create_report(user_2, user_3) + admin_action = self.create_admin_action( + user_1_admin, + user_3, + action_type="user_warning", + report_id=report.id, + ) + notification = Notification.query.filter_by( + from_user_id=user_1_admin.id, + to_user_id=user_3.id, + ).first() + + serialized_notification = notification.serialize() + + assert serialized_notification[ + "admin_action" + ] == admin_action.serialize(user_3) + assert serialized_notification["created_at"] == notification.created_at + assert serialized_notification["from"] is None + assert serialized_notification["id"] == notification.id + assert serialized_notification["marked_as_read"] is False + assert serialized_notification["type"] == "user_warning" + assert "comment" not in serialized_notification + assert "report" not in serialized_notification + assert "workout" not in serialized_notification + + def test_it_creates_notification_on_user_warning_on_workout_report( + self, + app: Flask, + user_1_admin: User, + user_2: User, + user_3: User, + sport_1_cycling: Sport, + workout_cycling_user_2: Workout, + ) -> None: + report = self.create_report(user_3, workout_cycling_user_2) + + admin_action = self.create_admin_action( + user_1_admin, + user_2, + action_type="user_warning", + report_id=report.id, + ) + + notification = Notification.query.filter_by( + from_user_id=user_1_admin.id, + to_user_id=user_2.id, + ).first() + assert notification.created_at == admin_action.created_at + assert notification.marked_as_read is False + assert notification.event_object_id == workout_cycling_user_2.id + assert notification.event_type == "user_warning" + + def test_it_serializes_user_warning_notification_on_workout_report( + self, + app: Flask, + user_1_admin: User, + user_2: User, + user_3: User, + sport_1_cycling: Sport, + workout_cycling_user_2: Workout, + ) -> None: + report = self.create_report(user_3, workout_cycling_user_2) + admin_action = self.create_admin_action( + user_1_admin, + user_2, + action_type="user_warning", + report_id=report.id, + ) + notification = Notification.query.filter_by( + from_user_id=user_1_admin.id, + to_user_id=user_2.id, + ).first() + + serialized_notification = notification.serialize() + + assert serialized_notification[ + "admin_action" + ] == admin_action.serialize(user_2) + assert serialized_notification["created_at"] == notification.created_at + assert serialized_notification["from"] is None + assert serialized_notification["id"] == notification.id + assert serialized_notification["marked_as_read"] is False + assert serialized_notification["type"] == "user_warning" + assert serialized_notification[ + "workout" + ] == workout_cycling_user_2.serialize(user_2) + assert "comment" not in serialized_notification + assert "report" not in serialized_notification + + def test_it_creates_notification_on_user_warning_on_comment_report( + self, + app: Flask, + user_1_admin: User, + user_2: User, + user_3: User, + sport_1_cycling: Sport, + workout_cycling_user_2: Workout, + ) -> None: + comment = self.comment_workout(user_3, workout_cycling_user_2) + report = self.create_report(user_2, comment) + + admin_action = self.create_admin_action( + user_1_admin, + user_3, + action_type="user_warning", + report_id=report.id, + ) + + notification = Notification.query.filter_by( + from_user_id=user_1_admin.id, + to_user_id=user_3.id, + ).first() + assert notification.created_at == admin_action.created_at + assert notification.marked_as_read is False + assert notification.event_object_id == comment.id + assert notification.event_type == "user_warning" + + def test_it_serializes_user_warning_notification_on_comment_report( + self, + app: Flask, + user_1_admin: User, + user_2: User, + user_3: User, + sport_1_cycling: Sport, + workout_cycling_user_2: Workout, + ) -> None: + comment = self.comment_workout(user_3, workout_cycling_user_2) + report = self.create_report(user_2, comment) + admin_action = self.create_admin_action( + user_1_admin, + user_3, + action_type="user_warning", + report_id=report.id, + ) + notification = Notification.query.filter_by( + from_user_id=user_1_admin.id, + to_user_id=user_3.id, + ).first() + + serialized_notification = notification.serialize() + + assert serialized_notification[ + "admin_action" + ] == admin_action.serialize(user_3) + assert serialized_notification["comment"] == comment.serialize(user_3) + assert serialized_notification["created_at"] == notification.created_at + assert serialized_notification["from"] is None + assert serialized_notification["id"] == notification.id + assert serialized_notification["marked_as_read"] is False + assert serialized_notification["type"] == "user_warning" + assert "report" not in serialized_notification + assert "workout" not in serialized_notification diff --git a/fittrackee/users/models.py b/fittrackee/users/models.py index d63760b4c..ead890112 100644 --- a/fittrackee/users/models.py +++ b/fittrackee/users/models.py @@ -41,13 +41,18 @@ NOTIFICATION_TYPES = [ 'comment_like', 'comment_reply', + 'comment_suspension', + 'comment_unsuspension', 'follow', 'follow_request', 'mention', 'report', 'suspension_appeal', + 'user_warning', 'workout_comment', 'workout_like', + 'workout_suspension', + 'workout_unsuspension', ] @@ -915,11 +920,24 @@ def serialize(self) -> Dict: }, } - from_user = User.query.filter_by(id=self.from_user_id).first() + if self.event_type in [ + "comment_suspension", + "comment_unsuspension", + "user_warning", + "workout_suspension", + "workout_unsuspension", + ]: + from_user = None + else: + from_user = User.query.filter_by(id=self.from_user_id).first() to_user = User.query.filter_by(id=self.to_user_id).first() serialized_notification = { **serialized_notification, - "from": from_user.serialize(current_user=to_user), + "from": ( + from_user.serialize(current_user=to_user) + if from_user + else None + ), } if self.event_type == "workout_like": @@ -958,4 +976,36 @@ def serialize(self) -> Dict: current_user=to_user ) + if self.event_type in [ + "comment_suspension", + "comment_unsuspension", + "user_warning", + "workout_suspension", + "workout_unsuspension", + ]: + from fittrackee.administration.models import AdminAction + from fittrackee.reports.models import Report + + admin_action = AdminAction.query.filter_by( + id=self.event_object_id + ).first() + serialized_notification["admin_action"] = admin_action.serialize( + current_user=to_user + ) + report = Report.query.filter_by(id=admin_action.report_id).first() + if report.object_type == "comment": + comment = Comment.query.filter_by( + id=report.reported_comment_id + ).first() + serialized_notification["comment"] = comment.serialize( + user=to_user + ) + elif report.object_type == "workout": + workout = Workout.query.filter_by( + id=report.reported_workout_id + ).first() + serialized_notification["workout"] = workout.serialize( + user=to_user + ) + return serialized_notification diff --git a/fittrackee_client/src/components/Notifications/NotificationDetail.vue b/fittrackee_client/src/components/Notifications/NotificationDetail.vue index 236d16242..9416cbb6f 100644 --- a/fittrackee_client/src/components/Notifications/NotificationDetail.vue +++ b/fittrackee_client/src/components/Notifications/NotificationDetail.vue @@ -11,7 +11,10 @@ class="fa notification-icon" aria-hidden="true" /> - + {{ notification.from.username }} {{ $t(getUserAction(notification.type)) }} @@ -49,6 +52,10 @@