Skip to content
Draft
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
51 changes: 43 additions & 8 deletions forum/admin.py
Original file line number Diff line number Diff line change
@@ -1,20 +1,23 @@
"""Admin module for forum."""

from django.contrib import admin

from forum.models import (
ForumUser,
CourseStat,
CommentThread,
AbuseFlagger,
Comment,
CommentThread,
CourseStat,
DiscussionMute,
DiscussionMuteException,
EditHistory,
AbuseFlagger,
ForumUser,
HistoricalAbuseFlagger,
ReadState,
LastReadTime,
UserVote,
Subscription,
MongoContent,
ModerationAuditLog,
MongoContent,
ReadState,
Subscription,
UserVote,
)


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


@admin.register(DiscussionMute)
class DiscussionMuteAdmin(admin.ModelAdmin): # type: ignore
"""Admin interface for DiscussionMute model."""

list_display = (
"muted_user",
"muted_by",
"course_id",
"scope",
"reason",
"is_active",
"created",
"modified",
)
search_fields = (
"muted_user__username",
"muted_by__username",
"reason",
"course_id",
)
list_filter = ("scope", "is_active", "created", "modified")


@admin.register(DiscussionMuteException)
class DiscussionMuteExceptionAdmin(admin.ModelAdmin): # type: ignore
"""Admin interface for DiscussionMuteException model."""

list_display = ("muted_user", "exception_user", "course_id", "created")
search_fields = ("muted_user__username", "exception_user__username", "course_id")
list_filter = ("created",)


@admin.register(MongoContent)
class MongoContentAdmin(admin.ModelAdmin): # type: ignore
"""Admin interface for MongoContent model."""
Expand Down
6 changes: 2 additions & 4 deletions forum/ai_moderation.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

import json
import logging
from typing import Dict, Optional, Any
from typing import Any, Dict, Optional

import requests
from django.conf import settings
Expand Down Expand Up @@ -216,9 +216,7 @@ def moderate_and_flag_content(
}
# Check if AI moderation is enabled
# pylint: disable=import-outside-toplevel
from forum.toggles import (
is_ai_moderation_enabled,
)
from forum.toggles import is_ai_moderation_enabled

course_key = CourseKey.from_string(course_id) if course_id else None
if not is_ai_moderation_enabled(course_key): # type: ignore[no-untyped-call]
Expand Down
17 changes: 14 additions & 3 deletions forum/api/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,14 @@
get_user_comments,
update_comment,
)
from .flags import (
update_comment_flag,
update_thread_flag,
from .flags import update_comment_flag, update_thread_flag
from .mutes import (
get_all_muted_users_for_course,
get_muted_users,
get_user_mute_status,
mute_and_report_user,
mute_user,
unmute_user,
)
from .pins import pin_thread, unpin_thread
from .search import search_threads
Expand Down Expand Up @@ -87,4 +92,10 @@
"update_user",
"update_username",
"update_users_in_course",
"mute_user",
"unmute_user",
"get_user_mute_status",
"get_muted_users",
"get_all_muted_users_for_course",
"mute_and_report_user",
]
222 changes: 222 additions & 0 deletions forum/api/mutes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,222 @@
"""
Native Python APIs for discussion moderation (mute/unmute).
"""

from datetime import datetime
from typing import Any, Dict, Optional

from forum.backend import get_backend
from forum.utils import ForumV2RequestError


def mute_user(
muted_user_id: str,
muted_by_id: str,
course_id: str,
scope: str = "personal",
reason: str = "",
**kwargs: Any,
) -> Dict[str, Any]:
"""
Mute a user in discussions.

Args:
muted_user_id: ID of user to mute
muted_by_id: ID of user performing the mute
course_id: Course identifier
scope: Mute scope ('personal' or 'course')
reason: Optional reason for mute

Returns:
Dictionary containing mute record data
"""
try:
backend = get_backend(course_id)()
return backend.mute_user(
muted_user_id=muted_user_id,
muted_by_id=muted_by_id,
course_id=course_id,
scope=scope,
reason=reason,
**kwargs,
)
except ValueError as e:
raise ForumV2RequestError(str(e)) from e
except Exception as e:
raise ForumV2RequestError(f"Failed to mute user: {str(e)}") from e


def unmute_user(
muted_user_id: str,
unmuted_by_id: str,
course_id: str,
scope: str = "personal",
muted_by_id: Optional[str] = None,
**kwargs: Any,
) -> Dict[str, Any]:
"""
Unmute a user in discussions.

Args:
muted_user_id: ID of user to unmute
unmuted_by_id: ID of user performing the unmute
course_id: Course identifier
scope: Mute scope ('personal' or 'course')
muted_by_id: Optional filter by who performed the original mute

Returns:
Dictionary containing unmute operation result
"""
try:
backend = get_backend(course_id)()
return backend.unmute_user(
muted_user_id=muted_user_id,
unmuted_by_id=unmuted_by_id,
course_id=course_id,
scope=scope,
muted_by_id=muted_by_id,
**kwargs,
)
except ValueError as e:
raise ForumV2RequestError(str(e)) from e
except Exception as e:
raise ForumV2RequestError(f"Failed to unmute user: {str(e)}") from e


