diff --git a/cms/djangoapps/contentstore/course_group_config.py b/cms/djangoapps/contentstore/course_group_config.py
index 569a1f72b372..7c58810aa43c 100644
--- a/cms/djangoapps/contentstore/course_group_config.py
+++ b/cms/djangoapps/contentstore/course_group_config.py
@@ -12,7 +12,7 @@
from cms.djangoapps.contentstore.utils import reverse_usage_url
from common.djangoapps.util.db import MYSQL_MAX_INT, generate_int_id
from lms.lib.utils import get_parent_unit
-from openedx.core.djangoapps.course_groups.partition_scheme import get_cohorted_user_partition
+from openedx.core.djangoapps.course_groups.partition_scheme import get_cohorted_user_partition, get_teamed_user_partition # lint-amnesty, pylint: disable=wrong-import-order
from xmodule.partitions.partitions import MINIMUM_STATIC_PARTITION_ID, ReadOnlyUserPartitionError, UserPartition # lint-amnesty, pylint: disable=wrong-import-order
from xmodule.partitions.partitions_service import get_all_partitions_for_course # lint-amnesty, pylint: disable=wrong-import-order
from xmodule.split_test_block import get_split_user_partitions # lint-amnesty, pylint: disable=wrong-import-order
@@ -22,6 +22,7 @@
RANDOM_SCHEME = "random"
COHORT_SCHEME = "cohort"
ENROLLMENT_SCHEME = "enrollment_track"
+TEAM_SCHEME = "team"
CONTENT_GROUP_CONFIGURATION_DESCRIPTION = _(
'The groups in this configuration can be mapped to cohorts in the Instructor Dashboard.'
@@ -114,7 +115,7 @@ def get_user_partition(self):
raise GroupConfigurationsValidationError(_("unable to load this type of group configuration")) # lint-amnesty, pylint: disable=raise-missing-from
@staticmethod
- def _get_usage_dict(course, unit, block, scheme_name=None):
+ def _get_usage_dict(course, unit, block, scheme_name=COHORT_SCHEME):
"""
Get usage info for unit/block.
"""
@@ -347,7 +348,7 @@ def update_partition_usage_info(store, course, configuration):
return partition_configuration
@staticmethod
- def get_or_create_content_group(store, course):
+ def get_or_create_content_group(store, course, scheme_name=COHORT_SCHEME):
"""
Returns the first user partition from the course which uses the
CohortPartitionScheme, or generates one if no such partition is
@@ -355,14 +356,17 @@ def get_or_create_content_group(store, course):
the client explicitly creates a group within the partition and
POSTs back.
"""
- content_group_configuration = get_cohorted_user_partition(course)
+ if scheme_name == COHORT_SCHEME:
+ content_group_configuration = get_cohorted_user_partition(course)
+ elif scheme_name == TEAM_SCHEME:
+ content_group_configuration = get_teamed_user_partition(course)
if content_group_configuration is None:
content_group_configuration = UserPartition(
id=generate_int_id(MINIMUM_GROUP_ID, MYSQL_MAX_INT, GroupConfiguration.get_used_ids(course)),
- name=CONTENT_GROUP_CONFIGURATION_NAME,
+ name=f"Content Groups for {scheme_name}",
description=CONTENT_GROUP_CONFIGURATION_DESCRIPTION,
groups=[],
- scheme_id=COHORT_SCHEME
+ scheme_id=scheme_name,
)
return content_group_configuration.to_json()
diff --git a/cms/djangoapps/contentstore/utils.py b/cms/djangoapps/contentstore/utils.py
index ba227ecbec0e..b73a6c6f7a08 100644
--- a/cms/djangoapps/contentstore/utils.py
+++ b/cms/djangoapps/contentstore/utils.py
@@ -763,6 +763,7 @@ def get_visibility_partition_info(xblock, course=None):
selectable_partitions = []
# We wish to display enrollment partitions before cohort partitions.
enrollment_user_partitions = get_user_partition_info(xblock, schemes=["enrollment_track"], course=course)
+ team_partitions = get_user_partition_info(xblock, schemes=["team"], course=course)
# For enrollment partitions, we only show them if there is a selected group or
# or if the number of groups > 1.
@@ -776,7 +777,7 @@ def get_visibility_partition_info(xblock, course=None):
selectable_partitions += get_user_partition_info(xblock, schemes=[CONTENT_TYPE_GATING_SCHEME], course=course)
# Now add the cohort user partitions.
- selectable_partitions = selectable_partitions + get_user_partition_info(xblock, schemes=["cohort"], course=course)
+ selectable_partitions = selectable_partitions + get_user_partition_info(xblock, schemes=["cohort"], course=course) + team_partitions
# Find the first partition with a selected group. That will be the one initially enabled in the dialog
# (if the course has only been added in Studio, only one partition should have a selected group).
diff --git a/cms/djangoapps/contentstore/views/course.py b/cms/djangoapps/contentstore/views/course.py
index b42c011c4b20..a603f5b84c52 100644
--- a/cms/djangoapps/contentstore/views/course.py
+++ b/cms/djangoapps/contentstore/views/course.py
@@ -81,6 +81,7 @@
COHORT_SCHEME,
ENROLLMENT_SCHEME,
RANDOM_SCHEME,
+ TEAM_SCHEME,
GroupConfiguration,
GroupConfigurationsValidationError
)
@@ -1608,11 +1609,13 @@ def group_configurations_list_handler(request, course_key_string):
# Add it to the front of the list if it should be shown.
if should_show_enrollment_track:
displayable_partitions.insert(0, partition)
+ elif partition['scheme'] == TEAM_SCHEME:
+ has_content_groups = True
+ displayable_partitions.append(partition)
elif partition['scheme'] != RANDOM_SCHEME:
# Experiment group configurations are handled explicitly above. We don't
# want to display their groups twice.
displayable_partitions.append(partition)
-
# Set the sort-order. Higher numbers sort earlier
scheme_priority = defaultdict(lambda: -1, {
ENROLLMENT_SCHEME: 1,
@@ -1623,6 +1626,7 @@ def group_configurations_list_handler(request, course_key_string):
# This will add ability to add new groups in the view.
if not has_content_groups:
displayable_partitions.append(GroupConfiguration.get_or_create_content_group(store, course))
+ displayable_partitions.append(GroupConfiguration.get_or_create_content_group(store, course, scheme_name="team"))
return render_to_response('group_configurations.html', {
'context_course': course,
'group_configuration_url': group_configuration_url,
diff --git a/lms/djangoapps/teams/static/teams/js/models/team.js b/lms/djangoapps/teams/static/teams/js/models/team.js
index 010116e3717e..3ccd17da12c7 100644
--- a/lms/djangoapps/teams/static/teams/js/models/team.js
+++ b/lms/djangoapps/teams/static/teams/js/models/team.js
@@ -16,7 +16,8 @@
country: '',
language: '',
membership: [],
- last_activity_at: ''
+ last_activity_at: '',
+ content_group: '',
},
initialize: function(options) {
diff --git a/lms/djangoapps/teams/static/teams/js/views/edit_team.js b/lms/djangoapps/teams/static/teams/js/views/edit_team.js
index 4413faf5de33..737449b62860 100644
--- a/lms/djangoapps/teams/static/teams/js/views/edit_team.js
+++ b/lms/djangoapps/teams/static/teams/js/views/edit_team.js
@@ -27,6 +27,9 @@
this.topic = options.topic;
this.collection = options.collection;
this.action = options.action;
+ this.contentGroupsNameMap = _.map(this.context.contentGroups, function(group) {
+ return [group.id, group.name]
+ });
if (this.action === 'create') {
this.teamModel = new TeamModel({});
@@ -57,6 +60,17 @@
+ 'goals or direction of the team (maximum 300 characters).')
});
+ this.teamContentGroupsField = new FieldViews.DropdownFieldView({
+ model: this.teamModel,
+ title: gettext('Content Groups'),
+ valueAttribute: 'content_group',
+ required: false,
+ showMessages: false,
+ options: this.contentGroupsNameMap,
+ helpMessage: gettext(
+ 'The content groups that the team is associated with.')
+ });
+
this.teamLanguageField = new FieldViews.DropdownFieldView({
model: this.teamModel,
title: gettext('Language'),
@@ -92,6 +106,7 @@
);
this.set(this.teamNameField, '.team-required-fields');
this.set(this.teamDescriptionField, '.team-required-fields');
+ this.set(this.teamContentGroupsField, '.team-optional-fields');
this.set(this.teamLanguageField, '.team-optional-fields');
this.set(this.teamCountryField, '.team-optional-fields');
return this;
@@ -105,17 +120,19 @@
this.$(selector).append(view.render().$el);
}
},
-
createOrUpdateTeam: function(event) {
event.preventDefault();
var view = this, // eslint-disable-line vars-on-top
teamLanguage = this.teamLanguageField.fieldValue(),
teamCountry = this.teamCountryField.fieldValue(),
+ teamContentGroup = this.teamContentGroupsField.fieldValue(),
data = {
name: this.teamNameField.fieldValue(),
description: this.teamDescriptionField.fieldValue(),
language: _.isNull(teamLanguage) ? '' : teamLanguage,
- country: _.isNull(teamCountry) ? '' : teamCountry
+ country: _.isNull(teamCountry) ? '' : teamCountry,
+ content_group: _.isNull(teamContentGroup) ? '' : teamContentGroup,
+ user_partition_id: this.context.partitionID
},
saveOptions = {
wait: true
diff --git a/lms/djangoapps/teams/templates/teams/teams.html b/lms/djangoapps/teams/templates/teams/teams.html
index 3dfa5b51aeb6..29a5fca4d652 100644
--- a/lms/djangoapps/teams/templates/teams/teams.html
+++ b/lms/djangoapps/teams/templates/teams/teams.html
@@ -24,6 +24,10 @@
<%include file="/courseware/course_navigation.html" args="active_page='teams'" />
+<%!
+from openedx.core.djangoapps.course_groups.partition_scheme import get_teamed_user_partition
+%>
+
@@ -38,7 +42,21 @@
<%include file="../discussion/_js_body_dependencies.html" />
<%static:require_module module_name="teams/js/teams_tab_factory" class_name="TeamsTabFactory">
- TeamsTabFactory({
+ <%
+ teamed_user_partition = get_teamed_user_partition(course)
+ content_groups = teamed_user_partition.groups if teamed_user_partition else []
+ %>
+ var groupUserPartitionId = ${teamed_user_partition.id if teamed_user_partition else None | n, dump_js_escaped_json},
+ contentGroups = [
+ % for content_group in content_groups:
+ {
+ id: ${content_group.id | n, dump_js_escaped_json},
+ name: "${content_group.name | n, js_escaped_string}",
+ user_partition_id: groupUserPartitionId
+ },
+ % endfor
+ ];
+ TeamsTabFactory({
courseID: '${str(course.id) | n, js_escaped_string}',
topics: ${topics | n, dump_js_escaped_json},
hasManagedTopic: ${has_managed_teamset | n, dump_js_escaped_json},
@@ -59,7 +77,9 @@
courseMaxTeamSize: ${course.teams_configuration.default_max_team_size | n, dump_js_escaped_json},
languages: ${languages | n, dump_js_escaped_json},
countries: ${countries | n, dump_js_escaped_json},
- teamsBaseUrl: '${teams_base_url | n, js_escaped_string}'
+ teamsBaseUrl: '${teams_base_url | n, js_escaped_string}',
+ contentGroups: contentGroups,
+ partitionID: groupUserPartitionId,
});
%static:require_module>
%block>
diff --git a/lms/djangoapps/teams/views.py b/lms/djangoapps/teams/views.py
index a5370ffa937f..3f6bc856894e 100644
--- a/lms/djangoapps/teams/views.py
+++ b/lms/djangoapps/teams/views.py
@@ -71,6 +71,8 @@
from .toggles import are_team_submissions_enabled
from .utils import emit_team_event
+from openedx.core.djangoapps.course_groups.api import *
+
TEAM_MEMBERSHIPS_PER_PAGE = 5
TOPICS_PER_PAGE = 12
MAXIMUM_SEARCH_SIZE = 10000
@@ -637,6 +639,13 @@ def post(self, request):
)
data = CourseTeamSerializer(team, context={"request": request}).data
+ group = add_team_to_course(
+ team.name,
+ course_key,
+ )
+ if request.data.get("user_partition_id") and request.data.get("content_group"):
+ link_team_to_partition_group(group, request.data["user_partition_id"], request.data["content_group"])
+
return Response(data)
def get_page(self):
@@ -838,6 +847,27 @@ def delete(self, request, team_id):
})
return Response(status=status.HTTP_204_NO_CONTENT)
+ def patch(self, request, team_id):
+ course_team = get_object_or_404(CourseTeam, team_id=team_id)
+ data = request.data
+
+ content_group_id = data["content_group"]
+ if not content_group_id:
+ return super().patch(request, team_id)
+
+ group = get_course_team_qs(course_team.course_id).filter(name=course_team.name).first()
+ partitioned_group_info = get_group_info_for_team(group)
+ if not partitioned_group_info:
+ link_team_to_partition_group(group, data["user_partition_id"], data["content_group"])
+ return super().patch(request, team_id)
+
+ existing_content_group_id, existing_partition_id = get_group_info_for_team(group)
+ if content_group_id != existing_content_group_id or data["user_partition_id"] != existing_partition_id:
+ unlink_team_partition_group(group)
+ link_team_to_partition_group(group, data["user_partition_id"], data["content_group"])
+
+ return super().patch(request, team_id)
+
class TeamsAssignmentsView(GenericAPIView):
"""
@@ -1479,6 +1509,8 @@ def post(self, request):
try:
membership = team.add_user(user)
+ group = get_course_team_qs(team.course_id).get(name=team.name)
+ add_user_to_team(group, user.username)
emit_team_event(
'edx.team.learner_added',
team.course_id,
diff --git a/openedx/core/djangoapps/course_groups/api.py b/openedx/core/djangoapps/course_groups/api.py
index a4dec915fbe5..929a17569987 100644
--- a/openedx/core/djangoapps/course_groups/api.py
+++ b/openedx/core/djangoapps/course_groups/api.py
@@ -1,11 +1,16 @@
"""
course_groups API
"""
-
-
from django.contrib.auth.models import User # lint-amnesty, pylint: disable=imported-auth-user
+from django.core.exceptions import ValidationError
+from django.core.validators import validate_email
+from django.http import Http404
+
+from common.djangoapps.student.models import get_user_by_username_or_email
+from openedx.core.djangoapps.course_groups.models import CohortMembership, CourseUserGroup, TeamMembership
+from openedx.core.lib.courses import get_course_by_id
-from openedx.core.djangoapps.course_groups.models import CohortMembership
+from .models import CourseUserGroupPartitionGroup, CourseTeamGroup
def remove_user_from_cohort(course_key, username, cohort_id=None):
@@ -27,3 +32,132 @@ def remove_user_from_cohort(course_key, username, cohort_id=None):
pass
else:
membership.delete()
+
+def get_assignment_type(user_group):
+ """
+ Get assignment type for cohort.
+ """
+ course_cohort = user_group.cohort
+ return course_cohort.assignment_type
+
+
+def get_team(user, course_key, assign=True, use_cached=False):
+ if user is None or user.is_anonymous:
+ return None
+
+ try:
+ membership = TeamMembership.objects.get(
+ course_id=course_key,
+ user_id=user.id,
+ )
+ return membership.course_user_group
+ except TeamMembership.DoesNotExist:
+ if not assign:
+ return None
+
+def get_team_by_id(course_key, group_id):
+ """
+ Return the CourseUserGroup object for the given cohort. Raises DoesNotExist
+ it isn't present. Uses the course_key for extra validation.
+ """
+ return CourseUserGroup.objects.get(
+ course_id=course_key,
+ group_type=CourseUserGroup.TEAM,
+ id=group_id
+ )
+
+
+def link_team_to_partition_group(group, partition_id, group_id):
+ """
+ Create group to partition_id/group_id link.
+ """
+ CourseUserGroupPartitionGroup(
+ course_user_group=group,
+ partition_id=partition_id,
+ group_id=group_id,
+ ).save()
+
+
+def unlink_team_partition_group(group):
+ """
+ Remove any existing group to partition_id/group_id link.
+ """
+ CourseUserGroupPartitionGroup.objects.filter(course_user_group=group).delete()
+
+
+def get_course_teams(course_id=None):
+ query_set = CourseUserGroup.objects.filter(
+ course_id=course_id,
+ group_type=CourseUserGroup.TEAM,
+ )
+ return list(query_set)
+
+def get_course_team_qs(course_id=None):
+ query_set = CourseUserGroup.objects.filter(
+ course_id=course_id,
+ group_type=CourseUserGroup.TEAM,
+ )
+ return query_set
+
+def is_team_exists(course_key, name):
+ """
+ Check if a group already exists.
+ """
+ return CourseUserGroup.objects.filter(course_id=course_key, group_type=CourseUserGroup.TEAM, name=name).exists()
+
+
+def add_team_to_course(name, course_key, professor=None):
+ """
+ Adds a group to a course.
+ """
+ if is_team_exists(course_key, name):
+ raise ValueError("You cannot create two groups with the same name")
+
+ try:
+ course = get_course_by_id(course_key)
+ except Http404:
+ raise ValueError("Invalid course_key") # lint-amnesty, pylint: disable=raise-missing-from
+
+ return CourseTeamGroup.create(
+ group_name=name,
+ course_id=course.id,
+ professor=professor,
+ ).course_user_group
+
+
+def add_user_to_team(group, username_or_email_or_user):
+ try:
+ if hasattr(username_or_email_or_user, 'email'):
+ user = username_or_email_or_user
+ else:
+ user = get_user_by_username_or_email(username_or_email_or_user)
+
+ return TeamMembership.assign(group, user)
+ except User.DoesNotExist as ex: # Note to self: TOO COHORT SPECIFIC!
+ # If username_or_email is an email address, store in database.
+ try:
+ return (None, None, True)
+ except ValidationError as invalid:
+ if "@" in username_or_email_or_user: # lint-amnesty, pylint: disable=no-else-raise
+ raise invalid
+ else:
+ raise ex # lint-amnesty, pylint: disable=raise-missing-from
+
+
+def get_group_info_for_team(team):
+ """
+ Get the ids of the group and partition to which this cohort has been linked
+ as a tuple of (int, int).
+
+ If the cohort has not been linked to any group/partition, both values in the
+ tuple will be None.
+
+ The partition group info is cached for the duration of a request. Pass
+ use_cached=True to use the cached value instead of fetching from the
+ database.
+ """
+ try:
+ partition_group = CourseUserGroupPartitionGroup.objects.get(course_user_group=team)
+ return partition_group.group_id, partition_group.partition_id
+ except CourseUserGroupPartitionGroup.DoesNotExist:
+ pass
diff --git a/openedx/core/djangoapps/course_groups/migrations/0004_auto_20231116_2038.py b/openedx/core/djangoapps/course_groups/migrations/0004_auto_20231116_2038.py
new file mode 100644
index 000000000000..d240041d10d4
--- /dev/null
+++ b/openedx/core/djangoapps/course_groups/migrations/0004_auto_20231116_2038.py
@@ -0,0 +1,38 @@
+# Generated by Django 3.2.22 on 2023-11-16 20:38
+
+from django.conf import settings
+from django.db import migrations, models
+import django.db.models.deletion
+import opaque_keys.edx.django.models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+ ('course_groups', '0003_auto_20170609_1455'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='courseusergroup',
+ name='group_type',
+ field=models.CharField(choices=[('cohort', 'Cohort'), ('team', 'Team')], max_length=20),
+ ),
+ migrations.CreateModel(
+ name='TeamMembership',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('course_id', opaque_keys.edx.django.models.CourseKeyField(max_length=255)),
+ ('course_user_group', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='course_groups.courseusergroup')),
+ ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
+ ],
+ ),
+ migrations.CreateModel(
+ name='CourseTeamGroup',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('course_user_group', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='group', to='course_groups.courseusergroup')),
+ ],
+ ),
+ ]
diff --git a/openedx/core/djangoapps/course_groups/models.py b/openedx/core/djangoapps/course_groups/models.py
index 0b731cc6b608..cb48f561d99b 100644
--- a/openedx/core/djangoapps/course_groups/models.py
+++ b/openedx/core/djangoapps/course_groups/models.py
@@ -63,7 +63,8 @@ class Meta:
# For now, only have group type 'cohort', but adding a type field to support
# things like 'question_discussion', 'friends', 'off-line-class', etc
COHORT = 'cohort' # If changing this string, update it in migration 0006.forwards() as well
- GROUP_TYPE_CHOICES = ((COHORT, 'Cohort'),)
+ TEAM = 'team'
+ GROUP_TYPE_CHOICES = ((COHORT, 'Cohort'), (TEAM, 'Team'),)
group_type = models.CharField(max_length=20, choices=GROUP_TYPE_CHOICES)
@classmethod
@@ -190,6 +191,33 @@ def save(self, force_insert=False, force_update=False, using=None, update_fields
)
+class TeamMembership(models.Model):
+ """
+ Used internally to enforce particular conditions.
+
+ .. no_pii:
+ """
+ course_user_group = models.ForeignKey(CourseUserGroup, on_delete=models.CASCADE)
+ user = models.ForeignKey(User, on_delete=models.CASCADE)
+ course_id = CourseKeyField(max_length=255)
+
+ def clean_fields(self, *args, **kwargs): # lint-amnesty, pylint: disable=signature-differs
+ if self.course_id is None:
+ self.course_id = self.course_user_group.course_id
+ super().clean_fields(*args, **kwargs)
+
+ @classmethod
+ def assign(cls, group, user):
+ with transaction.atomic():
+ membership, created = cls.objects.select_for_update().get_or_create(
+ user=user,
+ course_id=group.course_id,
+ course_user_group=group,
+ )
+ membership.course_user_group.users.add(user)
+ return membership
+
+
# Needs to exist outside class definition in order to use 'sender=CohortMembership'
@receiver(pre_delete, sender=CohortMembership)
def remove_user_from_cohort(sender, instance, **kwargs): # pylint: disable=unused-argument
@@ -295,6 +323,34 @@ def create(cls, cohort_name=None, course_id=None, course_user_group=None, assign
return course_cohort
+class CourseTeamGroup(models.Model):
+ """
+ This model represents the new group-type related info.
+
+ .. no_pii:
+ """
+ course_user_group = models.OneToOneField(
+ CourseUserGroup,
+ unique=True,
+ related_name='group',
+ on_delete=models.CASCADE,
+ )
+
+ @classmethod
+ def create(cls, group_name=None, course_id=None, course_user_group=None, professor=None):
+ if course_user_group is None:
+ course_user_group, __ = CourseUserGroup.create(
+ group_name,
+ course_id,
+ group_type=CourseUserGroup.TEAM,
+ )
+
+ course_group, __ = cls.objects.get_or_create(
+ course_user_group=course_user_group,
+ )
+
+ return course_group
+
class UnregisteredLearnerCohortAssignments(DeletableByUserValue, models.Model):
"""
Tracks the assignment of an unregistered learner to a course's cohort.
diff --git a/openedx/core/djangoapps/course_groups/partition_scheme.py b/openedx/core/djangoapps/course_groups/partition_scheme.py
index 60e7c5f915c2..70e9c6ea135b 100644
--- a/openedx/core/djangoapps/course_groups/partition_scheme.py
+++ b/openedx/core/djangoapps/course_groups/partition_scheme.py
@@ -13,6 +13,7 @@
from xmodule.partitions.partitions import NoSuchUserPartitionGroupError # lint-amnesty, pylint: disable=wrong-import-order
from .cohorts import get_cohort, get_group_info_for_cohort
+from .api import get_team, get_group_info_for_team
log = logging.getLogger(__name__)
@@ -99,7 +100,57 @@ def get_cohorted_user_partition(course):
one cohorted user partition.
"""
for user_partition in course.user_partitions:
- if user_partition.scheme == CohortPartitionScheme:
+ if user_partition.scheme == CohortPartitionScheme: # Deberíamos poder añadir el otro chequeo acá
return user_partition
return None
+
+
+def get_teamed_user_partition(course):
+ """
+ Returns the first user partition from the specified course which uses the CohortPartitionScheme,
+ or None if one is not found. Note that it is currently recommended that each course have only
+ one cohorted user partition.
+ """
+ for user_partition in course.user_partitions:
+ if user_partition.scheme == TeamPartitionScheme:
+ return user_partition
+
+ return None
+
+
+class TeamPartitionScheme:
+
+ @classmethod
+ def get_teamed_user_partition(cls, course):
+ for user_partition in course.user_partitions:
+ if user_partition.scheme == cls:
+ return user_partition
+
+ return None
+
+ @classmethod
+ def get_group_for_user(cls, course_key, user, user_partition):
+ """
+ Returns the (Content) Group from the specified user partition to which the user
+ is assigned, via their group-type membership and any mappings from groups
+ to partitions / (content) groups that might exist.
+
+ If the user has no group-type mapping, or there is no (valid) group ->
+ partition group mapping found, the function returns None.
+ """
+ team = get_team(user, course_key)
+ if team is None:
+ return None
+
+ team_id, partition_id = get_group_info_for_team(team)
+ if partition_id is None:
+ return None
+
+ if partition_id != user_partition.id:
+ return None
+
+ try:
+ return user_partition.get_group(team_id)
+ except NoSuchUserPartitionGroupError:
+ return None
diff --git a/setup.py b/setup.py
index f405f92a95b4..e7756536773f 100644
--- a/setup.py
+++ b/setup.py
@@ -104,6 +104,7 @@
"openedx.user_partition_scheme": [
"random = openedx.core.djangoapps.user_api.partition_schemes:RandomUserPartitionScheme",
"cohort = openedx.core.djangoapps.course_groups.partition_scheme:CohortPartitionScheme",
+ "team = openedx.core.djangoapps.course_groups.partition_scheme:TeamPartitionScheme",
"verification = openedx.core.djangoapps.user_api.partition_schemes:ReturnGroup1PartitionScheme",
"enrollment_track = openedx.core.djangoapps.verified_track_content.partition_scheme:EnrollmentTrackPartitionScheme", # lint-amnesty, pylint: disable=line-too-long
"content_type_gate = openedx.features.content_type_gating.partitions:ContentTypeGatingPartitionScheme",