Skip to content

Commit b31fe4d

Browse files
committed
feat: implement mute/unmute feature
1 parent 41179a1 commit b31fe4d

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

44 files changed

+2951
-61
lines changed

forum/admin.py

Lines changed: 43 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,23 @@
11
"""Admin module for forum."""
22

33
from django.contrib import admin
4+
45
from forum.models import (
5-
ForumUser,
6-
CourseStat,
7-
CommentThread,
6+
AbuseFlagger,
87
Comment,
8+
CommentThread,
9+
CourseStat,
10+
DiscussionMute,
11+
DiscussionMuteException,
912
EditHistory,
10-
AbuseFlagger,
13+
ForumUser,
1114
HistoricalAbuseFlagger,
12-
ReadState,
1315
LastReadTime,
14-
UserVote,
15-
Subscription,
16-
MongoContent,
1716
ModerationAuditLog,
17+
MongoContent,
18+
ReadState,
19+
Subscription,
20+
UserVote,
1821
)
1922

2023

@@ -149,6 +152,38 @@ class SubscriptionAdmin(admin.ModelAdmin): # type: ignore
149152
list_filter = ("source_content_type",)
150153

151154

155+
@admin.register(DiscussionMute)
156+
class DiscussionMuteAdmin(admin.ModelAdmin): # type: ignore
157+
"""Admin interface for DiscussionMute model."""
158+
159+
list_display = (
160+
"muted_user",
161+
"muted_by",
162+
"course_id",
163+
"scope",
164+
"reason",
165+
"is_active",
166+
"created",
167+
"modified",
168+
)
169+
search_fields = (
170+
"muted_user__username",
171+
"muted_by__username",
172+
"reason",
173+
"course_id",
174+
)
175+
list_filter = ("scope", "is_active", "created", "modified")
176+
177+
178+
@admin.register(DiscussionMuteException)
179+
class DiscussionMuteExceptionAdmin(admin.ModelAdmin): # type: ignore
180+
"""Admin interface for DiscussionMuteException model."""
181+
182+
list_display = ("muted_user", "exception_user", "course_id", "created")
183+
search_fields = ("muted_user__username", "exception_user__username", "course_id")
184+
list_filter = ("created",)
185+
186+
152187
@admin.register(MongoContent)
153188
class MongoContentAdmin(admin.ModelAdmin): # type: ignore
154189
"""Admin interface for MongoContent model."""

forum/ai_moderation.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
import json
66
import logging
7-
from typing import Dict, Optional, Any
7+
from typing import Any, Dict, Optional
88

99
import requests
1010
from django.conf import settings
@@ -216,9 +216,7 @@ def moderate_and_flag_content(
216216
}
217217
# Check if AI moderation is enabled
218218
# pylint: disable=import-outside-toplevel
219-
from forum.toggles import (
220-
is_ai_moderation_enabled,
221-
)
219+
from forum.toggles import is_ai_moderation_enabled
222220

223221
course_key = CourseKey.from_string(course_id) if course_id else None
224222
if not is_ai_moderation_enabled(course_key): # type: ignore[no-untyped-call]

forum/api/__init__.py

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,14 @@
1212
get_user_comments,
1313
update_comment,
1414
)
15-
from .flags import (
16-
update_comment_flag,
17-
update_thread_flag,
15+
from .flags import update_comment_flag, update_thread_flag
16+
from .mutes import (
17+
get_all_muted_users_for_course,
18+
get_muted_users,
19+
get_user_mute_status,
20+
mute_and_report_user,
21+
mute_user,
22+
unmute_user,
1823
)
1924
from .pins import pin_thread, unpin_thread
2025
from .search import search_threads
@@ -87,4 +92,10 @@
8792
"update_user",
8893
"update_username",
8994
"update_users_in_course",
95+
"mute_user",
96+
"unmute_user",
97+
"get_user_mute_status",
98+
"get_muted_users",
99+
"get_all_muted_users_for_course",
100+
"mute_and_report_user",
90101
]

forum/api/mutes.py

