Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
E
edx-platform
Overview
Overview
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
edx
edx-platform
Commits
69ee9d92
Commit
69ee9d92
authored
Jul 08, 2014
by
jmclaus
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
BLD-1104: List units that use group configuration.
parent
df71afd4
Hide whitespace changes
Inline
Side-by-side
Showing
14 changed files
with
651 additions
and
135 deletions
+651
-135
cms/djangoapps/contentstore/views/course.py
+66
-2
cms/djangoapps/contentstore/views/tests/test_group_configurations.py
+155
-0
cms/static/js/models/group_configuration.js
+2
-1
cms/static/js/spec/models/group_configuration_spec.js
+9
-1
cms/static/js/spec/views/group_configuration_spec.js
+71
-2
cms/static/js/views/group_configuration_details.js
+42
-2
cms/static/sass/views/_group-configuration.scss
+119
-97
cms/templates/group_configurations.html
+1
-0
cms/templates/js/group-configuration-details.underscore
+29
-8
common/test/acceptance/fixtures/course.py
+30
-8
common/test/acceptance/pages/studio/overview.py
+3
-1
common/test/acceptance/pages/studio/settings_group_configurations.py
+23
-0
common/test/acceptance/pages/studio/unit.py
+7
-0
common/test/acceptance/tests/test_studio_split_test.py
+94
-13
No files found.
cms/djangoapps/contentstore/views/course.py
View file @
69ee9d92
...
...
@@ -4,6 +4,7 @@ Views related to operations on course objects
import
json
import
random
import
string
# pylint: disable=W0402
import
logging
from
django.utils.translation
import
ugettext
as
_
import
django.utils
...
...
@@ -32,7 +33,8 @@ from contentstore.utils import (
get_lms_link_for_item
,
add_extra_panel_tab
,
remove_extra_panel_tab
,
reverse_course_url
reverse_course_url
,
reverse_usage_url
,
)
from
models.settings.course_details
import
CourseDetails
,
CourseSettingsEncoder
...
...
@@ -70,6 +72,8 @@ __all__ = ['course_info_handler', 'course_handler', 'course_info_update_handler'
'textbooks_list_handler'
,
'textbooks_detail_handler'
,
'group_configurations_list_handler'
,
'group_configurations_detail_handler'
]
log
=
logging
.
getLogger
(
__name__
)
class
AccessListFallback
(
Exception
):
"""
...
...
@@ -949,6 +953,62 @@ class GroupConfiguration(object):
groups
)
@staticmethod
def
_get_usage_info
(
course
,
modulestore
):
"""
Get all units names and their urls that have experiments and associated
with configurations.
Returns:
{'user_partition_id':
[
{'label': 'Unit Name / Experiment Name', 'url': 'url_to_unit_1'},
{'label': 'Another Unit Name / Another Experiment Name', 'url': 'url_to_unit_1'}
],
}
"""
usage_info
=
{}
descriptors
=
modulestore
.
get_items
(
course
.
id
,
category
=
'split_test'
)
for
split_test
in
descriptors
:
if
split_test
.
user_partition_id
not
in
usage_info
:
usage_info
[
split_test
.
user_partition_id
]
=
[]
unit_location
=
modulestore
.
get_parent_location
(
split_test
.
location
)
if
not
unit_location
:
log
.
warning
(
"Parent location of split_test module not found:
%
s"
,
split_test
.
location
)
continue
try
:
unit
=
modulestore
.
get_item
(
unit_location
)
except
ItemNotFoundError
:
log
.
warning
(
"Unit not found:
%
s"
,
unit_location
)
continue
unit_url
=
reverse_usage_url
(
'unit_handler'
,
course
.
location
.
course_key
.
make_usage_key
(
unit
.
location
.
block_type
,
unit
.
location
.
name
)
)
usage_info
[
split_test
.
user_partition_id
]
.
append
({
'label'
:
'{} / {}'
.
format
(
unit
.
display_name
,
split_test
.
display_name
),
'url'
:
unit_url
})
return
usage_info
@staticmethod
def
add_usage_info
(
course
,
modulestore
):
"""
Add usage information to group configurations json.
Returns json of group configurations updated with usage information.
"""
usage_info
=
GroupConfiguration
.
_get_usage_info
(
course
,
modulestore
)
configurations
=
[]
for
partition
in
course
.
user_partitions
:
configuration
=
partition
.
to_json
()
configuration
[
'usage'
]
=
usage_info
.
get
(
partition
.
id
,
[])
configurations
.
append
(
configuration
)
return
configurations
@require_http_methods
((
"GET"
,
"POST"
))
@login_required
...
...
@@ -968,12 +1028,16 @@ def group_configurations_list_handler(request, course_key_string):
if
'text/html'
in
request
.
META
.
get
(
'HTTP_ACCEPT'
,
'text/html'
):
group_configuration_url
=
reverse_course_url
(
'group_configurations_list_handler'
,
course_key
)
course_outline_url
=
reverse_course_url
(
'course_handler'
,
course_key
)
split_test_enabled
=
SPLIT_TEST_COMPONENT_TYPE
in
course
.
advanced_modules
configurations
=
GroupConfiguration
.
add_usage_info
(
course
,
store
)
return
render_to_response
(
'group_configurations.html'
,
{
'context_course'
:
course
,
'group_configuration_url'
:
group_configuration_url
,
'configurations'
:
[
u
.
to_json
()
for
u
in
course
.
user_partitions
]
if
split_test_enabled
else
None
,
'course_outline_url'
:
course_outline_url
,
'configurations'
:
configurations
if
split_test_enabled
else
None
,
})
elif
"application/json"
in
request
.
META
.
get
(
'HTTP_ACCEPT'
):
if
request
.
method
==
'POST'
:
...
...
cms/djangoapps/contentstore/views/tests/test_group_configurations.py
View file @
69ee9d92
...
...
@@ -6,8 +6,10 @@ from unittest import skipUnless
from
django.conf
import
settings
from
contentstore.utils
import
reverse_course_url
from
contentstore.views.component
import
SPLIT_TEST_COMPONENT_TYPE
from
contentstore.views.course
import
GroupConfiguration
from
contentstore.tests.utils
import
CourseTestCase
from
xmodule.partitions.partitions
import
Group
,
UserPartition
from
xmodule.modulestore.tests.factories
import
ItemFactory
GROUP_CONFIGURATION_JSON
=
{
...
...
@@ -20,6 +22,44 @@ GROUP_CONFIGURATION_JSON = {
}
class
HelperMethods
(
object
):
"""
Mixin that provides useful methods for Group Configuration tests.
"""
def
_create_content_experiment
(
self
,
cid
=
None
,
name_suffix
=
''
):
"""
Create content experiment.
Assign Group Configuration to the experiment if cid is provided.
"""
vertical
=
ItemFactory
.
create
(
category
=
'vertical'
,
parent_location
=
self
.
course
.
location
,
display_name
=
'Test Unit {}'
.
format
(
name_suffix
)
)
split_test
=
ItemFactory
.
create
(
category
=
'split_test'
,
parent_location
=
vertical
.
location
,
user_partition_id
=
cid
,
display_name
=
'Test Content Experiment {}'
.
format
(
name_suffix
)
)
self
.
save_course
()
return
(
vertical
,
split_test
)
def
_add_user_partitions
(
self
,
count
=
1
):
"""
Create user partitions for the course.
"""
partitions
=
[
UserPartition
(
i
,
'Name '
+
str
(
i
),
'Description '
+
str
(
i
),
[
Group
(
0
,
'Group A'
),
Group
(
1
,
'Group B'
),
Group
(
2
,
'Group C'
)]
)
for
i
in
xrange
(
count
)
]
self
.
course
.
user_partitions
=
partitions
self
.
save_course
()
# pylint: disable=no-member
class
GroupConfigurationsBaseTestCase
(
object
):
"""
...
...
@@ -286,3 +326,118 @@ class GroupConfigurationsDetailHandlerTestCase(CourseTestCase, GroupConfiguratio
self
.
assertEqual
(
len
(
user_partititons
[
0
]
.
groups
),
2
)
self
.
assertEqual
(
user_partititons
[
0
]
.
groups
[
0
]
.
name
,
u'New Group Name'
)
self
.
assertEqual
(
user_partititons
[
0
]
.
groups
[
1
]
.
name
,
u'Group C'
)
# pylint: disable=no-member
@skipUnless
(
settings
.
FEATURES
.
get
(
'ENABLE_GROUP_CONFIGURATIONS'
),
'Tests Group Configurations feature'
)
class
GroupConfigurationsUsageInfoTestCase
(
CourseTestCase
,
HelperMethods
):
"""
Tests for usage information of configurations.
"""
def
setUp
(
self
):
super
(
GroupConfigurationsUsageInfoTestCase
,
self
)
.
setUp
()
def
test_group_configuration_not_used
(
self
):
"""
Test that right data structure will be created if group configuration is not used.
"""
self
.
_add_user_partitions
()
actual
=
GroupConfiguration
.
add_usage_info
(
self
.
course
,
self
.
store
)
expected
=
[{
u'id'
:
0
,
u'name'
:
u'Name 0'
,
u'description'
:
u'Description 0'
,
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'
:
2
,
u'name'
:
u'Group C'
,
u'version'
:
1
},
],
u'usage'
:
[],
}]
self
.
assertEqual
(
actual
,
expected
)
def
test_can_get_correct_usage_info
(
self
):
"""
Test if group configurations json updated successfully with usage information.
"""
self
.
_add_user_partitions
(
count
=
2
)
self
.
_create_content_experiment
(
cid
=
0
,
name_suffix
=
'0'
)
self
.
_create_content_experiment
(
name_suffix
=
'1'
)
actual
=
GroupConfiguration
.
add_usage_info
(
self
.
course
,
self
.
store
)
expected
=
[{
u'id'
:
0
,
u'name'
:
u'Name 0'
,
u'description'
:
u'Description 0'
,
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'
:
2
,
u'name'
:
u'Group C'
,
u'version'
:
1
},
],
u'usage'
:
[{
'url'
:
'/unit/i4x://MITx/999/vertical/Test_Unit_0'
,
'label'
:
'Test Unit 0 / Test Content Experiment 0'
,
}],
},
{
u'id'
:
1
,
u'name'
:
u'Name 1'
,
u'description'
:
u'Description 1'
,
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'
:
2
,
u'name'
:
u'Group C'
,
u'version'
:
1
},
],
u'usage'
:
[],
}]
self
.
assertEqual
(
actual
,
expected
)
def
test_can_use_one_configuration_in_multiple_experiments
(
self
):
"""
Test if multiple experiments are present in usage info when they use same
group configuration.
"""
self
.
_add_user_partitions
()
self
.
_create_content_experiment
(
cid
=
0
,
name_suffix
=
'0'
)
self
.
_create_content_experiment
(
cid
=
0
,
name_suffix
=
'1'
)
actual
=
GroupConfiguration
.
add_usage_info
(
self
.
course
,
self
.
store
)
expected
=
[{
u'id'
:
0
,
u'name'
:
u'Name 0'
,
u'description'
:
u'Description 0'
,
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'
:
2
,
u'name'
:
u'Group C'
,
u'version'
:
1
},
],
u'usage'
:
[{
'url'
:
'/unit/i4x://MITx/999/vertical/Test_Unit_0'
,
'label'
:
'Test Unit 0 / Test Content Experiment 0'
,
},
{
'url'
:
'/unit/i4x://MITx/999/vertical/Test_Unit_1'
,
'label'
:
'Test Unit 1 / Test Content Experiment 1'
,
}],
}]
self
.
assertEqual
(
actual
,
expected
)
def
test_can_handle_without_parent
(
self
):
"""
Test if it possible to handle case when split_test has no parent.
"""
self
.
_add_user_partitions
()
# Create split test without parent.
ItemFactory
.
create
(
category
=
'split_test'
,
user_partition_id
=
0
,
display_name
=
'Test Content Experiment'
)
self
.
save_course
()
actual
=
GroupConfiguration
.
_get_usage_info
(
self
.
course
,
self
.
store
)
self
.
assertEqual
(
actual
,
{
0
:
[]})
cms/static/js/models/group_configuration.js
View file @
69ee9d92
...
...
@@ -22,7 +22,8 @@ function(Backbone, _, str, gettext, GroupModel, GroupCollection) {
}
]),
showGroups
:
false
,
editing
:
false
editing
:
false
,
usage
:
[]
};
},
...
...
cms/static/js/spec/models/group_configuration_spec.js
View file @
69ee9d92
...
...
@@ -9,6 +9,9 @@ define([
this
.
addMatchers
({
toBeInstanceOf
:
function
(
expected
)
{
return
this
.
actual
instanceof
expected
;
},
toBeEmpty
:
function
()
{
return
this
.
actual
.
length
===
0
;
}
});
});
...
...
@@ -40,6 +43,10 @@ define([
expect
(
groups
.
at
(
1
).
get
(
'name'
)).
toBe
(
'Group B'
);
});
it
(
'should have an empty usage by default'
,
function
()
{
expect
(
this
.
model
.
get
(
'usage'
)).
toBeEmpty
();
});
it
(
'should be able to reset itself'
,
function
()
{
this
.
model
.
set
(
'name'
,
'foobar'
);
this
.
model
.
reset
();
...
...
@@ -120,7 +127,8 @@ define([
'order'
:
1
,
'name'
:
'Group 2'
}
]
],
'usage'
:
[]
},
model
=
new
GroupConfigurationModel
(
serverModelSpec
,
{
parse
:
true
}
...
...
cms/static/js/spec/views/group_configuration_spec.js
View file @
69ee9d92
...
...
@@ -28,6 +28,12 @@ define([
inputGroupName
:
'.group-name'
,
inputName
:
'.group-configuration-name-input'
,
inputDescription
:
'.group-configuration-description-input'
,
usageCount
:
'.group-configuration-usage-count'
,
usage
:
'.group-configuration-usage'
,
usageText
:
'.group-configuration-usage-text'
,
usageTextAnchor
:
'.group-configuration-usage-text > a'
,
usageUnit
:
'.group-configuration-usage-unit'
,
usageUnitAnchor
:
'.group-configuration-usage-unit > a'
};
beforeEach
(
function
()
{
...
...
@@ -89,6 +95,7 @@ define([
});
this
.
collection
=
new
GroupConfigurationCollection
([
this
.
model
]);
this
.
collection
.
outlineUrl
=
'/outline'
;
this
.
view
=
new
GroupConfigurationDetails
({
model
:
this
.
model
});
...
...
@@ -126,6 +133,70 @@ define([
expect
(
this
.
view
.
$
(
SELECTORS
.
description
)).
not
.
toExist
();
expect
(
this
.
view
.
$
(
SELECTORS
.
groupsAllocation
)).
not
.
toExist
();
});
it
(
'should show empty usage appropriately'
,
function
()
{
this
.
model
.
set
(
'showGroups'
,
false
);
this
.
view
.
$
(
'.show-groups'
).
click
();
expect
(
this
.
view
.
$
(
SELECTORS
.
usageCount
)).
not
.
toExist
();
expect
(
this
.
view
.
$
(
SELECTORS
.
usageText
))
.
toContainText
(
'This Group Configuration is not in use. '
+
'Start by adding a content experiment to any '
+
'Unit via the'
);
expect
(
this
.
view
.
$
(
SELECTORS
.
usageTextAnchor
)).
toExist
();
expect
(
this
.
view
.
$
(
SELECTORS
.
usageUnit
)).
not
.
toExist
();
});
it
(
'should hide empty usage appropriately'
,
function
()
{
this
.
model
.
set
(
'showGroups'
,
true
);
this
.
view
.
$
(
'.hide-groups'
).
click
();
expect
(
this
.
view
.
$
(
SELECTORS
.
usageText
)).
not
.
toExist
();
expect
(
this
.
view
.
$
(
SELECTORS
.
usageUnit
)).
not
.
toExist
();
expect
(
this
.
view
.
$
(
SELECTORS
.
usageCount
))
.
toContainText
(
'Not in Use'
);
});
it
(
'should show non-empty usage appropriately'
,
function
()
{
var
usageUnitAnchors
;
this
.
model
.
set
(
'usage'
,
[
{
'label'
:
'label1'
,
'url'
:
'url1'
},
{
'label'
:
'label2'
,
'url'
:
'url2'
}
]
);
this
.
model
.
set
(
'showGroups'
,
false
);
this
.
view
.
$
(
'.show-groups'
).
click
();
usageUnitAnchors
=
this
.
view
.
$
(
SELECTORS
.
usageUnitAnchor
);
expect
(
this
.
view
.
$
(
SELECTORS
.
usageCount
)).
not
.
toExist
();
expect
(
this
.
view
.
$
(
SELECTORS
.
usageText
))
.
toContainText
(
'This Group Configuration is used in:'
);
expect
(
this
.
view
.
$
(
SELECTORS
.
usageUnit
).
length
).
toBe
(
2
);
expect
(
usageUnitAnchors
.
length
).
toBe
(
2
);
expect
(
usageUnitAnchors
.
eq
(
0
)).
toContainText
(
'label1'
);
expect
(
usageUnitAnchors
.
eq
(
0
).
attr
(
'href'
)).
toBe
(
'url1'
);
expect
(
usageUnitAnchors
.
eq
(
1
)).
toContainText
(
'label2'
);
expect
(
usageUnitAnchors
.
eq
(
1
).
attr
(
'href'
)).
toBe
(
'url2'
);
});
it
(
'should hide non-empty usage appropriately'
,
function
()
{
this
.
model
.
set
(
'usage'
,
[
{
'label'
:
'label1'
,
'url'
:
'url1'
},
{
'label'
:
'label2'
,
'url'
:
'url2'
}
]
);
this
.
model
.
set
(
'showGroups'
,
true
);
this
.
view
.
$
(
'.hide-groups'
).
click
();
expect
(
this
.
view
.
$
(
SELECTORS
.
usageText
)).
not
.
toExist
();
expect
(
this
.
view
.
$
(
SELECTORS
.
usageUnit
)).
not
.
toExist
();
expect
(
this
.
view
.
$
(
SELECTORS
.
usageCount
))
.
toContainText
(
'Used in 2 units'
);
});
});
describe
(
'GroupConfigurationEdit'
,
function
()
{
...
...
@@ -418,5 +489,3 @@ define([
});
});
});
cms/static/js/views/group_configuration_details.js
View file @
69ee9d92
define
([
'js/views/baseview'
,
'underscore'
,
'gettext'
'js/views/baseview'
,
'underscore'
,
'gettext'
,
'underscore.string'
],
function
(
BaseView
,
_
,
gettext
)
{
function
(
BaseView
,
_
,
gettext
,
str
)
{
'use strict'
;
var
GroupConfigurationDetails
=
BaseView
.
extend
({
tagName
:
'div'
,
...
...
@@ -30,6 +30,8 @@ function(BaseView, _, gettext) {
render
:
function
()
{
var
attrs
=
$
.
extend
({},
this
.
model
.
attributes
,
{
groupsCountMessage
:
this
.
getGroupsCountTitle
(),
usageCountMessage
:
this
.
getUsageCountTitle
(),
outlineAnchorMessage
:
this
.
getOutlineAnchorMessage
(),
index
:
this
.
model
.
collection
.
indexOf
(
this
.
model
)
});
...
...
@@ -64,6 +66,44 @@ function(BaseView, _, gettext) {
);
return
interpolate
(
message
,
{
count
:
count
},
true
);
},
getUsageCountTitle
:
function
()
{
var
count
=
this
.
model
.
get
(
'usage'
).
length
,
message
;
if
(
count
===
0
)
{
message
=
gettext
(
'Not in Use'
);
}
else
{
message
=
ngettext
(
/*
Translators: 'count' is number of units that the group
configuration is used in.
*/
'Used in %(count)s unit'
,
'Used in %(count)s units'
,
count
);
}
return
interpolate
(
message
,
{
count
:
count
},
true
);
},
getOutlineAnchorMessage
:
function
()
{
var
message
=
gettext
(
/*
Translators: 'outlineAnchor' is an anchor pointing to
the course outline page.
*/
'This Group Configuration is not in use. Start by adding a content experiment to any Unit via the %(outlineAnchor)s.'
),
anchor
=
str
.
sprintf
(
'<a href="%(url)s" title="%(text)s">%(text)s</a>'
,
{
url
:
this
.
model
.
collection
.
outlineUrl
,
text
:
gettext
(
'Course Outline'
)
}
);
return
str
.
sprintf
(
message
,
{
outlineAnchor
:
anchor
});
}
});
...
...
cms/static/sass/views/_group-configuration.scss
View file @
69ee9d92
...
...
@@ -42,142 +42,164 @@
outline
:
none
;
.group-configuration-details
{
padding
:
$baseline
(
$baseline
*
1
.5
);
.wrapper-group-configuration
{
padding
:
$baseline
(
$baseline
*
1
.5
);
.group-configuration-header
{
margin-bottom
:
0
;
border-bottom
:
0
;
}
.group-configuration-header
{
margin-bottom
:
0
;
border-bottom
:
0
;
}
.group-configuration-title
{
@extend
%t-title
;
@include
font-size
(
22
);
@include
line-height
(
22
);
overflow
:
hidden
;
text-overflow
:
ellipsis
;
margin-right
:
(
$baseline
*
14
);
font-weight
:
bold
;
.group-configuration-title
{
@extend
%t-title
;
@include
font-size
(
22
);
@include
line-height
(
22
);
overflow
:
hidden
;
text-overflow
:
ellipsis
;
margin-right
:
(
$baseline
*
14
);
font-weight
:
bold
;
.group-toggle
{
display
:
inline-block
;
padding-left
:
$baseline
;
color
:
$black
;
.group-toggle
{
display
:
inline-block
;
padding-left
:
$baseline
;
color
:
$black
;
&
:hover
,
&
:focus
{
color
:
$blue
;
&
:hover
,
&
:focus
{
color
:
$blue
;
}
}
}
}
.group-configuration-info
{
@extend
%t-copy-sub1
;
color
:
$gray-l1
;
margin-left
:
$baseline
;
.group-configuration-info
{
@extend
%t-copy-sub1
;
color
:
$gray-l1
;
margin-left
:
$baseline
;
&
.group-configuration-info-inline
{
display
:
table
;
width
:
70%
;
margin
:
(
$baseline
/
4
)
0
(
$baseline
/
2
)
$baseline
;
&
.group-configuration-info-inline
{
display
:
table
;
width
:
70%
;
margin
:
(
$baseline
/
4
)
0
(
$baseline
/
2
)
$baseline
;
li
{
@include
box-sizing
(
border-box
);
display
:
table-cell
;
margin-right
:
1%
;
li
{
@include
box-sizing
(
border-box
);
display
:
table-cell
;
margin-right
:
1%
;
&
.group-configuration-usage-count
{
font-style
:
italic
;
}
}
}
}
&
.group-configuration-info-block
{
li
{
padding
:
(
$baseline
/
4
)
0
;
&
.group-configuration-info-block
{
li
{
padding
:
(
$baseline
/
4
)
0
;
}
}
}
.group-configuration-label
{
text-transform
:
uppercase
;
}
.group-configuration-label
{
text-transform
:
uppercase
;
}
.group-configuration-description
{
overflow
:
hidden
;
text-overflow
:
ellipsis
;
.group-configuration-description
{
overflow
:
hidden
;
text-overflow
:
ellipsis
;
}
}
}
.ui-toggle-expansion
{
@include
transition
(
rotate
.15s
ease-in-out
.25s
);
@include
font-size
(
21
);
display
:
inline-block
;
width
:
(
$baseline
*
0
.75
);
vertical-align
:
baseline
;
margin-left
:
-
$baseline
;
}
&
.is-selectable
{
cursor
:
pointer
;
.ui-toggle-expansion
{
@include
transition
(
rotate
.15s
ease-in-out
.25s
);
@include
font-size
(
21
);
display
:
inline-block
;
width
:
(
$baseline
*
0
.75
);
vertical-align
:
baseline
;
margin-left
:
-
$baseline
;
}
&
:hover
{
c
olor
:
$blue
;
&
.is-selectable
{
c
ursor
:
pointer
;
.ui-toggle-expansion
{
&
:hover
{
color
:
$blue
;
.ui-toggle-expansion
{
color
:
$blue
;
}
}
}
}
.groups
{
margin-left
:
$baseline
;
margin-bottom
:
(
$baseline
*
0
.75
);
.groups
{
margin-left
:
$baseline
;
margin-bottom
:
(
$baseline
*
0
.75
);
.group
{
@extend
%t-copy-sub2
;
@include
font-size
(
18
);
@include
line-height
(
16
);
padding
:
(
$baseline
/
7
)
0
(
$baseline
/
4
);
border-top
:
1px
solid
$gray-l4
;
white-space
:
nowrap
;
.group
{
@extend
%t-copy-sub2
;
@include
font-size
(
18
);
@include
line-height
(
16
);
padding
:
(
$baseline
/
7
)
0
(
$baseline
/
4
);
border-top
:
1px
solid
$gray-l4
;
white-space
:
nowrap
;
&
:first-child
{
border-top
:
none
;
}
&
:first-child
{
border-top
:
none
;
}
.group-name
{
overflow
:
hidden
;
text-overflow
:
ellipsis
;
display
:
inline-block
;
vertical-align
:
middle
;
width
:
75%
;
margin-right
:
5%
;
.group-name
{
overflow
:
hidden
;
text-overflow
:
ellipsis
;
display
:
inline-block
;
vertical-align
:
middle
;
width
:
75%
;
margin-right
:
5%
;
}
.group-allocation
{
display
:
inline-block
;
vertical-align
:
middle
;
width
:
20%
;
color
:
$gray-l1
;
text-align
:
right
;
}
}
}
.group-allocation
{
.actions
{
@include
transition
(
opacity
.15s
.25s
ease-in-out
);
opacity
:
0
.0
;
position
:
absolute
;
top
:
$baseline
;
right
:
$baseline
;
.action
{
display
:
inline-block
;
vertical-align
:
middle
;
width
:
20%
;
color
:
$gray-l1
;
text-align
:
right
;
margin-right
:
(
$baseline
/
4
);
.edit
{
@include
blue-button
;
@extend
%t-action4
;
}
}
}
}
.actions
{
@include
transition
(
opacity
.15s
.25s
ease-in-out
);
opacity
:
0
.0
;
position
:
absolute
;
top
:
$baseline
;
right
:
$baseline
;
.wrapper-group-configuration-usages
{
@include
font-size
(
14
);
background-color
:
#f8f8f8
;
box-shadow
:
0
2px
2px
0
$shadow
inset
;
padding
:
$baseline
(
$baseline
*
1
.5
)
$baseline
(
$baseline
*
2
.5
);
.
action
{
display
:
inline-block
;
margin-
right
:
(
$baseline
/
4
)
;
.
group-configuration-usage
{
color
:
$gray-l1
;
margin-
left
:
$baseline
;
.edit
{
@include
blue-button
;
@extend
%t-action4
;
.group-configuration-usage-unit
{
padding
:
(
$baseline
/
4
)
0
;
}
}
}
}
&
:hover
.actions
{
&
:hover
.
wrapper-group-configuration
.
actions
{
opacity
:
1
.0
;
}
}
...
...
cms/templates/group_configurations.html
View file @
69ee9d92
...
...
@@ -26,6 +26,7 @@ function(doc, GroupConfigurationCollection, GroupConfigurationsPage) {
var
collection
=
new
GroupConfigurationCollection
(
$
{
json
.
dumps
(
configurations
)},
{
parse
:
true
});
collection
.
url
=
"${group_configuration_url}"
;
collection
.
outlineUrl
=
"${course_outline_url}"
;
new
GroupConfigurationsPage
({
el
:
$
(
'#content'
),
collection
:
collection
...
...
cms/templates/js/group-configuration-details.underscore
View file @
69ee9d92
...
...
@@ -23,19 +23,22 @@
<li class="group-configuration-groups-count">
<%= groupsCountMessage %>
</li>
<li class="group-configuration-usage-count">
<%= usageCountMessage %>
</li>
<% } %>
</ol>
<% if(showGroups) { %>
<% allocation = Math.floor(100 / groups.length) %>
<ol class="groups groups-<%= index %>">
<% groups.each(function(group, groupIndex) { %>
<li class="group group-<%= groupIndex %>"
><span class="group-name"><%= group.get('name') %></span
><span class="group-allocation"><%= allocation %>%</span
>
</li>
<% }) %>
</ol>
<ol class="groups groups-<%= index %>">
<% groups.each(function(group, groupIndex) { %>
<li class="group group-<%= groupIndex %>">
<span class="group-name"><%= group.get('name') %></span>
<span class="group-allocation"><%= allocation %>%</span>
</li>
<% }) %>
</ol>
<% } %>
<ul class="actions group-configuration-actions">
<li class="action action-edit">
...
...
@@ -43,3 +46,21 @@
</li>
</ul>
</div>
<% if(showGroups) { %>
<div class="wrapper-group-configuration-usages">
<% if (!_.isEmpty(usage)) { %>
<h4 class="group-configuration-usage-text"><%= gettext('This Group Configuration is used in:') %></h4>
<ol class="group-configuration-usage">
<% _.each(usage, function(unit) { %>
<li class="group-configuration-usage-unit">
<a href=<%= unit.url %> ><%= unit.label %></a>
</li>
<% }) %>
</ol>
<% } else { %>
<p class="group-configuration-usage-text">
<%= outlineAnchorMessage %>
</p>
<% } %>
</div>
<% } %>
common/test/acceptance/fixtures/course.py
View file @
69ee9d92
...
...
@@ -99,6 +99,7 @@ class XBlockFixtureDesc(object):
self
.
grader_type
=
grader_type
self
.
publish
=
publish
self
.
children
=
[]
self
.
locator
=
None
def
add_children
(
self
,
*
args
):
"""
...
...
@@ -137,11 +138,12 @@ class XBlockFixtureDesc(object):
metadata={2},
grader_type={3},
publish={4},
children={5}
children={5},
locator={6},
>
"""
)
.
strip
()
.
format
(
self
.
category
,
self
.
data
,
self
.
metadata
,
self
.
grader_type
,
self
.
publish
,
self
.
children
self
.
grader_type
,
self
.
publish
,
self
.
children
,
self
.
locator
)
...
...
@@ -199,7 +201,7 @@ class CourseFixture(StudioApiFixture):
self
.
_updates
=
[]
self
.
_handouts
=
[]
self
.
_
children
=
[]
self
.
children
=
[]
self
.
_assets
=
[]
self
.
_advanced_settings
=
{}
...
...
@@ -216,7 +218,7 @@ class CourseFixture(StudioApiFixture):
Returns the course fixture to allow chaining.
"""
self
.
_
children
.
extend
(
args
)
self
.
children
.
extend
(
args
)
return
self
def
add_update
(
self
,
update
):
...
...
@@ -257,7 +259,7 @@ class CourseFixture(StudioApiFixture):
self
.
_configure_course
()
self
.
_upload_assets
()
self
.
_add_advanced_settings
()
self
.
_create_xblock_children
(
self
.
_course_location
,
self
.
_
children
)
self
.
_create_xblock_children
(
self
.
_course_location
,
self
.
children
)
return
self
...
...
@@ -362,7 +364,7 @@ class CourseFixture(StudioApiFixture):
# Construct HTML with each of the handout links
handouts_li
=
[
'<li><a href="/static/{handout}">Example Handout</a></li>'
.
format
(
handout
=
handout
)
for
handout
in
self
.
_handouts
for
handout
in
self
.
_handouts
]
handouts_html
=
'<ol class="treeview-handoutsnav">{}</ol>'
.
format
(
""
.
join
(
handouts_li
))
...
...
@@ -446,12 +448,31 @@ class CourseFixture(StudioApiFixture):
Recursively create XBlock children.
"""
for
desc
in
xblock_descriptions
:
loc
=
self
.
_
create_xblock
(
parent_loc
,
desc
)
loc
=
self
.
create_xblock
(
parent_loc
,
desc
)
self
.
_create_xblock_children
(
loc
,
desc
.
children
)
self
.
_publish_xblock
(
parent_loc
)
def
_create_xblock
(
self
,
parent_loc
,
xblock_desc
):
def
get_nested_xblocks
(
self
,
category
=
None
):
"""
Return a list of nested XBlocks for the course that can be filtered by
category.
"""
xblocks
=
self
.
_get_nested_xblocks
(
self
)
if
category
:
xblocks
=
filter
(
lambda
x
:
x
.
category
==
category
,
xblocks
)
return
xblocks
def
_get_nested_xblocks
(
self
,
xblock_descriptor
):
"""
Return a list of nested XBlocks for the course.
"""
xblocks
=
list
(
xblock_descriptor
.
children
)
for
child
in
xblock_descriptor
.
children
:
xblocks
.
extend
(
self
.
_get_nested_xblocks
(
child
))
return
xblocks
def
create_xblock
(
self
,
parent_loc
,
xblock_desc
):
"""
Create an XBlock with `parent_loc` (the location of the parent block)
and `xblock_desc` (an `XBlockFixtureDesc` instance).
...
...
@@ -477,6 +498,7 @@ class CourseFixture(StudioApiFixture):
try
:
loc
=
response
.
json
()
.
get
(
'locator'
)
xblock_desc
.
locator
=
loc
except
ValueError
:
raise
CourseFixtureError
(
"Could not decode JSON from '{0}'"
.
format
(
response
.
content
))
...
...
common/test/acceptance/pages/studio/overview.py
View file @
69ee9d92
...
...
@@ -89,6 +89,9 @@ class CourseOutlineUnit(CourseOutlineChild):
"""
return
UnitPage
(
self
.
browser
,
self
.
locator
)
.
visit
()
def
is_browser_on_page
(
self
):
return
self
.
q
(
css
=
self
.
BODY_SELECTOR
)
.
present
class
CourseOutlineSubsection
(
CourseOutlineChild
,
CourseOutlineContainer
):
"""
...
...
@@ -197,4 +200,3 @@ class CourseOutlinePage(CoursePage, CourseOutlineContainer):
Open release date edit modal of first section in course outline
"""
self
.
q
(
css
=
'div.section-published-date a.edit-release-date'
)
.
first
.
click
()
common/test/acceptance/pages/studio/settings_group_configurations.py
View file @
69ee9d92
...
...
@@ -15,6 +15,7 @@ class GroupConfigurationsPage(CoursePage):
def
is_browser_on_page
(
self
):
return
self
.
q
(
css
=
'body.view-group-configurations'
)
.
present
@property
def
group_configurations
(
self
):
"""
Return list of the group configurations for the course.
...
...
@@ -68,6 +69,20 @@ class GroupConfiguration(object):
"""
return
self
.
find_css
(
css
)
.
first
.
text
[
0
]
def
click_outline_anchor
(
self
):
"""
Click on the `Course Outline` link.
"""
css
=
'p.group-configuration-usage-text a'
self
.
find_css
(
css
)
.
first
.
click
()
def
click_unit_anchor
(
self
,
index
=
0
):
"""
Click on the link to the unit.
"""
css
=
'li.group-configuration-usage-unit a'
self
.
find_css
(
css
)
.
nth
(
index
)
.
click
()
def
edit
(
self
):
"""
Open editing view for the group configuration.
...
...
@@ -115,6 +130,14 @@ class GroupConfiguration(object):
return
self
.
get_text
(
'.message-status.error'
)
@property
def
usages
(
self
):
"""
Return list of usages.
"""
css
=
'.group-configuration-usage-unit'
return
self
.
find_css
(
css
)
.
text
@property
def
name
(
self
):
"""
Return group configuration name.
...
...
common/test/acceptance/pages/studio/unit.py
View file @
69ee9d92
...
...
@@ -14,6 +14,8 @@ class UnitPage(PageObject):
Unit page in Studio
"""
NAME_SELECTOR
=
'#unit-display-name-input'
def
__init__
(
self
,
browser
,
unit_locator
):
super
(
UnitPage
,
self
)
.
__init__
(
browser
)
self
.
unit_locator
=
unit_locator
...
...
@@ -39,6 +41,10 @@ class UnitPage(PageObject):
)
@property
def
name
(
self
):
return
self
.
q
(
css
=
self
.
NAME_SELECTOR
)
.
attrs
(
'value'
)[
0
]
@property
def
components
(
self
):
"""
Return a list of components loaded on the unit page.
...
...
@@ -87,6 +93,7 @@ COMPONENT_BUTTONS = {
'save_settings'
:
'.action-save'
,
}
class
Component
(
PageObject
):
"""
A PageObject representing an XBlock child on the Studio UnitPage (including
...
...
common/test/acceptance/tests/test_studio_split_test.py
View file @
69ee9d92
...
...
@@ -8,13 +8,15 @@ import math
from
unittest
import
skip
,
skipUnless
from
xmodule.partitions.partitions
import
Group
,
UserPartition
from
bok_choy.promise
import
Promise
from
bok_choy.promise
import
Promise
,
EmptyPromise
from
..fixtures.course
import
XBlockFixtureDesc
from
..pages.studio.component_editor
import
ComponentEditorView
from
..pages.studio.overview
import
CourseOutlinePage
from
..pages.studio.settings_advanced
import
AdvancedSettingsPage
from
..pages.studio.settings_group_configurations
import
GroupConfigurationsPage
from
..pages.studio.utils
import
add_advanced_component
from
..pages.studio.unit
import
UnitPage
from
..pages.xblock.utils
import
wait_for_xblock_initialization
from
acceptance.tests.base_studio_test
import
StudioCourseTest
...
...
@@ -238,6 +240,13 @@ class GroupConfigurationsTest(ContainerBase, SplitTestMixin):
self
.
course_info
[
'run'
]
)
self
.
outline_page
=
CourseOutlinePage
(
self
.
browser
,
self
.
course_info
[
'org'
],
self
.
course_info
[
'number'
],
self
.
course_info
[
'run'
]
)
def
_assert_fields
(
self
,
config
,
cid
=
None
,
name
=
''
,
description
=
''
,
groups
=
None
):
self
.
assertEqual
(
config
.
mode
,
'details'
)
...
...
@@ -317,7 +326,7 @@ class GroupConfigurationsTest(ContainerBase, SplitTestMixin):
})
self
.
page
.
visit
()
config
=
self
.
page
.
group_configurations
()
[
0
]
config
=
self
.
page
.
group_configurations
[
0
]
# no groups when the the configuration is collapsed
self
.
assertEqual
(
len
(
config
.
groups
),
0
)
self
.
_assert_fields
(
...
...
@@ -327,7 +336,7 @@ class GroupConfigurationsTest(ContainerBase, SplitTestMixin):
groups
=
[
"Group 0"
,
"Group 1"
]
)
config
=
self
.
page
.
group_configurations
()
[
1
]
config
=
self
.
page
.
group_configurations
[
1
]
self
.
_assert_fields
(
config
,
...
...
@@ -350,10 +359,10 @@ class GroupConfigurationsTest(ContainerBase, SplitTestMixin):
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
)
self
.
assertEqual
(
len
(
self
.
page
.
group_configurations
),
0
)
# Create new group configuration
self
.
page
.
create
()
config
=
self
.
page
.
group_configurations
()
[
0
]
config
=
self
.
page
.
group_configurations
[
0
]
config
.
name
=
"New Group Configuration Name"
config
.
description
=
"New Description of the group configuration."
config
.
groups
[
1
]
.
name
=
"New Group Name"
...
...
@@ -418,7 +427,7 @@ class GroupConfigurationsTest(ContainerBase, SplitTestMixin):
self
.
page
.
visit
()
# Create new group configuration
self
.
page
.
create
()
config
=
self
.
page
.
group_configurations
()
[
0
]
config
=
self
.
page
.
group_configurations
[
0
]
config
.
name
=
"New Group Configuration Name"
# Add new group
config
.
add_group
()
...
...
@@ -435,7 +444,7 @@ class GroupConfigurationsTest(ContainerBase, SplitTestMixin):
self
.
verify_groups
(
container
,
[
'Group A'
,
'Group B'
,
'New group'
],
[])
self
.
page
.
visit
()
config
=
self
.
page
.
group_configurations
()
[
0
]
config
=
self
.
page
.
group_configurations
[
0
]
config
.
edit
()
config
.
name
=
"Second Group Configuration Name"
# Add new group
...
...
@@ -476,11 +485,11 @@ class GroupConfigurationsTest(ContainerBase, SplitTestMixin):
"""
self
.
page
.
visit
()
self
.
assertEqual
(
len
(
self
.
page
.
group_configurations
()
),
0
)
self
.
assertEqual
(
len
(
self
.
page
.
group_configurations
),
0
)
# Create new group configuration
self
.
page
.
create
()
config
=
self
.
page
.
group_configurations
()
[
0
]
config
=
self
.
page
.
group_configurations
[
0
]
config
.
name
=
"Name of the Group Configuration"
config
.
description
=
"Description of the group configuration."
# Add new group
...
...
@@ -488,7 +497,7 @@ class GroupConfigurationsTest(ContainerBase, SplitTestMixin):
# Cancel the configuration
config
.
cancel
()
self
.
assertEqual
(
len
(
self
.
page
.
group_configurations
()
),
0
)
self
.
assertEqual
(
len
(
self
.
page
.
group_configurations
),
0
)
def
test_can_cancel_editing_of_group_configuration
(
self
):
"""
...
...
@@ -508,8 +517,7 @@ class GroupConfigurationsTest(ContainerBase, SplitTestMixin):
},
})
self
.
page
.
visit
()
config
=
self
.
page
.
group_configurations
()[
0
]
config
=
self
.
page
.
group_configurations
[
0
]
config
.
name
=
"New Group Configuration Name"
config
.
description
=
"New Description of the group configuration."
# Add 2 new groups
...
...
@@ -552,7 +560,7 @@ class GroupConfigurationsTest(ContainerBase, SplitTestMixin):
# Create new group configuration
self
.
page
.
create
()
# Leave empty required field
config
=
self
.
page
.
group_configurations
()
[
0
]
config
=
self
.
page
.
group_configurations
[
0
]
config
.
description
=
"Description of the group configuration."
try_to_save_and_verify_error_message
(
"Group Configuration name is required"
)
...
...
@@ -574,3 +582,76 @@ class GroupConfigurationsTest(ContainerBase, SplitTestMixin):
description
=
"Description of the group configuration."
,
groups
=
[
"Group A"
,
"Group B"
]
)
def
test_group_configuration_empty_usage
(
self
):
"""
Scenario: When group configuration is not used, ensure that the link to outline page works correctly.
Given I have a course without group configurations
And I create new group configuration with 2 default groups
Then I see a link to the outline page
When I click on the outline link
Then I see the outline page
"""
# Create a new group configurations
self
.
course_fixture
.
_update_xblock
(
self
.
course_fixture
.
_course_location
,
{
"metadata"
:
{
u"user_partitions"
:
[
UserPartition
(
0
,
"Name"
,
"Description."
,
[
Group
(
"0"
,
"Group A"
),
Group
(
"1"
,
"Group B"
)])
.
to_json
(),
],
},
})
# Go to the Group Configuration Page and click on outline anchor
self
.
page
.
visit
()
config
=
self
.
page
.
group_configurations
[
0
]
config
.
toggle
()
config
.
click_outline_anchor
()
# Waiting for the page load and verify that we've landed on course outline page
EmptyPromise
(
lambda
:
self
.
outline_page
.
is_browser_on_page
(),
"loaded page {!r}"
.
format
(
self
.
outline_page
),
timeout
=
30
)
.
fulfill
()
def
test_group_configuration_non_empty_usage
(
self
):
"""
Scenario: When group configuration is used, ensure that the links to units using a group configuration work correctly.
Given I have a course without group configurations
And I create new group configuration with 2 default groups
And I create a unit and assign the newly created group configuration
And open the Group Configuration page
Then I see a link to the newly created unit
When I click on the unit link
Then I see correct unit page
"""
# Create a new group configurations
self
.
course_fixture
.
_update_xblock
(
self
.
course_fixture
.
_course_location
,
{
"metadata"
:
{
u"user_partitions"
:
[
UserPartition
(
0
,
"Name"
,
"Description."
,
[
Group
(
"0"
,
"Group A"
),
Group
(
"1"
,
"Group B"
)])
.
to_json
(),
],
},
})
# Assign newly created group configuration to unit
vertical
=
self
.
course_fixture
.
get_nested_xblocks
(
category
=
"vertical"
)[
0
]
self
.
course_fixture
.
create_xblock
(
vertical
.
locator
,
XBlockFixtureDesc
(
'split_test'
,
'Test Content Experiment'
,
metadata
=
{
'user_partition_id'
:
0
})
)
unit
=
UnitPage
(
self
.
browser
,
vertical
.
locator
)
# Go to the Group Configuration Page and click unit anchor
self
.
page
.
visit
()
config
=
self
.
page
.
group_configurations
[
0
]
config
.
toggle
()
usage
=
config
.
usages
[
0
]
config
.
click_unit_anchor
()
# Waiting for the page load and verify that we've landed on the unit page
EmptyPromise
(
lambda
:
unit
.
is_browser_on_page
(),
"loaded page {!r}"
.
format
(
unit
),
timeout
=
30
)
.
fulfill
()
self
.
assertIn
(
unit
.
name
,
usage
)
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment