Skip to content

Commit

Permalink
refactor: implementation with dynamic partition generator
Browse files Browse the repository at this point in the history
  • Loading branch information
mariajgrimaldi committed Nov 23, 2023
1 parent e800ae7 commit 7e1c023
Show file tree
Hide file tree
Showing 9 changed files with 348 additions and 6 deletions.
3 changes: 3 additions & 0 deletions cms/djangoapps/contentstore/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -770,6 +770,9 @@ def get_visibility_partition_info(xblock, course=None):
if len(partition["groups"]) > 1 or any(group["selected"] for group in partition["groups"]):
selectable_partitions.append(partition)

team_user_partitions = get_user_partition_info(xblock, schemes=["team"], course=course)
selectable_partitions += team_user_partitions

course_key = xblock.scope_ids.usage_id.course_key
is_library = isinstance(course_key, LibraryLocator)
if not is_library and ContentTypeGatingConfig.current(course_key=course_key).studio_override_enabled:
Expand Down
55 changes: 53 additions & 2 deletions cms/djangoapps/models/settings/course_metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,15 @@
from cms.djangoapps.contentstore import toggles
from common.djangoapps.xblock_django.models import XBlockStudioConfigurationFlag
from openedx.core.djangoapps.course_apps.toggles import exams_ida_enabled
from openedx.core.djangoapps.course_groups.partition_generator import MINIMUM_DYNAMIC_TEAM_PARTITION_ID
from openedx.core.djangoapps.course_groups.partition_scheme import CONTENT_GROUPS_FOR_TEAMS
from openedx.core.djangoapps.discussions.config.waffle_utils import legacy_discussion_experience_enabled
from openedx.core.lib.teams_config import TeamsetType
from openedx.features.course_experience import COURSE_ENABLE_UNENROLLED_ACCESS_FLAG
from xmodule.course_block import get_available_providers # lint-amnesty, pylint: disable=wrong-import-order
from xmodule.modulestore.django import modulestore # lint-amnesty, pylint: disable=wrong-import-order
from xmodule.modulestore.exceptions import InvalidProctoringProvider # lint-amnesty, pylint: disable=wrong-import-order
from xmodule.modulestore.exceptions import InvalidProctoringProvider # lint-amnesty, pylint: disable=wrong-import-order
from xmodule.partitions.partitions import MINIMUM_STATIC_PARTITION_ID

LOGGER = logging.getLogger(__name__)

Expand Down Expand Up @@ -267,6 +270,7 @@ def validate_and_update_from_json(cls, block, jsondict, user, filter_tabs=True):
did_validate = False
errors.append({'key': key, 'message': err_message, 'model': model})

cls.fill_teams_user_partitions_ids(block.id, filtered_dict)
team_setting_errors = cls.validate_team_settings(filtered_dict)
if team_setting_errors:
errors = errors + team_setting_errors
Expand All @@ -283,6 +287,37 @@ def validate_and_update_from_json(cls, block, jsondict, user, filter_tabs=True):

return did_validate, errors, updated_data

@classmethod
def fill_teams_user_partitions_ids(cls, course_key, settings_dict):
"""
Fill the `dynamic_user_partition_id` in the team settings if it is not set.
This is used by the Dynamic Team Partition Generator to create the dynamic user partitions
based on the team-sets defined in the course.
"""
if not CONTENT_GROUPS_FOR_TEAMS.is_enabled(course_key):
return

teams_configuration_model = settings_dict.get('teams_configuration', {})
if teams_configuration_model == {}:
return
json_value = teams_configuration_model.get('value')
if json_value == '':
return

proposed_topics = json_value.get('topics')

if proposed_topics is None:
proposed_teamsets = json_value.get('team_sets')
if proposed_teamsets is None:
return
else:
proposed_topics = proposed_teamsets

for index, proposed_topic in enumerate(proposed_topics):
if not proposed_topic.get('dynamic_user_partition_id'):
proposed_topic['dynamic_user_partition_id'] = MINIMUM_DYNAMIC_TEAM_PARTITION_ID + index

