openedx-forum 0.1.6__py2.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.
Files changed (84) hide show
  1. forum/__init__.py +5 -0
  2. forum/admin.py +154 -0
  3. forum/api/__init__.py +88 -0
  4. forum/api/commentables.py +22 -0
  5. forum/api/comments.py +321 -0
  6. forum/api/flags.py +121 -0
  7. forum/api/pins.py +68 -0
  8. forum/api/search.py +125 -0
  9. forum/api/subscriptions.py +117 -0
  10. forum/api/threads.py +405 -0
  11. forum/api/users.py +370 -0
  12. forum/api/votes.py +257 -0
  13. forum/apps.py +36 -0
  14. forum/backend.py +39 -0
  15. forum/backends/__init__.py +1 -0
  16. forum/backends/backend.py +469 -0
  17. forum/backends/mongodb/__init__.py +21 -0
  18. forum/backends/mongodb/api.py +1725 -0
  19. forum/backends/mongodb/base_model.py +127 -0
  20. forum/backends/mongodb/comments.py +365 -0
  21. forum/backends/mongodb/contents.py +256 -0
  22. forum/backends/mongodb/subscriptions.py +99 -0
  23. forum/backends/mongodb/threads.py +296 -0
  24. forum/backends/mongodb/users.py +130 -0
  25. forum/backends/mysql/__init__.py +5 -0
  26. forum/backends/mysql/api.py +2168 -0
  27. forum/backends/mysql/models.py +720 -0
  28. forum/constants.py +11 -0
  29. forum/handlers.py +141 -0
  30. forum/management/__init__.py +0 -0
  31. forum/management/commands/__init__.py +0 -0
  32. forum/management/commands/delete_unused_forum_indices.py +29 -0
  33. forum/management/commands/forum_create_mongodb_indexes.py +24 -0
  34. forum/management/commands/forum_delete_course_from_mongodb.py +52 -0
  35. forum/management/commands/forum_migrate_course_from_mongodb_to_mysql.py +55 -0
  36. forum/management/commands/initialize_forum_indices.py +45 -0
  37. forum/management/commands/rebuild_forum_indices.py +59 -0
  38. forum/management/commands/validate_forum_indices.py +26 -0
  39. forum/migration_helpers.py +353 -0
  40. forum/migrations/0001_initial.py +631 -0
  41. forum/migrations/0002_alter_readstate_course_id_and_more.py +48 -0
  42. forum/migrations/__init__.py +0 -0
  43. forum/models.py +4 -0
  44. forum/mongo.py +34 -0
  45. forum/pagination.py +42 -0
  46. forum/search/__init__.py +45 -0
  47. forum/search/base.py +118 -0
  48. forum/search/es.py +769 -0
  49. forum/search/meilisearch.py +232 -0
  50. forum/serializers/__init__.py +0 -0
  51. forum/serializers/comment.py +112 -0
  52. forum/serializers/contents.py +86 -0
  53. forum/serializers/custom_datetime.py +26 -0
  54. forum/serializers/subscriptions.py +26 -0
  55. forum/serializers/thread.py +290 -0
  56. forum/serializers/users.py +35 -0
  57. forum/serializers/votes.py +86 -0
  58. forum/settings/__init__.py +0 -0
  59. forum/settings/common.py +46 -0
  60. forum/settings/devstack.py +17 -0
  61. forum/settings/production.py +17 -0
  62. forum/settings/test.py +81 -0
  63. forum/signals.py +23 -0
  64. forum/templates/forum/base.html +26 -0
  65. forum/toggles.py +18 -0
  66. forum/urls.py +168 -0
  67. forum/utils.py +252 -0
  68. forum/views/__init__.py +0 -0
  69. forum/views/commentables.py +35 -0
  70. forum/views/comments.py +207 -0
  71. forum/views/flags.py +74 -0
  72. forum/views/pins.py +73 -0
  73. forum/views/proxy.py +47 -0
  74. forum/views/search.py +99 -0
  75. forum/views/subscriptions.py +128 -0
  76. forum/views/threads.py +166 -0
  77. forum/views/users.py +228 -0
  78. forum/views/votes.py +146 -0
  79. openedx_forum-0.1.6.dist-info/LICENSE.txt +664 -0
  80. openedx_forum-0.1.6.dist-info/METADATA +290 -0
  81. openedx_forum-0.1.6.dist-info/RECORD +84 -0
  82. openedx_forum-0.1.6.dist-info/WHEEL +6 -0
  83. openedx_forum-0.1.6.dist-info/entry_points.txt +2 -0
  84. openedx_forum-0.1.6.dist-info/top_level.txt +1 -0
