From 2a6a874a8fa1f7564b8ce52177d21888d7c64e4d Mon Sep 17 00:00:00 2001 From: polesye Date: Tue, 1 Jul 2014 17:41:36 +0300 Subject: [PATCH] BLD-1099: Edit group configurations. --- cms/djangoapps/contentstore/views/course.py | 168 +++++++--- .../views/tests/test_group_configurations.py | 313 ++++++++++++------ .../views/tests/test_textbooks.py | 1 + cms/static/js/models/group.js | 10 +- cms/static/js/models/group_configuration.js | 2 + .../spec/models/group_configuration_spec.js | 50 +-- .../js/spec/views/group_configuration_spec.js | 8 +- .../js/views/group_configuration_details.js | 6 + .../js/views/group_configuration_item.js | 1 + .../js/views/group_configurations_list.js | 1 + .../sass/views/_group-configuration.scss | 38 +++ cms/templates/group_configurations.html | 12 +- .../js/group-configuration-details.underscore | 5 + .../js/group-configuration-edit.underscore | 9 +- .../pages/studio/component_editor.py | 25 +- .../test/acceptance/pages/studio/container.py | 6 + .../studio/settings_group_configurations.py | 47 +-- .../tests/test_studio_split_test.py | 224 ++++++++++--- docs/config.ini | 3 +- 19 files changed, 676 insertions(+), 253 deletions(-) diff --git a/cms/djangoapps/contentstore/views/course.py b/cms/djangoapps/contentstore/views/course.py index b47b903175..bf4d176f10 100644 --- a/cms/djangoapps/contentstore/views/course.py +++ b/cms/djangoapps/contentstore/views/course.py @@ -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): diff --git a/cms/djangoapps/contentstore/views/tests/test_group_configurations.py b/cms/djangoapps/contentstore/views/tests/test_group_configurations.py index 9a48006461..ef6a34ebda 100644 --- a/cms/djangoapps/contentstore/views/tests/test_group_configurations.py +++ b/cms/djangoapps/contentstore/views/tests/test_group_configurations.py @@ -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') diff --git a/cms/djangoapps/contentstore/views/tests/test_textbooks.py b/cms/djangoapps/contentstore/views/tests/test_textbooks.py index 1475ccc419..3b0f0e8a62 100644 --- a/cms/djangoapps/contentstore/views/tests/test_textbooks.py +++ b/cms/djangoapps/contentstore/views/tests/test_textbooks.py @@ -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): diff --git a/cms/static/js/models/group.js b/cms/static/js/models/group.js index d87c414873..d1fe22f326 100644 --- a/cms/static/js/models/group.js +++ b/cms/static/js/models/group.js @@ -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) { diff --git a/cms/static/js/models/group_configuration.js b/cms/static/js/models/group_configuration.js index d4fcaa0129..07462aba7d 100644 --- a/cms/static/js/models/group_configuration.js +++ b/cms/static/js/models/group_configuration.js @@ -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() }; }, diff --git a/cms/static/js/spec/models/group_configuration_spec.js b/cms/static/js/spec/models/group_configuration_spec.js index 2d812b617b..a3478dd42c 100644 --- a/cms/static/js/spec/models/group_configuration_spec.js +++ b/cms/static/js/spec/models/group_configuration_spec.js @@ -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); diff --git a/cms/static/js/spec/views/group_configuration_spec.js b/cms/static/js/spec/views/group_configuration_spec.js index c1aac48557..40d966d083 100644 --- a/cms/static/js/spec/views/group_configuration_spec.js +++ b/cms/static/js/spec/views/group_configuration_spec.js @@ -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(); }); diff --git a/cms/static/js/views/group_configuration_details.js b/cms/static/js/views/group_configuration_details.js index 8aff004119..a2bd53ce78 100644 --- a/cms/static/js/views/group_configuration_details.js +++ b/cms/static/js/views/group_configuration_details.js @@ -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); diff --git a/cms/static/js/views/group_configuration_item.js b/cms/static/js/views/group_configuration_item.js index ef8bfdf0a5..9aae06a9d7 100644 --- a/cms/static/js/views/group_configuration_item.js +++ b/cms/static/js/views/group_configuration_item.js @@ -44,6 +44,7 @@ define([ } this.$el.html(this.view.render().el); + this.$el.focus(); return this; } diff --git a/cms/static/js/views/group_configurations_list.js b/cms/static/js/views/group_configurations_list.js index 1afd6d2cf4..06b56d335d 100644 --- a/cms/static/js/views/group_configurations_list.js +++ b/cms/static/js/views/group_configurations_list.js @@ -34,6 +34,7 @@ define([ this.$el.html([frag]); } + return this; }, diff --git a/cms/static/sass/views/_group-configuration.scss b/cms/static/sass/views/_group-configuration.scss index 4b2da59220..014b18d757 100644 --- a/cms/static/sass/views/_group-configuration.scss +++ b/cms/static/sass/views/_group-configuration.scss @@ -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); + } } } diff --git a/cms/templates/group_configurations.html b/cms/templates/group_configurations.html index 2cd43e313c..cb51d0d2e0 100644 --- a/cms/templates/group_configurations.html +++ b/cms/templates/group_configurations.html @@ -69,7 +69,17 @@ function(doc, GroupConfigurationCollection, GroupConfigurationsPage) { % endif