Skip to content

Commit 4369055

Browse files
fix: Add group_id and user_partition_id support to Cohorts API v1 (#37982)
1 parent b98e41e commit 4369055

3 files changed

Lines changed: 333 additions & 30 deletions

File tree

docs/lms-openapi.yaml

Lines changed: 95 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -629,10 +629,9 @@ paths:
629629
parameters: []
630630
responses:
631631
'200':
632-
description: ''
632+
description: Successful response with cohort details.
633633
schema:
634-
type: object
635-
properties: {}
634+
$ref: '#/definitions/Cohort'
636635
tags:
637636
- cohorts
638637
post:
@@ -643,32 +642,26 @@ paths:
643642
in: body
644643
required: true
645644
schema:
646-
type: object
647-
properties: {}
645+
$ref: '#/definitions/CohortCreate'
648646
responses:
649-
'201':
650-
description: ''
647+
'200':
648+
description: Successful response with created cohort details.
651649
schema:
652-
type: object
653-
properties: {}
650+
$ref: '#/definitions/Cohort'
654651
tags:
655652
- cohorts
656653
patch:
657654
operationId: cohorts_v1_courses_cohorts_partial_update
658-
description: Endpoint to update a cohort name and/or assignment type.
655+
description: Endpoint to update a cohort name, assignment type, and/or content group association.
659656
parameters:
660657
- name: data
661658
in: body
662659
required: true
663660
schema:
664-
type: object
665-
properties: {}
661+
$ref: '#/definitions/CohortUpdate'
666662
responses:
667-
'200':
668-
description: ''
669-
schema:
670-
type: object
671-
properties: {}
663+
'204':
664+
description: Successful update, no content returned.
672665
tags:
673666
- cohorts
674667
parameters:
@@ -11040,6 +11033,91 @@ paths:
1104011033
required: true
1104111034
type: string
1104211035
definitions:
11036+
Cohort:
11037+
description: A cohort representation
11038+
type: object
11039+
properties:
11040+
id:
11041+
title: ID
11042+
description: The integer identifier for a cohort.
11043+
type: integer
11044+
name:
11045+
title: Name
11046+
description: The string identifier for a cohort.
11047+
type: string
11048+
user_count:
11049+
title: User Count
11050+
description: The number of students in the cohort.
11051+
type: integer
11052+
assignment_type:
11053+
title: Assignment Type
11054+
description: The assignment type ("manual" or "random").
11055+
type: string
11056+
enum:
11057+
- manual
11058+
- random
11059+
user_partition_id:
11060+
title: User Partition ID
11061+
description: The integer identifier of the UserPartition (content group configuration).
11062+
type: integer
11063+
x-nullable: true
11064+
group_id:
11065+
title: Group ID
11066+
description: The integer identifier of the specific group in the partition.
11067+
type: integer
11068+
x-nullable: true
11069+
CohortCreate:
11070+
description: Request body for creating a new cohort
11071+
required:
11072+
- name
11073+
- assignment_type
11074+
type: object
11075+
properties:
11076+
name:
11077+
title: Name
11078+
description: The string identifier for a cohort.
11079+
type: string
11080+
minLength: 1
11081+
assignment_type:
11082+
title: Assignment Type
11083+
description: The assignment type ("manual" or "random").
11084+
type: string
11085+
enum:
11086+
- manual
11087+
- random
11088+
user_partition_id:
11089+
title: User Partition ID
11090+
description: The integer identifier of the UserPartition (content group configuration). Required if group_id is specified.
11091+
type: integer
11092+
group_id:
11093+
title: Group ID
11094+
description: The integer identifier of the specific group in the partition.
11095+
type: integer
11096+
CohortUpdate:
11097+
description: Request body for updating a cohort. At least one of name, assignment_type, or group_id must be provided.
11098+
type: object
11099+
properties:
11100+
name:
11101+
title: Name
11102+
description: The string identifier for a cohort.
11103+
type: string
11104+
minLength: 1
11105+
assignment_type:
11106+
title: Assignment Type
11107+
description: The assignment type ("manual" or "random").
11108+
type: string
11109+
enum:
11110+
- manual
11111+
- random
11112+
user_partition_id:
11113+
title: User Partition ID
11114+
description: The integer identifier of the UserPartition (content group configuration). Required if group_id is specified (non-null).
11115+
type: integer
11116+
group_id:
11117+
title: Group ID
11118+
description: The integer identifier of the specific group in the partition. Set to null to remove the content group association.
11119+
type: integer
11120+
x-nullable: true
1104311121
PermissionValidation:
1104411122
description: The permissions to validate
1104511123
required:

openedx/core/djangoapps/course_groups/tests/test_api_views.py

Lines changed: 154 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
"""
44

55

6-
import json
76
import tempfile
87

98
import ddt
@@ -15,16 +14,17 @@
1514
from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase # lint-amnesty, pylint: disable=wrong-import-order
1615
from xmodule.modulestore.tests.factories import ToyCourseFactory # lint-amnesty, pylint: disable=wrong-import-order
1716

18-
from .. import cohorts
19-
from .helpers import CohortFactory
17+
from openedx.core.djangoapps.course_groups import cohorts
18+
from openedx.core.djangoapps.course_groups.views import link_cohort_to_partition_group
19+
from openedx.core.djangoapps.course_groups.tests.helpers import CohortFactory
2020

2121
USERNAME = 'honor'
2222
USER_MAIL = 'honor@example.com'
2323
SETTINGS_PAYLOAD = '{"is_cohorted": true}'
2424
HANDLER_POST_PAYLOAD = '{"name":"Default","user_count":0,"assignment_type":"random","user_partition_id":null\
2525
,"group_id":null}'
2626
HANDLER_PATCH_PAYLOAD = '{"name":"Default Group","group_id":null,"user_partition_id":null,"assignment_type":"random"}'
27-
ADD_USER_PAYLOAD = json.dumps({'users': [USER_MAIL, ]})
27+
ADD_USER_PAYLOAD = {'users': [USER_MAIL, ]}
2828
CSV_DATA = f'''email,cohort\n{USER_MAIL},DEFAULT'''
2929

3030

@@ -307,7 +307,7 @@ def test_list_users_in_cohort(self, is_staff, status):
307307
assert response.status_code == status
308308

309309
if status == 200:
310-
results = json.loads(response.content.decode('utf-8'))['results']
310+
results = response.json()['results']
311311
expected_results = [{
312312
'username': user.username,
313313
'email': user.email,
@@ -406,7 +406,7 @@ def test_add_users_to_cohort_different_types_of_users(self):
406406
"invalid": ["foo@bar"],
407407
"present": ["user2"]
408408
}
409-
assert json.loads(response.content.decode('utf-8')) == expected_response
409+
assert response.json() == expected_response
410410

411411
def test_remove_user_from_cohort_missing_username(self):
412412
"""
@@ -458,3 +458,151 @@ def test_add_users_csv(self, is_staff, payload, status):
458458
response = self.client.post(path=path,
459459
data={'uploaded-file': file_pointer})
460460
assert response.status_code == status
461+
462+
def test_post_cohort_with_group_id(self):
463+
"""
464+
Test creating a cohort with group_id and user_partition_id.
465+
"""
466+
path = reverse('api_cohorts:cohort_handler', kwargs={'course_key_string': self.course_str})
467+
self.client.login(username=self.staff_user.username, password=self.password)
468+
469+
payload = {
470+
'name': 'TestCohort',
471+
'assignment_type': 'manual',
472+
'group_id': 1,
473+
'user_partition_id': 50
474+
}
475+
response = self.client.post(path=path, data=payload, content_type='application/json')
476+
assert response.status_code == 200
477+
478+
data = response.json()
479+
assert data['name'] == 'TestCohort'
480+
assert data['assignment_type'] == 'manual'
481+
assert data['group_id'] == 1
482+
assert data['user_partition_id'] == 50
483+
assert data['user_count'] == 0
484+
assert 'id' in data
485+
486+
def test_post_cohort_with_group_id_missing_partition_id(self):
487+
"""
488+
Test that creating a cohort with group_id but without user_partition_id returns an error.
489+
"""
490+
path = reverse('api_cohorts:cohort_handler', kwargs={'course_key_string': self.course_str})
491+
self.client.login(username=self.staff_user.username, password=self.password)
492+
493+
payload = {
494+
'name': 'TestCohort',
495+
'assignment_type': 'manual',
496+
'group_id': 1
497+
}
498+
response = self.client.post(path=path, data=payload, content_type='application/json')
499+
assert response.status_code == 400
500+
501+
data = response.json()
502+
assert data['developer_message'] == 'If group_id is specified, user_partition_id must also be specified.'
503+
assert data['error_code'] == 'missing-user-partition-id'
504+
505+
def test_patch_cohort_set_group_id(self):
506+
"""
507+
Test updating a cohort to set group_id and user_partition_id.
508+
"""
509+
cohort = cohorts.add_cohort(self.course_key, "TestCohort", "manual")
510+
path = reverse(
511+
'api_cohorts:cohort_handler',
512+
kwargs={'course_key_string': self.course_str, 'cohort_id': cohort.id}
513+
)
514+
self.client.login(username=self.staff_user.username, password=self.password)
515+
516+
payload = {
517+
'group_id': 2,
518+
'user_partition_id': 50
519+
}
520+
response = self.client.patch(path=path, data=payload, content_type='application/json')
521+
assert response.status_code == 204
522+
523+
# Verify by fetching the cohort
524+
response = self.client.get(path=path)
525+
data = response.json()
526+
assert data['id'] == cohort.id
527+
assert data['name'] == 'TestCohort'
528+
assert data['assignment_type'] == 'manual'
529+
assert data['group_id'] == 2
530+
assert data['user_partition_id'] == 50
531+
532+
def test_patch_cohort_remove_group_id(self):
533+
"""
534+
Test updating a cohort to remove the group_id association by setting it to null.
535+
"""
536+
cohort = cohorts.add_cohort(self.course_key, "TestCohort", "manual")
537+
link_cohort_to_partition_group(cohort, 50, 1)
538+
539+
path = reverse(
540+
'api_cohorts:cohort_handler',
541+
kwargs={'course_key_string': self.course_str, 'cohort_id': cohort.id}
542+
)
543+
self.client.login(username=self.staff_user.username, password=self.password)
544+
545+
# Verify the cohort has a group_id
546+
response = self.client.get(path=path)
547+
data = response.json()
548+
assert data['id'] == cohort.id
549+
assert data['name'] == 'TestCohort'
550+
assert data['group_id'] == 1
551+
assert data['user_partition_id'] == 50
552+
553+
# Remove the group_id by setting it to null
554+
payload = {'group_id': None}
555+
response = self.client.patch(path=path, data=payload, content_type='application/json')
556+
assert response.status_code == 204
557+
558+
# Verify the group_id was removed but other fields unchanged
559+
response = self.client.get(path=path)
560+
data = response.json()
561+
assert data['id'] == cohort.id
562+
assert data['name'] == 'TestCohort'
563+
assert data['assignment_type'] == 'manual'
564+
assert data['group_id'] is None
565+
assert data['user_partition_id'] is None
566+
567+
def test_patch_cohort_with_group_id_missing_partition_id(self):
568+
"""
569+
Test that updating a cohort with group_id but without user_partition_id returns an error.
570+
"""
571+
cohort = cohorts.add_cohort(self.course_key, "TestCohort", "manual")
572+
path = reverse(
573+
'api_cohorts:cohort_handler',
574+
kwargs={'course_key_string': self.course_str, 'cohort_id': cohort.id}
575+
)
576+
self.client.login(username=self.staff_user.username, password=self.password)
577+
578+
payload = {'group_id': 2}
579+
response = self.client.patch(path=path, data=payload, content_type='application/json')
580+
assert response.status_code == 400
581+
582+
data = response.json()
583+
assert data['developer_message'] == 'If group_id is specified, user_partition_id must also be specified.'
584+
assert data['error_code'] == 'missing-user-partition-id'
585+
586+
def test_patch_cohort_with_name_only(self):
587+
"""
588+
Test that PATCH with only name is now valid (previously required assignment_type too).
589+
"""
590+
cohort = cohorts.add_cohort(self.course_key, "OldName", "manual")
591+
path = reverse(
592+
'api_cohorts:cohort_handler',
593+
kwargs={'course_key_string': self.course_str, 'cohort_id': cohort.id}
594+
)
595+
self.client.login(username=self.staff_user.username, password=self.password)
596+
597+
payload = {'name': 'NewName'}
598+
response = self.client.patch(path=path, data=payload, content_type='application/json')
599+
assert response.status_code == 204
600+
601+
# Verify the name was updated and other fields unchanged
602+
response = self.client.get(path=path)
603+
data = response.json()
604+
assert data['id'] == cohort.id
605+
assert data['name'] == 'NewName'
606+
assert data['assignment_type'] == 'manual'
607+
assert data['group_id'] is None
608+
assert data['user_partition_id'] is None

0 commit comments

Comments
 (0)