Skip to content
Closed
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
28 changes: 28 additions & 0 deletions lms/djangoapps/discussion/django_comment_client/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
from django.urls import reverse
from django.utils.deprecation import MiddlewareMixin
from opaque_keys.edx.keys import CourseKey, UsageKey, i4xEncoder
from openedx_user_groups.models import UserGroupMembership
from pytz import UTC

from common.djangoapps.student.models import get_user_by_username_or_email
Expand Down Expand Up @@ -920,6 +921,33 @@ def get_group_id_for_user(user, course_discussion_settings):
return None


def get_user_group_ids_for_user(user: User, course_discussion_settings: CourseDiscussionSettings) -> list[int] | None:
"""
Get the group ids for the user in the given course.

Args:
user (User): The user to get the group ids for
course_discussion_settings (CourseDiscussionSettings): The course discussion settings

Returns:
list[int] | None: The group ids for the user in the given course.
None if the division scheme is not USER_GROUP.
"""
division_scheme = get_course_division_scheme(course_discussion_settings)
if division_scheme == CourseDiscussionSettings.USER_GROUP:
return list(UserGroupMembership.objects.filter(user=user, is_active=True).values_list("group__id", flat=True))
return None


@request_cached()
def get_user_group_ids_for_user_from_cache(user: User, course_id: CourseKey) -> list[int] | None:
"""
Caches the results of get_group_id_for_user, but serializes the course_id
instead of the course_discussions_settings object as cache keys.
"""
return get_user_group_ids_for_user(user, CourseDiscussionSettings.get(course_id))


