Skip to content
Merged
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
78 changes: 66 additions & 12 deletions forum/models/comments.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from bson import ObjectId

from forum.models.contents import BaseContents
from forum.models.users import Users


class Comment(BaseContents):
Expand All @@ -24,9 +25,10 @@ def insert(
self,
body: str,
course_id: str,
comment_thread_id: str,
author_id: str,
author_username: str,
parent_id: Optional[str] = None,
comment_thread_id: Optional[str] = None,
author_username: Optional[str] = None,
anonymous: bool = False,
anonymous_to_peers: bool = False,
depth: int = 0,
Expand All @@ -53,34 +55,37 @@ def insert(
Returns:
str: The ID of the inserted document.
"""
if abuse_flaggers is None:
abuse_flaggers = []
if historical_abuse_flaggers is None:
historical_abuse_flaggers = []
parent_comment = parent_id and self.get(parent_id)
parent_child_count = parent_comment and parent_comment.get("child_count") or 0
if parent_comment and not comment_thread_id:
comment_thread_id = parent_comment.get("comment_thread_id")

date = datetime.now()
comment_data = {
"votes": self.get_votes_dict(up=[], down=[]),
"visible": visible,
"abuse_flaggers": abuse_flaggers,
"historical_abuse_flaggers": historical_abuse_flaggers,
"parent_ids": [],
"abuse_flaggers": abuse_flaggers or [],
"historical_abuse_flaggers": historical_abuse_flaggers or [],
"parent_ids": [ObjectId(parent_id)] if parent_id else [],
"at_position_list": [],
"body": body,
"course_id": course_id,
"_type": self.content_type,
"endorsed": False,
"anonymous": anonymous,
"anonymous_to_peers": anonymous_to_peers,
"parent_id": ObjectId(parent_id),
"author_id": author_id,
"comment_thread_id": ObjectId(comment_thread_id),
"child_count": 0,
"depth": depth,
"author_username": author_username,
"author_username": author_username or self.get_author_username(author_id),
"created_at": date,
"updated_at": date,
}
result = self._collection.insert_one(comment_data)
if parent_id:
self.update(parent_id, child_count=parent_child_count + 1)
return str(result.inserted_id)

def update(
Expand All @@ -101,6 +106,12 @@ def update(
endorsed: Optional[bool] = None,
child_count: Optional[int] = None,
depth: Optional[int] = None,
closed: Optional[bool] = None,
edit_history: Optional[list[dict[str, Any]]] = None,
original_body: Optional[str] = None,
editing_user_id: Optional[str] = None,
edit_reason_code: Optional[str] = None,
endorsement_user_id: Optional[str] = None,
) -> int:
"""
Updates a comment document in the database.
Expand Down Expand Up @@ -142,15 +153,58 @@ def update(
("endorsed", endorsed),
("child_count", child_count),
("depth", depth),
("closed", closed),
]
update_data: Dict[str, Any] = {
field: value for field, value in fields if value is not None
}
if endorsed and endorsement_user_id:
update_data["endorsement"] = {
"user_id": endorsement_user_id,
"time": datetime.now(),
}

date = datetime.now()
update_data["updated_at"] = date
if editing_user_id:
edit_history = [] if edit_history is None else edit_history
edit_history.append(
{
"original_body": original_body,
"reason_code": edit_reason_code,
"editor_username": self.get_author_username(editing_user_id),
"created_at": datetime.now(),
}
)
update_data["edit_history"] = edit_history

update_data["updated_at"] = datetime.now()
result = self._collection.update_one(
{"_id": ObjectId(comment_id)},
{"$set": update_data},
)
return result.modified_count

def delete(self, _id: str) -> int:
"""
Deletes a comment from the database based on the id.

Args:
_id: The ID of the comment.

Returns:
The number of comments deleted.
"""
comment = self.get(_id)
parent_comment_id = comment and comment.get("parent_id")
parent_comment = parent_comment_id and self.get(parent_comment_id)
parent_comment_child_count = parent_comment and parent_comment.get(
"child_count",
)
result = self._collection.delete_one({"_id": ObjectId(_id)})
if parent_comment_id and parent_comment_child_count:
self.update(parent_comment_id, child_count=parent_comment_child_count - 1)
return result.deleted_count

def get_author_username(self, author_id: str) -> str | None:
"""Return username for the respective author_id(user_id)"""
user = Users().get(author_id)
return user.get("username") if user else None
45 changes: 44 additions & 1 deletion forum/serializers/comment.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,28 @@
from rest_framework import serializers

from forum.serializers.contents import UserContentSerializer
from forum.serializers.custom_datetime import CustomDateTimeField


class EndorsementSerializer(serializers.Serializer[dict[str, Any]]):
"""
Serializer for handling endorsement of a comment

Attributes:
user_id (str): The endorsement user id.
time (datetime): The timestamp of when the user has endorsed the comment.
"""

user_id = serializers.CharField()
time = CustomDateTimeField()

def create(self, validated_data: dict[str, Any]) -> Any:
"""Raise NotImplementedError"""
raise NotImplementedError

def update(self, instance: Any, validated_data: dict[str, Any]) -> Any:
"""Raise NotImplementedError"""
raise NotImplementedError


class UserCommentSerializer(UserContentSerializer):
Expand All @@ -21,13 +43,34 @@ class UserCommentSerializer(UserContentSerializer):
thread_id (str): The ID of the thread the comment belongs to.
parent_id (str or None): The ID of the parent comment, if any.
child_count (int): The number of child comments nested under this comment.
sk (str or None): sk field, has ids data in it.
endorsement: saves endorsement data.
"""

endorsed = serializers.BooleanField(default=False)
depth = serializers.IntegerField(default=0)
thread_id = serializers.CharField()
parent_id = serializers.CharField(default=None)
parent_id = serializers.CharField(default=None, allow_null=True)
child_count = serializers.IntegerField(default=0)
sk = serializers.SerializerMethodField()
endorsement = EndorsementSerializer(default=None, required=False, allow_null=True)

def __init__(self, *args: Any, **kwargs: Any) -> None:
exclude_fields = kwargs.pop("exclude_fields", None)
super().__init__(*args, **kwargs)
if exclude_fields:
for field in exclude_fields:
self.fields.pop(field, None)

def get_sk(self, obj: dict[str, Any]) -> str:
"""Return sk field"""
is_child = obj.get("parent_id")
if is_child is not None:
return "{parent_id}-{id}".format(
parent_id=obj.get("parent_id"), id=obj.get("_id")
)
else:
return "{id}".format(id=obj.get("_id"))

def create(self, validated_data: Dict[str, Any]) -> Any:
"""Raise NotImplementedError"""
Expand Down
10 changes: 6 additions & 4 deletions forum/serializers/contents.py
Original file line number Diff line number Diff line change
Expand Up @@ -126,14 +126,16 @@ class UserContentSerializer(serializers.Serializer[dict[str, Any]]):
course_id = serializers.CharField()
anonymous = serializers.BooleanField(default=False)
anonymous_to_peers = serializers.BooleanField(default=False)
created_at = CustomDateTimeField()
updated_at = CustomDateTimeField()
at_position_list = serializers.ListField(default=[])
created_at = CustomDateTimeField(allow_null=True)
updated_at = CustomDateTimeField(allow_null=True)
at_position_list = serializers.ListField(allow_null=True)
user_id = serializers.CharField(source="author_id")
username = serializers.CharField(source="author_username")
commentable_id = serializers.CharField(default="course")
votes = VoteSummarySerializer()
abuse_flaggers = serializers.ListField(default=[])
abuse_flaggers = serializers.ListField(
child=serializers.CharField(), allow_null=True
)
edit_history = EditHistorySerializer(default=[], many=True)
closed = serializers.BooleanField(default=False)
type = serializers.CharField()
Expand Down
14 changes: 11 additions & 3 deletions forum/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,15 @@

from django.urls import include, path

from forum.views.comments import CommentsAPIView
from forum.views.flags import CommentFlagAPIView, ThreadFlagAPIView
from forum.views.pins import PinThreadAPIView, UnpinThreadAPIView
from forum.views.proxy import ForumProxyAPIView
from forum.views.search import SearchThreadsView
from forum.views.votes import CommentVoteView, ThreadVoteView

api_patterns = [
# Thread APIs
# thread votes APIs
path(
"threads/<str:thread_id>/votes",
ThreadVoteView.as_view(),
Expand All @@ -22,7 +23,7 @@
CommentVoteView.as_view(),
name="comment-vote",
),
# Comment APIs
# abuse comment/thread APIs
path(
"comments/<str:comment_id>/abuse_<str:action>",
CommentFlagAPIView.as_view(),
Expand All @@ -33,13 +34,20 @@
ThreadFlagAPIView.as_view(),
name="thread-flags-api",
),
# Pin/Unpin thread APIs
# pin/unpin thread APIs
path("threads/<str:thread_id>/pin", PinThreadAPIView.as_view(), name="pin-thread"),
path(
"threads/<str:thread_id>/unpin",
UnpinThreadAPIView.as_view(),
name="unpin-thread",
),
# comments API
path(
"comments/<str:comment_id>",
CommentsAPIView.as_view(),
name="comments-api",
),
# search threads API
path(
"search/threads",
SearchThreadsView.as_view(),
Expand Down
5 changes: 5 additions & 0 deletions forum/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,3 +40,8 @@ def handle_proxy_requests(request: HttpRequest, suffix: str, method: str) -> Res
headers=request_headers,
timeout=5.0,
)


def str_to_bool(value: str) -> bool:
"""Convert str to bool."""
return value.lower() in ("true", "1")
Loading