forum/__init__.py ADDED
@@ -0,0 +1,5 @@
1
+ """
2
+ Openedx forum app.
3
+ """
4
+
5
+ __version__ = "0.1.6"
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,88 @@
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
+ update_comment,
13
+ )
14
+ from .flags import (
15
+ update_comment_flag,
16
+ update_thread_flag,
17
+ )
18
+ from .pins import pin_thread, unpin_thread
19
+ from .search import search_threads
20
+ from .subscriptions import (
21
+ create_subscription,
22
+ delete_subscription,
23
+ get_thread_subscriptions,
24
+ get_user_subscriptions,
25
+ )
26
+ from .threads import (
27
+ create_thread,
28
+ delete_thread,
29
+ get_course_id_by_thread,
30
+ get_thread,
31
+ get_user_threads,
32
+ update_thread,
33
+ )
34
+ from .users import (
35
+ create_user,
36
+ get_user,
37
+ get_user_active_threads,
38
+ get_user_course_stats,
39
+ mark_thread_as_read,
40
+ retire_user,
41
+ update_user,
42
+ update_username,
43
+ update_users_in_course,
44
+ )
45
+ from .votes import (
46
+ delete_comment_vote,
47
+ delete_thread_vote,
48
+ update_comment_votes,
49
+ update_thread_votes,
50
+ )
51
+
52
+ __all__ = [
53
+ "create_child_comment",
54
+ "create_parent_comment",
55
+ "create_subscription",
56
+ "create_thread",
57
+ "create_user",
58
+ "delete_comment",
59
+ "delete_comment_vote",
60
+ "delete_subscription",
61
+ "delete_thread",
62
+ "delete_thread_vote",
63
+ "get_commentables_stats",
64
+ "get_course_id_by_comment",
65
+ "get_course_id_by_thread",
66
+ "get_parent_comment",
67
+ "get_thread",
68
+ "get_thread_subscriptions",
69
+ "get_user",
70
+ "get_user_active_threads",
71
+ "get_user_course_stats",
72
+ "get_user_subscriptions",
73
+ "get_user_threads",
74
+ "mark_thread_as_read",
75
+ "pin_thread",
76
+ "retire_user",
77
+ "search_threads",
78
+ "unpin_thread",
79
+ "update_comment",
80
+ "update_comment_flag",
81
+ "update_comment_votes",
82
+ "update_thread",
83
+ "update_thread_flag",
84
+ "update_thread_votes",
85
+ "update_user",
86
+ "update_username",
87
+ "update_users_in_course",
88
+ ]
@@ -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,321 @@
1
+ """
2
+ Native Python Comments APIs.
3
+ """
4
+
5
+ import logging
6
+ from typing import Any, Optional
7
+
8
+ from django.core.exceptions import ObjectDoesNotExist
9
+ from rest_framework.serializers import ValidationError
10
+
11
+ from forum.backend import get_backend
12
+ from forum.serializers.comment import CommentSerializer
13
+ from forum.utils import ForumV2RequestError
14
+
15
+ log = logging.getLogger(__name__)
16
+
17
+
18
+ def prepare_comment_api_response(
19
+ comment: dict[str, Any],
20
+ backend: Any,
21
+ exclude_fields: Optional[list[str]] = None,
22
+ ) -> dict[str, Any]:
23
+ """
24
+ Return serialized validated data.
25
+
26
+ Parameters:
27
+ comment: The comment details that needs to be serialized.
28
+ exclude_fields: Any fields that need to be excluded from response.
29
+
30
+ Response:
31
+ serialized validated data of the comment.
32
+ """
33
+ comment_data = {
34
+ **comment,
35
+ "id": str(comment.get("_id")),
36
+ "user_id": comment.get("author_id"),
37
+ "thread_id": str(comment.get("comment_thread_id")),
38
+ "username": comment.get("author_username"),
39
+ "parent_id": str(comment.get("parent_id")),
40
+ "type": str(comment.get("_type", "")).lower(),
41
+ }
42
+ if not exclude_fields:
43
+ exclude_fields = []
44
+ exclude_fields.append("children")
45
+ serializer = CommentSerializer(
46
+ data=comment_data,
47
+ exclude_fields=exclude_fields,
48
+ backend=backend,
49
+ )
50
+ if not serializer.is_valid(raise_exception=True):
51
+ raise ValidationError(serializer.errors)
52
+
53
+ return serializer.data
54
+
55
+
56
+ def get_parent_comment(
57
+ comment_id: str, course_id: Optional[str] = None
58
+ ) -> dict[str, Any]:
59
+ """
60
+ Get a parent comment.
61
+
62
+ Parameters:
63
+ comment_id: The ID of the comment.
64
+ Body:
65
+ Empty.
66
+ Response:
67
+ The details of the comment for the given comment_id.
68
+ """
69
+ backend = get_backend(course_id)()
70
+ try:
71
+ comment = backend.validate_object("Comment", comment_id)
72
+ except ObjectDoesNotExist as exc:
73
+ log.error("Forumv2RequestError for get parent comment request.")
74
+ raise ForumV2RequestError(
75
+ f"Comment does not exists with Id: {comment_id}"
76
+ ) from exc
77
+ return prepare_comment_api_response(
78
+ comment,
79
+ backend,
80
+ exclude_fields=["sk"],
81
+ )
82
+
83
+
84
+ def create_child_comment(
85
+ parent_comment_id: str,
86
+ body: str,
87
+ user_id: str,
88
+ course_id: str,
89
+ anonymous: bool,
90
+ anonymous_to_peers: bool,
91
+ ) -> dict[str, Any]:
92
+ """
93
+ Create a new child comment.
94
+
95
+ Parameters:
96
+ comment_id: The ID of the parent comment for creating it's child comment.
97
+ body: The content of the comment.
98
+ course_id: The Id of the respective course.
99
+ user_id: The requesting user id.
100
+ anonymous: anonymous flag(True or False).
101
+ anonymous_to_peers: anonymous to peers flag(True or False).
102
+ Response:
103
+ The details of the comment that is created.
104
+ """
105
+ backend = get_backend(course_id)()
106
+ try:
107
+ parent_comment = backend.validate_object("Comment", parent_comment_id)
108
+ except ObjectDoesNotExist as exc:
109
+ log.error("Forumv2RequestError for create child comment request.")
110
+ raise ForumV2RequestError(
111
+ f"Comment does not exists with Id: {parent_comment_id}"
112
+ ) from exc
113
+
114
+ comment_id = backend.create_comment(
115
+ {
116
+ "body": body,
117
+ "author_id": user_id,
118
+ "course_id": course_id,
119
+ "anonymous": anonymous,
120
+ "anonymous_to_peers": anonymous_to_peers,
121
+ "depth": 1,
122
+ "comment_thread_id": backend.get_thread_id_by_comment_id(parent_comment_id),
123
+ "parent_id": parent_comment_id,
124
+ }
125
+ )
126
+ comment = backend.get_comment(comment_id)
127
+ if not comment:
128
+ log.error("Forumv2RequestError for create child comment request.")
129
+ raise ForumV2RequestError("comment is not created")
130
+
131
+ user = backend.get_user(user_id)
132
+ thread = backend.get_thread(parent_comment["comment_thread_id"])
133
+ if user and thread and comment:
134
+ backend.mark_as_read(user_id, parent_comment["comment_thread_id"])
135
+ try:
136
+ comment_data = prepare_comment_api_response(
137
+ comment,
138
+ backend,
139
+ exclude_fields=["endorsement", "sk"],
140
+ )
141
+ return comment_data
142
+ except ValidationError as error:
143
+ raise error
144
+
145
+
146
+ def update_comment(
147
+ comment_id: str,
148
+ body: Optional[str] = None,
149
+ course_id: Optional[str] = None,
150
+ user_id: Optional[str] = None,
151
+ anonymous: Optional[bool] = None,
152
+ anonymous_to_peers: Optional[bool] = None,
153
+ endorsed: Optional[bool] = None,
154
+ closed: Optional[bool] = None,
155
+ editing_user_id: Optional[str] = None,
156
+ edit_reason_code: Optional[str] = None,
157
+ endorsement_user_id: Optional[str] = None,
158
+ course_key: Optional[str] = None,
159
+ ) -> dict[str, Any]:
160
+ """
161
+ Update an existing child/parent comment.
162
+
163
+ Parameters:
164
+ comment_id: The ID of the comment to be edited.
165
+ body (Optional[str]): The content of the comment.
166
+ course_id (Optional[str]): The Id of the respective course.
167
+ user_id (Optional[str]): The requesting user id.
168
+ anonymous (Optional[bool]): anonymous flag(True or False).
169
+ anonymous_to_peers (Optional[bool]): anonymous to peers flag(True or False).
170
+ endorsed (Optional[bool]): Flag indicating if the comment is endorsed by any user.
171
+ closed (Optional[bool]): Flag indicating if the comment thread is closed.
172
+ editing_user_id (Optional[str]): The ID of the user editing the comment.
173
+ edit_reason_code (Optional[str]): The reason for editing the comment, typically represented by a code.
174
+ endorsement_user_id (Optional[str]): The ID of the user endorsing the comment.
175
+ Response:
176
+ The details of the comment that is updated.
177
+ """
178
+ backend = get_backend(course_key)()
179
+ try:
180
+ backend.validate_object("Comment", comment_id)
181
+ except ObjectDoesNotExist as exc:
182
+ log.error("Forumv2RequestError for update comment request.")
183
+ raise ForumV2RequestError(
184
+ f"Comment does not exists with Id: {comment_id}"
185
+ ) from exc
186
+
187
+ updated_comment = backend.update_comment_and_get_updated_comment(
188
+ comment_id,
189
+ body,
190
+ course_id,
191
+ user_id,
192
+ anonymous,
193
+ anonymous_to_peers,
194
+ endorsed,
195
+ closed,
196
+ editing_user_id,
197
+ edit_reason_code,
198
+ 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
+ It searches for comment_id both in mongodb and mysql.
312
+ """
313
+ # pylint: disable=C0415
314
+ from forum.backends.mongodb.api import MongoBackend
315
+ from forum.backends.mysql.api import MySQLBackend
316
+
317
+ return (
318
+ MongoBackend.get_course_id_by_comment_id(comment_id)
319
+ or MySQLBackend.get_course_id_by_comment_id(comment_id)
320
+ or None
321
+ )