Skip to content

Commit 7949141

Browse files
committed
feat(poc): add support for divided discussions with user groups
1 parent e4e8565 commit 7949141

File tree

10 files changed

+89
-6
lines changed

10 files changed

+89
-6
lines changed

lms/djangoapps/discussion/django_comment_client/utils.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
from django.utils.deprecation import MiddlewareMixin
1818
from opaque_keys.edx.keys import CourseKey, UsageKey, i4xEncoder
1919
from pytz import UTC
20+
from openedx_user_groups.models import UserGroupMembership
2021

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

922923

924+
def get_user_group_ids_for_user(
925+
user: User, course_discussion_settings: CourseDiscussionSettings
926+
) -> list[int] | None:
927+
"""
928+
Get the group ids for the user in the given course
929+
"""
930+
division_scheme = get_course_division_scheme(course_discussion_settings)
931+
if division_scheme == CourseDiscussionSettings.USER_GROUP:
932+
# Dummy data for testing
933+
user_group_ids = {
934+
"marie": [1, 2],
935+
"elon": [3, 4],
936+
}
937+
return user_group_ids.get(user.username, [])
938+
939+
# TODO: Filter by course
940+
# return UserGroupMembership.objects.filter(
941+
# user_id=user.id,
942+
# is_active=True
943+
# ).values_list('group_id', flat=True)
944+
else:
945+
return None
946+
947+
948+
@request_cached()
949+
def get_user_group_ids_for_user_from_cache(user: User, course_id: CourseKey) -> list[int] | None:
950+
"""
951+
Caches the results of get_group_id_for_user, but serializes the course_id
952+
instead of the course_discussions_settings object as cache keys.
953+
"""
954+
return get_user_group_ids_for_user(user, CourseDiscussionSettings.get(course_id))
955+
956+
923957
def is_comment_too_deep(parent):
924958
"""
925959
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
@@ -97,6 +97,7 @@
9797
)
9898
from ..django_comment_client.utils import (
9999
get_group_id_for_user,
100+
get_user_group_ids_for_user,
100101
get_user_role_names,
101102
has_discussion_privileges,
102103
is_commentable_divided
@@ -996,6 +997,8 @@ def get_thread_list(
996997
):
997998
group_id = get_group_id_for_user(request.user, CourseDiscussionSettings.get(course.id))
998999

1000+
user_group_ids = get_user_group_ids_for_user(request.user, CourseDiscussionSettings.get(course.id))
1001+
9991002
query_params = {
10001003
"user_id": str(request.user.id),
10011004
"group_id": group_id,
@@ -1007,6 +1010,7 @@ def get_thread_list(
10071010
"flagged": flagged,
10081011
"thread_type": thread_type,
10091012
"count_flagged": count_flagged,
1013+
"user_group_ids": user_group_ids,
10101014
}
10111015

10121016
if view:
@@ -1139,6 +1143,7 @@ def get_learner_active_thread_list(request, course_key, query_params):
11391143
context = get_context(course, request)
11401144

11411145
group_id = query_params.get('group_id', None)
1146+
user_group_ids = query_params.get('user_group_ids', None)
11421147
user_id = query_params.get('user_id', None)
11431148
count_flagged = query_params.get('count_flagged', None)
11441149
if user_id is None:
@@ -1149,10 +1154,12 @@ def get_learner_active_thread_list(request, course_key, query_params):
11491154
if "flagged" in query_params.keys() and not context["has_moderation_privilege"]:
11501155
raise PermissionDenied("Flagged filter is only available for moderators")
11511156

1152-
if group_id is None:
1153-
comment_client_user = comment_client.User(id=user_id, course_id=course_key)
1154-
else:
1157+
if group_id is not None:
11551158
comment_client_user = comment_client.User(id=user_id, course_id=course_key, group_id=group_id)
1159+
elif user_group_ids is not None:
1160+
comment_client_user = comment_client.User(id=user_id, course_id=course_key, user_group_ids=user_group_ids)
1161+
else:
1162+
comment_client_user = comment_client.User(id=user_id, course_id=course_key)
11561163

11571164
try:
11581165
threads, page, num_pages = comment_client_user.active_threads(query_params)
@@ -1478,6 +1485,10 @@ def create_thread(request, thread_data):
14781485
):
14791486
thread_data = thread_data.copy()
14801487
thread_data["group_id"] = get_group_id_for_user(user, discussion_settings)
1488+
1489+
if "user_group_ids" not in thread_data:
1490+
thread_data["user_group_ids"] = get_user_group_ids_for_user(user, discussion_settings)
1491+
14811492
serializer = ThreadSerializer(data=thread_data, context=context)
14821493
actions_form = ThreadActionsForm(thread_data)
14831494
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
@@ -322,6 +322,7 @@ class ThreadSerializer(_ContentSerializer):
322322
course_id = serializers.CharField()
323323
topic_id = serializers.CharField(source="commentable_id", validators=[validate_not_blank])
324324
group_id = serializers.IntegerField(required=False, allow_null=True)
325+
user_group_ids = serializers.ListField(required=False, allow_null=True)
325326
group_name = serializers.SerializerMethodField()
326327
type = serializers.ChoiceField(
327328
source="thread_type",

lms/djangoapps/discussion/rest_api/views.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,10 @@
2626
from lms.djangoapps.course_api.blocks.api import get_blocks
2727
from lms.djangoapps.course_goals.models import UserActivity
2828
from lms.djangoapps.discussion.django_comment_client import settings as cc_settings
29-
from lms.djangoapps.discussion.django_comment_client.utils import get_group_id_for_comments_service
29+
from lms.djangoapps.discussion.django_comment_client.utils import (
30+
get_group_id_for_comments_service,
31+
get_user_group_ids_for_user_from_cache,
32+
)
3033
from lms.djangoapps.instructor.access import update_forum_role
3134
from openedx.core.djangoapps.discussions.config.waffle import ENABLE_NEW_STRUCTURE_DISCUSSIONS
3235
from openedx.core.djangoapps.discussions.models import DiscussionsConfiguration, Provider
@@ -751,6 +754,8 @@ def get(self, request, course_id=None):
751754
except ValueError:
752755
pass
753756

757+
user_group_ids = get_user_group_ids_for_user_from_cache(request.user, course_key)
758+
754759
query_params = {
755760
"page": page_num,
756761
"per_page": threads_per_page,
@@ -760,6 +765,7 @@ def get(self, request, course_id=None):
760765
"count_flagged": count_flagged,
761766
"thread_type": thread_type,
762767
"sort_key": order_by,
768+
"user_group_ids": user_group_ids,
763769
}
764770
if post_status:
765771
if post_status not in ['flagged', 'unanswered', 'unread', 'unresponded']:

openedx/core/djangoapps/discussions/utils.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717
from xmodule.modulestore.django import modulestore # lint-amnesty, pylint: disable=wrong-import-order
1818
from xmodule.partitions.partitions import ENROLLMENT_TRACK_PARTITION_ID, Group # lint-amnesty, pylint: disable=wrong-import-order
1919
from xmodule.partitions.partitions_service import PartitionService # lint-amnesty, pylint: disable=wrong-import-order
20+
from openedx_user_groups.toggles import is_user_groups_enabled
21+
2022

2123
log = logging.getLogger(__name__)
2224

@@ -106,6 +108,8 @@ def available_division_schemes(course_key: CourseKey) -> List[str]:
106108
available_schemes.append(CourseDiscussionSettings.COHORT)
107109
if enrollment_track_group_count(course_key) > 1:
108110
available_schemes.append(CourseDiscussionSettings.ENROLLMENT_TRACK)
111+
if is_user_groups_enabled(course_key):
112+
available_schemes.append(CourseDiscussionSettings.USER_GROUP)
109113
return available_schemes
110114

111115

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -368,6 +368,7 @@ def handle_create_thread(self, course_id):
368368
thread_type=request_data.get("thread_type", "discussion"),
369369
group_id=request_data.get("group_id", None),
370370
context=request_data.get("context", None),
371+
user_group_ids=request_data.get("user_group_ids", None),
371372
)
372373
return response
373374

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,13 +24,15 @@ class Thread(models.Model):
2424
'abuse_flaggers', 'resp_skip', 'resp_limit', 'resp_total', 'thread_type',
2525
'endorsed_responses', 'non_endorsed_responses', 'non_endorsed_resp_total',
2626
'context', 'last_activity_at', 'closed_by', 'close_reason_code', 'edit_history',
27+
'user_group_ids',
2728
]
2829

2930
# updateable_fields are sent in PUT requests
3031
updatable_fields = [
3132
'title', 'body', 'anonymous', 'anonymous_to_peers', 'course_id', 'read',
3233
'closed', 'user_id', 'commentable_id', 'group_id', 'group_name', 'pinned', 'thread_type',
3334
'close_reason_code', 'edit_reason_code', 'closing_user_id', 'editing_user_id',
35+
'user_group_ids',
3436
]
3537

3638
# 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)