Commit 39f1f009 by Anton Stupak

Merge pull request #4424 from edx/anton/edit-group-configurations

Edit group configurations.
parents 1c595998 2a6a874a
......@@ -869,43 +869,90 @@ class GroupConfiguration(object):
"""
Prepare Group Configuration for the course.
"""
def __init__(self, json_string, course, configuration_id=None):
"""
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(configuration_json):
def parse(json_string):
"""
Parse given string that represents group configuration.
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
def validate(self):
"""
Validate group configuration representation.
"""
if not self.configuration.get("name"):
raise GroupConfigurationsValidationError(_("must have name of the configuration"))
if len(self.configuration.get('groups', [])) < 2:
raise GroupConfigurationsValidationError(_("must have at least two groups"))
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 group_configuration.get('groups'):
group_configuration['groups'] = [
if not self.configuration.get('groups'):
self.configuration['groups'] = [
{'name': 'Group A'}, {'name': 'Group B'},
]
for group in group_configuration['groups']:
group['version'] = Group.VERSION
return group_configuration
# Assign ids to every group in configuration.
for index, group in enumerate(self.configuration.get('groups', [])):
group['id'] = index
@staticmethod
def validate(group_configuration):
def get_used_ids(self):
"""
Validate group configuration representation.
Return a list of IDs that already in use.
"""
if not group_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:
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"))
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)
if not configuration.get("id"):
configuration["id"] = random.randint(100, 10**12)
# Assign ids to every group in configuration.
for index, group in enumerate(configuration.get('groups', [])):
group["id"] = index
try:
new_configuration = GroupConfiguration(request.body, course).get_user_partition()
except GroupConfigurationsValidationError as err:
return JsonResponse({"error": err.message}, status=400)
course.user_partitions.append(UserPartition.from_json(configuration))
store.update_item(course, request.user.id)
response = JsonResponse(configuration, status=201)
course.user_partitions.append(new_configuration)
response = JsonResponse(new_configuration.to_json(), 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):
......
"""
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
GROUP_CONFIGURATION_JSON = {
u'name': u'Test name',
u'description': u'Test description',
}
# pylint: disable=no-member
class GroupConfigurationsBaseTestCase(object):
"""
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"]]
return (configuration_id, group_ids)
def test_required_fields_are_absent(self):
"""
Test required fields are absent.
"""
bad_jsons = [
# 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(),
data=json.dumps(bad_json),
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_invalid_json(self):
"""
Test invalid json handling.
"""
# No property name.
invalid_json = "{u'name': 'Test Name', []}"
response = self.client.post(
self._url(),
data=invalid_json,
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)
# pylint: disable=no-member
@skipUnless(settings.FEATURES.get('ENABLE_GROUP_CONFIGURATIONS'), 'Tests Group Configurations feature')
class GroupConfigurationsCreateTestCase(CourseTestCase):
class GroupConfigurationsListHandlerTestCase(CourseTestCase, GroupConfigurationsBaseTestCase):
"""
Test cases for creating a new group configurations.
Test cases for group_configurations_list_handler.
"""
def setUp(self):
"""
Set up a url and group configuration content for tests.
Set up GroupConfigurationsListHandlerTestCase.
"""
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'
}
super(GroupConfigurationsListHandlerTestCase, self).setUp()
def test_index_page(self):
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)
response = self.client.get(self._url())
self.assertEqual(response.status_code, 200)
self.assertIn('New Group Configuration', response.content)
def test_group_success(self):
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_group_configuration = {
expected = {
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}
]
{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(self.group_configuration_json),
self._url(),
data=json.dumps(GROUP_CONFIGURATION_JSON),
content_type="application/json",
HTTP_ACCEPT="application/json",
HTTP_X_REQUESTED_WITH="XMLHttpRequest"
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)
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')
def test_bad_group(self):
# 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):
"""
Test if only one group in configuration exist.
Set up GroupConfigurationsDetailHandlerTestCase.
"""
# 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"
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},
)
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):
def test_can_create_new_group_configuration_if_it_is_not_exist(self):
"""
Test if configuration id is not numeric.
PUT new group configuration when no configurations exist in the course.
"""
# Configuration id is string here.
bad_group_configuration = {
u'description': u'Test description',
u'id': 'bad_id',
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}
]
{u'id': 1, u'name': u'Group B', u'version': 1},
],
}
response = self.client.post(
self.url,
data=json.dumps(bad_group_configuration),
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"
HTTP_X_REQUESTED_WITH="XMLHttpRequest",
)
self.assertEqual(response.status_code, 400)
self.assertNotIn("Location", response)
content = json.loads(response.content)
self.assertIn("error", 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_bad_json(self):
def test_can_edit_group_configuration(self):
"""
Test bad json handling.
Edit group configuration and check its id and modified fields.
"""
bad_jsons = [
{u'name': 'Test Name'},
{u'description': 'Test description'},
{}
self.course.user_partitions = [
UserPartition(self.ID, 'First name', 'First description', [Group(0, 'Group A'), Group(1, 'Group B'), Group(2, 'Group C')]),
]
for bad_json in bad_jsons:
response = self.client.post(
self.url,
data=json.dumps(bad_json),
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_invalid_json(self):
"""
Test invalid json handling.
"""
# No property name.
invalid_json = "{u'name': 'Test Name', []}"
self.save_course()
response = self.client.post(
self.url,
data=invalid_json,
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",
)
self.assertEqual(response.status_code, 400)
self.assertNotIn("Location", response)
content = json.loads(response.content)
self.assertIn("error", 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._assert_fields(
config,
name="New Group Configuration Name",
description="New Description of the group configuration.",
groups=["Group A", "Group B"]
)
# Edit the group configuration
config.edit()
# Update fields
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)
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.assertEqual("Group A", config.groups[0].name)
self.assertEqual("Group B", config.groups[1].name)
self.assertEqual("50%", config.groups[0].allocation)
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
\ No newline at end of file
en_us = en
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment