Skip to content
12 changes: 12 additions & 0 deletions cms/djangoapps/contentstore/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,18 @@ def is_currently_visible_to_students(xblock):
return True


def has_children_visible_to_specific_content_groups(xblock):
"""
Returns True if this xblock has children that are limited to specific content groups.
Note that this method is not recursive (it does not check grandchildren.
"""
for child in xblock.children:
if modulestore().get_item(child).group_access:
return True

return False


def find_release_date_source(xblock):
"""
Finds the ancestor of xblock that set its release date.
Expand Down
12 changes: 1 addition & 11 deletions cms/djangoapps/contentstore/views/course.py
Original file line number Diff line number Diff line change
Expand Up @@ -1343,7 +1343,7 @@ def group_configurations_list_handler(request, course_key_string):
'context_course': course,
'group_configuration_url': group_configuration_url,
'course_outline_url': course_outline_url,
'configurations': configurations if should_show_group_configurations_page(course) else None,
'configurations': configurations,
})
elif "application/json" in request.META.get('HTTP_ACCEPT'):
if request.method == 'POST':
Expand Down Expand Up @@ -1422,16 +1422,6 @@ def group_configurations_detail_handler(request, course_key_string, group_config
return JsonResponse(status=204)


def should_show_group_configurations_page(course):
"""
Returns true if Studio should show the "Group Configurations" page for the specified course.
"""
return (
SPLIT_TEST_COMPONENT_TYPE in ADVANCED_COMPONENT_TYPES and
SPLIT_TEST_COMPONENT_TYPE in course.advanced_modules
)


def _get_course_creator_status(user):
"""
Helper method for returning the course creator status for a particular user,
Expand Down
11 changes: 6 additions & 5 deletions cms/djangoapps/contentstore/views/item.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,14 +39,15 @@

from student.auth import has_course_author_access
from contentstore.utils import find_release_date_source, find_staff_lock_source, is_currently_visible_to_students, \
ancestor_has_staff_lock
ancestor_has_staff_lock, has_children_visible_to_specific_content_groups
from contentstore.views.helpers import is_unit, xblock_studio_url, xblock_primary_child_category, \
xblock_type_display_name, get_parent_xblock
from contentstore.views.preview import get_preview_fragment
from edxmako.shortcuts import render_to_string
from models.settings.course_grading import CourseGradingModel
from cms.lib.xblock.runtime import handler_url, local_resource_url
from opaque_keys.edx.keys import UsageKey, CourseKey
from cms.lib.xblock.authoring_mixin import VISIBILITY_VIEW

__all__ = ['orphan_handler', 'xblock_handler', 'xblock_view_handler', 'xblock_outline_handler']

Expand All @@ -58,7 +59,6 @@
NEVER = lambda x: False
ALWAYS = lambda x: True


# In order to allow descriptors to use a handler url, we need to
# monkey-patch the x_module library.
# TODO: Remove this code when Runtimes are no longer created by modulestores
Expand Down Expand Up @@ -215,14 +215,14 @@ def xblock_view_handler(request, usage_key_string, view_name):
request_token=request_token(request),
))

if view_name == STUDIO_VIEW:
if view_name in (STUDIO_VIEW, VISIBILITY_VIEW):
try:
fragment = xblock.render(STUDIO_VIEW)
fragment = xblock.render(view_name)
# catch exceptions indiscriminately, since after this point they escape the
# dungeon and surface as uneditable, unsaveable, and undeletable
# component-goblins.
except Exception as exc: # pylint: disable=broad-except
log.debug("unable to render studio_view for %r", xblock, exc_info=True)
log.debug("Unable to render %s for %r", view_name, xblock, exc_info=True)
fragment = Fragment(render_to_string('html_error.html', {'message': str(exc)}))

elif view_name in (PREVIEW_VIEWS + container_views):
Expand Down Expand Up @@ -757,6 +757,7 @@ def safe_get_username(user_id):
xblock_info["edited_by"] = safe_get_username(xblock.subtree_edited_by)
xblock_info["published_by"] = safe_get_username(xblock.published_by)
xblock_info["currently_visible_to_students"] = is_currently_visible_to_students(xblock)
xblock_info["has_content_group_components"] = has_children_visible_to_specific_content_groups(xblock)
if release_date:
xblock_info["release_date_from"] = _get_release_date_from(xblock)
if visibility_state == VisibilityState.staff_only:
Expand Down
9 changes: 8 additions & 1 deletion cms/envs/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
from path import path
from warnings import simplefilter

from cms.lib.xblock.authoring_mixin import AuthoringMixin
from lms.lib.xblock.mixin import LmsBlockMixin
from dealer.git import git
from xmodule.modulestore.edit_info import EditInfoMixin
Expand Down Expand Up @@ -255,7 +256,13 @@

