diff --git a/cms/djangoapps/models/settings/course_metadata.py b/cms/djangoapps/models/settings/course_metadata.py index 8e0d5d887e..263f237981 100644 --- a/cms/djangoapps/models/settings/course_metadata.py +++ b/cms/djangoapps/models/settings/course_metadata.py @@ -357,7 +357,7 @@ class CourseMetadata: """ error_list = [] valid_teamset_types = [TeamsetType.open.value, TeamsetType.public_managed.value, - TeamsetType.private_managed.value] + TeamsetType.private_managed.value, TeamsetType.open_managed.value] valid_keys = {'id', 'name', 'description', 'max_team_size', 'type'} teamset_type = topic_settings.get('type', {}) if teamset_type: diff --git a/cms/djangoapps/models/settings/tests/test_settings.py b/cms/djangoapps/models/settings/tests/test_settings.py index 314220d95b..67afe97274 100644 --- a/cms/djangoapps/models/settings/tests/test_settings.py +++ b/cms/djangoapps/models/settings/tests/test_settings.py @@ -41,6 +41,12 @@ working_config_block = { "type": "private_managed", "description": "Private Topic 2 desc", "name": "Private Topic 2 Name" + }, + { + "id": "open_managed_topic_1_id", + "type": "open_managed", + "description": "Open Managed Topic 1 desc", + "name": "Open Managed Topic 1 Name" } ] } diff --git a/lms/djangoapps/teams/static/teams/js/views/team_profile.js b/lms/djangoapps/teams/static/teams/js/views/team_profile.js index 8afa8df016..0ff32b9114 100644 --- a/lms/djangoapps/teams/static/teams/js/views/team_profile.js +++ b/lms/djangoapps/teams/static/teams/js/views/team_profile.js @@ -44,13 +44,14 @@ isMember = TeamUtils.isUserMemberOfTeam(memberships, this.context.userInfo.username), isAdminOrStaff = this.context.userInfo.privileged || this.context.userInfo.staff, isInstructorManagedTopic = TeamUtils.isInstructorManagedTopic(this.topic.attributes.type), + canJoinTeam = TeamUtils.canJoinTeam(this.context.userInfo, this.topic.attributes.type), maxTeamSize = this.topic.getMaxTeamSize(this.context.courseMaxTeamSize); // Assignments URL isn't provided if team assignments shouldn't be shown // so we can treat it like a toggle var showAssignments = !!this.context.teamsAssignmentsUrl; - var showLeaveLink = isMember && (isAdminOrStaff || !isInstructorManagedTopic); + var showLeaveLink = isMember && (isAdminOrStaff || !isInstructorManagedTopic || canJoinTeam); HtmlUtils.setHtml( this.$el, diff --git a/lms/djangoapps/teams/static/teams/js/views/team_profile_header_actions.js b/lms/djangoapps/teams/static/teams/js/views/team_profile_header_actions.js index e860377dd3..75fc8ad92d 100644 --- a/lms/djangoapps/teams/static/teams/js/views/team_profile_header_actions.js +++ b/lms/djangoapps/teams/static/teams/js/views/team_profile_header_actions.js @@ -47,6 +47,8 @@ } else if (!teamHasSpace) { showJoinButton = false; message = view.teamFullMessage; + } else if (info.canJoinTeam) { + showJoinButton = true; } else if (!info.isAdminOrStaff && info.isInstructorManagedTopic) { showJoinButton = false; message = view.notJoinInstructorManagedTeam; @@ -100,12 +102,14 @@ // this.topic.getMaxTeamSize() will return null for a managed team, // but the size is considered to be arbitarily large. var isInstructorManagedTopic = TeamUtils.isInstructorManagedTopic(this.topic.attributes.type); + var canJoinTeam = TeamUtils.canJoinTeam(this.context.userInfo, this.topic.attributes.type) var teamHasSpace = isInstructorManagedTopic || (this.model.get('membership').length < this.topic.getMaxTeamSize(courseMaxTeamSize)); info.memberOfCurrentTeam = TeamUtils.isUserMemberOfTeam(this.model.get('membership'), username); info.isAdminOrStaff = this.context.userInfo.privileged || this.context.userInfo.staff; info.isInstructorManagedTopic = isInstructorManagedTopic; + info.canJoinTeam = canJoinTeam; if (info.memberOfCurrentTeam) { info.alreadyInTeamset = true; diff --git a/lms/djangoapps/teams/static/teams/js/views/team_utils.js b/lms/djangoapps/teams/static/teams/js/views/team_utils.js index ae1d9c118d..3222afc03e 100644 --- a/lms/djangoapps/teams/static/teams/js/views/team_utils.js +++ b/lms/djangoapps/teams/static/teams/js/views/team_utils.js @@ -83,6 +83,10 @@ return topicType.toLowerCase() !== 'open'; }, + canJoinTeam: function(userInfo, topicType = '') { + return userInfo.privileged || userInfo.staff || topicType.includes("open"); + }, + /** Shows info/error banner for team membership CSV upload * @param: content - string or array for display * @param: isError - true sets error styling, false/none uses info styling diff --git a/lms/djangoapps/teams/tests/test_api.py b/lms/djangoapps/teams/tests/test_api.py index e9df8cd222..dceb940ef5 100644 --- a/lms/djangoapps/teams/tests/test_api.py +++ b/lms/djangoapps/teams/tests/test_api.py @@ -24,6 +24,7 @@ COURSE_KEY2 = CourseKey.from_string('course-v1:edx+math+1') TOPIC1 = 'topic-1' TOPIC2 = 'topic-2' TOPIC3 = 'topic-3' +TOPIC4 = 'topic-4' DISCUSSION_TOPIC_ID = uuid4().hex @@ -44,7 +45,8 @@ class PythonAPITests(SharedModuleStoreTestCase): topic_data = [ (TOPIC1, TeamsetType.private_managed.value), (TOPIC2, TeamsetType.open.value), - (TOPIC3, TeamsetType.public_managed.value) + (TOPIC3, TeamsetType.public_managed.value), + (TOPIC4, TeamsetType.open_managed.value), ] topics = [ { @@ -55,7 +57,7 @@ class PythonAPITests(SharedModuleStoreTestCase): } for topic_id, teamset_type in topic_data ] teams_config_1 = TeamsConfig({'topics': [topics[0]]}) - teams_config_2 = TeamsConfig({'topics': [topics[1], topics[2]]}) + teams_config_2 = TeamsConfig({'topics': [topics[1], topics[2], topics[3]]}) cls.course1 = CourseFactory( org=COURSE_KEY1.org, course=COURSE_KEY1.course, @@ -93,10 +95,12 @@ class PythonAPITests(SharedModuleStoreTestCase): topic_id=TOPIC2 ) cls.team3 = CourseTeamFactory(course_id=COURSE_KEY2, team_id='team3', topic_id=TOPIC3) + cls.team4 = CourseTeamFactory(course_id=COURSE_KEY2, team_id='team4', topic_id=TOPIC4) cls.team1.add_user(cls.user1) cls.team1.add_user(cls.user2) cls.team2.add_user(cls.user3) + cls.team4.add_user(cls.user3) cls.team1a.add_user(cls.user4) cls.team2a.add_user(cls.user4) @@ -122,21 +126,25 @@ class PythonAPITests(SharedModuleStoreTestCase): assert not teams_api.is_team_discussion_private(None) assert not teams_api.is_team_discussion_private(self.team2) assert not teams_api.is_team_discussion_private(self.team3) + assert not teams_api.is_team_discussion_private(self.team4) def test_is_instructor_managed_team(self): assert teams_api.is_instructor_managed_team(self.team1) assert not teams_api.is_instructor_managed_team(self.team2) assert teams_api.is_instructor_managed_team(self.team3) + assert not teams_api.is_instructor_managed_team(self.team4) def test_is_instructor_managed_topic(self): assert teams_api.is_instructor_managed_topic(COURSE_KEY1, TOPIC1) assert not teams_api.is_instructor_managed_topic(COURSE_KEY2, TOPIC2) assert teams_api.is_instructor_managed_topic(COURSE_KEY2, TOPIC3) + assert not teams_api.is_instructor_managed_topic(COURSE_KEY2, TOPIC4) def test_user_is_a_team_member(self): assert teams_api.user_is_a_team_member(self.user1, self.team1) assert not teams_api.user_is_a_team_member(self.user1, None) assert not teams_api.user_is_a_team_member(self.user1, self.team2) + assert not teams_api.user_is_a_team_member(self.user1, self.team4) def test_private_discussion_visible_by_user(self): assert teams_api.discussion_visible_by_user(DISCUSSION_TOPIC_ID, self.user1) @@ -147,6 +155,7 @@ class PythonAPITests(SharedModuleStoreTestCase): assert teams_api.discussion_visible_by_user(self.team2.discussion_topic_id, self.user1) assert teams_api.discussion_visible_by_user(self.team2.discussion_topic_id, self.user2) assert teams_api.discussion_visible_by_user('DO_NOT_EXISTS', self.user3) + assert teams_api.discussion_visible_by_user(self.team4.discussion_topic_id, self.user3) @ddt.unpack @ddt.data( diff --git a/lms/djangoapps/teams/views.py b/lms/djangoapps/teams/views.py index a5370ffa93..dcce363820 100644 --- a/lms/djangoapps/teams/views.py +++ b/lms/djangoapps/teams/views.py @@ -197,7 +197,10 @@ class TeamsDashboardView(GenericAPIView): "teams": user_teams_data }, "has_open_teamset": bool(teamset_counts_by_type[TeamsetType.open.value]), - "has_public_managed_teamset": bool(teamset_counts_by_type[TeamsetType.public_managed.value]), + "has_public_managed_teamset": bool( + teamset_counts_by_type[TeamsetType.public_managed.value] + + teamset_counts_by_type[TeamsetType.open_managed.value] + ), "has_managed_teamset": bool( teamset_counts_by_type[TeamsetType.public_managed.value] + teamset_counts_by_type[TeamsetType.private_managed.value] diff --git a/openedx/core/lib/teams_config.py b/openedx/core/lib/teams_config.py index 636c5073f8..48b0eafefc 100644 --- a/openedx/core/lib/teams_config.py +++ b/openedx/core/lib/teams_config.py @@ -304,6 +304,7 @@ class TeamsetType(Enum): open = "open" public_managed = "public_managed" private_managed = "private_managed" + open_managed = "open_managed" @classmethod def get_default(cls):