Remove most references to old teams config scheme (#22238)

This is a follow up from MST-16, which was commited
in 3858036a4e.

Changes:
* Enrich course teams_configuration from a plain Dict
  to a custom XBlock field that uses the new TeamsConfig
  wrapper class.
* Remove teams_conf property from course, as the previous
  change made it redundant.
* Update teams_enabled implementation.
* Remove teams_max_size field from course, which is
  no longer semantically correct, as max team size
  is now defined on a teamset level.
* Remove teams_topics in order to discourage use of raw
  teams config dict.
* Add convenience properties teamsets and teamsets_by_id
  to course.
* Allow periods and spaces in teamset IDs to avoid breaking
  existing course teams.

Some parts of the code still use the old raw config data
(identifiable by searching "cleaned_data_old_format"),
which we expect to be slowly factored away as we build
new teams features. MST-40 has been created to remove any
remaining references if necessary.

MST-18

* fix: bokchoy test

* fix: remove pdb break
This commit is contained in:
Kyle McCormick
2019-11-06 20:43:32 -05:00
committed by GitHub
parent d6099e08fd
commit 4f3262a40b
12 changed files with 203 additions and 168 deletions

View File

@@ -271,6 +271,33 @@ def get_available_providers():
return available_providers
class TeamsConfigField(Dict):
"""
XBlock field for teams configuration, including definitions for teamsets.
Serializes to JSON dictionary.
"""
_default = TeamsConfig({})
def from_json(self, value):
"""
Return a TeamsConfig instance from a dict.
"""
return TeamsConfig(value)
def to_json(self, value):
"""
Convert a TeamsConfig instance back to a dict.
If we have the data that was used to build the TeamsConfig instance,
return that instead of `value.cleaned_data`, thus preserving the
data in the form that the user entered it.
"""
if value.source_data is not None:
return value.source_data
return value.cleaned_data
class CourseFields(object):
lti_passports = List(
display_name=_("LTI Passports"),
@@ -797,7 +824,7 @@ class CourseFields(object):
scope=Scope.settings
)
teams_configuration = Dict(
teams_configuration = TeamsConfigField(
display_name=_("Teams Configuration"),
# Translators: please don't translate "id".
help=_(
@@ -808,6 +835,8 @@ class CourseFields(object):
'For example, to specify that teams should have a maximum of 5 participants and provide a list of '
'2 topics, enter the configuration in this format: {example_format}. '
'In "id" values, the only supported special characters are underscore, hyphen, and period.'
# Note that we also support space (" "), which may have been an accident, but it's in
# our DB now. Let's not advertise the fact, though.
),
help_format_args=dict(
# Put the sample JSON into a format variable so that translators
@@ -1471,65 +1500,32 @@ class CourseDescriptor(CourseFields, SequenceDescriptor, LicenseMixin):
"""
return course_metadata_utils.clean_course_key(self.location.course_key, padding_char)
@property
def teams_conf(self):
"""
Returns a TeamsConfig object wrapping the dict `teams_configuration`.
TODO: In MST-18, `teams_configuration` will be a custom field that
parses its input into a TeamsConfig object.
All references to this property will become references to
`.teams_configuration`.
"""
# If the `teams_configuration` dict hasn't changed, return a cached
# `TeamsConfig` to avoid re-parsing things.
# If it has changed, recompute the `TeamsConfig`.
try:
cached_teams_conf, cached_teams_conf_source = (
self._teams_conf, self._teams_conf_source
)
except AttributeError:
pass
else:
if cached_teams_conf_source == self.teams_configuration:
return cached_teams_conf
self._teams_conf_source = self.teams_configuration
self._teams_conf = TeamsConfig(self._teams_conf_source)
return self._teams_conf
@property
def teams_enabled(self):
"""
Returns whether or not teams has been enabled for this course.
Alias to `self.teams_configuration.is_enabled`, for convenience.
Currently, teams are considered enabled when at least one topic has been configured for the course.
Deprecated; please use `self.teams_conf.is_enabled` instead.
Will be removed (TODO MST-18).
Returns bool.
"""
if self.teams_configuration:
return len(self.teams_configuration.get('topics', [])) > 0
return False
return self.teams_configuration.is_enabled # pylint: disable=no-member
@property
def teams_max_size(self):
def teamsets(self):
"""
Returns the max size for teams if teams has been configured, else None.
Alias to `self.teams_configuration.teamsets`, for convenience.
Deprecated; please use `self.teams_conf.calc_max_team_size(...)` instead.
Will be removed (TODO MST-18).
Returns list[TeamsetConfig].
"""
return self.teams_configuration.get('max_team_size', None)
return self.teams_configuration.teamsets # pylint: disable=no-member
@property
def teams_topics(self):
def teamsets_by_id(self):
"""
Returns the topics that have been configured for teams for this course, else None.
Alias to `self.teams_configuration.teamsets_by_id`, for convenience.
Deprecated; please use `self.teams_conf.teamsets` instead.
Will be removed (TODO MST-18).
Returns dict[str: TeamsetConfig].
"""
return self.teams_configuration.get('topics', None)
return self.teams_configuration.teamsets_by_id
def set_user_partitions_for_scheme(self, partitions, scheme):
"""

View File

@@ -15,6 +15,7 @@ from opaque_keys.edx.keys import CourseKey
from pytz import utc
from xblock.runtime import DictKeyValueStore, KvsFieldData
from openedx.core.lib.teams_config import TeamsConfig
import xmodule.course_module
from xmodule.modulestore.xml import ImportSystem, XMLModuleStore
@@ -272,25 +273,21 @@ class DiscussionTopicsTestCase(unittest.TestCase):
class TeamsConfigurationTestCase(unittest.TestCase):
"""
Tests for the configuration of teams and the helper methods for accessing them.
Also tests new configuration wrapper, `.teams_conf`.
Eventually the tests for the old dict-based `.teams_configuration` field
wil be removed (TODO MST-18).
"""
def setUp(self):
super(TeamsConfigurationTestCase, self).setUp()
self.course = get_dummy_course('2012-12-02T12:00')
self.course.teams_configuration = dict()
self.course.teams_configuration = TeamsConfig(None)
self.count = itertools.count()
def add_team_configuration(self, max_team_size=3, topics=None):
""" Add a team configuration to the course. """
teams_configuration = {}
teams_configuration["topics"] = [] if topics is None else topics
teams_config_data = {}
teams_config_data["topics"] = [] if topics is None else topics
if max_team_size is not None:
teams_configuration["max_team_size"] = max_team_size
self.course.teams_configuration = teams_configuration
teams_config_data["max_team_size"] = max_team_size
self.course.teams_configuration = TeamsConfig(teams_config_data)
def make_topic(self):
""" Make a sample topic dictionary. """
@@ -303,66 +300,56 @@ class TeamsConfigurationTestCase(unittest.TestCase):
def test_teams_enabled_new_course(self):
# Make sure we can detect when no teams exist.
self.assertFalse(self.course.teams_enabled)
self.assertFalse(self.course.teams_conf.is_enabled)
# add topics
self.add_team_configuration(max_team_size=4, topics=[self.make_topic()])
self.assertTrue(self.course.teams_enabled)
self.assertTrue(self.course.teams_conf.is_enabled)
# remove them again
self.add_team_configuration(max_team_size=4, topics=[])
self.assertFalse(self.course.teams_enabled)
self.assertFalse(self.course.teams_conf.is_enabled)
def test_teams_enabled_max_size_only(self):
self.add_team_configuration(max_team_size=4)
self.assertFalse(self.course.teams_enabled)
self.assertFalse(self.course.teams_conf.is_enabled)
def test_teams_enabled_no_max_size(self):
self.add_team_configuration(max_team_size=None, topics=[self.make_topic()])
self.assertTrue(self.course.teams_enabled)
self.assertTrue(self.course.teams_conf.is_enabled)
def test_teams_max_size_no_teams_configuration(self):
self.assertIsNone(self.course.teams_max_size)
self.assertIsNone(self.course.teams_conf.max_team_size)
self.assertIsNone(self.course.teams_configuration.default_max_team_size)
def test_teams_max_size_with_teams_configured(self):
size = 4
self.add_team_configuration(max_team_size=size, topics=[self.make_topic(), self.make_topic()])
self.assertTrue(self.course.teams_enabled)
self.assertEqual(size, self.course.teams_max_size)
self.assertEqual(size, self.course.teams_conf.max_team_size)
self.assertEqual(size, self.course.teams_configuration.default_max_team_size)
def test_teams_topics_no_teams(self):
self.assertIsNone(self.course.teams_topics)
self.assertEqual(self.course.teams_conf.teamsets, [])
def test_teamsets_no_config(self):
self.assertEqual(self.course.teamsets, [])
def test_teams_topics_no_topics(self):
def test_teamsets_empty(self):
self.add_team_configuration(max_team_size=4)
self.assertEqual(self.course.teams_topics, [])
self.assertEqual(self.course.teams_conf.teamsets, [])
self.assertEqual(self.course.teamsets, [])
def test_teams_topics_with_topics(self):
def test_teamsets_present(self):
topics = [self.make_topic(), self.make_topic()]
self.add_team_configuration(max_team_size=4, topics=topics)
self.assertTrue(self.course.teams_enabled)
self.assertEqual(self.course.teams_topics, topics)
expected_teamsets_data = [
teamset.cleaned_data_old_format
for teamset in self.course.teams_conf.teamsets
for teamset in self.course.teamsets
]
self.assertEqual(expected_teamsets_data, topics)
def test_teams_conf_caching(self):
def test_teams_conf_cached_by_xblock_field(self):
self.add_team_configuration(max_team_size=5, topics=[self.make_topic()])
cold_cache_conf = self.course.teams_conf
warm_cache_conf = self.course.teams_conf
cold_cache_conf = self.course.teams_configuration
warm_cache_conf = self.course.teams_configuration
self.add_team_configuration(max_team_size=5, topics=[self.make_topic(), self.make_topic()])
new_cold_cache_conf = self.course.teams_conf
new_warm_cache_conf = self.course.teams_conf
new_cold_cache_conf = self.course.teams_configuration
new_warm_cache_conf = self.course.teams_configuration
self.assertIs(cold_cache_conf, warm_cache_conf)
self.assertIs(new_cold_cache_conf, new_warm_cache_conf)
self.assertIsNot(cold_cache_conf, new_cold_cache_conf)