Skip to content

Send websocket message for comments #3835

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion lego/apps/articles/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
from rest_framework.fields import CharField

from lego.apps.articles.models import Article
from lego.apps.comments.serializers import CommentSerializer
from lego.apps.comments.serializers.comments import CommentSerializer
from lego.apps.content.fields import ContentSerializerField
from lego.apps.files.fields import ImageField
from lego.apps.tags.serializers import TagSerializerMixin
Expand Down
4 changes: 4 additions & 0 deletions lego/apps/comments/action_handlers.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
from lego.apps.action_handlers.handler import Handler
from lego.apps.action_handlers.registry import register_handler
from lego.apps.comments.constants import SOCKET_ADD_SUCCESS, SOCKET_DELETE_SUCCESS
from lego.apps.comments.models import Comment
from lego.apps.comments.notifications import CommentReplyNotification
from lego.apps.comments.websockets import notify_comment
from lego.apps.feeds.activity import Activity
from lego.apps.feeds.feed_manager import feed_manager
from lego.apps.feeds.models import NotificationFeed, UserFeed
Expand All @@ -25,6 +27,7 @@ def get_activity(comment, reply=False):
)

def handle_create(self, instance, **kwargs):
notify_comment(SOCKET_ADD_SUCCESS, instance)
activity = self.get_activity(instance)
author = instance.created_by
for feeds, recipients in self.get_feeds_and_recipients(instance):
Expand All @@ -47,6 +50,7 @@ def handle_create(self, instance, **kwargs):
reply_notification.notify()