Lines changed: 222 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,222 @@
1+
"""
2+
Native Python APIs for discussion moderation (mute/unmute).
3+
"""
4+
5+
from datetime import datetime
6+
from typing import Any, Dict, Optional
7+
8+
from forum.backend import get_backend
9+
from forum.utils import ForumV2RequestError
10+
11+
12+
def mute_user(
13+
muted_user_id: str,
14+
muted_by_id: str,
15+
course_id: str,
16+
scope: str = "personal",
17+
reason: str = "",
18+
**kwargs: Any,
19+
) -> Dict[str, Any]:
20+
"""
21+
Mute a user in discussions.
22+
23+
Args:
24+
muted_user_id: ID of user to mute
25+
muted_by_id: ID of user performing the mute
26+
course_id: Course identifier
27+
scope: Mute scope ('personal' or 'course')
28+
reason: Optional reason for mute
29+
30+
Returns:
31+
Dictionary containing mute record data
32+
"""
33+
try:
34+
backend = get_backend(course_id)()
35+
return backend.mute_user(
36+
muted_user_id=muted_user_id,
37+
muted_by_id=muted_by_id,
38+
course_id=course_id,
39+
scope=scope,
40+
reason=reason,
41+
**kwargs,
42+
)
43+
except ValueError as e:
44+
raise ForumV2RequestError(str(e)) from e
45+
except Exception as e:
46+
raise ForumV2RequestError(f"Failed to mute user: {str(e)}") from e
47+
48+
49+
def unmute_user(
50+
muted_user_id: str,
51+
unmuted_by_id: str,
52+
course_id: str,
53+
scope: str = "personal",
54+
muted_by_id: Optional[str] = None,
55+
**kwargs: Any,
56+
) -> Dict[str, Any]:
57+
"""
58+
Unmute a user in discussions.
59+
60+
Args:
61+
muted_user_id: ID of user to unmute
62+
unmuted_by_id: ID of user performing the unmute
63+
course_id: Course identifier
64+
scope: Mute scope ('personal' or 'course')
65+
muted_by_id: Optional filter by who performed the original mute
66+
67+
Returns:
68+
Dictionary containing unmute operation result
69+
"""
70+
try:
71+
backend = get_backend(course_id)()
72+
return backend.unmute_user(
73+
muted_user_id=muted_user_id,
74+
unmuted_by_id=unmuted_by_id,
75+
course_id=course_id,
76+
scope=scope,
77+
muted_by_id=muted_by_id,
78+
**kwargs,
79+
)
80+
except ValueError as e:
81+
raise ForumV2RequestError(str(e)) from e
82+
except Exception as e:
83+
raise ForumV2RequestError(f"Failed to unmute user: {str(e)}") from e
84+
85+
86+
def get_user_mute_status(
87+
user_id: str, course_id: str, viewer_id: str, **kwargs: Any
88+
) -> Dict[str, Any]:
89+
"""
90+
Get mute status for a user in a course.
91+
92+
Args:
93+
user_id: ID of user to check
94+
course_id: Course identifier
95+
viewer_id: ID of user requesting the status
96+
97+
Returns:
98+
Dictionary containing mute status information
99+
"""
100+
try:
101+
backend = get_backend(course_id)()
102+
return backend.get_user_mute_status(
103+
muted_user_id=user_id,
104+
course_id=course_id,
105+
requesting_user_id=viewer_id,
106+
**kwargs,
107+
)
108+
except ValueError as e:
109+
raise ForumV2RequestError(str(e)) from e
110+
except Exception as e:
111+
raise ForumV2RequestError(f"Failed to get mute status: {str(e)}") from e
112+
113+
114+
def get_muted_users(
115+
muted_by_id: str, course_id: str, scope: str = "all", **kwargs: Any
116+
) -> list[dict[str, Any]]:
117+
"""
118+
Get list of users muted by a specific user.
119+
120+
Args:
121+
muted_by_id: ID of the user who muted others
122+
course_id: Course identifier
123+
scope: Scope filter ('personal', 'course', or 'all')
124+
125+
Returns:
126+
List of muted user records
127+
"""
128+
try:
129+
backend = get_backend(course_id)()
130+
return backend.get_muted_users(
131+
moderator_id=muted_by_id, course_id=course_id, scope=scope, **kwargs
132+
)
133+
except ValueError as e:
134+
raise ForumV2RequestError(str(e)) from e
135+
except Exception as e:
136+
raise ForumV2RequestError(f"Failed to get muted users: {str(e)}") from e
137+
138+
139+
def mute_and_report_user(
140+
muted_user_id: str,
141+
muted_by_id: str,
142+
course_id: str,
143+
scope: str = "personal",
144+
reason: str = "",
145+
**kwargs: Any,
146+
) -> Dict[str, Any]:
147+
"""
148+
Mute a user and create a report against them in discussions.
149+
150+
Args:
151+
muted_user_id: ID of user to mute
152+
muted_by_id: ID of user performing the mute
153+
course_id: Course identifier
154+
scope: Mute scope ('personal' or 'course')
155+
reason: Reason for muting and reporting
156+
157+
Returns:
158+
Dictionary containing mute and report operation result
159+
"""
160+
try:
161+
backend = get_backend(course_id)()
162+
163+
# Mute the user
164+
mute_result = backend.mute_user(
165+
muted_user_id=muted_user_id,
166+
muted_by_id=muted_by_id,
167+
course_id=course_id,
168+
scope=scope,
169+
reason=reason,
170+
**kwargs,
171+
)
172+
173+
# Create a basic report record (placeholder implementation)
174+
# In a full implementation, this would integrate with a proper reporting system
175+
report_result = {
176+
"status": "success",
177+
"report_id": f"report_{muted_user_id}_{muted_by_id}_{course_id}",
178+
"reported_user_id": muted_user_id,
179+
"reported_by_id": muted_by_id,
180+
"course_id": course_id,
181+
"reason": reason,
182+
"created": datetime.utcnow().isoformat(),
183+
}
184+
185+
return {
186+
"status": "success",
187+
"message": "User muted and reported",
188+
"mute_record": mute_result,
189+
"report_record": report_result,
190+
}
191+
except ValueError as e:
192+
raise ForumV2RequestError(str(e)) from e
193+
except Exception as e:
194+
raise ForumV2RequestError(f"Failed to mute and report user: {str(e)}") from e
195+
196+
197+
def get_all_muted_users_for_course(
198+
course_id: str,
199+
_requester_id: Optional[str] = None,
200+
scope: str = "all",
201+
**kwargs: Any,
202+
) -> Dict[str, Any]:
203+
"""
204+
Get all muted users in a course (requires appropriate permissions).
205+
206+
Args:
207+
course_id: Course identifier
208+
requester_id: ID of the user requesting the list (optional)
209+
scope: Scope filter ('personal', 'course', or 'all')
210+
211+
Returns:
212+
Dictionary containing list of all muted users in the course
213+
"""
214+
try:
215+
backend = get_backend(course_id)()
216+
return backend.get_all_muted_users_for_course(
217+
course_id=course_id, scope=scope, **kwargs
218+
)
219+
except ValueError as e:
220+
raise ForumV2RequestError(str(e)) from e
221+
except Exception as e:
222+
raise ForumV2RequestError(f"Failed to get course muted users: {str(e)}") from e

