Skip to content

Commit 4d352a8

Browse files
committed
feat(poc): add support for divided discussions with user groups
1 parent 51bfd3f commit 4d352a8

File tree

10 files changed

+82
-6
lines changed

10 files changed

+82
-6
lines changed

lms/djangoapps/discussion/django_comment_client/utils.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
from django.urls import reverse
1717
from django.utils.deprecation import MiddlewareMixin
1818
from opaque_keys.edx.keys import CourseKey, UsageKey, i4xEncoder
19+
from openedx_user_groups.models import UserGroupMembership
1920
from pytz import UTC
2021

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

922923

924+
def get_user_group_ids_for_user(user: User, course_discussion_settings: CourseDiscussionSettings) -> list[int] | None:
925+
"""
926+
Get the group ids for the user in the given course.
927+
928+
Args:
929+
user (User): The user to get the group ids for
930+
course_discussion_settings (CourseDiscussionSettings): The course discussion settings
931+
932+
Returns:
933+
list[int] | None: The group ids for the user in the given course.
934+
None if the division scheme is not USER_GROUP.
935+
"""
936+
division_scheme = get_course_division_scheme(course_discussion_settings)
937+
if division_scheme == CourseDiscussionSettings.USER_GROUP:
938+
return list(UserGroupMembership.objects.filter(user=user, is_active=True).values_list("group__id", flat=True))
939+
return None
940+
941+
942+
@request_cached()
943+
def get_user_group_ids_for_user_from_cache(user: User, course_id: CourseKey) -> list[int] | None:
944+
"""
945+
Caches the results of get_group_id_for_user, but serializes the course_id
946+
instead of the course_discussions_settings object as cache keys.
947+
"""
948+
return get_user_group_ids_for_user(user, CourseDiscussionSettings.get(course_id))
949+
950+
923951
def is_comment_too_deep(parent):
924952
"""
925953
Determine whether a comment with the given parent violates MAX_COMMENT_DEPTH

lms/djangoapps/discussion/rest_api/api.py

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@
9898
)
9999
from ..django_comment_client.utils import (
100100
get_group_id_for_user,
101+
get_user_group_ids_for_user,
101102
get_user_role_names,
102103
has_discussion_privileges,
103104
is_commentable_divided
@@ -1012,6 +1013,8 @@ def get_thread_list(
10121013
):
10131014
group_id = get_group_id_for_user(request.user, CourseDiscussionSettings.get(course.id))
10141015

1016+
user_group_ids = get_user_group_ids_for_user(request.user, CourseDiscussionSettings.get(course.id))
1017+
10151018
query_params = {
10161019
"user_id": str(request.user.id),
10171020
"group_id": group_id,
@@ -1023,6 +1026,7 @@ def get_thread_list(
10231026
"flagged": flagged,
10241027
"thread_type": thread_type,
10251028
"count_flagged": count_flagged,
1029+
"user_group_ids": user_group_ids,
10261030
}
10271031

10281032
if view:
@@ -1155,6 +1159,7 @@ def get_learner_active_thread_list(request, course_key, query_params):
11551159
context = get_context(course, request)
11561160

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

1168-
if group_id is None:
1169-
comment_client_user = comment_client.User(id=user_id, course_id=course_key)
1170-
else:
1173+
if group_id is not None:
11711174
comment_client_user = comment_client.User(id=user_id, course_id=course_key, group_id=group_id)
1175+
elif user_group_ids is not None:
1176+
comment_client_user = comment_client.User(id=user_id, course_id=course_key, user_group_ids=user_group_ids)
1177+
else:
1178+
comment_client_user = comment_client.User(id=user_id, course_id=course_key)
11721179

11731180
try:
11741181
threads, page, num_pages = comment_client_user.active_threads(query_params)
@@ -1496,6 +1503,10 @@ def create_thread(request, thread_data):
14961503
):
14971504
thread_data = thread_data.copy()
14981505
thread_data["group_id"] = get_group_id_for_user(user, discussion_settings)
1506+
1507+
if "user_group_ids" not in thread_data:
1508+
thread_data["user_group_ids"] = get_user_group_ids_for_user(user, discussion_settings)
1509+
14991510
serializer = ThreadSerializer(data=thread_data, context=context)
15001511
actions_form = ThreadActionsForm(thread_data)
15011512
if not (serializer.is_valid() and actions_form.is_valid()):

lms/djangoapps/discussion/rest_api/serializers.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -362,6 +362,7 @@ class ThreadSerializer(_ContentSerializer):
362362
course_id = serializers.CharField()
363363
topic_id = serializers.CharField(source="commentable_id", validators=[validate_not_blank])
364364
group_id = serializers.IntegerField(required=False, allow_null=True)
365+
user_group_ids = serializers.ListField(required=False, allow_null=True)
365366
group_name = serializers.SerializerMethodField()
366367
type = serializers.ChoiceField(
367368
source="thread_type",

lms/djangoapps/discussion/rest_api/views.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,10 @@
3232
from lms.djangoapps.discussion.rest_api.tasks import delete_course_post_for_user
3333
from lms.djangoapps.discussion.toggles import ONLY_VERIFIED_USERS_CAN_POST
3434
from lms.djangoapps.discussion.django_comment_client import settings as cc_settings
35-
from lms.djangoapps.discussion.django_comment_client.utils import get_group_id_for_comments_service
35+
from lms.djangoapps.discussion.django_comment_client.utils import (
36+
get_group_id_for_comments_service,
37+
get_user_group_ids_for_user_from_cache,
38+
)
3639
from lms.djangoapps.instructor.access import update_forum_role
3740
from openedx.core.djangoapps.discussions.config.waffle import ENABLE_NEW_STRUCTURE_DISCUSSIONS
3841
from openedx.core.djangoapps.discussions.models import DiscussionsConfiguration, Provider
@@ -783,6 +786,8 @@ def get(self, request, course_id=None):
783786
except ValueError:
784787
pass
785788

789+
user_group_ids = get_user_group_ids_for_user_from_cache(request.user, course_key)
790+
786791
query_params = {
787792
"page": page_num,
788793
"per_page": threads_per_page,
@@ -792,6 +797,7 @@ def get(self, request, course_id=None):
792797
"count_flagged": count_flagged,
793798
"thread_type": thread_type,
794799
"sort_key": order_by,
800+
"user_group_ids": user_group_ids,
795801
}
796802
if post_status:
797803
if post_status not in ['flagged', 'unanswered', 'unread', 'unresponded']:

openedx/core/djangoapps/discussions/utils.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from typing import Dict, List, Optional, Tuple
66

77
from opaque_keys.edx.keys import CourseKey
8+
from openedx_user_groups.toggles import is_user_groups_enabled
89

910
from lms.djangoapps.courseware.access import has_access
1011
from openedx.core.djangoapps.course_groups.cohorts import get_cohort_names, is_course_cohorted
@@ -106,6 +107,8 @@ def available_division_schemes(course_key: CourseKey) -> List[str]:
106107
available_schemes.append(CourseDiscussionSettings.COHORT)
107108
if enrollment_track_group_count(course_key) > 1:
108109
available_schemes.append(CourseDiscussionSettings.ENROLLMENT_TRACK)
110+
if is_user_groups_enabled(course_key):
111+
available_schemes.append(CourseDiscussionSettings.USER_GROUP)
109112
return available_schemes
110113

111114

openedx/core/djangoapps/django_comment_common/comment_client/models.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -340,6 +340,7 @@ def handle_create_thread(self, course_id):
340340
"anonymous_to_peers": request_data.get("anonymous_to_peers", False),
341341
"commentable_id": request_data.get("commentable_id", "course"),
342342
"thread_type": request_data.get("thread_type", "discussion"),
343+
"user_group_ids": request_data.get("user_group_ids", None),
343344
}
344345
if group_id := request_data.get("group_id"):
345346
params["group_id"] = group_id

openedx/core/djangoapps/django_comment_common/comment_client/thread.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,13 +33,15 @@ class Thread(models.Model):
3333
'abuse_flaggers', 'resp_skip', 'resp_limit', 'resp_total', 'thread_type',
3434
'endorsed_responses', 'non_endorsed_responses', 'non_endorsed_resp_total',
3535
'context', 'last_activity_at', 'closed_by', 'close_reason_code', 'edit_history',
36+
'user_group_ids',
3637
]
3738

3839
# updateable_fields are sent in PUT requests
3940
updatable_fields = [
4041
'title', 'body', 'anonymous', 'anonymous_to_peers', 'course_id', 'read',
4142
'closed', 'user_id', 'commentable_id', 'group_id', 'group_name', 'pinned', 'thread_type',
4243
'close_reason_code', 'edit_reason_code', 'closing_user_id', 'editing_user_id',
44+
'user_group_ids',
4345
]
4446

4547
# initializable_fields are sent in POST requests

openedx/core/djangoapps/django_comment_common/comment_client/user.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ class User(models.Model):
1414
'id', 'external_id', 'subscribed_user_ids', 'children', 'course_id',
1515
'group_id', 'subscribed_thread_ids', 'subscribed_commentable_ids',
1616
'subscribed_course_ids', 'threads_count', 'comments_count',
17-
'default_sort_key'
17+
'default_sort_key', 'user_group_ids',
1818
]
1919

2020
updatable_fields = ['username', 'external_id', 'default_sort_key']
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# Generated by Django 4.2.20 on 2025-06-19 03:15
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
('django_comment_common', '0009_coursediscussionsettings_reported_content_email_notifications'),
10+
]
11+
12+
operations = [
13+
migrations.AlterField(
14+
model_name='coursediscussionsettings',
15+
name='division_scheme',
16+
field=models.CharField(choices=[('none', 'None'), ('cohort', 'Cohort'), ('enrollment_track', 'Enrollment Track'), ('user_group', 'User Group')], default='none', max_length=20),
17+
),
18+
]

openedx/core/djangoapps/django_comment_common/models.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -244,8 +244,14 @@ class CourseDiscussionSettings(models.Model):
244244

245245
COHORT = 'cohort'
246246
ENROLLMENT_TRACK = 'enrollment_track'
247+
USER_GROUP = 'user_group'
247248
NONE = 'none'
248-
ASSIGNMENT_TYPE_CHOICES = ((NONE, 'None'), (COHORT, 'Cohort'), (ENROLLMENT_TRACK, 'Enrollment Track'))
249+
ASSIGNMENT_TYPE_CHOICES = (
250+
(NONE, "None"),
251+
(COHORT, "Cohort"),
252+
(ENROLLMENT_TRACK, "Enrollment Track"),
253+
(USER_GROUP, "User Group"),
254+
)
249255
division_scheme = models.CharField(max_length=20, choices=ASSIGNMENT_TYPE_CHOICES, default=NONE)
250256

251257
class Meta:

0 commit comments

Comments
 (0)