def get_user_mute_status(
user_id: str, course_id: str, viewer_id: str, **kwargs: Any
) -> Dict[str, Any]:
"""
Get mute status for a user in a course.

Args:
user_id: ID of user to check
course_id: Course identifier
viewer_id: ID of user requesting the status

Returns:
Dictionary containing mute status information
"""
try:
backend = get_backend(course_id)()
return backend.get_user_mute_status(
muted_user_id=user_id,
course_id=course_id,
requesting_user_id=viewer_id,
**kwargs,
)
except ValueError as e:
raise ForumV2RequestError(str(e)) from e
except Exception as e:
raise ForumV2RequestError(f"Failed to get mute status: {str(e)}") from e


def get_muted_users(
muted_by_id: str, course_id: str, scope: str = "all", **kwargs: Any
) -> list[dict[str, Any]]:
"""
Get list of users muted by a specific user.

Args:
muted_by_id: ID of the user who muted others
course_id: Course identifier
scope: Scope filter ('personal', 'course', or 'all')

Returns:
List of muted user records
"""
try:
backend = get_backend(course_id)()
return backend.get_muted_users(
moderator_id=muted_by_id, course_id=course_id, scope=scope, **kwargs
)
except ValueError as e:
raise ForumV2RequestError(str(e)) from e
except Exception as e:
raise ForumV2RequestError(f"Failed to get muted users: {str(e)}") from e


def mute_and_report_user(
muted_user_id: str,
muted_by_id: str,
course_id: str,
scope: str = "personal",
reason: str = "",
**kwargs: Any,
) -> Dict[str, Any]:
"""
Mute a user and create a report against them in discussions.

Args:
muted_user_id: ID of user to mute
muted_by_id: ID of user performing the mute
course_id: Course identifier
scope: Mute scope ('personal' or 'course')
reason: Reason for muting and reporting

Returns:
Dictionary containing mute and report operation result
"""
try:
backend = get_backend(course_id)()

# Mute the user
mute_result = backend.mute_user(
muted_user_id=muted_user_id,
muted_by_id=muted_by_id,
course_id=course_id,
scope=scope,
reason=reason,
**kwargs,
)

# Create a basic report record (placeholder implementation)
# In a full implementation, this would integrate with a proper reporting system
report_result = {
"status": "success",
"report_id": f"report_{muted_user_id}_{muted_by_id}_{course_id}",
"reported_user_id": muted_user_id,
"reported_by_id": muted_by_id,
"course_id": course_id,
"reason": reason,
"created": datetime.utcnow().isoformat(),
}

return {
"status": "success",
"message": "User muted and reported",
"mute_record": mute_result,
"report_record": report_result,
}
except ValueError as e:
raise ForumV2RequestError(str(e)) from e
except Exception as e:
raise ForumV2RequestError(f"Failed to mute and report user: {str(e)}") from e


def get_all_muted_users_for_course(
course_id: str,
_requester_id: Optional[str] = None,
scope: str = "all",
**kwargs: Any,
) -> Dict[str, Any]:
"""
Get all muted users in a course (requires appropriate permissions).

Args:
course_id: Course identifier
requester_id: ID of the user requesting the list (optional)
scope: Scope filter ('personal', 'course', or 'all')

Returns:
Dictionary containing list of all muted users in the course
"""
try:
backend = get_backend(course_id)()
return backend.get_all_muted_users_for_course(
course_id=course_id, scope=scope, **kwargs
)
except ValueError as e:
raise ForumV2RequestError(str(e)) from e
except Exception as e:
raise ForumV2RequestError(f"Failed to get course muted users: {str(e)}") from e
2 changes: 1 addition & 1 deletion forum/api/subscriptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,11 @@
from rest_framework.test import APIRequestFactory

from forum.backend import get_backend
from forum.constants import FORUM_DEFAULT_PAGE, FORUM_DEFAULT_PER_PAGE
from forum.pagination import ForumPagination
from forum.serializers.subscriptions import SubscriptionSerializer
from forum.serializers.thread import ThreadSerializer
from forum.utils import ForumV2RequestError
from forum.constants import FORUM_DEFAULT_PAGE, FORUM_DEFAULT_PER_PAGE


def validate_user_and_thread(
Expand Down
2 changes: 1 addition & 1 deletion forum/api/users.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,9 @@ def get_user(
course_id: Optional[str] = None,
complete: Optional[bool] = False,
) -> dict[str, Any]:
"""Get user data by user_id."""
"""
Get users data by user_id.

Parameters:
user_id (str): The ID of the requested User.
params (str): attributes for user's data filteration.
Expand Down
3 changes: 2 additions & 1 deletion forum/backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,10 @@ def is_mysql_backend_enabled(course_id: str | None) -> bool:
"""
try:
# pylint: disable=import-outside-toplevel
from forum.toggles import ENABLE_MYSQL_BACKEND
from opaque_keys import InvalidKeyError
from opaque_keys.edx.keys import CourseKey

from forum.toggles import ENABLE_MYSQL_BACKEND
except ImportError:
return True

Expand Down
Loading