forum/api/subscriptions.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,11 @@
99
from rest_framework.test import APIRequestFactory
1010

1111
from forum.backend import get_backend
12+
from forum.constants import FORUM_DEFAULT_PAGE, FORUM_DEFAULT_PER_PAGE
1213
from forum.pagination import ForumPagination
1314
from forum.serializers.subscriptions import SubscriptionSerializer
1415
from forum.serializers.thread import ThreadSerializer
1516
from forum.utils import ForumV2RequestError
16-
from forum.constants import FORUM_DEFAULT_PAGE, FORUM_DEFAULT_PER_PAGE
1717

1818

1919
def validate_user_and_thread(

forum/api/users.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,9 @@ def get_user(
2121
course_id: Optional[str] = None,
2222
complete: Optional[bool] = False,
2323
) -> dict[str, Any]:
24-
"""Get user data by user_id."""
2524
"""
2625
Get users data by user_id.
26+
2727
Parameters:
2828
user_id (str): The ID of the requested User.
2929
params (str): attributes for user's data filteration.

forum/backend.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,10 @@ def is_mysql_backend_enabled(course_id: str | None) -> bool:
1212
"""
1313
try:
1414
# pylint: disable=import-outside-toplevel
15-
from forum.toggles import ENABLE_MYSQL_BACKEND
1615
from opaque_keys import InvalidKeyError
1716
from opaque_keys.edx.keys import CourseKey
17+
18+
from forum.toggles import ENABLE_MYSQL_BACKEND
1819
except ImportError:
1920
return True
2021

0 commit comments

Comments
 (0)