def is_comment_too_deep(parent):
"""
Determine whether a comment with the given parent violates MAX_COMMENT_DEPTH
Expand Down
17 changes: 14 additions & 3 deletions lms/djangoapps/discussion/rest_api/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@
)
from ..django_comment_client.utils import (
get_group_id_for_user,
get_user_group_ids_for_user,
get_user_role_names,
has_discussion_privileges,
is_commentable_divided
Expand Down Expand Up @@ -1012,6 +1013,8 @@ def get_thread_list(
):
group_id = get_group_id_for_user(request.user, CourseDiscussionSettings.get(course.id))

user_group_ids = get_user_group_ids_for_user(request.user, CourseDiscussionSettings.get(course.id))

query_params = {
"user_id": str(request.user.id),
"group_id": group_id,
Expand All @@ -1023,6 +1026,7 @@ def get_thread_list(
"flagged": flagged,
"thread_type": thread_type,
"count_flagged": count_flagged,
"user_group_ids": user_group_ids,
}

if view:
Expand Down Expand Up @@ -1155,6 +1159,7 @@ def get_learner_active_thread_list(request, course_key, query_params):
context = get_context(course, request)

group_id = query_params.get('group_id', None)
user_group_ids = query_params.get('user_group_ids', None)
user_id = query_params.get('user_id', None)
count_flagged = query_params.get('count_flagged', None)
if user_id is None:
Expand All @@ -1165,10 +1170,12 @@ def get_learner_active_thread_list(request, course_key, query_params):
if "flagged" in query_params.keys() and not context["has_moderation_privilege"]:
raise PermissionDenied("Flagged filter is only available for moderators")

if group_id is None:
comment_client_user = comment_client.User(id=user_id, course_id=course_key)
else:
if group_id is not None:
comment_client_user = comment_client.User(id=user_id, course_id=course_key, group_id=group_id)
elif user_group_ids is not None:
comment_client_user = comment_client.User(id=user_id, course_id=course_key, user_group_ids=user_group_ids)
else:
comment_client_user = comment_client.User(id=user_id, course_id=course_key)

try:
threads, page, num_pages = comment_client_user.active_threads(query_params)
Expand Down Expand Up @@ -1496,6 +1503,10 @@ def create_thread(request, thread_data):
):
thread_data = thread_data.copy()
thread_data["group_id"] = get_group_id_for_user(user, discussion_settings)

if "user_group_ids" not in thread_data:
thread_data["user_group_ids"] = get_user_group_ids_for_user(user, discussion_settings)

serializer = ThreadSerializer(data=thread_data, context=context)
actions_form = ThreadActionsForm(thread_data)
if not (serializer.is_valid() and actions_form.is_valid()):
Expand Down
1 change: 1 addition & 0 deletions lms/djangoapps/discussion/rest_api/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -362,6 +362,7 @@ class ThreadSerializer(_ContentSerializer):
course_id = serializers.CharField()
topic_id = serializers.CharField(source="commentable_id", validators=[validate_not_blank])
group_id = serializers.IntegerField(required=False, allow_null=True)
user_group_ids = serializers.ListField(required=False, allow_null=True)
group_name = serializers.SerializerMethodField()
type = serializers.ChoiceField(
source="thread_type",
Expand Down
8 changes: 7 additions & 1 deletion lms/djangoapps/discussion/rest_api/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,10 @@
from lms.djangoapps.discussion.rest_api.tasks import delete_course_post_for_user
from lms.djangoapps.discussion.toggles import ONLY_VERIFIED_USERS_CAN_POST
from lms.djangoapps.discussion.django_comment_client import settings as cc_settings
from lms.djangoapps.discussion.django_comment_client.utils import get_group_id_for_comments_service
from lms.djangoapps.discussion.django_comment_client.utils import (
get_group_id_for_comments_service,
get_user_group_ids_for_user_from_cache,
)
from lms.djangoapps.instructor.access import update_forum_role
from openedx.core.djangoapps.discussions.config.waffle import ENABLE_NEW_STRUCTURE_DISCUSSIONS
from openedx.core.djangoapps.discussions.models import DiscussionsConfiguration, Provider
Expand Down Expand Up @@ -783,6 +786,8 @@ def get(self, request, course_id=None):
except ValueError:
pass

user_group_ids = get_user_group_ids_for_user_from_cache(request.user, course_key)

query_params = {
"page": page_num,
"per_page": threads_per_page,
Expand All @@ -792,6 +797,7 @@ def get(self, request, course_id=None):
"count_flagged": count_flagged,
"thread_type": thread_type,
"sort_key": order_by,
"user_group_ids": user_group_ids,
}
if post_status:
if post_status not in ['flagged', 'unanswered', 'unread', 'unresponded']:
Expand Down
3 changes: 3 additions & 0 deletions openedx/core/djangoapps/discussions/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from typing import Dict, List, Optional, Tuple

from opaque_keys.edx.keys import CourseKey
from openedx_user_groups.toggles import is_user_groups_enabled

from lms.djangoapps.courseware.access import has_access
from openedx.core.djangoapps.course_groups.cohorts import get_cohort_names, is_course_cohorted
Expand Down Expand Up @@ -106,6 +107,8 @@ def available_division_schemes(course_key: CourseKey) -> List[str]:
available_schemes.append(CourseDiscussionSettings.COHORT)
if enrollment_track_group_count(course_key) > 1:
available_schemes.append(CourseDiscussionSettings.ENROLLMENT_TRACK)
if is_user_groups_enabled(course_key):
available_schemes.append(CourseDiscussionSettings.USER_GROUP)
return available_schemes


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -340,6 +340,7 @@ def handle_create_thread(self, course_id):
"anonymous_to_peers": request_data.get("anonymous_to_peers", False),
"commentable_id": request_data.get("commentable_id", "course"),
"thread_type": request_data.get("thread_type", "discussion"),
"user_group_ids": request_data.get("user_group_ids", None),
}
if group_id := request_data.get("group_id"):
params["group_id"] = group_id
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,13 +33,15 @@ class Thread(models.Model):
'abuse_flaggers', 'resp_skip', 'resp_limit', 'resp_total', 'thread_type',
'endorsed_responses', 'non_endorsed_responses', 'non_endorsed_resp_total',
'context', 'last_activity_at', 'closed_by', 'close_reason_code', 'edit_history',
'user_group_ids',
]

# updateable_fields are sent in PUT requests
updatable_fields = [
'title', 'body', 'anonymous', 'anonymous_to_peers', 'course_id', 'read',
'closed', 'user_id', 'commentable_id', 'group_id', 'group_name', 'pinned', 'thread_type',
'close_reason_code', 'edit_reason_code', 'closing_user_id', 'editing_user_id',
'user_group_ids',
]

# initializable_fields are sent in POST requests
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ class User(models.Model):
'id', 'external_id', 'subscribed_user_ids', 'children', 'course_id',
'group_id', 'subscribed_thread_ids', 'subscribed_commentable_ids',
'subscribed_course_ids', 'threads_count', 'comments_count',
'default_sort_key'
'default_sort_key', 'user_group_ids',
]

updatable_fields = ['username', 'external_id', 'default_sort_key']
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Generated by Django 4.2.20 on 2025-06-19 03:15

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('django_comment_common', '0009_coursediscussionsettings_reported_content_email_notifications'),
]

operations = [
migrations.AlterField(
model_name='coursediscussionsettings',
name='division_scheme',
field=models.CharField(choices=[('none', 'None'), ('cohort', 'Cohort'), ('enrollment_track', 'Enrollment Track'), ('user_group', 'User Group')], default='none', max_length=20),
),
]
8 changes: 7 additions & 1 deletion openedx/core/djangoapps/django_comment_common/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -244,8 +244,14 @@ class CourseDiscussionSettings(models.Model):

COHORT = 'cohort'
ENROLLMENT_TRACK = 'enrollment_track'
USER_GROUP = 'user_group'
NONE = 'none'
ASSIGNMENT_TYPE_CHOICES = ((NONE, 'None'), (COHORT, 'Cohort'), (ENROLLMENT_TRACK, 'Enrollment Track'))
ASSIGNMENT_TYPE_CHOICES = (
(NONE, "None"),
(COHORT, "Cohort"),
(ENROLLMENT_TRACK, "Enrollment Track"),
(USER_GROUP, "User Group"),
)
division_scheme = models.CharField(max_length=20, choices=ASSIGNMENT_TYPE_CHOICES, default=NONE)

class Meta:
Expand Down
Loading