@classmethod
def update_from_dict(cls, key_values, block, user, save=True):
"""
Expand Down Expand Up @@ -342,6 +377,15 @@ def validate_team_settings(cls, settings_dict):
topic_validation_errors['model'] = teams_configuration_model
errors.append(topic_validation_errors)

for proposed_topic in proposed_topics:
dynamic_user_partition_id = proposed_topic.get('dynamic_user_partition_id')
if dynamic_user_partition_id is None:
continue
if dynamic_user_partition_id > MINIMUM_STATIC_PARTITION_ID or dynamic_user_partition_id < MINIMUM_DYNAMIC_TEAM_PARTITION_ID:
message = 'dynamic_user_partition_id must be greater than ' + str(MINIMUM_DYNAMIC_TEAM_PARTITION_ID) + \
' and less than ' + str(MINIMUM_STATIC_PARTITION_ID)
errors.append({'key': 'teams_configuration', 'message': message, 'model': teams_configuration_model})

return errors

@classmethod
Expand All @@ -359,7 +403,14 @@ def validate_single_topic(cls, topic_settings):
error_list = []
valid_teamset_types = [TeamsetType.open.value, TeamsetType.public_managed.value,
TeamsetType.private_managed.value]
valid_keys = {'id', 'name', 'description', 'max_team_size', 'type'}
valid_keys = {
'id',
'name',
'description',
'max_team_size',
'type',
'dynamic_user_partition_id',
}
teamset_type = topic_settings.get('type', {})
if teamset_type:
if teamset_type not in valid_teamset_types:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
from opaque_keys.edx.keys import CourseKey
from opaque_keys.edx.locator import LibraryLocator
from openedx.core import types
from openedx.core.djangoapps.content.learning_sequences.api.processors.team_partition_groups import TeamPartitionGroupsOutlineProcessor

from ..data import (
ContentErrorData,
Expand Down Expand Up @@ -330,6 +331,7 @@ def _get_user_course_outline_and_processors(course_key: CourseKey, # lint-amnes
('enrollment', EnrollmentOutlineProcessor),
('enrollment_track_partitions', EnrollmentTrackPartitionGroupsOutlineProcessor),
('cohorts_partitions', CohortPartitionGroupsOutlineProcessor),
('teams_partitions', TeamPartitionGroupsOutlineProcessor),
]

# Run each OutlineProcessor in order to figure out what items we have to
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
"""
Outline processors for applying team user partition groups.
"""
import logging
from datetime import datetime
from typing import Dict

from opaque_keys.edx.keys import CourseKey

from openedx.core import types
from openedx.core.djangoapps.content.learning_sequences.api.processors.base import OutlineProcessor
from openedx.core.djangoapps.course_groups.partition_generator import create_team_set_partition_with_course_id
from openedx.core.djangoapps.course_groups.partition_scheme import CONTENT_GROUPS_FOR_TEAMS
from xmodule.partitions.partitions import Group
from xmodule.partitions.partitions_service import get_user_partition_groups

log = logging.getLogger(__name__)


class TeamPartitionGroupsOutlineProcessor(OutlineProcessor):
"""
Processor for applying all team user partition groups.
This processor is used to remove content from the course outline based on
the user's team membership. It is used in the courseware API to remove
content from the course outline before it is returned to the client.
"""
def __init__(self, course_key: CourseKey, user: types.User, at_time: datetime):
super().__init__(course_key, user, at_time)
self.team_groups: Dict[str, Group] = {}
self.user_group = None

def load_data(self, _) -> None:
"""
Pull team groups for this course and which group the user is in.
"""
user_partitions = create_team_set_partition_with_course_id(self.course_key)
self.team_groups = get_user_partition_groups(
self.course_key,
user_partitions,
self.user,
partition_dict_key="id",
)
self.user_groups = []
for _, group in self.team_groups.items():
self.user_groups.append(group.id)

def _is_user_excluded_by_partition_group(self, user_partition_groups):
"""
Is the user part of the group to which the block is restricting content?
The user is excluded if the block is in a partition group, but the user
is not in that group.
"""
if not CONTENT_GROUPS_FOR_TEAMS.is_enabled(self.course_key):
return False

if not user_partition_groups:
return False

if not self.user_groups:
return False

for partition_id, groups in user_partition_groups.items():
if partition_id not in self.team_groups:
continue
if self.team_groups[partition_id].id in groups:
return False

return True

def usage_keys_to_remove(self, full_course_outline):
"""
Content group exclusions remove the content entirely.
This method returns the usage keys of all content that should be
removed from the course outline based on the user's team membership.
"""
removed_usage_keys = set()
for section in full_course_outline.sections:
remove_all_children = False
if self._is_user_excluded_by_partition_group(
section.user_partition_groups
):
removed_usage_keys.add(section.usage_key)
remove_all_children = True
for seq in section.sequences:
if remove_all_children or self._is_user_excluded_by_partition_group(
seq.user_partition_groups
):
removed_usage_keys.add(seq.usage_key)
return removed_usage_keys
74 changes: 74 additions & 0 deletions openedx/core/djangoapps/course_groups/partition_generator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
"""
The team dynamic partition generation to be part of the
openedx.dynamic_partition plugin.
"""
import logging

from django.conf import settings
from django.utils.translation import gettext_lazy as _
from openedx.core.djangoapps.course_groups.partition_scheme import CONTENT_GROUPS_FOR_TEAMS

from xmodule.partitions.partitions import UserPartition, UserPartitionError
from xmodule.services import TeamsConfigurationService

log = logging.getLogger(__name__)

FEATURES = getattr(settings, 'FEATURES', {})
MINIMUM_DYNAMIC_TEAM_PARTITION_ID = 51
TEAM_SCHEME = "team"


def create_team_set_partition_with_course_id(course_id, team_sets=None):
"""
Create and return the dynamic enrollment track user partition based only on course_id.
If it cannot be created, None is returned.
"""
if not team_sets:
team_sets = get_team_sets(course_id) or {}

try:
team_scheme = UserPartition.get_scheme(TEAM_SCHEME)
except UserPartitionError:
log.warning("No 'team' scheme registered, TeamUserPartition will not be created.")
return None

# Get team-sets from course and create user partitions for each team-set
# Then get teams from each team-set and create user groups for each team
partitions = []
for team_set in team_sets:
partition = team_scheme.create_user_partition(
id=team_set.dynamic_user_partition_id,
name=f"Team set {team_set.name} groups",
description=_("Partition for segmenting users by team-set"),
parameters={
"course_id": str(course_id),
"team_set_id": team_set.teamset_id,
}
)
if partition:
partitions.append(partition)

return partitions


def create_team_set_partition(course):
"""
Get the dynamic enrollment track user partition based on the team-sets of the course.
"""
if not CONTENT_GROUPS_FOR_TEAMS.is_enabled(course.id):
return []
return create_team_set_partition_with_course_id(
course.id,
get_team_sets(course.id),
)


def get_team_sets(course_key):
"""
Get team-sets of the course.
"""
team_sets = TeamsConfigurationService().get_teams_configuration(course_key).teamsets
if not team_sets:
return None

return team_sets
Loading

0 comments on commit 7e1c023

Please sign in to comment.