def handle_delete(self, instance, **kwargs):
notify_comment(SOCKET_DELETE_SUCCESS, instance)
if not (
isinstance(instance.parent, ObjectPermissionsModel)
and not instance.parent.require_auth
Expand Down
4 changes: 4 additions & 0 deletions lego/apps/comments/constants.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
SOCKET_ADD_SUCCESS = "Comment.SOCKET_ADD.SUCCESS"
SOCKET_ADD_FAILURE = "Comment.SOCKET_ADD.FAILURE"
SOCKET_DELETE_SUCCESS = "Comment.SOCKET_DELETE.SUCCESS"
SOCKET_DELETE_FAILURE = "Comment.SOCKET_DELETE.FAILURE"
Empty file.
6 changes: 6 additions & 0 deletions lego/apps/comments/serializers/sockets.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from lego.apps.comments.serializers.comments import CommentSerializer
from lego.apps.websockets.serializers import WebsocketSerializer


class CommentSocketSerializer(WebsocketSerializer):
payload = CommentSerializer()
5 changes: 4 additions & 1 deletion lego/apps/comments/views.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
from rest_framework import mixins, viewsets

from lego.apps.comments.models import Comment
from lego.apps.comments.serializers import CommentSerializer, UpdateCommentSerializer
from lego.apps.comments.serializers.comments import (
CommentSerializer,
UpdateCommentSerializer,
)
from lego.apps.permissions.api.views import AllowedPermissionsMixin


Expand Down
24 changes: 24 additions & 0 deletions lego/apps/comments/websockets.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
from __future__ import annotations

from typing import TYPE_CHECKING

from lego.apps.comments.serializers.sockets import CommentSocketSerializer
from lego.apps.websockets.groups import group_for_content_model
from lego.apps.websockets.notifiers import notify_group

if TYPE_CHECKING:
from lego.apps.comments.models import Comment


def notify_comment(action_type: str, comment: Comment, **kwargs):
# group = "global"
group = group_for_content_model(comment)
serializer = CommentSocketSerializer(
{
"type": action_type,
"payload": comment,
"meta": kwargs,
}
)
data = serializer.data
notify_group(group, data)
2 changes: 1 addition & 1 deletion lego/apps/companies/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from rest_framework import serializers
from rest_framework.fields import CharField

from lego.apps.comments.serializers import CommentSerializer
from lego.apps.comments.serializers.comments import CommentSerializer
from lego.apps.companies.constants import INTERESTED, NOT_CONTACTED
from lego.apps.companies.fields import SemesterField
from lego.apps.companies.models import (
Expand Down
2 changes: 1 addition & 1 deletion lego/apps/events/serializers/events.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from rest_framework import serializers
from rest_framework.fields import CharField

from lego.apps.comments.serializers import CommentSerializer
from lego.apps.comments.serializers.comments import CommentSerializer
from lego.apps.companies.fields import CompanyField
from lego.apps.companies.models import Company
from lego.apps.content.fields import ContentSerializerField
Expand Down
2 changes: 1 addition & 1 deletion lego/apps/forums/serializers.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from rest_framework import serializers
from rest_framework.fields import CharField

from lego.apps.comments.serializers import CommentSerializer
from lego.apps.comments.serializers.comments import CommentSerializer
from lego.apps.content.fields import ContentSerializerField
from lego.apps.forums.models import Forum, Thread
from lego.apps.users.serializers.users import PublicUserSerializer
Expand Down
2 changes: 1 addition & 1 deletion lego/apps/gallery/serializers.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from rest_framework import serializers
from rest_framework.fields import CharField

from lego.apps.comments.serializers import CommentSerializer
from lego.apps.comments.serializers.comments import CommentSerializer
from lego.apps.events.fields import PublicEventField
from lego.apps.events.models import Event
from lego.apps.files.fields import FileField, ImageField
Expand Down
2 changes: 1 addition & 1 deletion lego/apps/meetings/serializers.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from rest_framework import serializers
from rest_framework.fields import CharField

from lego.apps.comments.serializers import CommentSerializer
from lego.apps.comments.serializers.comments import CommentSerializer
from lego.apps.content.fields import ContentSerializerField
from lego.apps.meetings import constants
from lego.apps.meetings.models import Meeting, MeetingInvitation, ReportChangelog
Expand Down
2 changes: 1 addition & 1 deletion lego/apps/polls/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from rest_framework import serializers
from rest_framework.fields import CharField, IntegerField

from lego.apps.comments.serializers import CommentSerializer
from lego.apps.comments.serializers.comments import CommentSerializer
from lego.apps.polls.models import Option, Poll
from lego.apps.tags.serializers import TagSerializerMixin
from lego.utils.serializers import BasisModelSerializer
Expand Down
13 changes: 13 additions & 0 deletions lego/apps/websockets/constants.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
WS_STATUS_CONNECTED = "CONNECTED"
WS_STATUS_CLOSED = "CLOSED"
WS_STATUS_ERROR = "ERROR"

WS_GROUP_TYPES = ["global" "user", "event", "comment"]

WS_GROUP_JOIN_BEGIN = "Websockets.GROUP_JOIN.BEGIN"
WS_GROUP_JOIN_SUCCESS = "Websockets.GROUP_JOIN.SUCCESS"
WS_GROUP_JOIN_FAILURE = "Websockets.GROUP_JOIN.FAILURE"

WS_GROUP_LEAVE_BEGIN = "Websockets.GROUP_LEAVE.BEGIN"
WS_GROUP_LEAVE_SUCCESS = "Websockets.GROUP_LEAVE.SUCCESS"
WS_GROUP_LEAVE_FAILURE = "Websockets.GROUP_LEAVE.FAILURE"
66 changes: 59 additions & 7 deletions lego/apps/websockets/consumers.py
Original file line number Diff line number Diff line change
@@ -1,24 +1,76 @@
from asgiref.sync import AsyncToSync
from channels.generic.websocket import WebsocketConsumer
from channels.generic.websocket import JsonWebsocketConsumer

from lego.apps.events.websockets import find_event_groups
from lego.apps.users.models import User
from lego.apps.websockets.groups import group_for_user
from lego.apps.websockets import constants
from lego.apps.websockets.groups import group_for_user, verify_group_access


def find_groups(user: User):
return ["global", group_for_user(user.pk)] + find_event_groups(user)


class GroupConsumer(WebsocketConsumer):
class GroupConsumer(JsonWebsocketConsumer):
"""
Custom consumer for handling websocket groups.

Create own logic for tracking user groups as the WebsocketConsumer groups functionality
does not have access to the user object.
"""

user_groups = set()
user = None

def debug(self, message):
if self.user:
print(f"[{self.user.username.upper()}] {message}")
else:
print(f"[NO USER IN SCOPE] {message}")

def connect(self):
self.accept()
for group in find_groups(self.scope["user"]):
user = self.scope["user"]
for group in find_groups(user):
AsyncToSync(self.channel_layer.group_add)(group, self.channel_name)
self.user = user
self.user_groups = set(find_groups(user))
self.accept()

def disconnect(self, message):
for group in find_groups(self.scope["user"]):
def disconnect(self, code):
for group in self.user_groups:
AsyncToSync(self.channel_layer.group_discard)(group, self.channel_name)
self.user_groups.clear()

def receive_json(self, content, **kwargs):
type = content.get("type")
payload = content.get("payload")

if type == constants.WS_GROUP_JOIN_BEGIN:
group = payload.get("group")
if self.user and verify_group_access(self.user, group):
AsyncToSync(self.channel_layer.group_add)(group, self.channel_name)
self.user_groups.add(group)
self.send_message(
constants.WS_GROUP_JOIN_SUCCESS, payload={"group": group}
)
else:
self.send_message(
constants.WS_GROUP_JOIN_FAILURE, payload={"group": group}
)

if type == constants.WS_GROUP_LEAVE_BEGIN:
group = payload.get("group")
if group in self.groups:
AsyncToSync(self.channel_layer.group_discard)(group, self.channel_name)
self.user_groups.remove(group)
self.send_message(constants.WS_GROUP_LEAVE_SUCCESS)

def send_message(self, type: str, payload=None, meta=None):
"""
Send message on standardised format.
"""
content = {"type": type, "payload": payload, "meta": meta}
self.send_json(content)

def notification_message(self, event):
self.send(text_data=event["text"])
41 changes: 41 additions & 0 deletions lego/apps/websockets/groups.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

from typing import TYPE_CHECKING

from lego.apps.permissions.constants import LIST
from lego.apps.users.models import User
from lego.utils.content_types import instance_to_string, string_to_instance

if TYPE_CHECKING:
from lego.apps.events.models import Event

Expand All @@ -12,3 +16,40 @@ def group_for_user(user_id: str) -> str:

def group_for_event(event: Event, has_registrations_access: bool) -> str:
return f"event-{'full' if has_registrations_access else 'limited'}-{event.pk}"


def group_for_content_model(model) -> str:
modelname = model._meta.model_name
content_target_string = instance_to_string(model.content_object)
return f"{modelname}-{content_target_string}"


def verify_group_access(user: User, group):
if not group:
return False

group_type, rest = group.split("-", 1)

if group_type == "comment":
content_target = string_to_instance(rest)
if user and content_target:
return user.has_perm(LIST, content_target)

# if group_type == WS_GROUP_TYPE_USER:
# user_id = rest
# return user_id == str(user.pk)

# if group_type == WS_GROUP_TYPE_EVENT:
# event_access, event_id = rest.split("-", 1)
# """Not implemented"""

return False


def stringify_group(group):
pass

# if content_target:
# return f"{group.type}-{content_target}"

# return str(group.type)
8 changes: 5 additions & 3 deletions lego/utils/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

from lego.apps.users.fields import AbakusGroupField, PublicUserField
from lego.apps.users.models import AbakusGroup, User
from lego.utils.content_types import string_to_instance
from lego.utils.content_types import instance_to_string, string_to_instance


class GenericRelationField(serializers.CharField):
Expand All @@ -14,11 +14,13 @@ class GenericRelationField(serializers.CharField):
}

def __init__(self, *args, **kwargs):
kwargs["write_only"] = True
super().__init__(*args, **kwargs)

def to_representation(self, value):
return None
try:
return instance_to_string(value)
except Exception:
pass
Comment on lines 19 to +23
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dont know why GenericRelationField was write-only, it prevented comments from returning content_target. If it should stay how it was, it is easy enough to add it to the websocket meta data instead of this.


def to_internal_value(self, data):
try:
Expand Down