Commit 52b76c87 by Christina Roberts

Merge pull request #11 from edx/feature/christina/metadata-ui

Feature/christina/metadata ui
parents f27f664d 4d311f44
......@@ -5,8 +5,6 @@ from lettuce import world, step
from nose.tools import assert_true
from nose.tools import assert_equal
from xmodule.modulestore.django import _MODULESTORES, modulestore
from xmodule.templates import update_templates
from auth.authz import get_user_by_email
from selenium.webdriver.common.keys import Keys
......@@ -50,31 +48,31 @@ def i_press_the_category_delete_icon(step, category):
@step('I have opened a new course in Studio$')
def i_have_opened_a_new_course(step):
open_new_course()
####### HELPER FUNCTIONS ##############
def open_new_course():
world.clear_courses()
log_into_studio()
create_a_course()
####### HELPER FUNCTIONS ##############
def create_studio_user(
uname='robot',
email='robot+studio@edx.org',
password='test',
is_staff=False):
studio_user = world.UserFactory.build(
studio_user = world.UserFactory(
username=uname,
email=email,
password=password,
is_staff=is_staff)
studio_user.set_password(password)
studio_user.save()
registration = world.RegistrationFactory(user=studio_user)
registration.register(studio_user)
registration.activate()
user_profile = world.UserProfileFactory(user=studio_user)
def fill_in_course_info(
name='Robot Super Course',
......@@ -153,4 +151,13 @@ def set_date_and_time(date_css, desired_date, time_css, desired_time):
world.css_fill(time_css, desired_time)
e = world.css_find(time_css).first
e._element.send_keys(Keys.TAB)
time.sleep(float(1))
time.sleep(float(1))
@step('I have created a Video component$')
def i_created_a_video_component(step):
world.create_component_instance(
step, '.large-video-icon',
'i4x://edx/templates/video/default',
'.xmodule_VideoModule'
)
# disable missing docstring
#pylint: disable=C0111
from lettuce import world
from nose.tools import assert_equal
from terrain.steps import reload_the_page
@world.absorb
def create_component_instance(step, component_button_css, instance_id, expected_css):
click_new_component_button(step, component_button_css)
click_component_from_menu(instance_id, expected_css)
@world.absorb
def click_new_component_button(step, component_button_css):
step.given('I have opened a new course section in Studio')
step.given('I have added a new subsection')
step.given('I expand the first section')
world.css_click('a.new-unit-item')
world.css_click(component_button_css)
@world.absorb
def click_component_from_menu(instance_id, expected_css):
elem_css = "a[data-location='%s']" % instance_id
assert_equal(1, len(world.css_find(elem_css)))
world.css_click(elem_css)
assert_equal(1, len(world.css_find(expected_css)))
@world.absorb
def edit_component_and_select_settings():
world.css_click('a.edit-button')
world.css_click('#settings-mode')
@world.absorb
def verify_setting_entry(setting, display_name, value, explicitly_set):
assert_equal(display_name, setting.find_by_css('.setting-label')[0].value)
assert_equal(value, setting.find_by_css('.setting-input')[0].value)
settingClearButton = setting.find_by_css('.setting-clear')[0]
assert_equal(explicitly_set, settingClearButton.has_class('active'))
assert_equal(not explicitly_set, settingClearButton.has_class('inactive'))
@world.absorb
def verify_all_setting_entries(expected_entries):
settings = world.browser.find_by_css('.wrapper-comp-setting')
assert_equal(len(expected_entries), len(settings))
for (counter, setting) in enumerate(settings):
world.verify_setting_entry(
setting, expected_entries[counter][0],
expected_entries[counter][1], expected_entries[counter][2]
)
@world.absorb
def save_component_and_reopen(step):
world.css_click("a.save-button")
# We have a known issue that modifications are still shown within the edit window after cancel (though)
# they are not persisted. Refresh the browser to make sure the changes WERE persisted after Save.
reload_the_page(step)
edit_component_and_select_settings()
@world.absorb
def cancel_component(step):
world.css_click("a.cancel-button")
# We have a known issue that modifications are still shown within the edit window after cancel (though)
# they are not persisted. Refresh the browser to make sure the changes were not persisted.
reload_the_page(step)
@world.absorb
def revert_setting_entry(label):
get_setting_entry(label).find_by_css('.setting-clear')[0].click()
@world.absorb
def get_setting_entry(label):
settings = world.browser.find_by_css('.wrapper-comp-setting')
for setting in settings:
if setting.find_by_css('.setting-label')[0].value == label:
return setting
return None
Feature: Discussion Component Editor
As a course author, I want to be able to create discussion components.
Scenario: User can view metadata
Given I have created a Discussion Tag
And I edit and select Settings
Then I see three alphabetized settings and their expected values
Scenario: User can modify display name
Given I have created a Discussion Tag
And I edit and select Settings
Then I can modify the display name
And my display name change is persisted on save
# disable missing docstring
#pylint: disable=C0111
from lettuce import world, step
@step('I have created a Discussion Tag$')
def i_created_discussion_tag(step):
world.create_component_instance(
step, '.large-discussion-icon',
'i4x://edx/templates/discussion/Discussion_Tag',
'.xmodule_DiscussionModule'
)
@step('I see three alphabetized settings and their expected values$')
def i_see_only_the_settings_and_values(step):
world.verify_all_setting_entries(
[
['Category', "Week 1", True],
['Display Name', "Discussion Tag", True],
['Subcategory', "Topic-Level Student-Visible Label", True]
])
Feature: HTML Editor
As a course author, I want to be able to create HTML blocks.
Scenario: User can view metadata
Given I have created a Blank HTML Page
And I edit and select Settings
Then I see only the HTML display name setting
Scenario: User can modify display name
Given I have created a Blank HTML Page
And I edit and select Settings
Then I can modify the display name
And my display name change is persisted on save
# disable missing docstring
#pylint: disable=C0111
from lettuce import world, step
@step('I have created a Blank HTML Page$')
def i_created_blank_html_page(step):
world.create_component_instance(
step, '.large-html-icon', 'i4x://edx/templates/html/Blank_HTML_Page',
'.xmodule_HtmlModule'
)
@step('I see only the HTML display name setting$')
def i_see_only_the_html_display_name(step):
world.verify_all_setting_entries([['Display Name', "Blank HTML Page", True]])
Feature: Problem Editor
As a course author, I want to be able to create problems and edit their settings.
Scenario: User can view metadata
Given I have created a Blank Common Problem
And I edit and select Settings
Then I see five alphabetized settings and their expected values
And Edit High Level Source is not visible
Scenario: User can modify String values
Given I have created a Blank Common Problem
And I edit and select Settings
Then I can modify the display name
And my display name change is persisted on save
Scenario: User can specify special characters in String values
Given I have created a Blank Common Problem
And I edit and select Settings
Then I can specify special characters in the display name
And my special characters and persisted on save
Scenario: User can revert display name to unset
Given I have created a Blank Common Problem
And I edit and select Settings
Then I can revert the display name to unset
And my display name is unset on save
Scenario: User can select values in a Select
Given I have created a Blank Common Problem
And I edit and select Settings
Then I can select Per Student for Randomization
And my change to randomization is persisted
And I can revert to the default value for randomization
Scenario: User can modify float input values
Given I have created a Blank Common Problem
And I edit and select Settings
Then I can set the weight to "3.5"
And my change to weight is persisted
And I can revert to the default value of unset for weight
Scenario: User cannot type letters in float number field
Given I have created a Blank Common Problem
And I edit and select Settings
Then if I set the weight to "abc", it remains unset
Scenario: User cannot type decimal values integer number field
Given I have created a Blank Common Problem
And I edit and select Settings
Then if I set the max attempts to "2.34", it displays initially as "234", and is persisted as "234"
Scenario: User cannot type out of range values in an integer number field
Given I have created a Blank Common Problem
And I edit and select Settings
Then if I set the max attempts to "-3", it displays initially as "-3", and is persisted as "1"
Scenario: Settings changes are not saved on Cancel
Given I have created a Blank Common Problem
And I edit and select Settings
Then I can set the weight to "3.5"
And I can modify the display name
Then If I press Cancel my changes are not persisted
Scenario: Edit High Level source is available for LaTeX problem
Given I have created a LaTeX Problem
And I edit and select Settings
Then Edit High Level Source is visible
# disable missing docstring
#pylint: disable=C0111
from lettuce import world, step
from nose.tools import assert_equal
DISPLAY_NAME = "Display Name"
MAXIMUM_ATTEMPTS = "Maximum Attempts"
PROBLEM_WEIGHT = "Problem Weight"
RANDOMIZATION = 'Randomization'
SHOW_ANSWER = "Show Answer"
############### ACTIONS ####################
@step('I have created a Blank Common Problem$')
def i_created_blank_common_problem(step):
world.create_component_instance(
step,
'.large-problem-icon',
'i4x://edx/templates/problem/Blank_Common_Problem',
'.xmodule_CapaModule'
)
@step('I edit and select Settings$')
def i_edit_and_select_settings(step):
world.edit_component_and_select_settings()
@step('I see five alphabetized settings and their expected values$')
def i_see_five_settings_with_values(step):
world.verify_all_setting_entries(
[
[DISPLAY_NAME, "Blank Common Problem", True],
[MAXIMUM_ATTEMPTS, "", False],
[PROBLEM_WEIGHT, "", False],
[RANDOMIZATION, "Never", True],
[SHOW_ANSWER, "Finished", True]
])
@step('I can modify the display name')
def i_can_modify_the_display_name(step):
world.get_setting_entry(DISPLAY_NAME).find_by_css('.setting-input')[0].fill('modified')
verify_modified_display_name()
@step('my display name change is persisted on save')
def my_display_name_change_is_persisted_on_save(step):
world.save_component_and_reopen(step)
verify_modified_display_name()
@step('I can specify special characters in the display name')
def i_can_modify_the_display_name_with_special_chars(step):
world.get_setting_entry(DISPLAY_NAME).find_by_css('.setting-input')[0].fill("updated ' \" &")
verify_modified_display_name_with_special_chars()
@step('my special characters and persisted on save')
def special_chars_persisted_on_save(step):
world.save_component_and_reopen(step)
verify_modified_display_name_with_special_chars()
@step('I can revert the display name to unset')
def can_revert_display_name_to_unset(step):
world.revert_setting_entry(DISPLAY_NAME)
verify_unset_display_name()
@step('my display name is unset on save')
def my_display_name_is_persisted_on_save(step):
world.save_component_and_reopen(step)
verify_unset_display_name()
@step('I can select Per Student for Randomization')
def i_can_select_per_student_for_randomization(step):
world.browser.select(RANDOMIZATION, "Per Student")
verify_modified_randomization()
@step('my change to randomization is persisted')
def my_change_to_randomization_is_persisted(step):
world.save_component_and_reopen(step)
verify_modified_randomization()
@step('I can revert to the default value for randomization')
def i_can_revert_to_default_for_randomization(step):
world.revert_setting_entry(RANDOMIZATION)
world.save_component_and_reopen(step)
world.verify_setting_entry(world.get_setting_entry(RANDOMIZATION), RANDOMIZATION, "Always", False)
@step('I can set the weight to "(.*)"?')
def i_can_set_weight(step, weight):
set_weight(weight)
verify_modified_weight()
@step('my change to weight is persisted')
def my_change_to_weight_is_persisted(step):
world.save_component_and_reopen(step)
verify_modified_weight()
@step('I can revert to the default value of unset for weight')
def i_can_revert_to_default_for_unset_weight(step):
world.revert_setting_entry(PROBLEM_WEIGHT)
world.save_component_and_reopen(step)
world.verify_setting_entry(world.get_setting_entry(PROBLEM_WEIGHT), PROBLEM_WEIGHT, "", False)
@step('if I set the weight to "(.*)", it remains unset')
def set_the_weight_to_abc(step, bad_weight):
set_weight(bad_weight)
# We show the clear button immediately on type, hence the "True" here.
world.verify_setting_entry(world.get_setting_entry(PROBLEM_WEIGHT), PROBLEM_WEIGHT, "", True)
world.save_component_and_reopen(step)
# But no change was actually ever sent to the model, so on reopen, explicitly_set is False
world.verify_setting_entry(world.get_setting_entry(PROBLEM_WEIGHT), PROBLEM_WEIGHT, "", False)
@step('if I set the max attempts to "(.*)", it displays initially as "(.*)", and is persisted as "(.*)"')
def set_the_max_attempts(step, max_attempts_set, max_attempts_displayed, max_attempts_persisted):
world.get_setting_entry(MAXIMUM_ATTEMPTS).find_by_css('.setting-input')[0].fill(max_attempts_set)
world.verify_setting_entry(world.get_setting_entry(MAXIMUM_ATTEMPTS), MAXIMUM_ATTEMPTS, max_attempts_displayed, True)
world.save_component_and_reopen(step)
world.verify_setting_entry(world.get_setting_entry(MAXIMUM_ATTEMPTS), MAXIMUM_ATTEMPTS, max_attempts_persisted, True)
@step('Edit High Level Source is not visible')
def edit_high_level_source_not_visible(step):
verify_high_level_source(step, False)
@step('Edit High Level Source is visible')
def edit_high_level_source_visible(step):
verify_high_level_source(step, True)
@step('If I press Cancel my changes are not persisted')
def cancel_does_not_save_changes(step):
world.cancel_component(step)
step.given("I edit and select Settings")
step.given("I see five alphabetized settings and their expected values")
@step('I have created a LaTeX Problem')
def create_latex_problem(step):
world.click_new_component_button(step, '.large-problem-icon')
# Go to advanced tab (waiting for the tab to be visible)
world.css_find('#ui-id-2')
world.css_click('#ui-id-2')
world.click_component_from_menu("i4x://edx/templates/problem/Problem_Written_in_LaTeX", '.xmodule_CapaModule')
def verify_high_level_source(step, visible):
assert_equal(visible, world.is_css_present('.launch-latex-compiler'))
world.cancel_component(step)
assert_equal(visible, world.is_css_present('.upload-button'))
def verify_modified_weight():
world.verify_setting_entry(world.get_setting_entry(PROBLEM_WEIGHT), PROBLEM_WEIGHT, "3.5", True)
def verify_modified_randomization():
world.verify_setting_entry(world.get_setting_entry(RANDOMIZATION), RANDOMIZATION, "Per Student", True)
def verify_modified_display_name():
world.verify_setting_entry(world.get_setting_entry(DISPLAY_NAME), DISPLAY_NAME, 'modified', True)
def verify_modified_display_name_with_special_chars():
world.verify_setting_entry(world.get_setting_entry(DISPLAY_NAME), DISPLAY_NAME, "updated ' \" &", True)
def verify_unset_display_name():
world.verify_setting_entry(world.get_setting_entry(DISPLAY_NAME), DISPLAY_NAME, '', False)
def set_weight(weight):
world.get_setting_entry(PROBLEM_WEIGHT).find_by_css('.setting-input')[0].fill(weight)
......@@ -10,9 +10,7 @@ from nose.tools import assert_equal
@step('I have opened a new course section in Studio$')
def i_have_opened_a_new_course_section(step):
world.clear_courses()
log_into_studio()
create_a_course()
open_new_course()
add_section()
......
Feature: Video Component Editor
As a course author, I want to be able to create video components.
Scenario: User can view metadata
Given I have created a Video component
And I edit and select Settings
Then I see only the Video display name setting
Scenario: User can modify display name
Given I have created a Video component
And I edit and select Settings
Then I can modify the display name
And my display name change is persisted on save
# disable missing docstring
#pylint: disable=C0111
from lettuce import world, step
@step('I see only the video display name setting$')
def i_see_only_the_video_display_name(step):
world.verify_all_setting_entries([['Display Name', "default", True]])
......@@ -149,8 +149,7 @@ def edit_unit(request, location):
component_templates[category].append((
template.display_name_with_default,
template.location.url(),
hasattr(template, 'markdown') and template.markdown is not None,
template.cms.empty,
hasattr(template, 'markdown') and template.markdown is not None
))
components = [
......
......@@ -223,7 +223,8 @@ PIPELINE_JS = {
rooted_glob(PROJECT_ROOT / 'static/', 'coffee/src/**/*.js')
) + ['js/hesitate.js', 'js/base.js',
'js/models/feedback.js', 'js/views/feedback.js',
'js/models/section.js', 'js/views/section.js'],
'js/models/section.js', 'js/views/section.js',
'js/models/metadata_model.js', 'js/views/metadata_editor_view.js'],
'output_filename': 'js/cms-application.js',
'test_order': 0
},
......
../../../templates/js/metadata-editor.underscore
\ No newline at end of file
../../../templates/js/metadata-number-entry.underscore
\ No newline at end of file
../../../templates/js/metadata-option-entry.underscore
\ No newline at end of file
../../../templates/js/metadata-string-entry.underscore
\ No newline at end of file
describe "CMS.Models.Metadata", ->
it "knows when the value has not been modified", ->
model = new CMS.Models.Metadata(
{'value': 'original', 'explicitly_set': false})
expect(model.isModified()).toBeFalsy()
model = new CMS.Models.Metadata(
{'value': 'original', 'explicitly_set': true})
model.setValue('original')
expect(model.isModified()).toBeFalsy()
it "knows when the value has been modified", ->
model = new CMS.Models.Metadata(
{'value': 'original', 'explicitly_set': false})
model.setValue('original')
expect(model.isModified()).toBeTruthy()
model = new CMS.Models.Metadata(
{'value': 'original', 'explicitly_set': true})
model.setValue('modified')
expect(model.isModified()).toBeTruthy()
it "tracks when values have been explicitly set", ->
model = new CMS.Models.Metadata(
{'value': 'original', 'explicitly_set': false})
expect(model.isExplicitlySet()).toBeFalsy()
model.setValue('original')
expect(model.isExplicitlySet()).toBeTruthy()
it "has both 'display value' and a 'value' methods", ->
model = new CMS.Models.Metadata(
{'value': 'default', 'explicitly_set': false})
expect(model.getValue()).toBeNull
expect(model.getDisplayValue()).toBe('default')
model.setValue('modified')
expect(model.getValue()).toBe('modified')
expect(model.getDisplayValue()).toBe('modified')
it "has a clear method for reverting to the default", ->
model = new CMS.Models.Metadata(
{'value': 'original', 'default_value' : 'default', 'explicitly_set': true})
model.clear()
expect(model.getValue()).toBeNull
expect(model.getDisplayValue()).toBe('default')
expect(model.isExplicitlySet()).toBeFalsy()
it "has a getter for field name", ->
model = new CMS.Models.Metadata({'field_name': 'foo'})
expect(model.getFieldName()).toBe('foo')
it "has a getter for options", ->
model = new CMS.Models.Metadata({'options': ['foo', 'bar']})
expect(model.getOptions()).toEqual(['foo', 'bar'])
it "has a getter for type", ->
model = new CMS.Models.Metadata({'type': 'Integer'})
expect(model.getType()).toBe(CMS.Models.Metadata.INTEGER_TYPE)
......@@ -73,13 +73,3 @@ describe "CMS.Views.ModuleEdit", ->
expect(XModule.loadModule).toHaveBeenCalled()
expect(XModule.loadModule.mostRecentCall.args[0]).toBe($('.xmodule_display'))
describe "changedMetadata", ->
it "returns empty if no metadata loaded", ->
expect(@moduleEdit.changedMetadata()).toEqual({})
it "returns only changed values", ->
@moduleEdit.originalMetadata = {'foo', 'bar'}
spyOn(@moduleEdit, 'metadata').andReturn({'a': '', 'b': 'before', 'c': ''})
@moduleEdit.loadEdit()
@moduleEdit.metadata.andReturn({'a': '', 'b': 'after', 'd': 'only_after'})
expect(@moduleEdit.changedMetadata()).toEqual({'b' : 'after', 'd' : 'only_after'})
class CMS.Views.ModuleEdit extends Backbone.View
tagName: 'li'
className: 'component'
editorMode: 'editor-mode'
events:
"click .component-editor .cancel-button": 'clickCancelButton'
"click .component-editor .save-button": 'clickSaveButton'
"click .component-actions .edit-button": 'clickEditButton'
"click .component-actions .delete-button": 'onDelete'
"click .mode a": 'clickModeButton'
initialize: ->
@onDelete = @options.onDelete
......@@ -20,29 +22,30 @@ class CMS.Views.ModuleEdit extends Backbone.View
loadEdit: ->
if not @module
@module = XModule.loadModule(@$el.find('.xmodule_edit'))
@originalMetadata = @metadata()
metadata: ->
# cdodge: package up metadata which is separated into a number of input fields
# there's probably a better way to do this, but at least this lets me continue to move onwards
_metadata = {}
$metadata = @$component_editor().find('.metadata_edit')
if $metadata
# walk through the set of elments which have the 'xmetadata_name' attribute and
# build up a object to pass back to the server on the subsequent POST
_metadata[$(el).data("metadata-name")] = el.value for el in $('[data-metadata-name]', $metadata)
return _metadata
# At this point, metadata-edit.html will be loaded, and the metadata (as JSON) is available.
metadataEditor = @$el.find('.metadata_edit')
metadataData = metadataEditor.data('metadata')
models = [];
for key of metadataData
models.push(metadataData[key])
@metadataEditor = new CMS.Views.Metadata.Editor({
el: metadataEditor,
collection: new CMS.Models.MetadataCollection(models)
})
# Need to update set "active" class on data editor if there is one.
# If we are only showing settings, hide the data editor controls and update settings accordingly.
if @hasDataEditor()
@selectMode(@editorMode)
else
@hideDataEditor()
title = interpolate(gettext('<em>Editing:</em> %s'),
[@metadataEditor.getDisplayName()])
@$el.find('.component-name').html(title)
changedMetadata: ->
currentMetadata = @metadata()
changedMetadata = {}
for key of currentMetadata
if currentMetadata[key] != @originalMetadata[key]
changedMetadata[key] = currentMetadata[key]
return changedMetadata
return @metadataEditor.getModifiedMetadataValues()
cloneTemplate: (parent, template) ->
$.post("/clone_item", {
......@@ -77,7 +80,7 @@ class CMS.Views.ModuleEdit extends Backbone.View
@render()
@$el.removeClass('editing')
).fail( ->
showToastMessage("There was an error saving your changes. Please try again.", null, 3)
showToastMessage(gettext("There was an error saving your changes. Please try again."), null, 3)
)
clickCancelButton: (event) ->
......@@ -96,3 +99,38 @@ class CMS.Views.ModuleEdit extends Backbone.View
$modalCover.show().addClass('is-fixed')
@$component_editor().slideDown(150)
@loadEdit()
clickModeButton: (event) ->
event.preventDefault()
if not @hasDataEditor()
return
@selectMode(event.currentTarget.parentElement.id)
hasDataEditor: =>
return @$el.find('.wrapper-comp-editor').length > 0
selectMode: (mode) =>
dataEditor = @$el.find('.wrapper-comp-editor')
settingsEditor = @$el.find('.wrapper-comp-settings')
editorModeButton = @$el.find('#editor-mode').find("a")
settingsModeButton = @$el.find('#settings-mode').find("a")
if mode == @editorMode
# Because of CodeMirror editor, cannot hide the data editor when it is first loaded. Therefore
# we have to use a class of is-inactive instead of is-active.
dataEditor.removeClass('is-inactive')
editorModeButton.addClass('is-set')
settingsEditor.removeClass('is-active')
settingsModeButton.removeClass('is-set')
else
dataEditor.addClass('is-inactive')
editorModeButton.removeClass('is-set')
settingsEditor.addClass('is-active')
settingsModeButton.addClass('is-set')
hideDataEditor: =>
editorModeButtonParent = @$el.find('#editor-mode')
editorModeButtonParent.addClass('inactive-mode')
editorModeButtonParent.removeClass('active-mode')
@$el.find('.wrapper-comp-settings').addClass('is-active')
@$el.find('#settings-mode').find("a").addClass('is-set')
/**
* Model used for metadata setting editors. This model does not do its own saving,
* as that is done by module_edit.coffee.
*/
CMS.Models.Metadata = Backbone.Model.extend({
defaults: {
"field_name": null,
"display_name": null,
"value" : null,
"explicitly_set": null,
"default_value" : null,
"options" : null,
"type" : null
},
initialize: function() {
this.original_value = this.get('value');
this.original_explicitly_set = this.get('explicitly_set');
},
/**
* Returns true if the stored value is different, or if the "explicitly_set"
* property has changed.
*/
isModified : function() {
if (!this.get('explicitly_set') && !this.original_explicitly_set) {
return false;
}
if (this.get('explicitly_set') && this.original_explicitly_set) {
return this.get('value') !== this.original_value;
}
return true;
},
/**
* Returns true if a non-default/non-inherited value has been set.
*/
isExplicitlySet: function() {
return this.get('explicitly_set');
},
/**
* The value, as shown in the UI. This may be an inherited or default value.
*/
getDisplayValue : function () {
return this.get('value');
},
/**
* The value, as should be returned to the server. if 'isExplicitlySet'
* returns false, this method returns null to indicate that the value
* is not set at this level.
*/
getValue: function() {
return this.get('explicitly_set') ? this.get('value') : null;
},
/**
* Sets the displayed value.
*/
setValue: function (value) {
this.set({
explicitly_set: true,
value: value
});
},
/**
* Returns the field name, which should be used for persisting the metadata
* field to the server.
*/
getFieldName: function () {
return this.get('field_name');
},
/**
* Returns the options. This may be a array of possible values, or an object
* with properties like "max", "min" and "step".
*/
getOptions: function () {
return this.get('options');
},
/**
* Returns the type of this metadata field. Possible values are SELECT_TYPE,
* INTEGER_TYPE, and FLOAT_TYPE, GENERIC_TYPE.
*/
getType: function() {
return this.get('type');
},
/**
* Reverts the value to the default_value specified at construction, and updates the
* explicitly_set property.
*/
clear: function() {
this.set({
explicitly_set: false,
value: this.get('default_value')
});
}
});
CMS.Models.MetadataCollection = Backbone.Collection.extend({
model : CMS.Models.Metadata,
comparator: "display_name"
});
CMS.Models.Metadata.SELECT_TYPE = "Select";
CMS.Models.Metadata.INTEGER_TYPE = "Integer";
CMS.Models.Metadata.FLOAT_TYPE = "Float";
CMS.Models.Metadata.GENERIC_TYPE = "Generic";
......@@ -814,7 +814,7 @@ hr.divide {
line-height: 26px;
color: $white;
pointer-events: none;
opacity: 0;
opacity: 0.0;
&:after {
content: '▾';
......
......@@ -149,11 +149,11 @@ abbr[title] {
margin-left: 20px;
}
li {
opacity: .8;
opacity: 0.8;
&:ui-state-active {
background-color: rgba(255, 255, 255, .3);
opacity: 1;
opacity: 1.0;
font-weight: 400;
}
a:focus {
......
......@@ -95,12 +95,12 @@
// bounce in
@mixin bounceIn {
0% {
opacity: 0;
opacity: 0.0;
@include transform(scale(0.3));
}
50% {
opacity: 1;
opacity: 1.0;
@include transform(scale(1.05));
}
......@@ -128,12 +128,12 @@
// bounce in
@mixin bounceOut {
0% {
opacity: 0;
opacity: 0.0;
@include transform(scale(0.3));
}
50% {
opacity: 1;
opacity: 1.0;
@include transform(scale(1.05));
}
......@@ -146,12 +146,12 @@
}
50% {
opacity: 1;
opacity: 1.0;
@include transform(scale(1.05));
}
100% {
opacity: 0;
opacity: 0.0;
@include transform(scale(0.3));
}
}
......
......@@ -124,7 +124,6 @@ code {
.CodeMirror {
font-size: 13px;
border: 1px solid $darkGrey;
background: #fff;
}
......
......@@ -243,7 +243,7 @@
left: -7px;
top: 47px;
width: 140px;
opacity: 0;
opacity: 0.0;
pointer-events: none;
}
......@@ -558,7 +558,7 @@ body.signin .nav-not-signedin-signup {
.wrapper-nav-sub {
@include transition (opacity 1.0s ease-in-out 0s);
opacity: 0;
opacity: 0.0;
pointer-events: none;
&.is-shown {
......
......@@ -627,7 +627,7 @@
pointer-events: none;
.prompt {
opacity: 0;
opacity: 0.0;
}
}
......
......@@ -254,7 +254,7 @@ body.course.checklists {
.task-support {
@extend .t-copy-sub2;
@include transition(opacity .15s .25s ease-in-out);
opacity: 0;
opacity: 0.0;
pointer-events: none;
}
}
......@@ -267,7 +267,7 @@ body.course.checklists {
float: right;
width: flex-grid(2,9);
margin: ($baseline/2) 0 0 flex-gutter();
opacity: 0;
opacity: 0.0;
pointer-events: none;
text-align: right;
......
......@@ -59,7 +59,7 @@ body.dashboard {
top: 15px;
right: $baseline;
padding: ($baseline/4) ($baseline/2);
opacity: 0;
opacity: 0.0;
pointer-events: none;
&:hover {
......
......@@ -162,7 +162,7 @@ body.index {
position: absolute;
bottom: -30px;
right: ($baseline/2);
opacity: 0;
opacity: 0.0;
[class^="icon-"] {
@include font-size(18);
......
......@@ -21,7 +21,7 @@ body.course.settings {
font-size: 14px;
}
.message-status {
.message-status {
display: none;
@include border-top-radius(2px);
@include box-sizing(border-box);
......@@ -386,6 +386,11 @@ body.course.settings {
#course-overview {
height: ($baseline*20);
}
//adds back in CodeMirror border removed due to Unit page styling of component editors
.CodeMirror {
border: 1px solid $gray-l2;
}
}
// specific fields - video
......@@ -698,7 +703,7 @@ body.course.settings {
.tip {
@include transition (opacity 0.5s ease-in-out 0s);
opacity: 0;
opacity: 0.0;
position: absolute;
bottom: ($baseline*1.25);
}
......@@ -713,7 +718,7 @@ body.course.settings {
input.error {
& + .tip {
opacity: 0;
opacity: 0.0;
}
}
}
......
......@@ -41,38 +41,23 @@ body.course.static-pages {
@include edit-box;
@include box-shadow(none);
display: none;
padding: 20px;
padding: 0;
border-radius: 2px 2px 0 0;
.metadata_edit {
margin-bottom: 20px;
font-size: 13px;
li {
margin-bottom: 10px;
}
label {
display: inline-block;
margin-right: 10px;
}
//Overrides general edit-box mixin
.row {
margin-bottom: 0px;
}
h3 {
margin-bottom: 10px;
font-size: 18px;
font-weight: 700;
}
// This duplicates the styling from Unit page editing
.module-actions {
@include box-shadow(inset 0 1px 1px $shadow);
padding: 0px 0 10px 10px;
background-color: $gray-l6;
h5 {
margin-bottom: 8px;
color: #fff;
font-weight: 700;
}
.save-button {
margin-top: 10px;
margin: 15px 8px 0 0;
.save-button {
margin: ($baseline/2) 8px 0 0;
}
}
}
}
......@@ -215,3 +200,4 @@ body.course.static-pages {
outline: 0;
}
}
......@@ -212,6 +212,7 @@ body.course.updates {
@include edit-box;
position: absolute;
right: 0;
top: 0;
z-index: 10001;
width: 800px;
padding: 30px;
......
<%! from django.utils.translation import ugettext as _ %>
<%namespace name='static' file='static_content.html'/>
<script type="text/javascript" src="${static.url('js/models/metadata_model.js')}"></script>
<script type="text/javascript" src="${static.url('js/views/metadata_editor_view.js')}"></script>
<script src="${static.url('js/vendor/html5-input-polyfills/number-polyfill.js')}"></script>
<link rel="stylesheet" type="text/css" href="${static.url('css/vendor/html5-input-polyfills/number-polyfill.css')}" />
<div class="wrapper wrapper-component-editor">
<div class="component-editor">
<div class="module-editor">
${editor}
</div>
<div class="row module-actions">
<a href="#" class="save-button">Save</a>
<a href="#" class="cancel-button">Cancel</a>
</div>
</div>
<div class="component-edit-header">
<span class="component-name"></span>
<ul class="nav-edit-modes">
<li id="editor-mode" class="mode active-mode" aria-controls="editor-tab" role="tab">
<a href="#">${_("Editor")}</a>
</li>
<li id="settings-mode" class="mode active-mode" aria-controls="settings-tab" role="tab">
<a href="#">${_("Settings")}</a>
</li>
</ul>
</div> <!-- Editor Header -->
<div class="component-edit-modes">
<div class="module-editor">
${editor}
</div>
</div>
<div class="row module-actions">
<a href="#" class="save-button">${_("Save")}</a>
<a href="#" class="cancel-button">${_("Cancel")}</a>
</div> <!-- Module Actions-->
</div>
</div>
<div class="component-actions">
<a href="#" class="edit-button standard"><span class="edit-icon"></span>Edit</a>
<a href="#" class="delete-button standard"><span class="delete-icon"></span>Delete</a>
<a href="#" class="edit-button standard"><span class="edit-icon"></span>${_("Edit")}</a>
<a href="#" class="delete-button standard"><span class="delete-icon"></span>${_("Delete")}</a>
</div>
<a data-tooltip="Drag to reorder" href="#" class="drag-handle"></a>
<a data-tooltip='${_("Drag to reorder")}' href="#" class="drag-handle"></a>
${preview}
<ul class="list-input settings-list">
<% _.each(_.range(numEntries), function() { %>
<li class="field comp-setting-entry metadata_entry" id="settings-listing">
</li>
<% }) %>
</ul>
<div class="wrapper-comp-setting">
<label class="label setting-label" for="<%= uniqueId %>"><%= model.get('display_name') %></label>
<input class="input setting-input setting-input-number" type="number" id="<%= uniqueId %>" value='<%= model.get("value") %>'/>
<button class="action setting-clear inactive" type="button" name="setting-clear" value="<%= gettext("Clear") %>" data-tooltip="<%= gettext("Clear") %>">
<i class="icon-undo"></i><span class="sr">"<%= gettext("Clear Value") %>"</span>
</button>
</div>
<span class="tip setting-help"><%= model.get('help') %></span>
<div class="wrapper-comp-setting">
<label class="label setting-label" for="<%= uniqueId %>"><%= model.get('display_name') %></label>
<select class="input setting-input" id="<%= uniqueId %>" name="<%= model.get('display_name') %>">
<% _.each(model.get('options'), function(option) { %>
<% if (option.display_name !== undefined) { %>
<option value="<%= option['display_name'] %>"><%= option['display_name'] %></option>
<% } else { %>
<option value="<%= option %>"><%= option %></option>
<% } %>
<% }) %>
</select>
<button class="action setting-clear inactive" type="button" name="setting-clear" value="<%= gettext("Clear") %>" data-tooltip="<%= gettext("Clear") %>">
<i class="icon-undo"></i><span class="sr">"<%= gettext("Clear Value") %>"</span>
</button>
</div>
<span class="tip setting-help"><%= model.get('help') %></span>
<div class="wrapper-comp-setting">
<label class="label setting-label" for="<%= uniqueId %>"><%= model.get('display_name') %></label>
<input class="input setting-input" type="text" id="<%= uniqueId %>" value='<%= model.get("value") %>'/>
<button class="action setting-clear inactive" type="button" name="setting-clear" value="<%= gettext("Clear") %>" data-tooltip="<%= gettext("Clear") %>">
<i class="icon-undo"></i><span class="sr">"<%= gettext("Clear Value") %>"</span>
</button>
</div>
<span class="tip setting-help"><%= model.get('help') %></span>
......@@ -78,22 +78,13 @@
% endif
<div class="tab current" id="tab1">
<ul class="new-component-template">
% for name, location, has_markdown, is_empty in templates:
% for name, location, has_markdown in templates:
% if has_markdown or type != "problem":
% if is_empty:
<li class="editor-md empty">
<a href="#" data-location="${location}">
<span class="name"> ${name}</span>
</a>
</li>
% else:
<li class="editor-md">
<a href="#" data-location="${location}">
<span class="name"> ${name}</span>
</a>
</li>
% endif
<li class="editor-md">
<a href="#" id="${location}" data-location="${location}">
<span class="name"> ${name}</span>
</a>
</li>
% endif
%endfor
......@@ -102,23 +93,14 @@
% if type == "problem":
<div class="tab" id="tab2">
<ul class="new-component-template">
% for name, location, has_markdown, is_empty in templates:
% for name, location, has_markdown in templates:
% if not has_markdown:
% if is_empty:
<li class="editor-manual empty">
<a href="#" data-location="${location}">
<span class="name">${name}</span>
</a>
</li>
% else:
<li class="editor-manual">
<a href="#" data-location="${location}">
<span class="name"> ${name}</span>
</a>
<li class="editor-manual">
<a href="#" id="${location}" data-location="${location}">
<span class="name"> ${name}</span>
</a>
</li>
% endif
</li>
% endif
% endfor
</ul>
......
<%! from django.utils.translation import ugettext as _ %>
<div class="wrapper-comp-editor" id="editor-tab">
<section class="html-editor editor">
<ul class="editor-tabs">
<li><a href="#" class="visual-tab tab current" data-tab="visual">${_("Visual")}</a></li>
<li><a href="#" class="html-tab tab" data-tab="advanced">${_("HTML")}</a></li>
</ul>
<div class="row">
<textarea class="tiny-mce">${data | h}</textarea>
<textarea name="" class="edit-box">${data | h}</textarea>
</div>
</section>
</div>
<%include file="metadata-edit.html" />
<section class="html-editor editor">
<ul class="editor-tabs">
<li><a href="#" class="visual-tab tab current" data-tab="visual">Visual</a></li>
<li><a href="#" class="html-tab tab" data-tab="advanced">HTML</a></li>
</ul>
<div class="row">
<textarea class="tiny-mce">${data | h}</textarea>
<textarea name="" class="edit-box">${data | h}</textarea>
</div>
</section>
<%! from django.utils.translation import ugettext as _ %>
<%namespace name='static' file='../static_content.html'/>
<%
import hashlib
from xmodule.fields import StringyInteger, StringyFloat
import copy
import json
hlskey = hashlib.md5(module.location.url()).hexdigest()
%>
<section class="metadata_edit">
<ul>
% for field_name, field_value in editable_metadata_fields.items():
<li>
% if field_name == 'source_code':
% if field_value['explicitly_set'] is True:
<a href="#hls-modal-${hlskey}" style="color:yellow;" id="hls-trig-${hlskey}" >Edit High Level Source</a>
% endif
% else:
<label>${field_value['field'].display_name}:</label>
<input type='text' data-metadata-name='${field_value["field"].display_name}'
## This is a hack to keep current behavior for weight and attempts (empty will parse OK as unset).
## This hack will go away with our custom editors.
% if field_value["value"] == None and (isinstance(field_value["field"], StringyFloat) or isinstance(field_value["field"], StringyInteger)):
value = ''
% else:
value='${field_value["field"].to_json(field_value["value"])}'
% endif
size='60' />
## Change to True to see all the information being passed through.
% if False:
<label>Help: ${field_value['field'].help}</label>
<label>Type: ${type(field_value['field']).__name__}</label>
<label>Inheritable: ${field_value['inheritable']}</label>
<label>Showing inherited value: ${field_value['inheritable'] and not field_value['explicitly_set']}</label>
<label>Explicitly set: ${field_value['explicitly_set']}</label>
<label>Default value: ${field_value['default_value']}</label>
% if field_value['field'].values:
<label>Possible values:</label>
% for value in field_value['field'].values:
<label>${value}</label>
% endfor
% endif
% endif
% endif
</li>
% endfor
</ul>
% if 'source_code' in editable_metadata_fields and editable_metadata_fields['source_code']['explicitly_set']:
## js templates
<script id="metadata-editor-tpl" type="text/template">
<%static:include path="js/metadata-editor.underscore" />
</script>
<script id="metadata-number-entry" type="text/template">
<%static:include path="js/metadata-number-entry.underscore" />
</script>
<script id="metadata-string-entry" type="text/template">
<%static:include path="js/metadata-string-entry.underscore" />
</script>
<script id="metadata-option-entry" type="text/template">
<%static:include path="js/metadata-option-entry.underscore" />
</script>
<% showHighLevelSource='source_code' in editable_metadata_fields and editable_metadata_fields['source_code']['explicitly_set'] %>
<% metadata_field_copy = copy.copy(editable_metadata_fields) %>
## Delete 'source_code' field (if it exists) so metadata editor view does not attempt to render it.
% if 'source_code' in editable_metadata_fields:
## source-edit.html needs access to the 'source_code' value, so delete from a copy.
<% del metadata_field_copy['source_code'] %>
% endif
% if showHighLevelSource:
<div class="launch-latex-compiler">
<a href="#hls-modal-${hlskey}" id="hls-trig-${hlskey}">${_("Launch Latex Source Compiler")}</a>
</div>
<%include file="source-edit.html" />
% endif
% endif
</section>
<div class="wrapper-comp-settings metadata_edit" id="settings-tab" data-metadata='${json.dumps(metadata_field_copy) | h}'/>
\ No newline at end of file
<%include file="metadata-edit.html" />
<div class="wrapper-comp-editor" id="editor-tab">
<section class="combinedopenended-editor editor">
<div class="row">
%if enable_markdown:
......@@ -93,3 +93,5 @@
</div>
</article>
</script>
</div>
<%include file="metadata-edit.html" />
<%include file="metadata-edit.html" />
<%! from django.utils.translation import ugettext as _ %>
<div class="wrapper-comp-editor" id="editor-tab">
<section class="problem-editor editor">
<div class="row">
%if enable_markdown:
<div class="editor-bar">
<ul class="format-buttons">
<li><a href="#" class="header-button" data-tooltip="Heading 1"><span
<li><a href="#" class="header-button" data-tooltip='${_("Heading 1")}'><span
class="problem-editor-icon heading1"></span></a></li>
<li><a href="#" class="multiple-choice-button" data-tooltip="Multiple Choice"><span
<li><a href="#" class="multiple-choice-button" data-tooltip='${_("Multiple Choice")}'><span
class="problem-editor-icon multiple-choice"></span></a></li>
<li><a href="#" class="checks-button" data-tooltip="Checkboxes"><span
<li><a href="#" class="checks-button" data-tooltip='${_("Checkboxes")}'><span
class="problem-editor-icon checks"></span></a></li>
<li><a href="#" class="string-button" data-tooltip="Text Input"><span
<li><a href="#" class="string-button" data-tooltip='${_("Text Input")}'><span
class="problem-editor-icon string"></span></a></li>
<li><a href="#" class="number-button" data-tooltip="Numerical Input"><span
<li><a href="#" class="number-button" data-tooltip='${_("Numerical Input")}'><span
class="problem-editor-icon number"></span></a></li>
<li><a href="#" class="dropdown-button" data-tooltip="Dropdown"><span
<li><a href="#" class="dropdown-button" data-tooltip='${_("Dropdown")}'><span
class="problem-editor-icon dropdown"></span></a></li>
<li><a href="#" class="explanation-button" data-tooltip="Explanation"><span
<li><a href="#" class="explanation-button" data-tooltip='${_("Explanation")}'><span
class="problem-editor-icon explanation"></span></a></li>
</ul>
<ul class="editor-tabs">
<li><a href="#" class="xml-tab advanced-toggle" data-tab="xml">Advanced Editor</a></li>
<li><a href="#" class="cheatsheet-toggle" data-tooltip="Toggle Cheatsheet">?</a></li>
<li><a href="#" class="xml-tab advanced-toggle" data-tab="xml">${_("Advanced Editor")}</a></li>
<li><a href="#" class="cheatsheet-toggle" data-tooltip='${_("Toggle Cheatsheet")}'>?</a></li>
</ul>
</div>
<textarea class="markdown-box">${markdown | h}</textarea>
......@@ -34,7 +36,7 @@
<article class="simple-editor-cheatsheet">
<div class="cheatsheet-wrapper">
<div class="row">
<h6>Heading 1</h6>
<h6>${_("Heading 1")}</h6>
<div class="col sample heading-1">
<img src="/static/img/header-example.png" />
</div>
......@@ -45,7 +47,7 @@
</div>
</div>
<div class="row">
<h6>Multiple Choice</h6>
<h6>${_("Multiple Choice")}</h6>
<div class="col sample multiple-choice">
<img src="/static/img/choice-example.png" />
</div>
......@@ -56,7 +58,7 @@
</div>
</div>
<div class="row">
<h6>Checkboxes</h6>
<h6>${_("Checkboxes")}</h6>
<div class="col sample check-multiple">
<img src="/static/img/multi-example.png" />
</div>
......@@ -67,7 +69,7 @@
</div>
</div>
<div class="row">
<h6>Text Input</h6>
<h6>${_("Text Input")}</h6>
<div class="col sample string-response">
<img src="/static/img/string-example.png" />
</div>
......@@ -76,7 +78,7 @@
</div>
</div>
<div class="row">
<h6>Numerical Input</h6>
<h6>${_("Numerical Input")}</h6>
<div class="col sample numerical-response">
<img src="/static/img/number-example.png" />
</div>
......@@ -85,7 +87,7 @@
</div>
</div>
<div class="row">
<h6>Dropdown</h6>
<h6>${_("Dropdown")}</h6>
<div class="col sample option-reponse">
<img src="/static/img/select-example.png" />
</div>
......@@ -94,7 +96,7 @@
</div>
</div>
<div class="row">
<h6>Explanation</h6>
<h6>${_("Explanation")}</h6>
<div class="col sample explanation">
<img src="/static/img/explanation-example.png" />
</div>
......@@ -105,3 +107,5 @@
</div>
</article>
</script>
</div>
<%include file="metadata-edit.html" />
<div class="wrapper-comp-editor" id="editor-tab">
<section class="raw-edit">
<textarea name="" class="edit-box" rows="8" cols="40">${data | h}</textarea>
</section>
</div>
<%include file="metadata-edit.html" />
<section class="raw-edit">
<textarea name="" class="edit-box" rows="8" cols="40">${data | h}</textarea>
</section>
......@@ -29,7 +29,6 @@
</ul>
</section>
<%include file="metadata-edit.html" />
<div class="content">
<section class="modules">
<ol>
......@@ -50,5 +49,6 @@
</ol>
</section>
</div>
<%include file="metadata-edit.html" />
</section>
......@@ -28,4 +28,4 @@ class CmsNamespace(Namespace):
"""
published_date = DateTuple(help="Date when the module was published", scope=Scope.settings)
published_by = String(help="Id of the user who published this module", scope=Scope.settings)
empty = StringyBoolean(help="Whether this is an empty template", scope=Scope.settings, default=False)
......@@ -13,6 +13,7 @@ class UserFactory(sf.UserFactory):
"""
User account for lms / cms
"""
FACTORY_DJANGO_GET_OR_CREATE = ('username',)
pass
......@@ -21,6 +22,7 @@ class UserProfileFactory(sf.UserProfileFactory):
"""
Demographics etc for the User
"""
FACTORY_DJANGO_GET_OR_CREATE = ('user',)
pass
......@@ -29,6 +31,7 @@ class RegistrationFactory(sf.RegistrationFactory):
"""
Activation key for registering the user account
"""
FACTORY_DJANGO_GET_OR_CREATE = ('user',)
pass
......
......@@ -4,7 +4,7 @@
from lettuce import world
import time
from urllib import quote_plus
from selenium.common.exceptions import WebDriverException
from selenium.common.exceptions import WebDriverException, StaleElementReferenceException
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
......@@ -63,7 +63,7 @@ def css_click(css_selector):
# Occassionally, MathJax or other JavaScript can cover up
# an element temporarily.
# If this happens, wait a second, then try again
time.sleep(1)
world.wait(1)
world.browser.find_by_css(css_selector).click()
......@@ -80,6 +80,14 @@ def css_click_at(css, x=10, y=10):
@world.absorb
def id_click(elem_id):
"""
Perform a click on an element as specified by its id
"""
world.css_click('#%s' % elem_id)
@world.absorb
def css_fill(css_selector, text):
world.browser.find_by_css(css_selector).first.fill(text)
......@@ -94,7 +102,12 @@ def css_text(css_selector):
# Wait for the css selector to appear
if world.is_css_present(css_selector):
return world.browser.find_by_css(css_selector).first.text
try:
return world.browser.find_by_css(css_selector).first.text
except StaleElementReferenceException:
# The DOM was still redrawing. Wait a second and try again.
world.wait(1)
return world.browser.find_by_css(css_selector).first.text
else:
return ""
......
......@@ -66,22 +66,51 @@ class ComplexEncoder(json.JSONEncoder):
class CapaFields(object):
attempts = StringyInteger(help="Number of attempts taken by the student on this problem", default=0, scope=Scope.user_state)
max_attempts = StringyInteger(help="Maximum number of attempts that a student is allowed", scope=Scope.settings)
max_attempts = StringyInteger(
display_name="Maximum Attempts",
help="Defines the number of times a student can try to answer this problem. If the value is not set, infinite attempts are allowed.",
values={"min": 1}, scope=Scope.settings
)
due = Date(help="Date that this problem is due by", scope=Scope.settings)
graceperiod = Timedelta(help="Amount of time after the due date that submissions will be accepted", scope=Scope.settings)
showanswer = String(help="When to show the problem answer to the student", scope=Scope.settings, default="closed",
values=["answered", "always", "attempted", "closed", "never"])
showanswer = String(
display_name="Show Answer",
help="Defines when to show the answer to the problem. A default value can be set in Advanced Settings.",
scope=Scope.settings, default="closed",
values=[
{"display_name": "Always", "value": "always"},
{"display_name": "Answered", "value": "answered"},
{"display_name": "Attempted", "value": "attempted"},
{"display_name": "Closed", "value": "closed"},
{"display_name": "Finished", "value": "finished"},
{"display_name": "Past Due", "value": "past_due"},
{"display_name": "Never", "value": "never"}]
)
force_save_button = Boolean(help="Whether to force the save button to appear on the page", scope=Scope.settings, default=False)
rerandomize = Randomization(help="When to rerandomize the problem", default="always", scope=Scope.settings)
rerandomize = Randomization(
display_name="Randomization", help="Defines how often inputs are randomized when a student loads the problem. This setting only applies to problems that can have randomly generated numeric values. A default value can be set in Advanced Settings.",
default="always", scope=Scope.settings, values=[{"display_name": "Always", "value": "always"},
{"display_name": "On Reset", "value": "onreset"},
{"display_name": "Never", "value": "never"},
{"display_name": "Per Student", "value": "per_student"}]
)
data = String(help="XML data for the problem", scope=Scope.content)
correct_map = Object(help="Dictionary with the correctness of current student answers", scope=Scope.user_state, default={})
input_state = Object(help="Dictionary for maintaining the state of inputtypes", scope=Scope.user_state)
student_answers = Object(help="Dictionary with the current student responses", scope=Scope.user_state)
done = Boolean(help="Whether the student has answered the problem", scope=Scope.user_state)
seed = StringyInteger(help="Random seed for this student", scope=Scope.user_state)
weight = StringyFloat(help="How much to weight this problem by", scope=Scope.settings)
weight = StringyFloat(
display_name="Problem Weight",
help="Defines the number of points each problem is worth. If the value is not set, each response field in the problem is worth one point.",
values={"min": 0, "step": .1},
scope=Scope.settings
)
markdown = String(help="Markdown source of this module", scope=Scope.settings)
source_code = String(help="Source code for LaTeX and Word problems. This feature is not well-supported.", scope=Scope.settings)
source_code = String(
help="Source code for LaTeX and Word problems. This feature is not well-supported.",
scope=Scope.settings
)
class CapaModule(CapaFields, XModule):
......
......@@ -5,7 +5,7 @@ from pkg_resources import resource_string
from xmodule.raw_module import RawDescriptor
from .x_module import XModule
from xblock.core import Integer, Scope, String, Boolean, List
from xblock.core import Integer, Scope, String, List
from xmodule.open_ended_grading_classes.combined_open_ended_modulev1 import CombinedOpenEndedV1Module, CombinedOpenEndedV1Descriptor
from collections import namedtuple
from .fields import Date, StringyFloat, StringyInteger, StringyBoolean
......@@ -48,27 +48,49 @@ class VersionInteger(Integer):
class CombinedOpenEndedFields(object):
display_name = String(help="Display name for this module", default="Open Ended Grading", scope=Scope.settings)
display_name = String(
display_name="Display Name",
help="This name appears in the horizontal navigation at the top of the page.",
default="Open Ended Grading", scope=Scope.settings
)
current_task_number = StringyInteger(help="Current task that the student is on.", default=0, scope=Scope.user_state)
task_states = List(help="List of state dictionaries of each task within this module.", scope=Scope.user_state)
state = String(help="Which step within the current task that the student is on.", default="initial",
scope=Scope.user_state)
student_attempts = StringyInteger(help="Number of attempts taken by the student on this problem", default=0,
scope=Scope.user_state)
ready_to_reset = StringyBoolean(help="If the problem is ready to be reset or not.", default=False,
scope=Scope.user_state)
attempts = StringyInteger(help="Maximum number of attempts that a student is allowed.", default=1, scope=Scope.settings)
is_graded = StringyBoolean(help="Whether or not the problem is graded.", default=False, scope=Scope.settings)
accept_file_upload = StringyBoolean(help="Whether or not the problem accepts file uploads.", default=False,
scope=Scope.settings)
skip_spelling_checks = StringyBoolean(help="Whether or not to skip initial spelling checks.", default=True,
scope=Scope.settings)
ready_to_reset = StringyBoolean(
help="If the problem is ready to be reset or not.", default=False,
scope=Scope.user_state
)
attempts = StringyInteger(
display_name="Maximum Attempts",
help="The number of times the student can try to answer this problem.", default=1,
scope=Scope.settings, values = {"min" : 1 }
)
is_graded = StringyBoolean(display_name="Graded", help="Whether or not the problem is graded.", default=False, scope=Scope.settings)
accept_file_upload = StringyBoolean(
display_name="Allow File Uploads",
help="Whether or not the student can submit files as a response.", default=False, scope=Scope.settings
)
skip_spelling_checks = StringyBoolean(
display_name="Disable Quality Filter",
help="If False, the Quality Filter is enabled and submissions with poor spelling, short length, or poor grammar will not be peer reviewed.",
default=False, scope=Scope.settings
)
due = Date(help="Date that this problem is due by", default=None, scope=Scope.settings)
graceperiod = String(help="Amount of time after the due date that submissions will be accepted", default=None,
scope=Scope.settings)
graceperiod = String(
help="Amount of time after the due date that submissions will be accepted",
default=None,
scope=Scope.settings
)
version = VersionInteger(help="Current version number", default=DEFAULT_VERSION, scope=Scope.settings)
data = String(help="XML data for the problem", scope=Scope.content)
weight = StringyFloat(help="How much to weight this problem by", scope=Scope.settings)
weight = StringyFloat(
display_name="Problem Weight",
help="Defines the number of points each problem is worth. If the value is not set, each problem is worth one point.",
scope=Scope.settings, values = {"min" : 0 , "step": ".1"}
)
markdown = String(help="Markdown source of this module", scope=Scope.settings)
......@@ -244,6 +266,6 @@ class CombinedOpenEndedDescriptor(CombinedOpenEndedFields, RawDescriptor):
def non_editable_metadata_fields(self):
non_editable_fields = super(CombinedOpenEndedDescriptor, self).non_editable_metadata_fields
non_editable_fields.extend([CombinedOpenEndedDescriptor.due, CombinedOpenEndedDescriptor.graceperiod,
CombinedOpenEndedDescriptor.markdown])
CombinedOpenEndedDescriptor.markdown, CombinedOpenEndedDescriptor.version])
return non_editable_fields
......@@ -10,8 +10,6 @@
position: relative;
@include linear-gradient(top, #d4dee8, #c9d5e2);
padding: 5px;
border: 1px solid #3c3c3c;
border-radius: 3px 3px 0 0;
border-bottom-color: #a5aaaf;
@include clearfix;
......
......@@ -5,7 +5,7 @@
.advanced-toggle {
@include white-button;
height: auto;
margin-top: -1px;
margin-top: -4px;
padding: 3px 9px;
font-size: 12px;
......@@ -16,7 +16,7 @@
color: $darkGrey !important;
pointer-events: none;
cursor: none;
&:hover {
box-shadow: 0 0 0 0 !important;
}
......@@ -27,7 +27,7 @@
width: 21px;
height: 21px;
padding: 0;
margin: 0 5px 0 15px;
margin: -1px 5px 0 15px;
border-radius: 22px;
border: 1px solid #a5aaaf;
background: #e5ecf3;
......@@ -99,6 +99,13 @@
}
}
.problem-editor {
// adding padding to simple editor only - adjacent selector is needed since there are no toggles for CodeMirror
.markdown-box+.CodeMirror {
padding: 10px;
}
}
.problem-editor-icon {
display: inline-block;
width: 26px;
......
......@@ -170,7 +170,7 @@ nav.sequence-nav {
font-family: $sans-serif;
line-height: lh();
left: 0px;
opacity: 0;
opacity: 0.0;
padding: 6px;
position: absolute;
top: 48px;
......@@ -204,7 +204,7 @@ nav.sequence-nav {
p {
display: block;
margin-top: 4px;
opacity: 1;
opacity: 1.0;
}
}
}
......@@ -248,12 +248,12 @@ nav.sequence-nav {
}
&:hover {
opacity: .5;
opacity: 0.5;
}
&.disabled {
cursor: normal;
opacity: .4;
opacity: 0.4;
}
}
}
......@@ -320,12 +320,12 @@ nav.sequence-bottom {
outline: 0;
&:hover {
opacity: .5;
opacity: 0.5;
background-position: center 15px;
}
&.disabled {
opacity: .4;
opacity: 0.4;
}
&:focus {
......
......@@ -41,7 +41,7 @@ div.video {
&:hover {
ul, div {
opacity: 1;
opacity: 1.0;
}
}
......@@ -158,7 +158,7 @@ div.video {
ol.video_speeds {
display: block;
opacity: 1;
opacity: 1.0;
padding: 0;
margin: 0;
list-style: none;
......@@ -208,7 +208,7 @@ div.video {
}
&:hover, &:active, &:focus {
opacity: 1;
opacity: 1.0;
background-color: #444;
}
}
......@@ -221,7 +221,7 @@ div.video {
border: 1px solid #000;
bottom: 46px;
display: none;
opacity: 0;
opacity: 0.0;
position: absolute;
width: 133px;
z-index: 10;
......@@ -264,7 +264,7 @@ div.video {
&.open {
.volume-slider-container {
display: block;
opacity: 1;
opacity: 1.0;
}
}
......@@ -302,7 +302,7 @@ div.video {
border: 1px solid #000;
bottom: 46px;
display: none;
opacity: 0;
opacity: 0.0;
position: absolute;
width: 45px;
height: 125px;
......@@ -395,7 +395,7 @@ div.video {
font-weight: 800;
line-height: 46px; //height of play pause buttons
margin-left: 0;
opacity: 1;
opacity: 1.0;
padding: 0 lh(.5);
position: relative;
text-indent: -9999px;
......@@ -410,7 +410,7 @@ div.video {
}
&.off {
opacity: .7;
opacity: 0.7;
}
}
}
......@@ -418,7 +418,7 @@ div.video {
&:hover section.video-controls {
ul, div {
opacity: 1;
opacity: 1.0;
}
div.slider {
......
......@@ -41,7 +41,7 @@ div.video {
&:hover {
ul, div {
opacity: 1;
opacity: 1.0;
}
}
......@@ -158,7 +158,7 @@ div.video {
ol.video_speeds {
display: block;
opacity: 1;
opacity: 1.0;
padding: 0;
margin: 0;
list-style: none;
......@@ -208,7 +208,7 @@ div.video {
}
&:hover, &:active, &:focus {
opacity: 1;
opacity: 1.0;
background-color: #444;
}
}
......@@ -221,7 +221,7 @@ div.video {
border: 1px solid #000;
bottom: 46px;
display: none;
opacity: 0;
opacity: 0.0;
position: absolute;
width: 133px;
z-index: 10;
......@@ -264,7 +264,7 @@ div.video {
&.open {
.volume-slider-container {
display: block;
opacity: 1;
opacity: 1.0;
}
}
......@@ -302,7 +302,7 @@ div.video {
border: 1px solid #000;
bottom: 46px;
display: none;
opacity: 0;
opacity: 0.0;
position: absolute;
width: 45px;
height: 125px;
......@@ -395,7 +395,7 @@ div.video {
font-weight: 800;
line-height: 46px; //height of play pause buttons
margin-left: 0;
opacity: 1;
opacity: 1.0;
padding: 0 lh(.5);
position: relative;
text-indent: -9999px;
......@@ -410,7 +410,7 @@ div.video {
}
&.off {
opacity: .7;
opacity: 0.7;
}
}
}
......@@ -418,7 +418,7 @@ div.video {
&:hover section.video-controls {
ul, div {
opacity: 1;
opacity: 1.0;
}
div.slider {
......
......@@ -8,8 +8,16 @@ from xblock.core import String, Scope
class DiscussionFields(object):
discussion_id = String(scope=Scope.settings)
discussion_category = String(scope=Scope.settings)
discussion_target = String(scope=Scope.settings)
discussion_category = String(
display_name="Category",
help="A category name for the discussion. This name appears in the left pane of the discussion forum for the course.",
scope=Scope.settings
)
discussion_target = String(
display_name="Subcategory",
help="A subcategory name for the discussion. This name appears in the left pane of the discussion forum for the course.",
scope=Scope.settings
)
sort_key = String(scope=Scope.settings)
......
......@@ -10,7 +10,7 @@ from .x_module import XModule
from xmodule.raw_module import RawDescriptor
from xmodule.modulestore.django import modulestore
from .timeinfo import TimeInfo
from xblock.core import Object, Integer, Boolean, String, Scope
from xblock.core import Object, String, Scope
from xmodule.fields import Date, StringyFloat, StringyInteger, StringyBoolean
from xmodule.open_ended_grading_classes.peer_grading_service import PeerGradingService, GradingServiceError, MockPeerGradingService
......@@ -22,24 +22,43 @@ USE_FOR_SINGLE_LOCATION = False
LINK_TO_LOCATION = ""
TRUE_DICT = [True, "True", "true", "TRUE"]
MAX_SCORE = 1
IS_GRADED = True
IS_GRADED = False
EXTERNAL_GRADER_NO_CONTACT_ERROR = "Failed to contact external graders. Please notify course staff."
class PeerGradingFields(object):
use_for_single_location = StringyBoolean(help="Whether to use this for a single location or as a panel.",
default=USE_FOR_SINGLE_LOCATION, scope=Scope.settings)
link_to_location = String(help="The location this problem is linked to.", default=LINK_TO_LOCATION,
scope=Scope.settings)
is_graded = StringyBoolean(help="Whether or not this module is scored.", default=IS_GRADED, scope=Scope.settings)
use_for_single_location = StringyBoolean(
display_name="Show Single Problem",
help='When True, only the single problem specified by "Link to Problem Location" is shown. '
'When False, a panel is displayed with all problems available for peer grading.',
default=USE_FOR_SINGLE_LOCATION, scope=Scope.settings
)
link_to_location = String(
display_name="Link to Problem Location",
help='The location of the problem being graded. Only used when "Show Single Problem" is True.',
default=LINK_TO_LOCATION, scope=Scope.settings
)
is_graded = StringyBoolean(
display_name="Graded",
help='Defines whether the student gets credit for grading this problem. Only used when "Show Single Problem" is True.',
default=IS_GRADED, scope=Scope.settings
)
due_date = Date(help="Due date that should be displayed.", default=None, scope=Scope.settings)
grace_period_string = String(help="Amount of grace to give on the due date.", default=None, scope=Scope.settings)
max_grade = StringyInteger(help="The maximum grade that a student can receieve for this problem.", default=MAX_SCORE,
scope=Scope.settings)
student_data_for_location = Object(help="Student data for a given peer grading problem.",
scope=Scope.user_state)
weight = StringyFloat(help="How much to weight this problem by", scope=Scope.settings)
max_grade = StringyInteger(
help="The maximum grade that a student can receive for this problem.", default=MAX_SCORE,
scope=Scope.settings, values={"min": 0}
)
student_data_for_location = Object(
help="Student data for a given peer grading problem.",
scope=Scope.user_state
)
weight = StringyFloat(
display_name="Problem Weight",
help="Defines the number of points each problem is worth. If the value is not set, each problem is worth one point.",
scope=Scope.settings, values={"min": 0, "step": ".1"}
)
class PeerGradingModule(PeerGradingFields, XModule):
......@@ -590,3 +609,11 @@ class PeerGradingDescriptor(PeerGradingFields, RawDescriptor):
#Specify whether or not to pass in open ended interface
needs_open_ended_interface = True
@property
def non_editable_metadata_fields(self):
non_editable_fields = super(PeerGradingDescriptor, self).non_editable_metadata_fields
non_editable_fields.extend([PeerGradingFields.due_date, PeerGradingFields.grace_period_string,
PeerGradingFields.max_grade])
return non_editable_fields
---
metadata:
display_name: Open Ended Response
attempts: 1
is_graded: False
version: 1
skip_spelling_checks: False
accept_file_upload: False
weight: ""
markdown: ""
data: |
<combinedopenended>
......
---
metadata:
display_name: Blank HTML Page
empty: True
data: |
......
---
metadata:
display_name: Peer Grading Interface
use_for_single_location: False
link_to_location: None
is_graded: False
max_grade: 1
weight: ""
data: |
<peergrading>
</peergrading>
......
......@@ -3,9 +3,7 @@
metadata:
display_name: Circuit Schematic Builder
rerandomize: never
showanswer: always
weight: ""
attempts: ""
showanswer: finished
data: |
<problem >
Please make a voltage divider that splits the provided voltage evenly.
......
......@@ -2,9 +2,7 @@
metadata:
display_name: Custom Python-Evaluated Input
rerandomize: never
showanswer: always
weight: ""
attempts: ""
showanswer: finished
data: |
<problem>
<p>
......
......@@ -2,11 +2,8 @@
metadata:
display_name: Blank Common Problem
rerandomize: never
showanswer: always
showanswer: finished
markdown: ""
weight: ""
empty: True
attempts: ""
data: |
<problem>
</problem>
......
......@@ -2,10 +2,7 @@
metadata:
display_name: Blank Advanced Problem
rerandomize: never
showanswer: always
weight: ""
attempts: ""
empty: True
showanswer: finished
data: |
<problem>
</problem>
......
......@@ -2,9 +2,7 @@
metadata:
display_name: Math Expression Input
rerandomize: never
showanswer: always
weight: ""
attempts: ""
showanswer: finished
data: |
<problem>
<p>
......
......@@ -2,9 +2,7 @@
metadata:
display_name: Image Mapped Input
rerandomize: never
showanswer: always
weight: ""
attempts: ""
showanswer: finished
data: |
<problem>
<p>
......
......@@ -2,9 +2,7 @@
metadata:
display_name: Multiple Choice
rerandomize: never
showanswer: always
weight: ""
attempts: ""
showanswer: finished
markdown:
"A multiple choice problem presents radio buttons for student input. Students can only select a single
option presented. Multiple Choice questions have been the subject of many areas of research due to the early
......
......@@ -2,9 +2,7 @@
metadata:
display_name: Numerical Input
rerandomize: never
showanswer: always
weight: ""
attempts: ""
showanswer: finished
markdown:
"A numerical input problem accepts a line of text input from the
student, and evaluates the input for correctness based on its
......
......@@ -2,9 +2,7 @@
metadata:
display_name: Dropdown
rerandomize: never
showanswer: always
weight: ""
attempts: ""
showanswer: finished
markdown:
"Dropdown problems give a limited set of options for students to respond with, and present those options
in a format that encourages them to search for a specific answer rather than being immediately presented
......
......@@ -2,9 +2,7 @@
metadata:
display_name: Text Input
rerandomize: never
showanswer: always
weight: ""
attempts: ""
showanswer: finished
# Note, the extra newlines are needed to make the yaml parser add blank lines instead of folding
markdown:
"A text input problem accepts a line of text from the
......
---
metadata:
display_name: Word cloud
version: 1
num_inputs: 5
num_top_words: 250
display_student_percents: True
data: {}
children: []
......@@ -460,8 +460,8 @@ class ImportTestCase(BaseCourseTestCase):
)
module = modulestore.get_instance(course.id, location)
self.assertEqual(len(module.get_children()), 0)
self.assertEqual(module.num_inputs, '5')
self.assertEqual(module.num_top_words, '250')
self.assertEqual(module.num_inputs, 5)
self.assertEqual(module.num_top_words, 250)
def test_cohort_config(self):
"""
......
......@@ -2,7 +2,7 @@
generate and view word cloud.
On the client side we show:
If student does not yet anwered - `num_inputs` numbers of text inputs.
If student does not yet answered - `num_inputs` numbers of text inputs.
If student have answered - words he entered and cloud.
"""
......@@ -14,7 +14,8 @@ from xmodule.raw_module import RawDescriptor
from xmodule.editing_module import MetadataOnlyEditingDescriptor
from xmodule.x_module import XModule
from xblock.core import Scope, String, Object, Boolean, List, Integer
from xblock.core import Scope, Object, Boolean, List
from fields import StringyBoolean, StringyInteger
log = logging.getLogger(__name__)
......@@ -31,22 +32,23 @@ def pretty_bool(value):
class WordCloudFields(object):
"""XFields for word cloud."""
display_name = String(
help="Display name for this module",
scope=Scope.settings
)
num_inputs = Integer(
help="Number of inputs.",
num_inputs = StringyInteger(
display_name="Inputs",
help="Number of text boxes available for students to input words/sentences.",
scope=Scope.settings,
default=5
default=5,
values={"min": 1}
)
num_top_words = Integer(
help="Number of max words, which will be displayed.",
num_top_words = StringyInteger(
display_name="Maximum Words",
help="Maximum number of words to be displayed in generated word cloud.",
scope=Scope.settings,
default=250
default=250,
values={"min": 1}
)
display_student_percents = Boolean(
help="Display usage percents for each word?",
display_student_percents = StringyBoolean(
display_name="Show Percents",
help="Statistics are shown for entered words near that word.",
scope=Scope.settings,
default=True
)
......@@ -205,7 +207,7 @@ class WordCloudModule(WordCloudFields, XModule):
# Update top_words.
self.top_words = self.top_dict(
temp_all_words,
int(self.num_top_words)
self.num_top_words
)
# Save all_words in database.
......@@ -226,7 +228,7 @@ class WordCloudModule(WordCloudFields, XModule):
'element_id': self.location.html_id(),
'element_class': self.location.category,
'ajax_url': self.system.ajax_url,
'num_inputs': int(self.num_inputs),
'num_inputs': self.num_inputs,
'submitted': self.submitted
}
self.content = self.system.render_template('word_cloud.html', context)
......
import logging
import copy
import yaml
import os
......@@ -9,7 +10,7 @@ from pkg_resources import resource_listdir, resource_string, resource_isdir
from xmodule.modulestore import Location
from xmodule.modulestore.exceptions import ItemNotFoundError
from xblock.core import XBlock, Scope, String
from xblock.core import XBlock, Scope, String, Integer, Float
log = logging.getLogger(__name__)
......@@ -75,12 +76,13 @@ class HTMLSnippet(object):
"""
raise NotImplementedError(
"get_html() must be provided by specific modules - not present in {0}"
.format(self.__class__))
.format(self.__class__))
class XModuleFields(object):
display_name = String(
help="Display name for this module",
display_name="Display Name",
help="This name appears in the horizontal navigation at the top of the page.",
scope=Scope.settings,
default=None
)
......@@ -356,7 +358,7 @@ class XModuleDescriptor(XModuleFields, HTMLSnippet, ResourceTemplates, XBlock):
metadata_translations = {
'slug': 'url_name',
'name': 'display_name',
}
}
# ============================= STRUCTURAL MANIPULATION ===================
def __init__(self,
......@@ -458,7 +460,6 @@ class XModuleDescriptor(XModuleFields, HTMLSnippet, ResourceTemplates, XBlock):
"""
return False
# ================================= JSON PARSING ===========================
@staticmethod
def load_from_json(json_data, system, default_class=None):
......@@ -523,10 +524,10 @@ class XModuleDescriptor(XModuleFields, HTMLSnippet, ResourceTemplates, XBlock):
# ================================= XML PARSING ============================
@staticmethod
def load_from_xml(xml_data,
system,
org=None,
course=None,
default_class=None):
system,
org=None,
course=None,
default_class=None):
"""
This method instantiates the correct subclass of XModuleDescriptor based
on the contents of xml_data.
......@@ -541,7 +542,7 @@ class XModuleDescriptor(XModuleFields, HTMLSnippet, ResourceTemplates, XBlock):
class_ = XModuleDescriptor.load_class(
etree.fromstring(xml_data).tag,
default_class
)
)
# leave next line, commented out - useful for low-level debugging
# log.debug('[XModuleDescriptor.load_from_xml] tag=%s, class_=%s' % (
# etree.fromstring(xml_data).tag,class_))
......@@ -625,7 +626,7 @@ class XModuleDescriptor(XModuleFields, HTMLSnippet, ResourceTemplates, XBlock):
"""
inherited_metadata = getattr(self, '_inherited_metadata', {})
inheritable_metadata = getattr(self, '_inheritable_metadata', {})
metadata = {}
metadata_fields = {}
for field in self.fields:
if field.scope != Scope.settings or field in self.non_editable_metadata_fields:
......@@ -641,13 +642,39 @@ class XModuleDescriptor(XModuleFields, HTMLSnippet, ResourceTemplates, XBlock):
if field.name in inherited_metadata:
explicitly_set = False
metadata[field.name] = {'field': field,
'value': value,
'default_value': default_value,
'inheritable': inheritable,
'explicitly_set': explicitly_set }
return metadata
# We support the following editors:
# 1. A select editor for fields with a list of possible values (includes Booleans).
# 2. Number editors for integers and floats.
# 3. A generic string editor for anything else (editing JSON representation of the value).
type = "Generic"
values = [] if field.values is None else copy.deepcopy(field.values)
if isinstance(values, tuple):
values = list(values)
if isinstance(values, list):
if len(values) > 0:
type = "Select"
for index, choice in enumerate(values):
json_choice = copy.deepcopy(choice)
if isinstance(json_choice, dict) and 'value' in json_choice:
json_choice['value'] = field.to_json(json_choice['value'])
else:
json_choice = field.to_json(json_choice)
values[index] = json_choice
elif isinstance(field, Integer):
type = "Integer"
elif isinstance(field, Float):
type = "Float"
metadata_fields[field.name] = {'field_name': field.name,
'type': type,
'display_name': field.display_name,
'value': field.to_json(value),
'options': values,
'default_value': field.to_json(default_value),
'inheritable': inheritable,
'explicitly_set': explicitly_set,
'help': field.help}
return metadata_fields
class DescriptorSystem(object):
......@@ -740,7 +767,7 @@ class ModuleSystem(object):
s3_interface=None,
cache=None,
can_execute_unsafe_code=None,
):
):
'''
Create a closure around the system environment.
......
/* HTML5 Number polyfill | Jonathan Stipe | https://github.com/jonstipe/number-polyfill*/
div.number-spin-btn-container {
display: inline-block;
position: absolute;
vertical-align: middle;
margin: 0 0 0 3px;
padding: 0;
left: 74%;
top: 6px;
}
div.number-spin-btn {
-moz-box-sizing: border-box;
-webkit-box-sizing: border-box;
box-sizing: border-box;
border-width: 2px;
border-color: #ededed #777777 #777777 #ededed;
border-style: solid;
background-color: #eeeeee;
width: 1em;
font-size: 14px; }
div.number-spin-btn:hover {
/* added blue hover color */
background-color: rgb(85, 151, 221);
cursor: pointer; }
div.number-spin-btn:active {
border-width: 2px;
border-color: #5e5e5e #d8d8d8 #d8d8d8 #5e5e5e;
border-style: solid;
background-color: #999999; }
div.number-spin-btn-up {
border-bottom-width: 1px;
-moz-border-radius: 0px;
-webkit-border-radius: 0px;
border-radius: 0px;
font-size: 14px; }
div.number-spin-btn-up:before {
border-width: 0 0.3em 0.3em 0.3em;
border-color: transparent transparent black transparent;
top: 25%; }
div.number-spin-btn-up:active {
border-bottom-width: 1px; }
div.number-spin-btn-up:active:before {
border-bottom-color: white;
top: 26%;
left: 51%; }
div.number-spin-btn-down {
border-top-width: 1px;
-moz-border-radius: 0px 0px 3px 3px;
-webkit-border-radius: 0px 0px 3px 3px;
border-radius: 0px 0px 3px 3px; }
div.number-spin-btn-down:before {
border-width: 0.3em 0.3em 0 0.3em;
border-color: black transparent transparent transparent;
top: 75%; }
div.number-spin-btn-down:active {
border-top-width: 1px; }
div.number-spin-btn-down:active:before {
border-top-color: white;
top: 76%;
left: 51%; }
div.number-spin-btn-up:before,
div.number-spin-btn-down:before {
content: "";
width: 0;
height: 0;
border-style: solid;
position: absolute;
left: 50%;
margin: -0.15em 0 0 -0.3em;
padding: 0; }
input:disabled + div.number-spin-btn-container > div.number-spin-btn-up:active, input:disabled + div.number-spin-btn-container > div.number-spin-btn-down:active {
border-color: #ededed #777777 #777777 #ededed;
border-style: solid;
background-color: #cccccc; }
input:disabled + div.number-spin-btn-container > div.number-spin-btn-up:before, input:disabled + div.number-spin-btn-container > div.number-spin-btn-up:active:before {
border-bottom-color: #999999;
top: 25%;
left: 50%; }
input:disabled + div.number-spin-btn-container > div.number-spin-btn-down:before, input:disabled + div.number-spin-btn-container > div.number-spin-btn-down:active:before {
border-top-color: #999999;
top: 75%;
left: 50%; }
......@@ -25,8 +25,8 @@
/* Layout */
.studioSkin table.mceLayout {border:0;}
.studioSkin table.mceLayout tr.mceFirst td {border-top:1px solid #3c3c3c;}
.studioSkin table.mceLayout tr.mceLast td {border-bottom:1px solid #3c3c3c;}
.studioSkin table.mceLayout tr.mceFirst td {border-top: 1px solid #D1DCE6; border-left: none; border-right:none;}
.studioSkin table.mceLayout tr.mceLast td {border-bottom:none;}
.studioSkin table.mceToolbar, .studioSkin tr.mceFirst .mceToolbar tr td, .studioSkin tr.mceLast .mceToolbar tr td {border:0; margin:0; padding:0;}
.studioSkin td.mceToolbar {
background: -webkit-linear-gradient(top, #d4dee8, #c9d5e2);
......@@ -36,11 +36,11 @@
background: linear-gradient(top, #d4dee8, #c9d5e2);
border: 1px solid #3c3c3c;
border-bottom-color: #a5aaaf;
border-radius: 3px 3px 0 0;
border-radius: 0;
padding: 10px 10px 9px;
vertical-align: top;
}
.studioSkin .mceIframeContainer {border: 1px solid #3c3c3c; border-top: none;}
.studioSkin .mceIframeContainer {border: 1px solid white; border-top: none;}
.studioSkin .mceStatusbar {background:#F0F0EE; font-size:9pt; line-height:16px; overflow:visible; color:#000; display:block; height:20px}
.studioSkin .mceStatusbar div {float:left; margin:2px}
.studioSkin .mceStatusbar a.mceResize {display:block; float:right; background:url(../../img/studio-icons.png) -800px 0; width:20px; height:20px; cursor:se-resize; outline:0}
......
......@@ -85,8 +85,8 @@
}
@-webkit-keyframes fadeIn {
0% { opacity: 0; }
100% { opacity: 1; }
0% { opacity: 0.0; }
100% { opacity: 1.0; }
}
......@@ -736,11 +736,11 @@ body.discussion {
&.is-open {
.browse-topic-drop-btn span {
opacity: 1;
opacity: 1.0;
}
.browse-topic-drop-icon {
opacity: 0;
opacity: 0.0;
}
&.is-dropped {
......@@ -788,7 +788,7 @@ body.discussion {
&::-webkit-input-placeholder,
&:-moz-placeholder,
&:-ms-input-placeholder {
opacity: 1;
opacity: 1.0;
}
}
}
......@@ -818,7 +818,7 @@ body.discussion {
line-height: 58px;
color: #333;
text-shadow: 0 1px 0 rgba(255, 255, 255, .8);
opacity: 0;
opacity: 0.0;
@include transition(opacity .2s);
}
}
......@@ -833,7 +833,7 @@ body.discussion {
height: 16px;
margin-left: -12px;
background: url(../images/browse-icon.png) no-repeat;
opacity: 1;
opacity: 1.0;
@include transition(none);
}
......@@ -967,7 +967,7 @@ body.discussion {
&::-webkit-input-placeholder,
&:-moz-placeholder,
&:-ms-input-placeholder {
opacity: 0;
opacity: 0.0;
@include transition(opacity .2s);
}
......@@ -2454,7 +2454,7 @@ body.discussion {
font-style: italic;
cursor:pointer;
margin-right: 10px;
opacity:.8;
opacity: 0.8;
span {
cursor: pointer;
......@@ -2462,7 +2462,7 @@ body.discussion {
&:hover {
@include transition(opacity .2s);
opacity: 1;
opacity: 1.0;
}
}
......@@ -2475,7 +2475,7 @@ body.discussion {
top:-13px;
margin-right:35px;
margin-top:13px;
opacity: 1;
opacity: 1.0;
}
.notpinned .icon {
......@@ -2523,11 +2523,11 @@ display:none;
padding-right: 5px;
font-style: italic;
cursor:pointer;
opacity:.8;
opacity: 0.8;
&:hover {
@include transition(opacity .2s);
opacity: 1;
opacity: 1.0;
}
}
......
......@@ -9,12 +9,12 @@
@mixin home-header-pop-up-keyframes {
0% {
opacity: 0;
opacity: 0.0;
top: 300px;
//@include transform(scale(0.9));
}
45% {
opacity: 1;
opacity: 1.0;
}
65% {
top: -40px;
......@@ -43,19 +43,19 @@
@mixin title-appear-keyframes {
0% {
opacity: 0;
opacity: 0.0;
top: 60px;
@include transform(scale(0.9));
}
20% {
opacity: 1;
opacity: 1.0;
}
27% { // this % of total-time should be ~ 1.25s
top: 40px;
@include transform(scale(1));
}
90% { // this % of total-time is when 2nd half of animation starts
opacity: 1;
opacity: 1.0;
top: 40px;
@include transform(scale(1));
}
......@@ -79,24 +79,24 @@
@mixin home-appear-keyframes {
0% {
opacity: 0;
opacity: 0.0;
top: 60px;
@include transform(scale(0.9));
}
20% {
opacity: 1;
opacity: 1.0;
}
30% { // this % of total-time should be ~ 1.25s
top: 40px;
@include transform(scale(1));
}
80% { // this % of total-time is when 2nd half of animation starts
opacity: 1;
opacity: 1.0;
top: 40px;
@include transform(scale(1));
}
100% {
opacity: 0;
opacity: 0.0;
top: 60px;
@include transform(scale(0.7));
}
......@@ -117,10 +117,10 @@
@mixin edx-appear-keyframes {
0% {
opacity: 0;
opacity: 0.0;
}
100% {
opacity: 1;
opacity: 1.0;
}
}
......@@ -231,7 +231,7 @@
opacity: 0.9;
}
80% {
opacity: 1;
opacity: 1.0;
}
100% {
bottom: 0px;
......
......@@ -38,7 +38,7 @@ div.book-wrapper {
line-height: 2.1em;
text-align: right;
color: #9a9a9a;
opacity: 0;
opacity: 0.0;
@include transition(opacity .15s);
}
......@@ -55,7 +55,7 @@ div.book-wrapper {
background-color: transparent;
.page-number {
opacity: 1;
opacity: 1.0;
}
}
}
......@@ -119,7 +119,7 @@ div.book-wrapper {
@include box-sizing(border-box);
display: table;
height: 100%;
opacity: 0;
opacity: 0.0;
filter: alpha(opacity=0);
text-indent: -9999px;
@include transition;
......@@ -127,7 +127,7 @@ div.book-wrapper {
width: 100%;
&:hover {
opacity: 1;
opacity: 1.0;
filter: alpha(opacity=100);
}
}
......
......@@ -61,7 +61,7 @@ section.course-index {
span.ui-icon {
left: 0;
background-image: url("/static/images/ui-icons_222222_256x240.png");
opacity: .3;
opacity: 0.3;
}
}
}
......@@ -146,7 +146,7 @@ section.course-index {
@include box-shadow(inset 0 1px 14px 0 rgba(0,0,0, 0.1));
&:after {
opacity: 1;
opacity: 1.0;
right: 15px;
}
}
......@@ -174,7 +174,7 @@ section.course-index {
background: $sidebar-active-image;
&:after {
opacity: 1;
opacity: 1.0;
right: 15px;
}
......
......@@ -27,7 +27,7 @@ div.calc-main {
width: 16px;
&:hover {
opacity: .8;
opacity: 0.8;
}
&.closed {
......@@ -136,7 +136,7 @@ div.calc-main {
&.shown {
display: block;
opacity: 1;
opacity: 1.0;
}
dt {
......
......@@ -26,7 +26,7 @@ header.global {
}
h2 {
opacity: 1;
opacity: 1.0;
}
}
......@@ -51,7 +51,7 @@ header.global {
text-decoration: none;
&::before {
opacity: 1;
opacity: 1.0;
}
.name {
......
......@@ -429,7 +429,7 @@
&:hover {
.sharing-message {
opacity: 1;
opacity: 1.0;
top: 56px;
}
}
......@@ -470,7 +470,7 @@
width: 44px;
&:hover {
opacity: 1;
opacity: 1.0;
}
img {
......@@ -514,7 +514,7 @@
&:hover {
.icon {
opacity: 1;
opacity: 1.0;
}
}
......
......@@ -74,7 +74,7 @@
&:hover {
.title .icon {
opacity: 1;
opacity: 1.0;
}
}
......
......@@ -53,7 +53,7 @@
@include box-sizing(border-box);
@include inline-block;
left: 0px;
opacity: 1;
opacity: 1.0;
padding: 20px 30px;
top: 0px;
@include transition(all, 0.2s, linear);
......@@ -312,7 +312,7 @@
text-decoration: none;
&::before {
opacity: 1;
opacity: 1.0;
}
.name {
......
......@@ -117,7 +117,7 @@
.info-link {
color: $link-color;
opacity: 1;
opacity: 1.0;
}
h2 {
......
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