Merge pull request #4424 from edx/anton/edit-group-configurations
Edit group configurations.
This commit is contained in:
@@ -869,43 +869,90 @@ class GroupConfiguration(object):
|
||||
"""
|
||||
Prepare Group Configuration for the course.
|
||||
"""
|
||||
@staticmethod
|
||||
def parse(configuration_json):
|
||||
def __init__(self, json_string, course, configuration_id=None):
|
||||
"""
|
||||
Parse given string that represents group configuration.
|
||||
Receive group configuration as a json (`json_string`), deserialize it
|
||||
and validate.
|
||||
"""
|
||||
self.configuration = GroupConfiguration.parse(json_string)
|
||||
self.course = course
|
||||
self.assign_id(configuration_id)
|
||||
self.assign_group_ids()
|
||||
self.validate()
|
||||
|
||||
@staticmethod
|
||||
def parse(json_string):
|
||||
"""
|
||||
Deserialize given json that represents group configuration.
|
||||
"""
|
||||
try:
|
||||
group_configuration = json.loads(configuration_json)
|
||||
configuration = json.loads(json_string)
|
||||
except ValueError:
|
||||
raise GroupConfigurationsValidationError(_("invalid JSON"))
|
||||
|
||||
if not group_configuration.get('version'):
|
||||
group_configuration['version'] = UserPartition.VERSION
|
||||
return configuration
|
||||
|
||||
# this is temporary logic, we are going to build default groups on front-end
|
||||
if not group_configuration.get('groups'):
|
||||
group_configuration['groups'] = [
|
||||
{'name': 'Group A'}, {'name': 'Group B'},
|
||||
]
|
||||
|
||||
for group in group_configuration['groups']:
|
||||
group['version'] = Group.VERSION
|
||||
return group_configuration
|
||||
|
||||
@staticmethod
|
||||
def validate(group_configuration):
|
||||
def validate(self):
|
||||
"""
|
||||
Validate group configuration representation.
|
||||
"""
|
||||
if not group_configuration.get("name"):
|
||||
if not self.configuration.get("name"):
|
||||
raise GroupConfigurationsValidationError(_("must have name of the configuration"))
|
||||
if not isinstance(group_configuration.get("description"), basestring):
|
||||
raise GroupConfigurationsValidationError(_("must have description of the configuration"))
|
||||
if len(group_configuration.get('groups')) < 2:
|
||||
if len(self.configuration.get('groups', [])) < 2:
|
||||
raise GroupConfigurationsValidationError(_("must have at least two groups"))
|
||||
group_id = unicode(group_configuration.get("id", ""))
|
||||
if group_id and not group_id.isdigit():
|
||||
raise GroupConfigurationsValidationError(_("group configuration ID must be numeric"))
|
||||
|
||||
def generate_id(self):
|
||||
"""
|
||||
Generate unique id for the group configuration.
|
||||
If this id is already used, we generate new one.
|
||||
"""
|
||||
used_ids = self.get_used_ids()
|
||||
cid = random.randint(100, 10 ** 12)
|
||||
|
||||
while cid in used_ids:
|
||||
cid = random.randint(100, 10 ** 12)
|
||||
|
||||
return cid
|
||||
|
||||
def assign_id(self, configuration_id=None):
|
||||
"""
|
||||
Assign id for the json representation of group configuration.
|
||||
"""
|
||||
self.configuration['id'] = int(configuration_id) if configuration_id else self.generate_id()
|
||||
|
||||
def assign_group_ids(self):
|
||||
"""
|
||||
Assign ids for the group_configuration's groups.
|
||||
"""
|
||||
# this is temporary logic, we are going to build default groups on front-end
|
||||
if not self.configuration.get('groups'):
|
||||
self.configuration['groups'] = [
|
||||
{'name': 'Group A'}, {'name': 'Group B'},
|
||||
]
|
||||
|
||||
# Assign ids to every group in configuration.
|
||||
for index, group in enumerate(self.configuration.get('groups', [])):
|
||||
group['id'] = index
|
||||
|
||||
def get_used_ids(self):
|
||||
"""
|
||||
Return a list of IDs that already in use.
|
||||
"""
|
||||
return set([p.id for p in self.course.user_partitions])
|
||||
|
||||
def get_user_partition(self):
|
||||
"""
|
||||
Get user partition for saving in course.
|
||||
"""
|
||||
groups = [Group(g["id"], g["name"]) for g in self.configuration["groups"]]
|
||||
|
||||
return UserPartition(
|
||||
self.configuration["id"],
|
||||
self.configuration["name"],
|
||||
self.configuration["description"],
|
||||
groups
|
||||
)
|
||||
|
||||
|
||||
@require_http_methods(("GET", "POST"))
|
||||
@login_required
|
||||
@@ -932,40 +979,63 @@ def group_configurations_list_handler(request, course_key_string):
|
||||
'group_configuration_url': group_configuration_url,
|
||||
'configurations': [u.to_json() for u in course.user_partitions] if split_test_enabled else None,
|
||||
})
|
||||
elif "application/json" in request.META.get('HTTP_ACCEPT') and request.method == 'POST':
|
||||
elif "application/json" in request.META.get('HTTP_ACCEPT'):
|
||||
if request.method == 'POST':
|
||||
# create a new group configuration for the course
|
||||
try:
|
||||
configuration = GroupConfiguration.parse(request.body)
|
||||
GroupConfiguration.validate(configuration)
|
||||
except GroupConfigurationsValidationError as err:
|
||||
return JsonResponse({"error": err.message}, status=400)
|
||||
try:
|
||||
new_configuration = GroupConfiguration(request.body, course).get_user_partition()
|
||||
except GroupConfigurationsValidationError as err:
|
||||
return JsonResponse({"error": err.message}, status=400)
|
||||
|
||||
if not configuration.get("id"):
|
||||
configuration["id"] = random.randint(100, 10**12)
|
||||
course.user_partitions.append(new_configuration)
|
||||
response = JsonResponse(new_configuration.to_json(), status=201)
|
||||
|
||||
# Assign ids to every group in configuration.
|
||||
for index, group in enumerate(configuration.get('groups', [])):
|
||||
group["id"] = index
|
||||
|
||||
course.user_partitions.append(UserPartition.from_json(configuration))
|
||||
store.update_item(course, request.user.id)
|
||||
response = JsonResponse(configuration, status=201)
|
||||
|
||||
response["Location"] = reverse_course_url(
|
||||
'group_configurations_detail_handler',
|
||||
course.id,
|
||||
kwargs={'group_configuration_id': configuration["id"]}
|
||||
)
|
||||
return response
|
||||
response["Location"] = reverse_course_url(
|
||||
'group_configurations_detail_handler',
|
||||
course.id,
|
||||
kwargs={'group_configuration_id': new_configuration.id} # pylint: disable=no-member
|
||||
)
|
||||
store.update_item(course, request.user.id)
|
||||
return response
|
||||
else:
|
||||
return HttpResponse(status=406)
|
||||
|
||||
|
||||
@require_http_methods(("GET", "POST"))
|
||||
@login_required
|
||||
@ensure_csrf_cookie
|
||||
@require_http_methods(("POST", "PUT"))
|
||||
def group_configurations_detail_handler(request, course_key_string, group_configuration_id):
|
||||
return JsonResponse(status=404)
|
||||
"""
|
||||
JSON API endpoint for manipulating a group configuration via its internal ID.
|
||||
Used by the Backbone application.
|
||||
|
||||
POST or PUT
|
||||
json: update group configuration based on provided information
|
||||
"""
|
||||
course_key = CourseKey.from_string(course_key_string)
|
||||
course = _get_course_module(course_key, request.user)
|
||||
store = modulestore()
|
||||
matching_id = [p for p in course.user_partitions
|
||||
if unicode(p.id) == unicode(group_configuration_id)]
|
||||
if matching_id:
|
||||
configuration = matching_id[0]
|
||||
else:
|
||||
configuration = None
|
||||
|
||||
if request.method in ('POST', 'PUT'): # can be either and sometimes
|
||||
# django is rewriting one to the other
|
||||
try:
|
||||
new_configuration = GroupConfiguration(request.body, course, group_configuration_id).get_user_partition()
|
||||
except GroupConfigurationsValidationError as err:
|
||||
return JsonResponse({"error": err.message}, status=400)
|
||||
|
||||
if configuration:
|
||||
index = course.user_partitions.index(configuration)
|
||||
course.user_partitions[index] = new_configuration
|
||||
else:
|
||||
course.user_partitions.append(new_configuration)
|
||||
store.update_item(course, request.user.id)
|
||||
return JsonResponse(new_configuration.to_json(), status=201)
|
||||
|
||||
|
||||
def _get_course_creator_status(user):
|
||||
|
||||
@@ -1,126 +1,65 @@
|
||||
"""
|
||||
Group Configuration Tests.
|
||||
"""
|
||||
import json
|
||||
from unittest import skipUnless
|
||||
from django.conf import settings
|
||||
from contentstore.utils import reverse_course_url
|
||||
from contentstore.tests.utils import CourseTestCase
|
||||
from xmodule.partitions.partitions import Group, UserPartition
|
||||
|
||||
|
||||
@skipUnless(settings.FEATURES.get('ENABLE_GROUP_CONFIGURATIONS'), 'Tests Group Configurations feature')
|
||||
class GroupConfigurationsCreateTestCase(CourseTestCase):
|
||||
GROUP_CONFIGURATION_JSON = {
|
||||
u'name': u'Test name',
|
||||
u'description': u'Test description',
|
||||
}
|
||||
|
||||
|
||||
# pylint: disable=no-member
|
||||
class GroupConfigurationsBaseTestCase(object):
|
||||
"""
|
||||
Test cases for creating a new group configurations.
|
||||
Mixin with base test cases for the group configurations.
|
||||
"""
|
||||
def _remove_ids(self, content):
|
||||
"""
|
||||
Remove ids from the response. We cannot predict IDs, because they're
|
||||
generated randomly.
|
||||
We use this method to clean up response when creating new group configurations.
|
||||
Returns a tuple that contains removed group configuration ID and group IDs.
|
||||
"""
|
||||
configuration_id = content.pop("id")
|
||||
group_ids = [group.pop("id") for group in content["groups"]]
|
||||
|
||||
def setUp(self):
|
||||
"""
|
||||
Set up a url and group configuration content for tests.
|
||||
"""
|
||||
super(GroupConfigurationsCreateTestCase, self).setUp()
|
||||
self.url = reverse_course_url('group_configurations_list_handler', self.course.id)
|
||||
self.group_configuration_json = {
|
||||
u'description': u'Test description',
|
||||
u'name': u'Test name'
|
||||
}
|
||||
return (configuration_id, group_ids)
|
||||
|
||||
def test_index_page(self):
|
||||
def test_required_fields_are_absent(self):
|
||||
"""
|
||||
Check that the group configuration index page responds correctly.
|
||||
"""
|
||||
response = self.client.get(self.url)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertIn('New Group Configuration', response.content)
|
||||
|
||||
def test_group_success(self):
|
||||
"""
|
||||
Test that you can create a group configuration.
|
||||
"""
|
||||
expected_group_configuration = {
|
||||
u'description': u'Test description',
|
||||
u'name': u'Test name',
|
||||
u'version': 1,
|
||||
u'groups': [
|
||||
{u'id': 0, u'name': u'Group A', u'version': 1},
|
||||
{u'id': 1, u'name': u'Group B', u'version': 1}
|
||||
]
|
||||
}
|
||||
response = self.client.post(
|
||||
self.url,
|
||||
data=json.dumps(self.group_configuration_json),
|
||||
content_type="application/json",
|
||||
HTTP_ACCEPT="application/json",
|
||||
HTTP_X_REQUESTED_WITH="XMLHttpRequest"
|
||||
)
|
||||
self.assertEqual(response.status_code, 201)
|
||||
self.assertIn("Location", response)
|
||||
group_configuration = json.loads(response.content)
|
||||
del group_configuration['id'] # do not check for id, it is unique
|
||||
self.assertEqual(expected_group_configuration, group_configuration)
|
||||
|
||||
def test_bad_group(self):
|
||||
"""
|
||||
Test if only one group in configuration exist.
|
||||
"""
|
||||
# Only one group in group configuration here.
|
||||
bad_group_configuration = {
|
||||
u'description': u'Test description',
|
||||
u'id': 1,
|
||||
u'name': u'Test name',
|
||||
u'version': 1,
|
||||
u'groups': [
|
||||
{u'id': 0, u'name': u'Group A', u'version': 1},
|
||||
]
|
||||
}
|
||||
response = self.client.post(
|
||||
self.url,
|
||||
data=json.dumps(bad_group_configuration),
|
||||
content_type="application/json",
|
||||
HTTP_ACCEPT="application/json",
|
||||
HTTP_X_REQUESTED_WITH="XMLHttpRequest"
|
||||
)
|
||||
self.assertEqual(response.status_code, 400)
|
||||
self.assertNotIn("Location", response)
|
||||
content = json.loads(response.content)
|
||||
self.assertIn("error", content)
|
||||
|
||||
def test_bad_configuration_id(self):
|
||||
"""
|
||||
Test if configuration id is not numeric.
|
||||
"""
|
||||
# Configuration id is string here.
|
||||
bad_group_configuration = {
|
||||
u'description': u'Test description',
|
||||
u'id': 'bad_id',
|
||||
u'name': u'Test name',
|
||||
u'version': 1,
|
||||
u'groups': [
|
||||
{u'id': 0, u'name': u'Group A', u'version': 1},
|
||||
{u'id': 1, u'name': u'Group B', u'version': 1}
|
||||
]
|
||||
}
|
||||
response = self.client.post(
|
||||
self.url,
|
||||
data=json.dumps(bad_group_configuration),
|
||||
content_type="application/json",
|
||||
HTTP_ACCEPT="application/json",
|
||||
HTTP_X_REQUESTED_WITH="XMLHttpRequest"
|
||||
)
|
||||
self.assertEqual(response.status_code, 400)
|
||||
self.assertNotIn("Location", response)
|
||||
content = json.loads(response.content)
|
||||
self.assertIn("error", content)
|
||||
|
||||
def test_bad_json(self):
|
||||
"""
|
||||
Test bad json handling.
|
||||
Test required fields are absent.
|
||||
"""
|
||||
bad_jsons = [
|
||||
{u'name': 'Test Name'},
|
||||
{u'description': 'Test description'},
|
||||
{}
|
||||
# must have name of the configuration
|
||||
{
|
||||
u'description': 'Test description',
|
||||
u'groups': [
|
||||
{u'name': u'Group A'},
|
||||
{u'name': u'Group B'},
|
||||
],
|
||||
},
|
||||
# must have at least two groups
|
||||
{
|
||||
u'name': u'Test name',
|
||||
u'description': u'Test description',
|
||||
u'groups': [
|
||||
{u'name': u'Group A'},
|
||||
],
|
||||
},
|
||||
# an empty json
|
||||
{},
|
||||
]
|
||||
|
||||
for bad_json in bad_jsons:
|
||||
response = self.client.post(
|
||||
self.url,
|
||||
self._url(),
|
||||
data=json.dumps(bad_json),
|
||||
content_type="application/json",
|
||||
HTTP_ACCEPT="application/json",
|
||||
@@ -139,7 +78,7 @@ class GroupConfigurationsCreateTestCase(CourseTestCase):
|
||||
invalid_json = "{u'name': 'Test Name', []}"
|
||||
|
||||
response = self.client.post(
|
||||
self.url,
|
||||
self._url(),
|
||||
data=invalid_json,
|
||||
content_type="application/json",
|
||||
HTTP_ACCEPT="application/json",
|
||||
@@ -149,3 +88,163 @@ class GroupConfigurationsCreateTestCase(CourseTestCase):
|
||||
self.assertNotIn("Location", response)
|
||||
content = json.loads(response.content)
|
||||
self.assertIn("error", content)
|
||||
|
||||
|
||||
# pylint: disable=no-member
|
||||
@skipUnless(settings.FEATURES.get('ENABLE_GROUP_CONFIGURATIONS'), 'Tests Group Configurations feature')
|
||||
class GroupConfigurationsListHandlerTestCase(CourseTestCase, GroupConfigurationsBaseTestCase):
|
||||
"""
|
||||
Test cases for group_configurations_list_handler.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
"""
|
||||
Set up GroupConfigurationsListHandlerTestCase.
|
||||
"""
|
||||
super(GroupConfigurationsListHandlerTestCase, self).setUp()
|
||||
|
||||
def _url(self):
|
||||
"""
|
||||
Return url for the handler.
|
||||
"""
|
||||
return reverse_course_url('group_configurations_list_handler', self.course.id)
|
||||
|
||||
def test_can_retrieve_html(self):
|
||||
"""
|
||||
Check that the group configuration index page responds correctly.
|
||||
"""
|
||||
response = self.client.get(self._url())
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertIn('New Group Configuration', response.content)
|
||||
|
||||
def test_unsupported_http_accept_header(self):
|
||||
"""
|
||||
Test if not allowed header present in request.
|
||||
"""
|
||||
response = self.client.get(
|
||||
self._url(),
|
||||
HTTP_ACCEPT="text/plain",
|
||||
)
|
||||
self.assertEqual(response.status_code, 406)
|
||||
|
||||
def test_can_create_group_configuration(self):
|
||||
"""
|
||||
Test that you can create a group configuration.
|
||||
"""
|
||||
expected = {
|
||||
u'description': u'Test description',
|
||||
u'name': u'Test name',
|
||||
u'version': 1,
|
||||
u'groups': [
|
||||
{u'name': u'Group A', u'version': 1},
|
||||
{u'name': u'Group B', u'version': 1},
|
||||
],
|
||||
}
|
||||
response = self.client.post(
|
||||
self._url(),
|
||||
data=json.dumps(GROUP_CONFIGURATION_JSON),
|
||||
content_type="application/json",
|
||||
HTTP_ACCEPT="application/json",
|
||||
HTTP_X_REQUESTED_WITH="XMLHttpRequest",
|
||||
)
|
||||
self.assertEqual(response.status_code, 201)
|
||||
self.assertIn("Location", response)
|
||||
content = json.loads(response.content)
|
||||
configuration_id, group_ids = self._remove_ids(content) # pylint: disable=unused-variable
|
||||
self.assertEqual(content, expected)
|
||||
# IDs are unique
|
||||
self.assertEqual(len(group_ids), len(set(group_ids)))
|
||||
self.assertEqual(len(group_ids), 2)
|
||||
self.reload_course()
|
||||
# Verify that user_partitions in the course contains the new group configuration.
|
||||
self.assertEqual(len(self.course.user_partitions), 1)
|
||||
self.assertEqual(self.course.user_partitions[0].name, u'Test name')
|
||||
|
||||
|
||||
# pylint: disable=no-member
|
||||
@skipUnless(settings.FEATURES.get('ENABLE_GROUP_CONFIGURATIONS'), 'Tests Group Configurations feature')
|
||||
class GroupConfigurationsDetailHandlerTestCase(CourseTestCase, GroupConfigurationsBaseTestCase):
|
||||
"""
|
||||
Test cases for group_configurations_detail_handler.
|
||||
"""
|
||||
|
||||
ID = 000000000000
|
||||
|
||||
def setUp(self):
|
||||
"""
|
||||
Set up GroupConfigurationsDetailHandlerTestCase.
|
||||
"""
|
||||
super(GroupConfigurationsDetailHandlerTestCase, self).setUp()
|
||||
|
||||
def _url(self, cid=None):
|
||||
"""
|
||||
Return url for the handler.
|
||||
"""
|
||||
cid = cid if cid is not None else self.ID
|
||||
return reverse_course_url(
|
||||
'group_configurations_detail_handler',
|
||||
self.course.id,
|
||||
kwargs={'group_configuration_id': cid},
|
||||
)
|
||||
|
||||
def test_can_create_new_group_configuration_if_it_is_not_exist(self):
|
||||
"""
|
||||
PUT new group configuration when no configurations exist in the course.
|
||||
"""
|
||||
expected = {
|
||||
u'id': 999,
|
||||
u'name': u'Test name',
|
||||
u'description': u'Test description',
|
||||
u'version': 1,
|
||||
u'groups': [
|
||||
{u'id': 0, u'name': u'Group A', u'version': 1},
|
||||
{u'id': 1, u'name': u'Group B', u'version': 1},
|
||||
],
|
||||
}
|
||||
|
||||
response = self.client.put(
|
||||
self._url(cid=999),
|
||||
data=json.dumps(GROUP_CONFIGURATION_JSON),
|
||||
content_type="application/json",
|
||||
HTTP_ACCEPT="application/json",
|
||||
HTTP_X_REQUESTED_WITH="XMLHttpRequest",
|
||||
)
|
||||
content = json.loads(response.content)
|
||||
self.assertEqual(content, expected)
|
||||
self.reload_course()
|
||||
# Verify that user_partitions in the course contains the new group configuration.
|
||||
self.assertEqual(len(self.course.user_partitions), 1)
|
||||
self.assertEqual(self.course.user_partitions[0].name, u'Test name')
|
||||
|
||||
def test_can_edit_group_configuration(self):
|
||||
"""
|
||||
Edit group configuration and check its id and modified fields.
|
||||
"""
|
||||
self.course.user_partitions = [
|
||||
UserPartition(self.ID, 'First name', 'First description', [Group(0, 'Group A'), Group(1, 'Group B'), Group(2, 'Group C')]),
|
||||
]
|
||||
self.save_course()
|
||||
|
||||
expected = {
|
||||
u'id': self.ID,
|
||||
u'name': u'New Test name',
|
||||
u'description': u'New Test description',
|
||||
u'version': 1,
|
||||
u'groups': [
|
||||
{u'id': 0, u'name': u'Group A', u'version': 1},
|
||||
{u'id': 1, u'name': u'Group B', u'version': 1},
|
||||
],
|
||||
}
|
||||
response = self.client.put(
|
||||
self._url(),
|
||||
data=json.dumps(expected),
|
||||
content_type="application/json",
|
||||
HTTP_ACCEPT="application/json",
|
||||
HTTP_X_REQUESTED_WITH="XMLHttpRequest",
|
||||
)
|
||||
content = json.loads(response.content)
|
||||
self.assertEqual(content, expected)
|
||||
self.reload_course()
|
||||
# Verify that user_partitions is properly updated in the course.
|
||||
self.assertEqual(len(self.course.user_partitions), 1)
|
||||
self.assertEqual(self.course.user_partitions[0].name, u'New Test name')
|
||||
|
||||
@@ -65,6 +65,7 @@ class TextbookIndexTestCase(CourseTestCase):
|
||||
)
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
obj = json.loads(resp.content)
|
||||
|
||||
self.assertEqual(content, obj)
|
||||
|
||||
def test_view_index_xhr_put(self):
|
||||
|
||||
@@ -4,7 +4,10 @@ define([
|
||||
'use strict';
|
||||
var Group = Backbone.AssociatedModel.extend({
|
||||
defaults: function() {
|
||||
return { name: '' };
|
||||
return {
|
||||
name: '',
|
||||
version: null
|
||||
};
|
||||
},
|
||||
|
||||
isEmpty: function() {
|
||||
@@ -12,7 +15,10 @@ define([
|
||||
},
|
||||
|
||||
toJSON: function() {
|
||||
return { name: this.get('name') };
|
||||
return {
|
||||
name: this.get('name'),
|
||||
version: this.get('version')
|
||||
};
|
||||
},
|
||||
|
||||
validate: function(attrs) {
|
||||
|
||||
@@ -10,6 +10,7 @@ function(Backbone, _, str, gettext, GroupModel, GroupCollection) {
|
||||
return {
|
||||
name: '',
|
||||
description: '',
|
||||
version: null,
|
||||
groups: new GroupCollection([]),
|
||||
showGroups: false,
|
||||
editing: false
|
||||
@@ -51,6 +52,7 @@ function(Backbone, _, str, gettext, GroupModel, GroupCollection) {
|
||||
id: this.get('id'),
|
||||
name: this.get('name'),
|
||||
description: this.get('description'),
|
||||
version: this.get('version'),
|
||||
groups: this.get('groups').toJSON()
|
||||
};
|
||||
},
|
||||
|
||||
@@ -90,30 +90,36 @@ define([
|
||||
|
||||
it('should match server model to client model', function() {
|
||||
var serverModelSpec = {
|
||||
'id': 10,
|
||||
'name': 'My Group Configuration',
|
||||
'description': 'Some description',
|
||||
'groups': [
|
||||
{
|
||||
'name': 'Group 1'
|
||||
}, {
|
||||
'name': 'Group 2'
|
||||
}
|
||||
]
|
||||
'id': 10,
|
||||
'name': 'My Group Configuration',
|
||||
'description': 'Some description',
|
||||
'version': 1,
|
||||
'groups': [
|
||||
{
|
||||
'version': 1,
|
||||
'name': 'Group 1'
|
||||
}, {
|
||||
'version': 1,
|
||||
'name': 'Group 2'
|
||||
}
|
||||
]
|
||||
},
|
||||
clientModelSpec = {
|
||||
'id': 10,
|
||||
'name': 'My Group Configuration',
|
||||
'description': 'Some description',
|
||||
'showGroups': false,
|
||||
'editing': false,
|
||||
'groups': [
|
||||
{
|
||||
'name': 'Group 1'
|
||||
}, {
|
||||
'name': 'Group 2'
|
||||
}
|
||||
]
|
||||
'id': 10,
|
||||
'name': 'My Group Configuration',
|
||||
'description': 'Some description',
|
||||
'showGroups': false,
|
||||
'editing': false,
|
||||
'version': 1,
|
||||
'groups': [
|
||||
{
|
||||
'version': 1,
|
||||
'name': 'Group 1'
|
||||
}, {
|
||||
'version': 1,
|
||||
'name': 'Group 2'
|
||||
}
|
||||
]
|
||||
},
|
||||
model = new GroupConfigurationModel(serverModelSpec);
|
||||
|
||||
|
||||
@@ -273,7 +273,9 @@ define([
|
||||
|
||||
describe('GroupConfigurationItem', function() {
|
||||
beforeEach(function() {
|
||||
this.model = new GroupConfigurationModel({ });
|
||||
view_helpers.installTemplate('group-configuration-edit', true);
|
||||
view_helpers.installTemplate('group-configuration-details');
|
||||
this.model = new GroupConfigurationModel({ id: 0 });
|
||||
this.collection = new GroupConfigurationCollection([ this.model ]);
|
||||
this.view = new GroupConfigurationItem({
|
||||
model: this.model
|
||||
@@ -284,10 +286,10 @@ define([
|
||||
it('should render properly', function() {
|
||||
// Details view by default
|
||||
expect(this.view.$(SELECTORS.detailsView)).toExist();
|
||||
this.model.set('editing', true);
|
||||
this.view.$('.action-edit .edit').click();
|
||||
expect(this.view.$(SELECTORS.editView)).toExist();
|
||||
expect(this.view.$(SELECTORS.detailsView)).not.toExist();
|
||||
this.model.set('editing', false);
|
||||
this.view.$('.action-cancel').click();
|
||||
expect(this.view.$(SELECTORS.detailsView)).toExist();
|
||||
expect(this.view.$(SELECTORS.editView)).not.toExist();
|
||||
});
|
||||
|
||||
@@ -6,6 +6,7 @@ function(BaseView, _, gettext) {
|
||||
var GroupConfigurationDetails = BaseView.extend({
|
||||
tagName: 'div',
|
||||
events: {
|
||||
'click .edit': 'editConfiguration',
|
||||
'click .show-groups': 'showGroups',
|
||||
'click .hide-groups': 'hideGroups'
|
||||
},
|
||||
@@ -36,6 +37,11 @@ function(BaseView, _, gettext) {
|
||||
return this;
|
||||
},
|
||||
|
||||
editConfiguration: function(event) {
|
||||
if(event && event.preventDefault) { event.preventDefault(); }
|
||||
this.model.set('editing', true);
|
||||
},
|
||||
|
||||
showGroups: function(e) {
|
||||
if(e && e.preventDefault) { e.preventDefault(); }
|
||||
this.model.set('showGroups', true);
|
||||
|
||||
@@ -44,6 +44,7 @@ define([
|
||||
}
|
||||
|
||||
this.$el.html(this.view.render().el);
|
||||
this.$el.focus();
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
@@ -34,6 +34,7 @@ define([
|
||||
|
||||
this.$el.html([frag]);
|
||||
}
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
|
||||
@@ -157,6 +157,24 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.actions {
|
||||
@include transition(opacity .15s .25s ease-in-out);
|
||||
opacity: 0.0;
|
||||
position: absolute;
|
||||
top: $baseline;
|
||||
right: $baseline;
|
||||
|
||||
.action {
|
||||
display: inline-block;
|
||||
margin-right: ($baseline/4);
|
||||
|
||||
.edit {
|
||||
@include blue-button;
|
||||
@extend %t-action4;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:hover .actions {
|
||||
@@ -322,6 +340,26 @@
|
||||
|
||||
&.add-group-configuration-name label {
|
||||
@extend %t-title5;
|
||||
display: inline-block;
|
||||
width: 50%;
|
||||
padding-right: 5%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
vertical-align: bottom;
|
||||
}
|
||||
|
||||
.group-configuration-id {
|
||||
display: inline-block;
|
||||
width: 45%;
|
||||
text-align: right;
|
||||
vertical-align: top;
|
||||
color: $gray-l1;
|
||||
|
||||
.group-configuration-value {
|
||||
font-weight: 600;
|
||||
white-space: nowrap;
|
||||
margin-left: ($baseline*0.5);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -69,7 +69,17 @@ function(doc, GroupConfigurationCollection, GroupConfigurationsPage) {
|
||||
% endif
|
||||
</article>
|
||||
<aside class="content-supplementary" role="complimentary">
|
||||
<div class="bit">
|
||||
<div class="bit">
|
||||
<h3 class="title-3">${_("What can I do on this page?")}</h3>
|
||||
<p>${_("You can create and edit group configurations.")}</p>
|
||||
|
||||
<p>${_("A group configuration defines how many groups of students are in an experiment. When you create a content experiment, you select the group configuration to use.")}</p>
|
||||
|
||||
<p>${_("Click {em_start}New Group Configuration{em_end} to add a new configuration. To edit an existing configuration, hover over its box and click {em_start}Edit{em_end}.").format(em_start='<strong>', em_end="</strong>")}</p>
|
||||
|
||||
<p><a href="${get_online_help_info(online_help_token())['doc_url']}" target="_blank">${_("Learn More")}</a></p>
|
||||
</div>
|
||||
<div class="bit">
|
||||
% if context_course:
|
||||
<%
|
||||
details_url = utils.reverse_course_url('settings_handler', context_course.id)
|
||||
|
||||
@@ -37,4 +37,9 @@
|
||||
<% }) %>
|
||||
</ol>
|
||||
<% } %>
|
||||
<ul class="actions group-configuration-actions">
|
||||
<li class="action action-edit">
|
||||
<button class="edit"><%= gettext("Edit") %></button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
@@ -8,7 +8,14 @@
|
||||
<fieldset class="group-configuration-fields">
|
||||
<legend class="sr"><%= gettext("Group Configuration information") %></legend>
|
||||
<div class="input-wrap field text required add-group-configuration-name <% if(error && error.attributes && error.attributes.name) { print('error'); } %>">
|
||||
<label for="group-configuration-name-<%= uniqueId %>"><%= gettext("Group Configuration Name") %></label>
|
||||
<label for="group-configuration-name-<%= uniqueId %>"><%= gettext("Group Configuration Name") %></label><%
|
||||
if (!_.isUndefined(id)) {
|
||||
%><span class="group-configuration-id">
|
||||
<span class="group-configuration-label"><%= gettext('Group Configuration ID') %></span>
|
||||
<span class="group-configuration-value"><%= id %></span>
|
||||
</span><%
|
||||
}
|
||||
%>
|
||||
<input id="group-configuration-name-<%= uniqueId %>" class="group-configuration-name-input input-text" name="group-configuration-name" type="text" placeholder="<%= gettext("This is the Name of the Group Configuration") %>" value="<%= name %>">
|
||||
<span class="tip tip-stacked"><%= gettext("Name or short description of the configuration") %></span>
|
||||
</div>
|
||||
|
||||
@@ -63,7 +63,7 @@ class ComponentEditorView(PageObject):
|
||||
action = action.send_keys(Keys.BACKSPACE)
|
||||
# Send the new text, then Tab to move to the next field (so change event is triggered).
|
||||
action.send_keys(value).send_keys(Keys.TAB).perform()
|
||||
click_css(self, 'a.action-save')
|
||||
self.save()
|
||||
|
||||
def set_select_value_and_save(self, label, value):
|
||||
"""
|
||||
@@ -72,4 +72,27 @@ class ComponentEditorView(PageObject):
|
||||
elem = self.get_setting_element(label)
|
||||
select = Select(elem)
|
||||
select.select_by_value(value)
|
||||
self.save()
|
||||
|
||||
def get_selected_option_text(self, label):
|
||||
"""
|
||||
Returns the text of the first selected option for the select with given label (display name).
|
||||
"""
|
||||
elem = self.get_setting_element(label)
|
||||
if elem:
|
||||
select = Select(elem)
|
||||
return select.first_selected_option.text
|
||||
else:
|
||||
return None
|
||||
|
||||
def save(self):
|
||||
"""
|
||||
Clicks save button.
|
||||
"""
|
||||
click_css(self, 'a.action-save')
|
||||
|
||||
def cancel(self):
|
||||
"""
|
||||
Clicks cancel button.
|
||||
"""
|
||||
click_css(self, 'a.action-cancel', require_notification=False)
|
||||
|
||||
@@ -134,6 +134,12 @@ class ContainerPage(PageObject):
|
||||
"""
|
||||
return self.q(css='.add-missing-groups-button').present
|
||||
|
||||
def get_xblock_information_message(self):
|
||||
"""
|
||||
Returns an information message for the container page.
|
||||
"""
|
||||
return self.q(css=".xblock-message.information").first.text[0]
|
||||
|
||||
|
||||
class XBlockWrapper(PageObject):
|
||||
"""
|
||||
|
||||
@@ -17,7 +17,7 @@ class GroupConfigurationsPage(CoursePage):
|
||||
|
||||
def group_configurations(self):
|
||||
"""
|
||||
Returns list of the group configurations for the course.
|
||||
Return list of the group configurations for the course.
|
||||
"""
|
||||
css = '.group-configurations-list-item'
|
||||
return [GroupConfiguration(self, index) for index in xrange(len(self.q(css=css)))]
|
||||
@@ -55,6 +55,19 @@ class GroupConfiguration(object):
|
||||
css = 'a.group-toggle'
|
||||
self.find_css(css).first.click()
|
||||
|
||||
def get_text(self, css):
|
||||
"""
|
||||
Return text for the defined by css locator.
|
||||
"""
|
||||
return self.find_css(css).first.text[0]
|
||||
|
||||
def edit(self):
|
||||
"""
|
||||
Open editing view for the group configuration.
|
||||
"""
|
||||
css = '.action-edit .edit'
|
||||
self.find_css(css).first.click()
|
||||
|
||||
def save(self):
|
||||
"""
|
||||
Save group configuration.
|
||||
@@ -73,7 +86,7 @@ class GroupConfiguration(object):
|
||||
@property
|
||||
def mode(self):
|
||||
"""
|
||||
Returns group configuration mode.
|
||||
Return group configuration mode.
|
||||
"""
|
||||
if self.find_css('.group-configuration-edit').present:
|
||||
return 'edit'
|
||||
@@ -83,31 +96,28 @@ class GroupConfiguration(object):
|
||||
@property
|
||||
def id(self):
|
||||
"""
|
||||
Returns group configuration id.
|
||||
Return group configuration id.
|
||||
"""
|
||||
css = '.group-configuration-id .group-configuration-value'
|
||||
return self.find_css(css).first.text[0]
|
||||
return self.get_text('.group-configuration-id .group-configuration-value')
|
||||
|
||||
@property
|
||||
def validation_message(self):
|
||||
"""
|
||||
Returns validation message.
|
||||
Return validation message.
|
||||
"""
|
||||
css = '.message-status.error'
|
||||
return self.find_css(css).first.text[0]
|
||||
return self.get_text('.message-status.error')
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""
|
||||
Returns group configuration name.
|
||||
Return group configuration name.
|
||||
"""
|
||||
css = '.group-configuration-title'
|
||||
return self.find_css(css).first.text[0]
|
||||
return self.get_text('.group-configuration-title')
|
||||
|
||||
@name.setter
|
||||
def name(self, value):
|
||||
"""
|
||||
Sets group configuration name.
|
||||
Set group configuration name.
|
||||
"""
|
||||
css = '.group-configuration-name-input'
|
||||
self.find_css(css).first.fill(value)
|
||||
@@ -115,15 +125,14 @@ class GroupConfiguration(object):
|
||||
@property
|
||||
def description(self):
|
||||
"""
|
||||
Returns group configuration description.
|
||||
Return group configuration description.
|
||||
"""
|
||||
css = '.group-configuration-description'
|
||||
return self.find_css(css).first.text[0]
|
||||
return self.get_text('.group-configuration-description')
|
||||
|
||||
@description.setter
|
||||
def description(self, value):
|
||||
"""
|
||||
Sets group configuration description.
|
||||
Set group configuration description.
|
||||
"""
|
||||
css = '.group-configuration-description-input'
|
||||
self.find_css(css).first.fill(value)
|
||||
@@ -131,7 +140,7 @@ class GroupConfiguration(object):
|
||||
@property
|
||||
def groups(self):
|
||||
"""
|
||||
Returns list of groups.
|
||||
Return list of groups.
|
||||
"""
|
||||
css = '.group'
|
||||
|
||||
@@ -161,7 +170,7 @@ class Group(object):
|
||||
@property
|
||||
def name(self):
|
||||
"""
|
||||
Returns group name.
|
||||
Return group name.
|
||||
"""
|
||||
css = '.group-name'
|
||||
return self.find_css(css).first.text[0]
|
||||
@@ -169,7 +178,7 @@ class Group(object):
|
||||
@property
|
||||
def allocation(self):
|
||||
"""
|
||||
Returns allocation for the group.
|
||||
Return allocation for the group.
|
||||
"""
|
||||
css = '.group-allocation'
|
||||
return self.find_css(css).first.text[0]
|
||||
|
||||
@@ -4,6 +4,7 @@ Acceptance tests for Studio related to the split_test module.
|
||||
|
||||
import json
|
||||
import os
|
||||
import math
|
||||
from unittest import skip, skipUnless
|
||||
|
||||
from xmodule.partitions.partitions import Group, UserPartition
|
||||
@@ -243,10 +244,42 @@ class GroupConfigurationsTest(ContainerBase):
|
||||
self.course_info['run']
|
||||
)
|
||||
|
||||
def _assert_fields(self, config, cid=None, name='', description='', groups=None):
|
||||
self.assertEqual(config.mode, 'details')
|
||||
|
||||
if name:
|
||||
self.assertIn(name, config.name)
|
||||
|
||||
if cid:
|
||||
self.assertEqual(cid, config.id)
|
||||
else:
|
||||
# To make sure that id is present on the page and it is not an empty.
|
||||
# We do not check the value of the id, because it's generated randomly and we cannot
|
||||
# predict this value
|
||||
self.assertTrue(config.id)
|
||||
|
||||
# Expand the configuration
|
||||
config.toggle()
|
||||
|
||||
if description:
|
||||
self.assertIn(description, config.description)
|
||||
|
||||
if groups:
|
||||
allocation = int(math.floor(100 / len(groups)))
|
||||
for index, group in enumerate(groups):
|
||||
self.assertEqual(group, config.groups[index].name)
|
||||
self.assertEqual(str(allocation) + "%", config.groups[index].allocation)
|
||||
# Collapse the configuration
|
||||
config.toggle()
|
||||
|
||||
def test_no_group_configurations_added(self):
|
||||
"""
|
||||
Ensure that message telling me to create a new group configuration is
|
||||
Scenario: Ensure that message telling me to create a new group configuration is
|
||||
shown when group configurations were not added.
|
||||
Given I have a course without group configurations
|
||||
When I go to the Group Configuration page in Studio
|
||||
Then I see "You haven't created any group configurations yet." message
|
||||
And "Create new Group Configuration" button is available
|
||||
"""
|
||||
self.page.visit()
|
||||
css = ".wrapper-content .no-group-configurations-content"
|
||||
@@ -258,8 +291,14 @@ class GroupConfigurationsTest(ContainerBase):
|
||||
|
||||
def test_group_configurations_have_correct_data(self):
|
||||
"""
|
||||
Ensure that the group configuration is rendered correctly in
|
||||
expanded/collapsed mode.
|
||||
Scenario: Ensure that the group configuration is rendered correctly in expanded/collapsed mode.
|
||||
Given I have a course with 2 group configurations
|
||||
And I go to the Group Configuration page in Studio
|
||||
And I work with the first group configuration
|
||||
And I see `name`, `id` are visible and have correct values
|
||||
When I expand the first group configuration
|
||||
Then I see `description` and `groups` appear and also have correct values
|
||||
And I do the same checks for the second group configuration
|
||||
"""
|
||||
self.course_fix.add_advanced_settings({
|
||||
u"user_partitions": {
|
||||
@@ -274,66 +313,93 @@ class GroupConfigurationsTest(ContainerBase):
|
||||
self.page.visit()
|
||||
|
||||
config = self.page.group_configurations()[0]
|
||||
self.assertIn("Name of the Group Configuration", config.name)
|
||||
self.assertEqual(config.id, '0')
|
||||
# Expand the configuration
|
||||
config.toggle()
|
||||
self.assertIn("Description of the group configuration.", config.description)
|
||||
self.assertEqual(len(config.groups), 2)
|
||||
|
||||
self.assertEqual("Group 0", config.groups[0].name)
|
||||
self.assertEqual("50%", config.groups[0].allocation)
|
||||
# no groups when the the configuration is collapsed
|
||||
self.assertEqual(len(config.groups), 0)
|
||||
self._assert_fields(
|
||||
config,
|
||||
cid="0", name="Name of the Group Configuration",
|
||||
description="Description of the group configuration.",
|
||||
groups=["Group 0", "Group 1"]
|
||||
)
|
||||
|
||||
config = self.page.group_configurations()[1]
|
||||
self.assertIn("Name of second Group Configuration", config.name)
|
||||
self.assertEqual(len(config.groups), 0) # no groups when the partition is collapsed
|
||||
# Expand the configuration
|
||||
config.toggle()
|
||||
self.assertEqual(len(config.groups), 3)
|
||||
|
||||
self.assertEqual("Beta", config.groups[1].name)
|
||||
self.assertEqual("33%", config.groups[1].allocation)
|
||||
self._assert_fields(
|
||||
config,
|
||||
name="Name of second Group Configuration",
|
||||
description="Second group configuration.",
|
||||
groups=["Alpha", "Beta", "Gamma"]
|
||||
)
|
||||
|
||||
def test_can_create_group_configuration(self):
|
||||
def test_can_create_and_edit_group_configuration(self):
|
||||
"""
|
||||
Ensure that the group configuration can be created correctly.
|
||||
Scenario: Ensure that the group configuration can be created and edited correctly.
|
||||
Given I have a course without group configurations
|
||||
When I click button 'Create new Group Configuration'
|
||||
And I set new name and description
|
||||
And I click button 'Create'
|
||||
Then I see the new group configuration is added
|
||||
When I edit the group group_configuration
|
||||
And I change the name and description
|
||||
And I click button 'Save'
|
||||
Then I see the group configuration is saved successfully and has the new data
|
||||
"""
|
||||
self.page.visit()
|
||||
|
||||
self.assertEqual(len(self.page.group_configurations()), 0)
|
||||
# Create new group configuration
|
||||
self.page.create()
|
||||
|
||||
config = self.page.group_configurations()[0]
|
||||
config.name = "New Group Configuration Name"
|
||||
config.description = "New Description of the group configuration."
|
||||
self.assertEqual(config.get_text('.action-primary'), "CREATE")
|
||||
# Save the configuration
|
||||
config.save()
|
||||
|
||||
self.assertEqual(config.mode, 'details')
|
||||
self.assertIn("New Group Configuration Name", config.name)
|
||||
self.assertTrue(config.id)
|
||||
# Expand the configuration
|
||||
config.toggle()
|
||||
self.assertIn("New Description of the group configuration.", config.description)
|
||||
self.assertEqual(len(config.groups), 2)
|
||||
self._assert_fields(
|
||||
config,
|
||||
name="New Group Configuration Name",
|
||||
description="New Description of the group configuration.",
|
||||
groups=["Group A", "Group B"]
|
||||
)
|
||||
|
||||
self.assertEqual("Group A", config.groups[0].name)
|
||||
self.assertEqual("Group B", config.groups[1].name)
|
||||
self.assertEqual("50%", config.groups[0].allocation)
|
||||
# Edit the group configuration
|
||||
config.edit()
|
||||
# Update fields
|
||||
self.assertTrue(config.id)
|
||||
config.name = "Second Group Configuration Name"
|
||||
config.description = "Second Description of the group configuration."
|
||||
self.assertEqual(config.get_text('.action-primary'), "SAVE")
|
||||
# Save the configuration
|
||||
config.save()
|
||||
|
||||
self._assert_fields(
|
||||
config,
|
||||
name="Second Group Configuration Name",
|
||||
description="Second Description of the group configuration."
|
||||
)
|
||||
|
||||
def test_use_group_configuration(self):
|
||||
"""
|
||||
Create and use group configuration
|
||||
Scenario: Ensure that the group configuration can be used by split_module correctly
|
||||
Given I have a course without group configurations
|
||||
When I create new group configuration
|
||||
And I set new name, save the group configuration
|
||||
And I go to the unit page in Studio
|
||||
And I add new advanced module "Content Experiment"
|
||||
When I assign created group configuration to the module
|
||||
Then I see the module has correct groups
|
||||
And I go to the Group Configuration page in Studio
|
||||
And I edit the name of the group configuration
|
||||
And I go to the unit page in Studio
|
||||
And I edit the unit
|
||||
Then I see the group configuration name is changed in `Group Configuration` dropdown
|
||||
And the group configuration name is changed on container page
|
||||
"""
|
||||
self.page.visit()
|
||||
self.assertEqual(len(self.page.group_configurations()), 0)
|
||||
# Create new group configuration
|
||||
self.page.create()
|
||||
|
||||
config = self.page.group_configurations()[0]
|
||||
config.name = "New Group Configuration Name"
|
||||
config.description = "New Description of the group configuration."
|
||||
# Save the configuration
|
||||
config.save()
|
||||
|
||||
@@ -345,9 +411,34 @@ class GroupConfigurationsTest(ContainerBase):
|
||||
component_editor.set_select_value_and_save('Group Configuration', 'New Group Configuration Name')
|
||||
self.verify_groups(container, ['Group A', 'Group B'], [])
|
||||
|
||||
self.page.visit()
|
||||
config = self.page.group_configurations()[0]
|
||||
config.edit()
|
||||
config.name = "Second Group Configuration Name"
|
||||
# Save the configuration
|
||||
config.save()
|
||||
|
||||
container = self.go_to_container_page()
|
||||
container.edit()
|
||||
component_editor = ComponentEditorView(self.browser, container.locator)
|
||||
self.assertEqual(
|
||||
"Second Group Configuration Name",
|
||||
component_editor.get_selected_option_text('Group Configuration')
|
||||
)
|
||||
component_editor.cancel()
|
||||
self.assertIn(
|
||||
"Second Group Configuration Name",
|
||||
container.get_xblock_information_message()
|
||||
)
|
||||
|
||||
def test_can_cancel_creation_of_group_configuration(self):
|
||||
"""
|
||||
Ensure that creation of the group configuration can be canceled correctly.
|
||||
Scenario: Ensure that creation of the group configuration can be canceled correctly.
|
||||
Given I have a course without group configurations
|
||||
When I click button 'Create new Group Configuration'
|
||||
And I set new name and description
|
||||
And I click button 'Cancel'
|
||||
Then I see that there is no new group configurations in the course
|
||||
"""
|
||||
self.page.visit()
|
||||
|
||||
@@ -363,9 +454,49 @@ class GroupConfigurationsTest(ContainerBase):
|
||||
|
||||
self.assertEqual(len(self.page.group_configurations()), 0)
|
||||
|
||||
def test_can_cancel_editing_of_group_configuration(self):
|
||||
"""
|
||||
Scenario: Ensure that editing of the group configuration can be canceled correctly.
|
||||
Given I have a course with group configuration
|
||||
When I go to the edit mode of the group configuration
|
||||
And I set new name and description
|
||||
And I click button 'Cancel'
|
||||
Then I see that new changes were discarded
|
||||
"""
|
||||
self.course_fix.add_advanced_settings({
|
||||
u"user_partitions": {
|
||||
"value": [
|
||||
UserPartition(0, 'Name of the Group Configuration', 'Description of the group configuration.', [Group("0", 'Group 0'), Group("1", 'Group 1')]).to_json(),
|
||||
UserPartition(1, 'Name of second Group Configuration', 'Second group configuration.', [Group("0", 'Alpha'), Group("1", 'Beta'), Group("2", 'Gamma')]).to_json()
|
||||
],
|
||||
},
|
||||
})
|
||||
self.course_fix._add_advanced_settings()
|
||||
self.page.visit()
|
||||
|
||||
config = self.page.group_configurations()[0]
|
||||
|
||||
config.name = "New Group Configuration Name"
|
||||
config.description = "New Description of the group configuration."
|
||||
# Cancel the configuration
|
||||
config.cancel()
|
||||
|
||||
self._assert_fields(
|
||||
config,
|
||||
name="Name of the Group Configuration",
|
||||
description="Description of the group configuration.",
|
||||
groups=["Group 0", "Group 1"]
|
||||
)
|
||||
|
||||
def test_group_configuration_validation(self):
|
||||
"""
|
||||
Ensure that validation of the group configuration works correctly.
|
||||
Scenario: Ensure that validation of the group configuration works correctly.
|
||||
Given I have a course without group configurations
|
||||
And I create new group configuration with 2 default groups
|
||||
When I set only description and try to save
|
||||
Then I see error message "Group Configuration name is required"
|
||||
When I set new name and try to save
|
||||
Then I see the group configuration is saved successfully
|
||||
"""
|
||||
self.page.visit()
|
||||
|
||||
@@ -387,11 +518,10 @@ class GroupConfigurationsTest(ContainerBase):
|
||||
config.name = "Name of the Group Configuration"
|
||||
# Save the configuration
|
||||
config.save()
|
||||
# Verify the configuration is saved and it is shown in `details` mode.
|
||||
self.assertEqual(config.mode, 'details')
|
||||
# Verify the configuration for the data correctness
|
||||
self.assertIn("Name of the Group Configuration", config.name)
|
||||
self.assertTrue(config.id)
|
||||
# Expand the configuration
|
||||
config.toggle()
|
||||
self.assertIn("Description of the group configuration.", config.description)
|
||||
|
||||
self._assert_fields(
|
||||
config,
|
||||
name="Name of the Group Configuration",
|
||||
description="Description of the group configuration.",
|
||||
groups=["Group A", "Group B"]
|
||||
)
|
||||
|
||||
@@ -33,9 +33,10 @@ export = building_course/export_import_course.html#export-a-course
|
||||
welcome = getting_started/get_started.html
|
||||
login = getting_started/get_started.html
|
||||
register = getting_started/get_started.html
|
||||
group_configurations = content_experiments/content_experiments_configure.html#set-up-group-configurations-in-edx-studio
|
||||
|
||||
# below are the language directory names for the different locales
|
||||
[locales]
|
||||
default = en
|
||||
en = en
|
||||
en_us = en
|
||||
en_us = en
|
||||
|
||||
Reference in New Issue
Block a user