# This should be moved into an XBlock Runtime/Application object
# once the responsibility of XBlock creation is moved out of modulestore - cpennington
XBLOCK_MIXINS = (LmsBlockMixin, InheritanceMixin, XModuleMixin, EditInfoMixin)
XBLOCK_MIXINS = (
LmsBlockMixin,
InheritanceMixin,
XModuleMixin,
EditInfoMixin,
AuthoringMixin,
)

# Allow any XBlock in Studio
# You should also enable the ALLOW_ALL_ADVANCED_COMPONENTS feature flag, so that
Expand Down
50 changes: 50 additions & 0 deletions cms/lib/xblock/authoring_mixin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
"""
Mixin class that provides authoring capabilities for XBlocks.
"""

import logging

from xblock.core import XBlock
from xblock.fields import XBlockMixin
from xblock.fragment import Fragment

logger = logging.getLogger(__name__)

VISIBILITY_VIEW = 'visibility_view'


@XBlock.needs("i18n")
class AuthoringMixin(XBlockMixin):
"""
Mixin class that provides authoring capabilities for XBlocks.
"""
_services_requested = {
'i18n': 'need',
}

def _get_studio_resource_url(self, relative_url):
"""
Returns the Studio URL to a static resource.
"""
# TODO: is there a cleaner way to do this?
from cms.envs.common import STATIC_URL
return STATIC_URL + '/js/xblock/authoring.js'


def visibility_view(self, context=None):
"""
Render the view to manage an xblock's visibility settings in Studio.
Args:
context: Not actively used for this view.
Returns:
(Fragment): An HTML fragment for editing the visibility of this XBlock.
"""
fragment = Fragment()
from contentstore.utils import reverse_course_url
fragment.add_content(self.system.render_template('visibility_editor.html', {
'xblock': self,
'manage_groups_url': reverse_course_url('group_configurations_list_handler', self.location.course_key),
}))
fragment.add_javascript_url(self._get_studio_resource_url('/js/xblock/authoring.js'))
fragment.initialize_js('VisibilityEditorInit')
return fragment
7 changes: 6 additions & 1 deletion cms/static/js/models/xblock_info.js
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,12 @@ function(Backbone, _, str, ModuleUtils) {
/**
* True iff this xblock should display a "Contains staff only content" message.
*/
'staff_only_message': null
'staff_only_message': null,
/**
* True iff this xblock is a unit, and it has children that are only visible to certain
* content groups. Note that this is not a recursive property.
*/
'has_content_group_components': null
},

initialize: function () {
Expand Down
13 changes: 12 additions & 1 deletion cms/static/js/views/modals/base_modal.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
/**
* This is a base modal implementation that provides common utilities.
*
* A modal implementation should override the following methods:
*
* getTitle():
* returns the title for the modal.
* getHTMLContent():
* returns the HTML content to be shown inside the modal.
*/
define(["jquery", "underscore", "gettext", "js/views/baseview"],
function($, _, gettext, BaseView) {
Expand Down Expand Up @@ -41,14 +48,18 @@ define(["jquery", "underscore", "gettext", "js/views/baseview"],
name: this.options.modalName,
type: this.options.modalType,
size: this.options.modalSize,
title: this.options.title,
title: this.getTitle(),
viewSpecificClasses: this.options.viewSpecificClasses
}));
this.addActionButtons();
this.renderContents();
this.parentElement.append(this.$el);
},

getTitle: function() {
return this.options.title;
},

