diff --git a/comments/models/comment.py b/comments/models/comment.py index 4b8aec1a15d7154ba1eec9b58734ae9fd34e5f77..9f024189dc4412b70fec89c60b9571c7a55df2e9 100644 --- a/comments/models/comment.py +++ b/comments/models/comment.py @@ -34,6 +34,7 @@ from openbook_common.utils.model_loaders import ( get_user_model, ) from openbook_moderation.models import ModeratedObject +from openbook_notifications.services import NotificationsService from .comment_reaction import CommentReaction from .enabled_commentable_type import EnabledCommentableType @@ -182,7 +183,11 @@ class Comment(ModelWithUUID): return post_comment def react(self, reactor, reaction_type): - return CommentReaction.create_reaction(reactor=reactor, reaction_type=reaction_type, comment=self) + reaction = CommentReaction.create_reaction(reactor=reactor, reaction_type=reaction_type, comment=self) + NotificationsService.send_comment_reaction_notification( + comment=self, reactor=reactor, recipient=self.commenter + ) + return reaction def clear_reaction(self, reactor, reaction_type): return CommentReaction.remove_reaction(reactor=reactor, reaction_type=reaction_type, comment=self) diff --git a/comments/tests/test_graphql.py b/comments/tests/test_graphql.py index d9f0b2ea579e35ddc249c35d555039da7424ef5f..2d8fb452b609a5967266a956176689e74e7402d1 100644 --- a/comments/tests/test_graphql.py +++ b/comments/tests/test_graphql.py @@ -1,5 +1,6 @@ import logging from unittest import TestCase, mock +from unittest.mock import patch import pytest from django.core.management import call_command @@ -18,8 +19,10 @@ from openbook_common.tests.helpers import ( make_post, make_space_post_comment, make_user, + make_community, ) from openbook_insights.models.discussion import Discussion +from openbook_notifications.notifications import CommentReactionNotification from .helpers import ( get_comment_by_id, @@ -183,12 +186,13 @@ class TestPostComments(TestCase): def test_should_delete_comment(self): posting_user = make_user() + space = make_community(creator=posting_user) comment_user = make_user() post_text = make_fake_post_text() post_title = make_fake_post_title() - post = posting_user.create_post(text=post_text, title=post_title, is_public=True) + post = posting_user.create_post(text=post_text, title=post_title, is_public=True, community_id=space.id) comment = post.comment(text="comment text", commenter=comment_user) reaction = comment.react(posting_user, "HEART") post.comment(text="comment text 2", commenter=comment_user) @@ -203,13 +207,14 @@ class TestPostComments(TestCase): def test_should_not_be_able_to_delete_others_comments(self): posting_user = make_user() + space = make_community(creator=posting_user) comment_user = make_user() other_user = make_user() post_text = make_fake_post_text() post_title = make_fake_post_title() - post = posting_user.create_post(text=post_text, title=post_title, is_public=True) + post = posting_user.create_post(text=post_text, title=post_title, is_public=True, community_id=space.id) comment = post.comment(text="comment text", commenter=comment_user) reaction = comment.react(posting_user, "HEART") post.comment(text="comment text 2", commenter=comment_user) @@ -223,12 +228,13 @@ class TestPostComments(TestCase): def test_should_not_be_able_to_delete_a_comment_that_does_not_exist(self): posting_user = make_user() + space = make_community(creator=posting_user) comment_user = make_user() post_text = make_fake_post_text() post_title = make_fake_post_title() - post = posting_user.create_post(text=post_text, title=post_title, is_public=True) + post = posting_user.create_post(text=post_text, title=post_title, is_public=True, community_id=space.id) comment = post.comment(text="comment text", commenter=comment_user) reaction = comment.react(posting_user, "HEART") post.comment(text="comment text 2", commenter=comment_user) @@ -240,17 +246,21 @@ class TestPostComments(TestCase): assert comment.id in post.comments.values_list("id", flat=True) assert reaction.id in CommentReaction.objects.filter(post_comment=comment).values_list("id", flat=True) - def test_should_be_able_to_react_to_a_comment(self): + @patch.object(CommentReactionNotification, "send") + def test_should_be_able_to_react_to_a_comment(self, mock_send_comment_reaction_notification): posting_user = make_user() + space = make_community(creator=posting_user) other_user = make_user(name="Other") post_text = make_fake_post_text() post_title = make_fake_post_title() - post = posting_user.create_post(text=post_text, title=post_title, is_public=True) + post = posting_user.create_post(text=post_text, title=post_title, is_public=True, community_id=space.id) comment = post.comment(text="comment text", commenter=other_user) react_to_post_comment(comment, "SET", posting_user) + mock_send_comment_reaction_notification.assert_called_once() + executed = react_to_post_comment(comment, "SET", posting_user) # setting twice is same as setting once updated_post_comment = executed.data["updatePostCommentReaction"] diff --git a/feed_posts/enums.py b/feed_posts/enums.py index 844b0163e2c25f0e2340db5a4ecf293cbb882e0b..67a750ab93a244df45b0733c6f783e84db484ddb 100644 --- a/feed_posts/enums.py +++ b/feed_posts/enums.py @@ -6,3 +6,9 @@ from django.db.models import TextChoices class VisibilityLevel(TextChoices): PUBLIC = ("PUBLIC",) REGISTERED_USERS_ONLY = ("REGISTERED_USERS_ONLY",) + + +@strawberry.enum +class AspectRatio(TextChoices): + SIXTEEN_NINE = ("SIXTEEN_NINE",) + ONE_ONE = ("ONE_ONE",) diff --git a/feed_posts/migrations/0016_feedpostimage_aspect_ratio.py b/feed_posts/migrations/0016_feedpostimage_aspect_ratio.py new file mode 100644 index 0000000000000000000000000000000000000000..2e31b4630c31ff59ca6d60218865fcd9aef7170c --- /dev/null +++ b/feed_posts/migrations/0016_feedpostimage_aspect_ratio.py @@ -0,0 +1,23 @@ +# Generated by Django 5.0.8 on 2024-09-23 09:18 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("feed_posts", "0015_alter_feedpost_language_code"), + ] + + operations = [ + migrations.AddField( + model_name="feedpostimage", + name="aspect_ratio", + field=models.CharField( + blank=True, + choices=[("SIXTEEN_NINE", "Sixteen Nine"), ("ONE_ONE", "One One")], + max_length=20, + null=True, + ), + ), + ] diff --git a/feed_posts/models.py b/feed_posts/models.py index 8e2d448c60bc31fc56d0650f440f8e77d4cdd366..0e1cee9ee5e35744665c1d2b1ae90b0b0a1996c6 100644 --- a/feed_posts/models.py +++ b/feed_posts/models.py @@ -13,7 +13,7 @@ from rest_framework.exceptions import ValidationError from comments.models import Comment from comments.models.commentable_notification_context import CommentNotificationMixin, CommentNotificationContext -from feed_posts.enums import VisibilityLevel +from feed_posts.enums import VisibilityLevel, AspectRatio from feed_posts.helpers import upload_to_feed_post_images_directory, upload_to_new_feed_post_images_directory from openbook import settings from openbook_auth.models import User, UserRole @@ -167,6 +167,7 @@ class FeedPostImage(ModelWithUUID): order = models.PositiveIntegerField() uploaded_at = models.DateTimeField(editable=False, auto_now_add=True) alt_text = models.TextField(max_length=settings.CONTENT_IMAGE_ALT_TEXT_MAX_LENGTH, blank=True, null=True) + aspect_ratio = models.CharField(max_length=20, choices=AspectRatio.choices, blank=True, null=True) class Meta: ordering = ["order"] diff --git a/feed_posts/repositories.py b/feed_posts/repositories.py index 5168d9fb74d94b6e2bed21923040a74e032d861d..a02d3ea72505a5c157774a1df70e892a913fe9ef 100644 --- a/feed_posts/repositories.py +++ b/feed_posts/repositories.py @@ -149,6 +149,7 @@ class FeedPostRepository: image=obj["image"], order=obj["order"], alt_text=obj.get("alt_text") or None, + aspect_ratio=obj.get("aspect_ratio") or None, ) image.save() @@ -177,6 +178,7 @@ class FeedPostRepository: if image_data: image.order = image_data.get("order", image.order) image.alt_text = image_data.get("alt_text", image.alt_text) + image.aspect_ratio = image_data.get("aspect_ratio", image.aspect_ratio) image.save() if new_images is not None: diff --git a/feed_posts/schema/types.py b/feed_posts/schema/types.py index f224c32d12a3296fcd84e125435c5d67ec8383a9..65309aad95e1af6d92a319b5ba523b67a7fdf317 100644 --- a/feed_posts/schema/types.py +++ b/feed_posts/schema/types.py @@ -6,7 +6,7 @@ import strawberry_django from strawberry.file_uploads import Upload from comments.models.enabled_commentable_type import EnabledCommentableType -from feed_posts.enums import VisibilityLevel +from feed_posts.enums import VisibilityLevel, AspectRatio from feed_posts.models import ( FeedPost as FeedPostModel, FeedPostReaction as FeedPostReactionModel, @@ -28,6 +28,7 @@ class FeedPostImage: image_blurhash: str order: int alt_text: Optional[str] + aspect_ratio: Optional[AspectRatio] @strawberry_django.type(FeedPostModel) @@ -106,6 +107,7 @@ class FeedPostImageInput: image: Upload order: int alt_text: Optional[str] = strawberry.UNSET + aspect_ratio: Optional[AspectRatio] = strawberry.UNSET @strawberry.input @@ -129,6 +131,7 @@ class FeedPostImageMetadata: id: uuid.UUID order: int alt_text: Optional[str] = strawberry.UNSET + aspect_ratio: Optional[AspectRatio] = strawberry.UNSET @strawberry.input diff --git a/feed_posts/serializers.py b/feed_posts/serializers.py index 3dfddc53a9b625a8f45f6ec3336fbd313744fc07..374aa769e55641ac6d5753f1085006dab903883a 100644 --- a/feed_posts/serializers.py +++ b/feed_posts/serializers.py @@ -1,6 +1,6 @@ from rest_framework import serializers -from feed_posts.enums import VisibilityLevel +from feed_posts.enums import VisibilityLevel, AspectRatio from feed_posts.validators import ( validate_topic_exists_by_id, validate_feed_post_category_exists_by_id, @@ -23,6 +23,7 @@ class FeedPostImageSerializer(serializers.Serializer): alt_text = serializers.CharField( required=False, max_length=settings.CONTENT_IMAGE_ALT_TEXT_MAX_LENGTH, allow_blank=True ) + aspect_ratio = serializers.ChoiceField(required=False, choices=AspectRatio.choices) class FeedPostImageMetadataSerializer(serializers.Serializer): @@ -31,6 +32,7 @@ class FeedPostImageMetadataSerializer(serializers.Serializer): alt_text = serializers.CharField( required=False, max_length=settings.CONTENT_IMAGE_ALT_TEXT_MAX_LENGTH, allow_blank=True ) + aspect_ratio = serializers.ChoiceField(required=False, choices=AspectRatio.choices) class CreateFeedPostSerializer(serializers.Serializer): diff --git a/feed_posts/services.py b/feed_posts/services.py index 39e47e6a0834b11b1ea65edfa321a145bfa2853c..a240774cf61ce674017fbbef63cf1588cb82e537 100644 --- a/feed_posts/services.py +++ b/feed_posts/services.py @@ -8,6 +8,7 @@ from openbook_common.helpers import extract_urls_from_string from openbook_common.models import LinkPreview from openbook_common.tracking import track, TrackingEvent from openbook_common.utils.helpers import extract_usernames_from_string +from openbook_notifications.services import NotificationsService from openbook_terms.models import FeedPostCategory, Topic @@ -135,7 +136,11 @@ class FeedPostReactionService: return self.repository.get_reaction_by_user(feed_post=feed_post, reactor=reactor) def create_reaction(self, feed_post, reactor, reaction_type): - return self.repository.create_reaction(feed_post=feed_post, reactor=reactor, reaction_type=reaction_type) + reaction = self.repository.create_reaction(feed_post=feed_post, reactor=reactor, reaction_type=reaction_type) + NotificationsService.send_post_reaction_notification( + post=feed_post, reactor=reactor, recipient=feed_post.creator + ) + return reaction def remove_reaction(self, feed_post, reactor, reaction_type): return self.repository.remove_reaction(feed_post=feed_post, reactor=reactor, reaction_type=reaction_type) diff --git a/feed_posts/tests/helpers.py b/feed_posts/tests/helpers.py index 7c8b7f28f3c0bae85c498cff9b061a07c3d94072..cb99c26d5c34fc504cf3bb3bedcaf63604dfd4a7 100644 --- a/feed_posts/tests/helpers.py +++ b/feed_posts/tests/helpers.py @@ -40,6 +40,7 @@ def create_feed_post(user, input): image order altText + aspectRatio } } } @@ -84,6 +85,7 @@ def update_feed_post(user, input): image order altText + aspectRatio } } } diff --git a/feed_posts/tests/test_graphql.py b/feed_posts/tests/test_graphql.py index 7573356f910898878ce8bdf229860129b4e8e212..32d62f8b12418e242fcf5eba17085a6689a07dea 100644 --- a/feed_posts/tests/test_graphql.py +++ b/feed_posts/tests/test_graphql.py @@ -1,4 +1,5 @@ import logging +from unittest.mock import patch import pytest import pytz @@ -7,7 +8,7 @@ from django.core.files.uploadedfile import SimpleUploadedFile from django.test import AsyncClient, TestCase from faker import Faker -from feed_posts.enums import VisibilityLevel +from feed_posts.enums import VisibilityLevel, AspectRatio from feed_posts.repositories import FeedPostRepository from feed_posts.tests.helpers import ( create_feed_post, @@ -32,6 +33,7 @@ from openbook_common.tests.helpers import ( make_community, make_feed_post_founder_update_category, ) +from openbook_notifications.notifications import PostReactionNotification logger = logging.getLogger(__name__) fake = Faker() @@ -148,6 +150,7 @@ class TestFeedPosts(TestCase): "image": SimpleUploadedFile(name="image_2.jpg", content=simple_jpeg_bytes), "order": 1, "altText": "image description", + "aspectRatio": AspectRatio.ONE_ONE, } ], "location": "Munich, Germany", @@ -169,6 +172,7 @@ class TestFeedPosts(TestCase): "image": SimpleUploadedFile(name="image_3.jpg", content=simple_jpeg_bytes), "order": 0, "altText": "image description", + "aspectRatio": AspectRatio.ONE_ONE, } ], "location": "Munich, Germany", @@ -419,7 +423,14 @@ class TestFeedPosts(TestCase): post_id = mutation_response.data["createFeedPost"]["id"] images = mutation_response.data["createFeedPost"]["images"] - existing_images = [{"id": images[0]["id"], "order": 9, "altText": "new image description"}] + existing_images = [ + { + "id": images[0]["id"], + "order": 9, + "altText": "new image description", + "aspectRatio": AspectRatio.SIXTEEN_NINE, + } + ] input_data = update_input_data( post_id=post_id, input_data=self.registered_users_only_feed_post_update_input_data, @@ -562,7 +573,8 @@ class TestFeedPosts(TestCase): assert "Unauthorized" in str(response.errors[0].message) - def test_can_react_to_feed_post_if_authenticated_user(self): + @patch.object(PostReactionNotification, "send") + def test_can_react_to_feed_post_if_authenticated_user(self, mock_send_post_reaction_notification): mutation_response = create_feed_post(user=self.user, input=self.public_feed_post_input_data) assert mutation_response.errors is None @@ -579,6 +591,7 @@ class TestFeedPosts(TestCase): assert data["myReaction"] == {"HEART": 1} assert data["reactions"] == {"HEART": 1} + mock_send_post_reaction_notification.assert_called_once() def test_can_remove_reaction_to_feed_post_if_authenticated_user(self): mutation_response = create_feed_post(user=self.user, input=self.public_feed_post_input_data) diff --git a/openbook/graphql_schema.py b/openbook/graphql_schema.py index 741dcb13072969a7e11bdf71676080119649dda1..8b2c6bd0b5a4abf5741e3950937b5d8637d1053b 100644 --- a/openbook/graphql_schema.py +++ b/openbook/graphql_schema.py @@ -58,6 +58,10 @@ class HoliSchema(strawberry.Schema): GraphQLErrorCodes.UNAUTHORIZED, ] ) + and not ( + # don't error log UUID parsing failures of user parameters (-> BAD_USER_INPUT) + error.message.startswith("Value cannot represent a UUID:") + ) ] super().process_errors(real_errors, execution_context) diff --git a/openbook_auth/models.py b/openbook_auth/models.py index 5e097c8235505b71c6b1ccdd7dcd365c551eebf0..d3e2f8ab35b9690edb8d3e344f8dbe3512c55d3b 100644 --- a/openbook_auth/models.py +++ b/openbook_auth/models.py @@ -2941,6 +2941,7 @@ class User(ModelWithUUID, AbstractUser): is_public=None, circles_ids=None, language=None, + community_id=None, ): Post = get_post_model() return Post.create_post( @@ -2954,6 +2955,7 @@ class User(ModelWithUUID, AbstractUser): is_draft=is_draft, circles_ids=circles_ids, language=language, + community_id=community_id, ) def create_poll( diff --git a/openbook_notifications/notifications.py b/openbook_notifications/notifications.py index 2d51e1e8c717120b94786e9afe74abc5b73a8363..dc0d11dc89356a72ced9f34a044c0aab99eaeae2 100644 --- a/openbook_notifications/notifications.py +++ b/openbook_notifications/notifications.py @@ -26,6 +26,8 @@ from openbook_notifications.payloads import ( SpaceMembershipRequestPayload, SpaceNewPostPayload, PollUpdatesPayload, + PostReactionPayload, + CommentReactionPayload, ) from openbook_notifications.toggles import notifications_toggle @@ -48,6 +50,8 @@ class Workflow(Enum): SPACE_APPOINTMENT_UPDATE = "space-appointment-update" SPACE_APPOINTMENT_DELETION = "space-appointment-deletion" POLL_UPDATES = "poll-updates" + POST_REACTIONS = "post-reactions" + COMMENT_REACTIONS = "comment-reactions" # INTERNAL USE ONLY, use NotificationsService to trigger notifications @@ -343,3 +347,33 @@ class PollUpdatesNotification(Notification): payload=payload.to_dict(), transaction_id=transaction_id if transaction_id else str(uuid.uuid4()), ) + + +class PostReactionNotification(Notification): + def __init__( + self, + author: str, + payload: PostReactionPayload, + transaction_id: Optional[str] = None, + ): + super().__init__( + workflow=Workflow.POST_REACTIONS.value, + recipients=author, + payload=payload.to_dict(), + transaction_id=transaction_id if transaction_id else str(uuid.uuid4()), + ) + + +class CommentReactionNotification(Notification): + def __init__( + self, + commenter: str, + payload: CommentReactionPayload, + transaction_id: Optional[str] = None, + ): + super().__init__( + workflow=Workflow.COMMENT_REACTIONS.value, + recipients=commenter, + payload=payload.to_dict(), + transaction_id=transaction_id if transaction_id else str(uuid.uuid4()), + ) diff --git a/openbook_notifications/payloads.py b/openbook_notifications/payloads.py index ca230676d0d2d3181da4a6be22d72a99e07c0dc0..5b755cc71df126faff2139105c782200235c9979 100644 --- a/openbook_notifications/payloads.py +++ b/openbook_notifications/payloads.py @@ -11,7 +11,6 @@ from openbook_notifications.helpers import replace_mentions_with_full_names if TYPE_CHECKING: from openbook_auth.models import User from openbook_communities.models import Community - from openbook_posts.models import Post from comments.models import Comment from openbook_appointments.models import Appointment from openbook_polls.models import Poll @@ -35,6 +34,8 @@ class NotificationType(Enum): SPACE_APPOINTMENT_UPDATE = "SPACE_APPOINTMENT_UPDATE" SPACE_APPOINTMENT_DELETION = "SPACE_APPOINTMENT_DELETION" POLL_UPDATES = "POLL_UPDATES" + POST_REACTION = "POST_REACTION" + COMMENT_REACTION = "COMMENT_REACTION" class UserData: @@ -186,30 +187,43 @@ class AppointmentData: class PostData: - def __init__(self, id: str, title: str, text: str, context: str, is_public: bool): + def __init__( + self, id: str, title: str, text: str = None, community=None, is_public: bool = None, description: str = None + ): self.id = id self.title = title self.text = text - self.context = context + self.community = community self.is_public = is_public + self.description = description def to_dict(self): - return { + data = { "id": self.id, "title": self.title, - "text": self.text, - "context": self.context, - "isPublic": self.is_public, } + if self.text: + data["text"] = self.text + if self.community: + data["context"] = self.community.title + data["link"] = f"/spaces/{self.community.id}/updates/{self.id}" + else: + data["link"] = f"/feed-post/{self.id}" + if self.description: + data["description"] = self.description + if self.is_public: + data["isPublic"] = self.is_public + return data @classmethod - def from_post(cls, post: Post): + def from_post(cls, post): return cls( id=str(post.id), title=post.title if post.title else "", - text=post.text, - context=post.community.title, - is_public=post.is_public, + text=post.text if hasattr(post, "text") else None, + community=post.community if hasattr(post, "community") else None, + is_public=post.is_public if hasattr(post, "is_public") else None, + description=post.description if hasattr(post, "description") else None, ) @@ -435,7 +449,7 @@ class SpaceNewPostPayload(Payload): def __init__( self, - post: Post, + post, notification_type: NotificationType, ): super().__init__( @@ -510,3 +524,33 @@ class PollUpdatesPayload(Payload): notification_type=NotificationType.POLL_UPDATES.value, notification_version=PollUpdatesPayload.NOTIFICATION_VERSION, ) + + +class PostReactionPayload(Payload): + NOTIFICATION_VERSION = "1.0.0" + + def __init__(self, reactor: User, post): + super().__init__( + data={ + "user": UserData.from_user(reactor).to_dict(), + "post": PostData.from_post(post).to_dict(), + }, + notification_type=NotificationType.POST_REACTION.value, + notification_version=PostReactionPayload.NOTIFICATION_VERSION, + ) + + +class CommentReactionPayload(Payload): + NOTIFICATION_VERSION = "1.0.0" + + def __init__(self, reactor: User, comment: Comment, context_id: str): + comment_dict = CommentData.from_comment(comment).to_dict() + comment_dict["link"] = CommentData.create_link(comment, context_id) + super().__init__( + data={ + "user": UserData.from_user(reactor).to_dict(), + "comment": comment_dict, + }, + notification_type=NotificationType.COMMENT_REACTION.value, + notification_version=CommentReactionPayload.NOTIFICATION_VERSION, + ) diff --git a/openbook_notifications/services.py b/openbook_notifications/services.py index a90e4a5ebda5d66be11d660d73570d8869957e01..59e9020c63d2285ab543f65fe63ae0e1284cf5b6 100644 --- a/openbook_notifications/services.py +++ b/openbook_notifications/services.py @@ -25,6 +25,8 @@ from openbook_notifications.notifications import ( SpaceAppointmentUpdateNotification, SpaceAppointmentDeletionNotification, PollUpdatesNotification, + CommentReactionNotification, + PostReactionNotification, ) from openbook_notifications.payloads import ( SpaceMembershipRequestPayload, @@ -42,6 +44,8 @@ from openbook_notifications.payloads import ( SpaceAppointmentUpdatePayload, SpaceAppointmentDeletionPayload, PollUpdatesPayload, + PostReactionPayload, + CommentReactionPayload, ) if TYPE_CHECKING: @@ -329,3 +333,28 @@ class NotificationsService: payload=PollUpdatesPayload(poll=poll), ) notification.send() + + @staticmethod + def send_post_reaction_notification( + post, + reactor: User, + recipient: User, + ): + notification = PostReactionNotification( + author=str(recipient.username), + payload=PostReactionPayload(reactor=reactor, post=post), + ) + notification.send() + + @staticmethod + def send_comment_reaction_notification( + comment: Comment, + reactor: User, + recipient: User, + ): + context_id = NotificationsService._find_comment_commentable_context_id(comment) + notification = CommentReactionNotification( + commenter=str(recipient.username), + payload=CommentReactionPayload(reactor=reactor, comment=comment, context_id=context_id), + ) + notification.send() diff --git a/openbook_posts/models.py b/openbook_posts/models.py index 42b9b660515f8f8b9ce7bc8a72edeb3c0dcc52ed..94de5ae53b51091513c888f884daac2bd0fc9323 100644 --- a/openbook_posts/models.py +++ b/openbook_posts/models.py @@ -65,6 +65,7 @@ from openbook_common.utils.model_loaders import ( get_user_notifications_subscription_model, ) from openbook_moderation.models import ModeratedObject +from openbook_notifications.services import NotificationsService from openbook_posts.checkers import ( check_can_add_media, check_can_be_published, @@ -537,7 +538,9 @@ class Post(CommentNotificationMixin, ModelWithUUID): commenter.delete_comment_with_id(post_comment_id=id) def react(self, reactor, reaction_type): - return PostReaction.create_reaction(reactor=reactor, reaction_type=reaction_type, post=self) + reaction = PostReaction.create_reaction(reactor=reactor, reaction_type=reaction_type, post=self) + NotificationsService.send_post_reaction_notification(post=self, reactor=reactor, recipient=self.creator) + return reaction def clear_reaction(self, reactor, reaction_type): return PostReaction.remove_reaction(reactor=reactor, reaction_type=reaction_type, post=self) diff --git a/openbook_posts/tests/test_graphql.py b/openbook_posts/tests/test_graphql.py index 4a9d3d93aed9ab527c058c46893df1c5de909bd5..759654428895e703d861cae30ea7b5e67c5d3ac6 100644 --- a/openbook_posts/tests/test_graphql.py +++ b/openbook_posts/tests/test_graphql.py @@ -1,5 +1,6 @@ import logging from unittest import TestCase, mock +from unittest.mock import patch, Mock import pytest from django.core.files.uploadedfile import SimpleUploadedFile @@ -19,6 +20,7 @@ from openbook_common.tests.helpers import ( ) from openbook_communities.models import CommunityNotificationsSubscription from openbook_notifications.models import CommunityNewPostNotification +from openbook_notifications.notifications import PostReactionNotification from openbook_posts.models import ( Post, PostImage, @@ -203,7 +205,8 @@ class TestPosts(TestCase): assert post_data is None assert "Forbidden" in response.errors[0].message - def test_react_to_post(self): + @patch.object(PostReactionNotification, "send") + def test_react_to_post(self, mock_send_post_reaction_notification: Mock): posting_user = make_user() other_user = make_user(name="Other") @@ -213,6 +216,7 @@ class TestPosts(TestCase): post = posting_user.create_post(text=post_text, title=post_title, is_public=True) react_to_post(post, "SET", posting_user) + mock_send_post_reaction_notification.assert_called_once() executed = react_to_post(post, "SET", posting_user) # setting twice is same as setting once updated_post = executed.data["updatePostReaction"]