openedx-forum 0.4.7__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- forum/__init__.py +9 -0
- forum/admin.py +154 -0
- forum/api/__init__.py +94 -0
- forum/api/commentables.py +22 -0
- forum/api/comments.py +366 -0
- forum/api/flags.py +121 -0
- forum/api/pins.py +68 -0
- forum/api/search.py +129 -0
- forum/api/subscriptions.py +147 -0
- forum/api/threads.py +407 -0
- forum/api/users.py +383 -0
- forum/api/votes.py +257 -0
- forum/apps.py +36 -0
- forum/backend.py +10 -0
- forum/backends/__init__.py +1 -0
- forum/backends/backend.py +488 -0
- forum/backends/mysql/__init__.py +5 -0
- forum/backends/mysql/api.py +2332 -0
- forum/backends/mysql/models.py +776 -0
- forum/conf/locale/config.yaml +85 -0
- forum/constants.py +11 -0
- forum/handlers.py +141 -0
- forum/management/__init__.py +0 -0
- forum/management/commands/__init__.py +0 -0
- forum/management/commands/delete_unused_forum_indices.py +29 -0
- forum/management/commands/forum_delete_course_from_mongodb.py +52 -0
- forum/management/commands/forum_migrate_course_from_mongodb_to_mysql.py +42 -0
- forum/management/commands/initialize_forum_indices.py +45 -0
- forum/management/commands/rebuild_forum_indices.py +59 -0
- forum/management/commands/validate_forum_indices.py +26 -0
- forum/migration_helpers.py +480 -0
- forum/migrations/0001_initial.py +631 -0
- forum/migrations/0002_alter_readstate_course_id_and_more.py +48 -0
- forum/migrations/0003_alter_commentthread_title.py +18 -0
- forum/migrations/0004_add_author_username_fields.py +43 -0
- forum/migrations/0005_alter_commentthread_pinned.py +26 -0
- forum/migrations/__init__.py +0 -0
- forum/models.py +4 -0
- forum/mongo.py +34 -0
- forum/pagination.py +42 -0
- forum/search/__init__.py +45 -0
- forum/search/base.py +118 -0
- forum/search/es.py +699 -0
- forum/search/meilisearch.py +233 -0
- forum/search/typesense.py +460 -0
- forum/serializers/__init__.py +0 -0
- forum/serializers/comment.py +112 -0
- forum/serializers/contents.py +89 -0
- forum/serializers/custom_datetime.py +26 -0
- forum/serializers/subscriptions.py +26 -0
- forum/serializers/thread.py +295 -0
- forum/serializers/users.py +35 -0
- forum/serializers/votes.py +86 -0
- forum/settings/__init__.py +0 -0
- forum/settings/common.py +52 -0
- forum/settings/devstack.py +17 -0
- forum/settings/production.py +17 -0
- forum/settings/test.py +85 -0
- forum/signals.py +23 -0
- forum/templates/forum/base.html +26 -0
- forum/urls.py +174 -0
- forum/utils.py +264 -0
- forum/views/__init__.py +0 -0
- forum/views/commentables.py +35 -0
- forum/views/comments.py +209 -0
- forum/views/flags.py +76 -0
- forum/views/pins.py +75 -0
- forum/views/proxy.py +47 -0
- forum/views/search.py +99 -0
- forum/views/subscriptions.py +133 -0
- forum/views/threads.py +167 -0
- forum/views/users.py +262 -0
- forum/views/votes.py +150 -0
- openedx_forum-0.4.7.dist-info/METADATA +228 -0
- openedx_forum-0.4.7.dist-info/RECORD +79 -0
- openedx_forum-0.4.7.dist-info/WHEEL +5 -0
- openedx_forum-0.4.7.dist-info/entry_points.txt +2 -0
- openedx_forum-0.4.7.dist-info/licenses/LICENSE.txt +664 -0
- openedx_forum-0.4.7.dist-info/top_level.txt +1 -0
forum/__init__.py
ADDED
forum/admin.py
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
"""Admin module for forum."""
|
|
2
|
+
|
|
3
|
+
from django.contrib import admin
|
|
4
|
+
from forum.models import (
|
|
5
|
+
ForumUser,
|
|
6
|
+
CourseStat,
|
|
7
|
+
CommentThread,
|
|
8
|
+
Comment,
|
|
9
|
+
EditHistory,
|
|
10
|
+
AbuseFlagger,
|
|
11
|
+
HistoricalAbuseFlagger,
|
|
12
|
+
ReadState,
|
|
13
|
+
LastReadTime,
|
|
14
|
+
UserVote,
|
|
15
|
+
Subscription,
|
|
16
|
+
MongoContent,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@admin.register(ForumUser)
|
|
21
|
+
class ForumUserAdmin(admin.ModelAdmin): # type: ignore
|
|
22
|
+
"""Admin interface for ForumUser model."""
|
|
23
|
+
|
|
24
|
+
list_display = ("user", "default_sort_key")
|
|
25
|
+
search_fields = ("user__username", "user__email")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@admin.register(CourseStat)
|
|
29
|
+
class CourseStatAdmin(admin.ModelAdmin): # type: ignore
|
|
30
|
+
"""Admin interface for CourseStat model."""
|
|
31
|
+
|
|
32
|
+
list_display = (
|
|
33
|
+
"user",
|
|
34
|
+
"course_id",
|
|
35
|
+
"active_flags",
|
|
36
|
+
"inactive_flags",
|
|
37
|
+
"threads",
|
|
38
|
+
"responses",
|
|
39
|
+
"replies",
|
|
40
|
+
"last_activity_at",
|
|
41
|
+
)
|
|
42
|
+
search_fields = ("user__username", "course_id")
|
|
43
|
+
list_filter = ("course_id",)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@admin.register(CommentThread)
|
|
47
|
+
class CommentThreadAdmin(admin.ModelAdmin): # type: ignore
|
|
48
|
+
"""Admin interface for CommentThread model."""
|
|
49
|
+
|
|
50
|
+
list_display = (
|
|
51
|
+
"title",
|
|
52
|
+
"author",
|
|
53
|
+
"course_id",
|
|
54
|
+
"thread_type",
|
|
55
|
+
"context",
|
|
56
|
+
"closed",
|
|
57
|
+
"pinned",
|
|
58
|
+
"created_at",
|
|
59
|
+
"updated_at",
|
|
60
|
+
)
|
|
61
|
+
search_fields = ("title", "body", "author__username", "course_id")
|
|
62
|
+
list_filter = ("thread_type", "context", "closed", "pinned")
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@admin.register(Comment)
|
|
66
|
+
class CommentAdmin(admin.ModelAdmin): # type: ignore
|
|
67
|
+
"""Admin interface for Comment model."""
|
|
68
|
+
|
|
69
|
+
list_display = (
|
|
70
|
+
"comment_thread",
|
|
71
|
+
"author",
|
|
72
|
+
"body",
|
|
73
|
+
"created_at",
|
|
74
|
+
"updated_at",
|
|
75
|
+
"endorsed",
|
|
76
|
+
"anonymous",
|
|
77
|
+
)
|
|
78
|
+
search_fields = ("body", "author__username", "comment_thread__title")
|
|
79
|
+
list_filter = ("endorsed", "anonymous")
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
@admin.register(EditHistory)
|
|
83
|
+
class EditHistoryAdmin(admin.ModelAdmin): # type: ignore
|
|
84
|
+
"""Admin interface for EditHistory model."""
|
|
85
|
+
|
|
86
|
+
list_display = ("editor", "content_object_id", "created_at", "reason_code")
|
|
87
|
+
search_fields = ("editor__username", "original_body")
|
|
88
|
+
list_filter = ("reason_code",)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
@admin.register(AbuseFlagger)
|
|
92
|
+
class AbuseFlaggerAdmin(admin.ModelAdmin): # type: ignore
|
|
93
|
+
"""Admin interface for AbuseFlagger model."""
|
|
94
|
+
|
|
95
|
+
list_display = ("user", "content_object_id", "flagged_at")
|
|
96
|
+
search_fields = ("user__username",)
|
|
97
|
+
list_filter = ("content_type",)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
@admin.register(HistoricalAbuseFlagger)
|
|
101
|
+
class HistoricalAbuseFlaggerAdmin(admin.ModelAdmin): # type: ignore
|
|
102
|
+
"""Admin interface for HistoricalAbuseFlagger model."""
|
|
103
|
+
|
|
104
|
+
list_display = ("user", "content_object_id", "flagged_at")
|
|
105
|
+
search_fields = ("user__username",)
|
|
106
|
+
list_filter = ("content_type",)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
@admin.register(ReadState)
|
|
110
|
+
class ReadStateAdmin(admin.ModelAdmin): # type: ignore
|
|
111
|
+
"""Admin interface for ReadState model."""
|
|
112
|
+
|
|
113
|
+
list_display = ("user", "course_id")
|
|
114
|
+
search_fields = ("user__username", "course_id")
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
@admin.register(LastReadTime)
|
|
118
|
+
class LastReadTimeAdmin(admin.ModelAdmin): # type: ignore
|
|
119
|
+
"""Admin interface for LastReadTime model."""
|
|
120
|
+
|
|
121
|
+
list_display = ("read_state", "comment_thread", "timestamp")
|
|
122
|
+
search_fields = ("read_state__user__username", "comment_thread__title")
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
@admin.register(UserVote)
|
|
126
|
+
class UserVoteAdmin(admin.ModelAdmin): # type: ignore
|
|
127
|
+
"""Admin interface for UserVote model."""
|
|
128
|
+
|
|
129
|
+
list_display = ("user", "content_object_id", "vote")
|
|
130
|
+
search_fields = ("user__username",)
|
|
131
|
+
list_filter = ("vote",)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
@admin.register(Subscription)
|
|
135
|
+
class SubscriptionAdmin(admin.ModelAdmin): # type: ignore
|
|
136
|
+
"""Admin interface for Subscription model."""
|
|
137
|
+
|
|
138
|
+
list_display = (
|
|
139
|
+
"subscriber",
|
|
140
|
+
"source_object_id",
|
|
141
|
+
"source_content_type",
|
|
142
|
+
"created_at",
|
|
143
|
+
"updated_at",
|
|
144
|
+
)
|
|
145
|
+
search_fields = ("subscriber__username",)
|
|
146
|
+
list_filter = ("source_content_type",)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
@admin.register(MongoContent)
|
|
150
|
+
class MongoContentAdmin(admin.ModelAdmin): # type: ignore
|
|
151
|
+
"""Admin interface for MongoContent model."""
|
|
152
|
+
|
|
153
|
+
list_display = ("mongo_id", "content_object_id", "content_type")
|
|
154
|
+
search_fields = ("mongo_id",)
|
forum/api/__init__.py
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Native Python APIs.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from .commentables import get_commentables_stats
|
|
6
|
+
from .comments import (
|
|
7
|
+
create_child_comment,
|
|
8
|
+
create_parent_comment,
|
|
9
|
+
delete_comment,
|
|
10
|
+
get_course_id_by_comment,
|
|
11
|
+
get_parent_comment,
|
|
12
|
+
get_user_comments,
|
|
13
|
+
update_comment,
|
|
14
|
+
)
|
|
15
|
+
from .flags import (
|
|
16
|
+
update_comment_flag,
|
|
17
|
+
update_thread_flag,
|
|
18
|
+
)
|
|
19
|
+
from .pins import pin_thread, unpin_thread
|
|
20
|
+
from .search import search_threads
|
|
21
|
+
from .subscriptions import (
|
|
22
|
+
create_subscription,
|
|
23
|
+
delete_subscription,
|
|
24
|
+
get_thread_subscriptions,
|
|
25
|
+
get_user_subscriptions,
|
|
26
|
+
)
|
|
27
|
+
from .threads import (
|
|
28
|
+
create_thread,
|
|
29
|
+
delete_thread,
|
|
30
|
+
get_course_id_by_thread,
|
|
31
|
+
get_thread,
|
|
32
|
+
get_user_threads,
|
|
33
|
+
update_thread,
|
|
34
|
+
)
|
|
35
|
+
from .users import (
|
|
36
|
+
create_user,
|
|
37
|
+
delete_user_posts,
|
|
38
|
+
get_user,
|
|
39
|
+
get_user_active_threads,
|
|
40
|
+
get_user_course_stats,
|
|
41
|
+
get_user_post_counts,
|
|
42
|
+
mark_thread_as_read,
|
|
43
|
+
retire_user,
|
|
44
|
+
update_user,
|
|
45
|
+
update_username,
|
|
46
|
+
update_users_in_course,
|
|
47
|
+
)
|
|
48
|
+
from .votes import (
|
|
49
|
+
delete_comment_vote,
|
|
50
|
+
delete_thread_vote,
|
|
51
|
+
update_comment_votes,
|
|
52
|
+
update_thread_votes,
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
__all__ = [
|
|
56
|
+
"create_child_comment",
|
|
57
|
+
"create_parent_comment",
|
|
58
|
+
"create_subscription",
|
|
59
|
+
"create_thread",
|
|
60
|
+
"create_user",
|
|
61
|
+
"delete_comment",
|
|
62
|
+
"delete_comment_vote",
|
|
63
|
+
"delete_subscription",
|
|
64
|
+
"delete_thread",
|
|
65
|
+
"delete_thread_vote",
|
|
66
|
+
"delete_user_posts",
|
|
67
|
+
"get_commentables_stats",
|
|
68
|
+
"get_course_id_by_comment",
|
|
69
|
+
"get_course_id_by_thread",
|
|
70
|
+
"get_parent_comment",
|
|
71
|
+
"get_thread",
|
|
72
|
+
"get_thread_subscriptions",
|
|
73
|
+
"get_user",
|
|
74
|
+
"get_user_active_threads",
|
|
75
|
+
"get_user_comments",
|
|
76
|
+
"get_user_course_stats",
|
|
77
|
+
"get_user_post_counts",
|
|
78
|
+
"get_user_subscriptions",
|
|
79
|
+
"get_user_threads",
|
|
80
|
+
"mark_thread_as_read",
|
|
81
|
+
"pin_thread",
|
|
82
|
+
"retire_user",
|
|
83
|
+
"search_threads",
|
|
84
|
+
"unpin_thread",
|
|
85
|
+
"update_comment",
|
|
86
|
+
"update_comment_flag",
|
|
87
|
+
"update_comment_votes",
|
|
88
|
+
"update_thread",
|
|
89
|
+
"update_thread_flag",
|
|
90
|
+
"update_thread_votes",
|
|
91
|
+
"update_user",
|
|
92
|
+
"update_username",
|
|
93
|
+
"update_users_in_course",
|
|
94
|
+
]
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Native Python Commenttables APIs.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from forum.backend import get_backend
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def get_commentables_stats(course_id: str) -> dict[str, int]:
|
|
9
|
+
"""
|
|
10
|
+
Get the threads count based on thread_type and group them by commentable_id.
|
|
11
|
+
|
|
12
|
+
Parameters:
|
|
13
|
+
course_id: The ID of the course.
|
|
14
|
+
Body:
|
|
15
|
+
Empty.
|
|
16
|
+
Response:
|
|
17
|
+
The threads count for the given course_id based on thread_type.
|
|
18
|
+
e.g.
|
|
19
|
+
reponse = {'course': {'discussion': 1, 'question': 1}}
|
|
20
|
+
"""
|
|
21
|
+
backend = get_backend(course_id)()
|
|
22
|
+
return backend.get_commentables_counts_based_on_type(course_id)
|
forum/api/comments.py
ADDED
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Native Python Comments APIs.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
import math
|
|
7
|
+
from typing import Any, Optional
|
|
8
|
+
|
|
9
|
+
from django.core.exceptions import ObjectDoesNotExist
|
|
10
|
+
from rest_framework.serializers import ValidationError
|
|
11
|
+
|
|
12
|
+
from forum.backend import get_backend
|
|
13
|
+
from forum.backends.mysql.api import MySQLBackend
|
|
14
|
+
from forum.serializers.comment import CommentSerializer
|
|
15
|
+
from forum.utils import ForumV2RequestError
|
|
16
|
+
|
|
17
|
+
log = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def prepare_comment_api_response(
|
|
21
|
+
comment: dict[str, Any],
|
|
22
|
+
backend: Any,
|
|
23
|
+
exclude_fields: Optional[list[str]] = None,
|
|
24
|
+
) -> dict[str, Any]:
|
|
25
|
+
"""
|
|
26
|
+
Return serialized validated data.
|
|
27
|
+
|
|
28
|
+
Parameters:
|
|
29
|
+
comment: The comment details that needs to be serialized.
|
|
30
|
+
exclude_fields: Any fields that need to be excluded from response.
|
|
31
|
+
|
|
32
|
+
Response:
|
|
33
|
+
serialized validated data of the comment.
|
|
34
|
+
"""
|
|
35
|
+
comment_data = {
|
|
36
|
+
**comment,
|
|
37
|
+
"id": str(comment.get("_id")),
|
|
38
|
+
"user_id": comment.get("author_id"),
|
|
39
|
+
"thread_id": str(comment.get("comment_thread_id")),
|
|
40
|
+
"username": comment.get("author_username"),
|
|
41
|
+
"parent_id": str(comment.get("parent_id")),
|
|
42
|
+
"type": str(comment.get("_type", "")).lower(),
|
|
43
|
+
}
|
|
44
|
+
if not exclude_fields:
|
|
45
|
+
exclude_fields = []
|
|
46
|
+
exclude_fields.append("children")
|
|
47
|
+
serializer = CommentSerializer(
|
|
48
|
+
data=comment_data,
|
|
49
|
+
exclude_fields=exclude_fields,
|
|
50
|
+
backend=backend,
|
|
51
|
+
)
|
|
52
|
+
if not serializer.is_valid(raise_exception=True):
|
|
53
|
+
raise ValidationError(serializer.errors)
|
|
54
|
+
|
|
55
|
+
return serializer.data
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def get_parent_comment(
|
|
59
|
+
comment_id: str, course_id: Optional[str] = None
|
|
60
|
+
) -> dict[str, Any]:
|
|
61
|
+
"""
|
|
62
|
+
Get a parent comment.
|
|
63
|
+
|
|
64
|
+
Parameters:
|
|
65
|
+
comment_id: The ID of the comment.
|
|
66
|
+
Body:
|
|
67
|
+
Empty.
|
|
68
|
+
Response:
|
|
69
|
+
The details of the comment for the given comment_id.
|
|
70
|
+
"""
|
|
71
|
+
backend = get_backend(course_id)()
|
|
72
|
+
try:
|
|
73
|
+
comment = backend.validate_object("Comment", comment_id)
|
|
74
|
+
except ObjectDoesNotExist as exc:
|
|
75
|
+
log.error("Forumv2RequestError for get parent comment request.")
|
|
76
|
+
raise ForumV2RequestError(
|
|
77
|
+
f"Comment does not exists with Id: {comment_id}"
|
|
78
|
+
) from exc
|
|
79
|
+
return prepare_comment_api_response(
|
|
80
|
+
comment,
|
|
81
|
+
backend,
|
|
82
|
+
exclude_fields=["sk"],
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def create_child_comment(
|
|
87
|
+
parent_comment_id: str,
|
|
88
|
+
body: str,
|
|
89
|
+
user_id: str,
|
|
90
|
+
course_id: str,
|
|
91
|
+
anonymous: bool,
|
|
92
|
+
anonymous_to_peers: bool,
|
|
93
|
+
) -> dict[str, Any]:
|
|
94
|
+
"""
|
|
95
|
+
Create a new child comment.
|
|
96
|
+
|
|
97
|
+
Parameters:
|
|
98
|
+
comment_id: The ID of the parent comment for creating it's child comment.
|
|
99
|
+
body: The content of the comment.
|
|
100
|
+
course_id: The Id of the respective course.
|
|
101
|
+
user_id: The requesting user id.
|
|
102
|
+
anonymous: anonymous flag(True or False).
|
|
103
|
+
anonymous_to_peers: anonymous to peers flag(True or False).
|
|
104
|
+
Response:
|
|
105
|
+
The details of the comment that is created.
|
|
106
|
+
"""
|
|
107
|
+
backend = get_backend(course_id)()
|
|
108
|
+
try:
|
|
109
|
+
parent_comment = backend.validate_object("Comment", parent_comment_id)
|
|
110
|
+
except ObjectDoesNotExist as exc:
|
|
111
|
+
log.error("Forumv2RequestError for create child comment request.")
|
|
112
|
+
raise ForumV2RequestError(
|
|
113
|
+
f"Comment does not exists with Id: {parent_comment_id}"
|
|
114
|
+
) from exc
|
|
115
|
+
|
|
116
|
+
comment_id = backend.create_comment(
|
|
117
|
+
{
|
|
118
|
+
"body": body,
|
|
119
|
+
"author_id": user_id,
|
|
120
|
+
"course_id": course_id,
|
|
121
|
+
"anonymous": anonymous,
|
|
122
|
+
"anonymous_to_peers": anonymous_to_peers,
|
|
123
|
+
"depth": 1,
|
|
124
|
+
"comment_thread_id": backend.get_thread_id_by_comment_id(parent_comment_id),
|
|
125
|
+
"parent_id": parent_comment_id,
|
|
126
|
+
}
|
|
127
|
+
)
|
|
128
|
+
comment = backend.get_comment(comment_id)
|
|
129
|
+
if not comment:
|
|
130
|
+
log.error("Forumv2RequestError for create child comment request.")
|
|
131
|
+
raise ForumV2RequestError("comment is not created")
|
|
132
|
+
|
|
133
|
+
user = backend.get_user(user_id)
|
|
134
|
+
thread = backend.get_thread(parent_comment["comment_thread_id"])
|
|
135
|
+
if user and thread and comment:
|
|
136
|
+
backend.mark_as_read(user_id, parent_comment["comment_thread_id"])
|
|
137
|
+
try:
|
|
138
|
+
comment_data = prepare_comment_api_response(
|
|
139
|
+
comment,
|
|
140
|
+
backend,
|
|
141
|
+
exclude_fields=["endorsement", "sk"],
|
|
142
|
+
)
|
|
143
|
+
return comment_data
|
|
144
|
+
except ValidationError as error:
|
|
145
|
+
raise error
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def update_comment(
|
|
149
|
+
comment_id: str,
|
|
150
|
+
body: Optional[str] = None,
|
|
151
|
+
course_id: Optional[str] = None,
|
|
152
|
+
user_id: Optional[str] = None,
|
|
153
|
+
anonymous: Optional[bool] = None,
|
|
154
|
+
anonymous_to_peers: Optional[bool] = None,
|
|
155
|
+
endorsed: Optional[bool] = None,
|
|
156
|
+
closed: Optional[bool] = None,
|
|
157
|
+
editing_user_id: Optional[str] = None,
|
|
158
|
+
edit_reason_code: Optional[str] = None,
|
|
159
|
+
endorsement_user_id: Optional[str] = None,
|
|
160
|
+
) -> dict[str, Any]:
|
|
161
|
+
"""
|
|
162
|
+
Update an existing child/parent comment.
|
|
163
|
+
|
|
164
|
+
Parameters:
|
|
165
|
+
comment_id: The ID of the comment to be edited.
|
|
166
|
+
body (Optional[str]): The content of the comment.
|
|
167
|
+
course_id (Optional[str]): The Id of the respective course.
|
|
168
|
+
user_id (Optional[str]): The requesting user id.
|
|
169
|
+
anonymous (Optional[bool]): anonymous flag(True or False).
|
|
170
|
+
anonymous_to_peers (Optional[bool]): anonymous to peers flag(True or False).
|
|
171
|
+
endorsed (Optional[bool]): Flag indicating if the comment is endorsed by any user.
|
|
172
|
+
closed (Optional[bool]): Flag indicating if the comment thread is closed.
|
|
173
|
+
editing_user_id (Optional[str]): The ID of the user editing the comment.
|
|
174
|
+
edit_reason_code (Optional[str]): The reason for editing the comment, typically represented by a code.
|
|
175
|
+
endorsement_user_id (Optional[str]): The ID of the user endorsing the comment.
|
|
176
|
+
Response:
|
|
177
|
+
The details of the comment that is updated.
|
|
178
|
+
"""
|
|
179
|
+
backend = get_backend(course_id)()
|
|
180
|
+
try:
|
|
181
|
+
backend.validate_object("Comment", comment_id)
|
|
182
|
+
except ObjectDoesNotExist as exc:
|
|
183
|
+
log.error("Forumv2RequestError for update comment request.")
|
|
184
|
+
raise ForumV2RequestError(
|
|
185
|
+
f"Comment does not exists with Id: {comment_id}"
|
|
186
|
+
) from exc
|
|
187
|
+
|
|
188
|
+
updated_comment = backend.update_comment_and_get_updated_comment(
|
|
189
|
+
comment_id,
|
|
190
|
+
body=body,
|
|
191
|
+
user_id=user_id,
|
|
192
|
+
anonymous=anonymous,
|
|
193
|
+
anonymous_to_peers=anonymous_to_peers,
|
|
194
|
+
endorsed=endorsed,
|
|
195
|
+
closed=closed,
|
|
196
|
+
editing_user_id=editing_user_id,
|
|
197
|
+
edit_reason_code=edit_reason_code,
|
|
198
|
+
endorsement_user_id=endorsement_user_id,
|
|
199
|
+
)
|
|
200
|
+
if not updated_comment:
|
|
201
|
+
log.error("Forumv2RequestError for create child comment request.")
|
|
202
|
+
raise ForumV2RequestError("comment is not updated")
|
|
203
|
+
try:
|
|
204
|
+
return prepare_comment_api_response(
|
|
205
|
+
updated_comment,
|
|
206
|
+
backend,
|
|
207
|
+
exclude_fields=(
|
|
208
|
+
["endorsement", "sk"] if updated_comment.get("parent_id") else ["sk"]
|
|
209
|
+
),
|
|
210
|
+
)
|
|
211
|
+
except ValidationError as error:
|
|
212
|
+
raise error
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def delete_comment(comment_id: str, course_id: Optional[str] = None) -> dict[str, Any]:
|
|
216
|
+
"""
|
|
217
|
+
Delete a comment.
|
|
218
|
+
|
|
219
|
+
Parameters:
|
|
220
|
+
comment_id: The ID of the comment to be deleted.
|
|
221
|
+
Body:
|
|
222
|
+
Empty.
|
|
223
|
+
Response:
|
|
224
|
+
The details of the comment that is deleted.
|
|
225
|
+
"""
|
|
226
|
+
backend = get_backend(course_id)()
|
|
227
|
+
try:
|
|
228
|
+
comment = backend.validate_object("Comment", comment_id)
|
|
229
|
+
except ObjectDoesNotExist as exc:
|
|
230
|
+
log.error("Forumv2RequestError for delete comment request.")
|
|
231
|
+
raise ForumV2RequestError(
|
|
232
|
+
f"Comment does not exists with Id: {comment_id}"
|
|
233
|
+
) from exc
|
|
234
|
+
data = prepare_comment_api_response(
|
|
235
|
+
comment,
|
|
236
|
+
backend,
|
|
237
|
+
exclude_fields=["endorsement", "sk"],
|
|
238
|
+
)
|
|
239
|
+
backend.delete_comment(comment_id)
|
|
240
|
+
author_id = comment["author_id"]
|
|
241
|
+
comment_course_id = comment["course_id"]
|
|
242
|
+
parent_comment_id = data["parent_id"]
|
|
243
|
+
if parent_comment_id:
|
|
244
|
+
backend.update_stats_for_course(author_id, comment_course_id, replies=-1)
|
|
245
|
+
else:
|
|
246
|
+
backend.update_stats_for_course(author_id, comment_course_id, responses=-1)
|
|
247
|
+
return data
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def create_parent_comment(
|
|
251
|
+
thread_id: str,
|
|
252
|
+
body: str,
|
|
253
|
+
user_id: str,
|
|
254
|
+
course_id: str,
|
|
255
|
+
anonymous: bool,
|
|
256
|
+
anonymous_to_peers: bool,
|
|
257
|
+
) -> dict[str, Any]:
|
|
258
|
+
"""
|
|
259
|
+
Create a new parent comment.
|
|
260
|
+
|
|
261
|
+
Parameters:
|
|
262
|
+
thread_id: The ID of the thread for creating a comment on it.
|
|
263
|
+
body: The content of the comment.
|
|
264
|
+
course_id: The Id of the respective course.
|
|
265
|
+
user_id: The requesting user id.
|
|
266
|
+
anonymous: anonymous flag(True or False).
|
|
267
|
+
anonymous_to_peers: anonymous to peers flag(True or False).
|
|
268
|
+
Response:
|
|
269
|
+
The details of the comment that is created.
|
|
270
|
+
"""
|
|
271
|
+
backend = get_backend(course_id)()
|
|
272
|
+
try:
|
|
273
|
+
backend.validate_object("CommentThread", thread_id)
|
|
274
|
+
except ObjectDoesNotExist as exc:
|
|
275
|
+
log.error("Forumv2RequestError for create parent comment request.")
|
|
276
|
+
raise ForumV2RequestError(
|
|
277
|
+
f"Thread does not exists with Id: {thread_id}"
|
|
278
|
+
) from exc
|
|
279
|
+
|
|
280
|
+
comment_id = backend.create_comment(
|
|
281
|
+
{
|
|
282
|
+
"body": body,
|
|
283
|
+
"author_id": user_id,
|
|
284
|
+
"course_id": course_id,
|
|
285
|
+
"anonymous": anonymous,
|
|
286
|
+
"anonymous_to_peers": anonymous_to_peers,
|
|
287
|
+
"depth": 0,
|
|
288
|
+
"comment_thread_id": thread_id,
|
|
289
|
+
}
|
|
290
|
+
)
|
|
291
|
+
if not comment_id:
|
|
292
|
+
log.error("Forumv2RequestError for create parent comment request.")
|
|
293
|
+
raise ForumV2RequestError("comment is not created")
|
|
294
|
+
comment = backend.get_comment(comment_id) or {}
|
|
295
|
+
user = backend.get_user(user_id)
|
|
296
|
+
if user and comment:
|
|
297
|
+
backend.mark_as_read(user_id, thread_id)
|
|
298
|
+
try:
|
|
299
|
+
return prepare_comment_api_response(
|
|
300
|
+
comment,
|
|
301
|
+
backend,
|
|
302
|
+
exclude_fields=["endorsement", "sk"],
|
|
303
|
+
)
|
|
304
|
+
except ValidationError as error:
|
|
305
|
+
raise error
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
def get_course_id_by_comment(comment_id: str) -> str | None:
|
|
309
|
+
"""
|
|
310
|
+
Return course_id for the matching comment.
|
|
311
|
+
"""
|
|
312
|
+
return MySQLBackend.get_course_id_by_comment_id(comment_id) or None
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
def get_user_comments(
|
|
316
|
+
user_id: str,
|
|
317
|
+
course_id: str,
|
|
318
|
+
flagged: Optional[bool] = False,
|
|
319
|
+
page: int = 1,
|
|
320
|
+
per_page: int = 10,
|
|
321
|
+
) -> dict[str, Any]:
|
|
322
|
+
"""
|
|
323
|
+
Get all comments made by a user in a specific course.
|
|
324
|
+
|
|
325
|
+
Args:
|
|
326
|
+
user_id: The ID of the user
|
|
327
|
+
course_id: The ID of the course
|
|
328
|
+
flagged: Filter for flagged comments
|
|
329
|
+
page: Page number for pagination
|
|
330
|
+
per_page: Number of items per page
|
|
331
|
+
|
|
332
|
+
Returns:
|
|
333
|
+
A dictionary containing paginated comment results
|
|
334
|
+
"""
|
|
335
|
+
|
|
336
|
+
backend = get_backend(course_id)()
|
|
337
|
+
|
|
338
|
+
# Build query parameters
|
|
339
|
+
query_params: dict[str, Any] = {
|
|
340
|
+
"author_id": str(user_id),
|
|
341
|
+
"course_id": course_id,
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
if flagged:
|
|
345
|
+
query_params["abuse_flaggers"] = {"$ne": [], "$exists": True}
|
|
346
|
+
|
|
347
|
+
# Get total count
|
|
348
|
+
comment_count = backend.get_comments_count(**query_params)
|
|
349
|
+
|
|
350
|
+
# Calculate pagination
|
|
351
|
+
num_pages = max(1, math.ceil(comment_count / per_page))
|
|
352
|
+
skip = (page - 1) * per_page
|
|
353
|
+
|
|
354
|
+
# Get paginated comments
|
|
355
|
+
query_params["resp_skip"] = skip
|
|
356
|
+
query_params["resp_limit"] = per_page
|
|
357
|
+
query_params["sort"] = -1 # Sort by newest first
|
|
358
|
+
|
|
359
|
+
comments = backend.get_comments(**query_params)
|
|
360
|
+
|
|
361
|
+
return {
|
|
362
|
+
"collection": comments,
|
|
363
|
+
"comment_count": comment_count,
|
|
364
|
+
"num_pages": num_pages,
|
|
365
|
+
"page": page,
|
|
366
|
+
}
|