renderContents: function() {
var contentHtml = this.getContentHtml();
this.$('.modal-content').html(contentHtml);
Expand Down
9 changes: 6 additions & 3 deletions cms/static/js/views/modals/edit_xblock.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@ define(["jquery", "underscore", "gettext", "js/views/modals/base_modal", "js/vie
options: $.extend({}, BaseModal.prototype.options, {
modalName: 'edit-xblock',
addSaveButton: true,
viewSpecificClasses: 'modal-editor confirm'
view: 'studio_view',
viewSpecificClasses: 'modal-editor confirm',
titleFormat: gettext("Editing: %(title)s")
}),

initialize: function() {
Expand Down Expand Up @@ -56,7 +58,8 @@ define(["jquery", "underscore", "gettext", "js/views/modals/base_modal", "js/vie
displayXBlock: function() {
this.editorView = new XBlockEditorView({
el: this.$('.xblock-editor'),
model: this.xblockInfo
model: this.xblockInfo,
view: this.options.view
});
this.editorView.render({
success: _.bind(this.onDisplayXBlock, this)
Expand Down Expand Up @@ -111,7 +114,7 @@ define(["jquery", "underscore", "gettext", "js/views/modals/base_modal", "js/vie
if (!displayName) {
displayName = gettext('Component');
}
return interpolate(gettext("Editing: %(title)s"), { title: displayName }, true);
return interpolate(this.options.titleFormat, { title: displayName }, true);
},

addDefaultModes: function() {
Expand Down
14 changes: 12 additions & 2 deletions cms/static/js/views/pages/container.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ define(["jquery", "underscore", "gettext", "js/views/pages/base_page", "js/views

events: {
"click .edit-button": "editXBlock",
"click .visibility-button": "editVisibilitySettings",
"click .duplicate-button": "duplicateXBlock",
"click .delete-button": "deleteXBlock"
},
Expand Down Expand Up @@ -136,10 +137,10 @@ define(["jquery", "underscore", "gettext", "js/views/pages/base_page", "js/views
});
},

editXBlock: function(event) {
editXBlock: function(event, options) {
var xblockElement = this.findXBlockElement(event.target),
self = this,
modal = new EditXBlockModal({ });
modal = new EditXBlockModal(options);
event.preventDefault();

modal.edit(xblockElement, this.model, {
Expand All @@ -149,6 +150,15 @@ define(["jquery", "underscore", "gettext", "js/views/pages/base_page", "js/views
});
},

editVisibilitySettings: function(event) {
this.editXBlock(event, {
view: 'visibility_view',
titleFormat: gettext("Editing visibility for: %(title)s"),
viewSpecificClasses: '',
modalSize: 'med'
});
},

duplicateXBlock: function(event) {
event.preventDefault();
this.duplicateComponent(this.findXBlockElement(event.target));
Expand Down
3 changes: 2 additions & 1 deletion cms/static/js/views/pages/container_subviews.js
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,8 @@ define(["jquery", "underscore", "gettext", "js/views/baseview", "js/views/utils/
releaseDate: this.model.get('release_date'),
releaseDateFrom: this.model.get('release_date_from'),
hasExplicitStaffLock: this.model.get('has_explicit_staff_lock'),
staffLockFrom: this.model.get('staff_lock_from')
staffLockFrom: this.model.get('staff_lock_from'),
hasContentGroupComponents: this.model.get('has_content_group_components')
}));

return this;
Expand Down
2 changes: 1 addition & 1 deletion cms/static/js/views/pages/course_outline.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ define(["jquery", "underscore", "gettext", "js/views/pages/base_page", "js/views
});
this.model.on('change', this.setCollapseExpandVisibility, this);
$('.dismiss-button').bind('click', ViewUtils.deleteNotificationHandler(function () {
$('.wrapper-alert-announcement').removeClass('is-shown').addClass('is-hidden')
$('.wrapper-alert-announcement').removeClass('is-shown').addClass('is-hidden');
}));
},

Expand Down
26 changes: 26 additions & 0 deletions cms/static/js/xblock/authoring.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/**
* Client-side logic to support XBlock authoring.
*/
var edx = edx || {};

(function($) {
'use strict';

edx.studio = edx.studio || {};
edx.studio.xblock = edx.studio.xblock || {};

function initializeVisibilityEditor(runtime, element) {
element.find('.field-visibility-level input').change(function(event) {
if ($(event.target).hasClass('visibility-level-all')) {
element.find('.field-visibility-content-group input').prop('checked', false);
}
});
element.find('.field-visibility-content-group input').change(function(event) {
element.find('.visibility-level-all').prop('checked', true);
element.find('.visibility-level-specific').prop('checked', true);
});
}

// XBlock initialization functions must be global
window.VisibilityEditorInit = initializeVisibilityEditor;
})($);
1 change: 1 addition & 0 deletions cms/static/sass/_variables.scss
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,7 @@ $color-ready: $green;
$color-warning: $orange-l2;
$color-error: $red-l2;
$color-staff-only: $black;
$color-visibility-set: $black;

$color-heading-base: $gray-d2;
$color-copy-base: $gray-l1;
Expand Down
29 changes: 28 additions & 1 deletion cms/static/sass/elements/_forms.scss
Original file line number Diff line number Diff line change
@@ -1,7 +1,24 @@
// studio - elements - forms
// ====================

// element-specific utilities
// --------------------
// UI: checkbox/radio inputs
%input-tickable {

~ label {
color: $color-copy-base;
}

// STATE: checked/selected
&:checked ~ label {
@extend %t-strong;
color: $ui-action-primary-color-focus;
}
}

// forms - general
// --------------------
input[type="text"],
input[type="email"],
input[type="password"],
Expand Down Expand Up @@ -99,8 +116,18 @@ form {
}
}

// CASE: checkbox input
.field-checkbox .input-checkbox {
@extend %input-tickable;
}

// CASE: radio input
.field-radio .input-radio {
@extend %input-tickable;
}

// CASE: file input
input[type=file] {
input[type="file"] {
@extend %t-copy-sub1;
}

Expand Down
Loading