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 ...@@ -5,8 +5,6 @@ from lettuce import world, step
from nose.tools import assert_true from nose.tools import assert_true
from nose.tools import assert_equal 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 auth.authz import get_user_by_email
from selenium.webdriver.common.keys import Keys from selenium.webdriver.common.keys import Keys
...@@ -50,31 +48,31 @@ def i_press_the_category_delete_icon(step, category): ...@@ -50,31 +48,31 @@ def i_press_the_category_delete_icon(step, category):
@step('I have opened a new course in Studio$') @step('I have opened a new course in Studio$')
def i_have_opened_a_new_course(step): def i_have_opened_a_new_course(step):
open_new_course()
####### HELPER FUNCTIONS ##############
def open_new_course():
world.clear_courses() world.clear_courses()
log_into_studio() log_into_studio()
create_a_course() create_a_course()
####### HELPER FUNCTIONS ##############
def create_studio_user( def create_studio_user(
uname='robot', uname='robot',
email='robot+studio@edx.org', email='robot+studio@edx.org',
password='test', password='test',
is_staff=False): is_staff=False):
studio_user = world.UserFactory.build( studio_user = world.UserFactory(
username=uname, username=uname,
email=email, email=email,
password=password, password=password,
is_staff=is_staff) is_staff=is_staff)
studio_user.set_password(password)
studio_user.save()
registration = world.RegistrationFactory(user=studio_user) registration = world.RegistrationFactory(user=studio_user)
registration.register(studio_user) registration.register(studio_user)
registration.activate() registration.activate()
user_profile = world.UserProfileFactory(user=studio_user)
def fill_in_course_info( def fill_in_course_info(
name='Robot Super Course', name='Robot Super Course',
...@@ -153,4 +151,13 @@ def set_date_and_time(date_css, desired_date, time_css, desired_time): ...@@ -153,4 +151,13 @@ def set_date_and_time(date_css, desired_date, time_css, desired_time):
world.css_fill(time_css, desired_time) world.css_fill(time_css, desired_time)
e = world.css_find(time_css).first e = world.css_find(time_css).first
e._element.send_keys(Keys.TAB) 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 ...@@ -10,9 +10,7 @@ from nose.tools import assert_equal
@step('I have opened a new course section in Studio$') @step('I have opened a new course section in Studio$')
def i_have_opened_a_new_course_section(step): def i_have_opened_a_new_course_section(step):
world.clear_courses() open_new_course()
log_into_studio()
create_a_course()
add_section() 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): ...@@ -149,8 +149,7 @@ def edit_unit(request, location):
component_templates[category].append(( component_templates[category].append((
template.display_name_with_default, template.display_name_with_default,
template.location.url(), template.location.url(),
hasattr(template, 'markdown') and template.markdown is not None, hasattr(template, 'markdown') and template.markdown is not None
template.cms.empty,
)) ))
components = [ components = [
......
...@@ -223,7 +223,8 @@ PIPELINE_JS = { ...@@ -223,7 +223,8 @@ PIPELINE_JS = {
rooted_glob(PROJECT_ROOT / 'static/', 'coffee/src/**/*.js') rooted_glob(PROJECT_ROOT / 'static/', 'coffee/src/**/*.js')
) + ['js/hesitate.js', 'js/base.js', ) + ['js/hesitate.js', 'js/base.js',
'js/models/feedback.js', 'js/views/feedback.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', 'output_filename': 'js/cms-application.js',
'test_order': 0 '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", -> ...@@ -73,13 +73,3 @@ describe "CMS.Views.ModuleEdit", ->
expect(XModule.loadModule).toHaveBeenCalled() expect(XModule.loadModule).toHaveBeenCalled()
expect(XModule.loadModule.mostRecentCall.args[0]).toBe($('.xmodule_display')) 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 class CMS.Views.ModuleEdit extends Backbone.View
tagName: 'li' tagName: 'li'
className: 'component' className: 'component'
editorMode: 'editor-mode'
events: events:
"click .component-editor .cancel-button": 'clickCancelButton' "click .component-editor .cancel-button": 'clickCancelButton'
"click .component-editor .save-button": 'clickSaveButton' "click .component-editor .save-button": 'clickSaveButton'
"click .component-actions .edit-button": 'clickEditButton' "click .component-actions .edit-button": 'clickEditButton'
"click .component-actions .delete-button": 'onDelete' "click .component-actions .delete-button": 'onDelete'
"click .mode a": 'clickModeButton'
initialize: -> initialize: ->
@onDelete = @options.onDelete @onDelete = @options.onDelete
...@@ -20,29 +22,30 @@ class CMS.Views.ModuleEdit extends Backbone.View ...@@ -20,29 +22,30 @@ class CMS.Views.ModuleEdit extends Backbone.View
loadEdit: -> loadEdit: ->
if not @module if not @module
@module = XModule.loadModule(@$el.find('.xmodule_edit')) @module = XModule.loadModule(@$el.find('.xmodule_edit'))
@originalMetadata = @metadata() # At this point, metadata-edit.html will be loaded, and the metadata (as JSON) is available.
metadataEditor = @$el.find('.metadata_edit')
metadata: -> metadataData = metadataEditor.data('metadata')
# cdodge: package up metadata which is separated into a number of input fields models = [];
# there's probably a better way to do this, but at least this lets me continue to move onwards for key of metadataData
_metadata = {} models.push(metadataData[key])
@metadataEditor = new CMS.Views.Metadata.Editor({
$metadata = @$component_editor().find('.metadata_edit') el: metadataEditor,
collection: new CMS.Models.MetadataCollection(models)
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 # Need to update set "active" class on data editor if there is one.
_metadata[$(el).data("metadata-name")] = el.value for el in $('[data-metadata-name]', $metadata) # If we are only showing settings, hide the data editor controls and update settings accordingly.
if @hasDataEditor()
return _metadata @selectMode(@editorMode)
else
@hideDataEditor()
title = interpolate(gettext('<em>Editing:</em> %s'),
[@metadataEditor.getDisplayName()])
@$el.find('.component-name').html(title)
changedMetadata: -> changedMetadata: ->
currentMetadata = @metadata() return @metadataEditor.getModifiedMetadataValues()
changedMetadata = {}
for key of currentMetadata
if currentMetadata[key] != @originalMetadata[key]
changedMetadata[key] = currentMetadata[key]
return changedMetadata
cloneTemplate: (parent, template) -> cloneTemplate: (parent, template) ->
$.post("/clone_item", { $.post("/clone_item", {
...@@ -77,7 +80,7 @@ class CMS.Views.ModuleEdit extends Backbone.View ...@@ -77,7 +80,7 @@ class CMS.Views.ModuleEdit extends Backbone.View
@render() @render()
@$el.removeClass('editing') @$el.removeClass('editing')
).fail( -> ).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) -> clickCancelButton: (event) ->
...@@ -96,3 +99,38 @@ class CMS.Views.ModuleEdit extends Backbone.View ...@@ -96,3 +99,38 @@ class CMS.Views.ModuleEdit extends Backbone.View
$modalCover.show().addClass('is-fixed') $modalCover.show().addClass('is-fixed')
@$component_editor().slideDown(150) @$component_editor().slideDown(150)
@loadEdit() @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 { ...@@ -814,7 +814,7 @@ hr.divide {
line-height: 26px; line-height: 26px;
color: $white; color: $white;
pointer-events: none; pointer-events: none;
opacity: 0; opacity: 0.0;
&:after { &:after {
content: '▾'; content: '▾';
......
...@@ -149,11 +149,11 @@ abbr[title] { ...@@ -149,11 +149,11 @@ abbr[title] {
margin-left: 20px; margin-left: 20px;
} }
li { li {
opacity: .8; opacity: 0.8;
&:ui-state-active { &:ui-state-active {
background-color: rgba(255, 255, 255, .3); background-color: rgba(255, 255, 255, .3);
opacity: 1; opacity: 1.0;
font-weight: 400; font-weight: 400;
} }
a:focus { a:focus {
......
...@@ -95,12 +95,12 @@ ...@@ -95,12 +95,12 @@
// bounce in // bounce in
@mixin bounceIn { @mixin bounceIn {
0% { 0% {
opacity: 0; opacity: 0.0;
@include transform(scale(0.3)); @include transform(scale(0.3));
} }
50% { 50% {
opacity: 1; opacity: 1.0;
@include transform(scale(1.05)); @include transform(scale(1.05));
} }
...@@ -128,12 +128,12 @@ ...@@ -128,12 +128,12 @@
// bounce in // bounce in
@mixin bounceOut { @mixin bounceOut {
0% { 0% {
opacity: 0; opacity: 0.0;
@include transform(scale(0.3)); @include transform(scale(0.3));
} }
50% { 50% {
opacity: 1; opacity: 1.0;
@include transform(scale(1.05)); @include transform(scale(1.05));
} }
...@@ -146,12 +146,12 @@ ...@@ -146,12 +146,12 @@
} }
50% { 50% {
opacity: 1; opacity: 1.0;
@include transform(scale(1.05)); @include transform(scale(1.05));
} }
100% { 100% {
opacity: 0; opacity: 0.0;
@include transform(scale(0.3)); @include transform(scale(0.3));
} }
} }
......
...@@ -124,7 +124,6 @@ code { ...@@ -124,7 +124,6 @@ code {
.CodeMirror { .CodeMirror {
font-size: 13px; font-size: 13px;
border: 1px solid $darkGrey;
background: #fff; background: #fff;
} }
......
...@@ -243,7 +243,7 @@ ...@@ -243,7 +243,7 @@
left: -7px; left: -7px;
top: 47px; top: 47px;
width: 140px; width: 140px;
opacity: 0; opacity: 0.0;
pointer-events: none; pointer-events: none;
} }
...@@ -558,7 +558,7 @@ body.signin .nav-not-signedin-signup { ...@@ -558,7 +558,7 @@ body.signin .nav-not-signedin-signup {
.wrapper-nav-sub { .wrapper-nav-sub {
@include transition (opacity 1.0s ease-in-out 0s); @include transition (opacity 1.0s ease-in-out 0s);
opacity: 0; opacity: 0.0;
pointer-events: none; pointer-events: none;
&.is-shown { &.is-shown {
......
...@@ -627,7 +627,7 @@ ...@@ -627,7 +627,7 @@
pointer-events: none; pointer-events: none;
.prompt { .prompt {
opacity: 0; opacity: 0.0;
} }
} }
......
...@@ -254,7 +254,7 @@ body.course.checklists { ...@@ -254,7 +254,7 @@ body.course.checklists {
.task-support { .task-support {
@extend .t-copy-sub2; @extend .t-copy-sub2;
@include transition(opacity .15s .25s ease-in-out); @include transition(opacity .15s .25s ease-in-out);
opacity: 0; opacity: 0.0;
pointer-events: none; pointer-events: none;
} }
} }
...@@ -267,7 +267,7 @@ body.course.checklists { ...@@ -267,7 +267,7 @@ body.course.checklists {
float: right; float: right;
width: flex-grid(2,9); width: flex-grid(2,9);
margin: ($baseline/2) 0 0 flex-gutter(); margin: ($baseline/2) 0 0 flex-gutter();
opacity: 0; opacity: 0.0;
pointer-events: none; pointer-events: none;
text-align: right; text-align: right;
......
...@@ -59,7 +59,7 @@ body.dashboard { ...@@ -59,7 +59,7 @@ body.dashboard {
top: 15px; top: 15px;
right: $baseline; right: $baseline;
padding: ($baseline/4) ($baseline/2); padding: ($baseline/4) ($baseline/2);
opacity: 0; opacity: 0.0;
pointer-events: none; pointer-events: none;
&:hover { &:hover {
......
...@@ -162,7 +162,7 @@ body.index { ...@@ -162,7 +162,7 @@ body.index {
position: absolute; position: absolute;
bottom: -30px; bottom: -30px;
right: ($baseline/2); right: ($baseline/2);
opacity: 0; opacity: 0.0;
[class^="icon-"] { [class^="icon-"] {
@include font-size(18); @include font-size(18);
......
...@@ -21,7 +21,7 @@ body.course.settings { ...@@ -21,7 +21,7 @@ body.course.settings {
font-size: 14px; font-size: 14px;
} }
.message-status { .message-status {
display: none; display: none;
@include border-top-radius(2px); @include border-top-radius(2px);
@include box-sizing(border-box); @include box-sizing(border-box);
...@@ -386,6 +386,11 @@ body.course.settings { ...@@ -386,6 +386,11 @@ body.course.settings {
#course-overview { #course-overview {
height: ($baseline*20); 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 // specific fields - video
...@@ -698,7 +703,7 @@ body.course.settings { ...@@ -698,7 +703,7 @@ body.course.settings {
.tip { .tip {
@include transition (opacity 0.5s ease-in-out 0s); @include transition (opacity 0.5s ease-in-out 0s);
opacity: 0; opacity: 0.0;
position: absolute; position: absolute;
bottom: ($baseline*1.25); bottom: ($baseline*1.25);
} }
...@@ -713,7 +718,7 @@ body.course.settings { ...@@ -713,7 +718,7 @@ body.course.settings {
input.error { input.error {
& + .tip { & + .tip {
opacity: 0; opacity: 0.0;
} }
} }
} }
......
...@@ -41,38 +41,23 @@ body.course.static-pages { ...@@ -41,38 +41,23 @@ body.course.static-pages {
@include edit-box; @include edit-box;
@include box-shadow(none); @include box-shadow(none);
display: none; display: none;
padding: 20px; padding: 0;
border-radius: 2px 2px 0 0; border-radius: 2px 2px 0 0;
.metadata_edit { //Overrides general edit-box mixin
margin-bottom: 20px; .row {
font-size: 13px; margin-bottom: 0px;
li {
margin-bottom: 10px;
}
label {
display: inline-block;
margin-right: 10px;
}
} }
h3 { // This duplicates the styling from Unit page editing
margin-bottom: 10px; .module-actions {
font-size: 18px; @include box-shadow(inset 0 1px 1px $shadow);
font-weight: 700; padding: 0px 0 10px 10px;
} background-color: $gray-l6;
h5 { .save-button {
margin-bottom: 8px; margin: ($baseline/2) 8px 0 0;
color: #fff; }
font-weight: 700;
}
.save-button {
margin-top: 10px;
margin: 15px 8px 0 0;
} }
} }
} }
...@@ -215,3 +200,4 @@ body.course.static-pages { ...@@ -215,3 +200,4 @@ body.course.static-pages {
outline: 0; outline: 0;
} }
} }
...@@ -212,6 +212,7 @@ body.course.updates { ...@@ -212,6 +212,7 @@ body.course.updates {
@include edit-box; @include edit-box;
position: absolute; position: absolute;
right: 0; right: 0;
top: 0;
z-index: 10001; z-index: 10001;
width: 800px; width: 800px;
padding: 30px; 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="wrapper wrapper-component-editor">
<div class="component-editor"> <div class="component-editor">
<div class="module-editor"> <div class="component-edit-header">
${editor} <span class="component-name"></span>
</div> <ul class="nav-edit-modes">
<div class="row module-actions"> <li id="editor-mode" class="mode active-mode" aria-controls="editor-tab" role="tab">
<a href="#" class="save-button">Save</a> <a href="#">${_("Editor")}</a>
<a href="#" class="cancel-button">Cancel</a> </li>
</div> <li id="settings-mode" class="mode active-mode" aria-controls="settings-tab" role="tab">
</div> <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>
<div class="component-actions"> <div class="component-actions">
<a href="#" class="edit-button standard"><span class="edit-icon"></span>Edit</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> <a href="#" class="delete-button standard"><span class="delete-icon"></span>${_("Delete")}</a>
</div> </div>
<a data-tooltip="Drag to reorder" href="#" class="drag-handle"></a> <a data-tooltip='${_("Drag to reorder")}' href="#" class="drag-handle"></a>
${preview} ${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 @@ ...@@ -78,22 +78,13 @@
% endif % endif
<div class="tab current" id="tab1"> <div class="tab current" id="tab1">
<ul class="new-component-template"> <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 has_markdown or type != "problem":
% if is_empty: <li class="editor-md">
<li class="editor-md empty"> <a href="#" id="${location}" data-location="${location}">
<a href="#" data-location="${location}"> <span class="name"> ${name}</span>
<span class="name"> ${name}</span> </a>
</a> </li>
</li>
% else:
<li class="editor-md">
<a href="#" data-location="${location}">
<span class="name"> ${name}</span>
</a>
</li>
% endif
% endif % endif
%endfor %endfor
...@@ -102,23 +93,14 @@ ...@@ -102,23 +93,14 @@
% if type == "problem": % if type == "problem":
<div class="tab" id="tab2"> <div class="tab" id="tab2">
<ul class="new-component-template"> <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 not has_markdown:
% if is_empty: <li class="editor-manual">
<li class="editor-manual empty"> <a href="#" id="${location}" data-location="${location}">
<a href="#" data-location="${location}"> <span class="name"> ${name}</span>
<span class="name">${name}</span> </a>
</a>
</li>
% else:
<li class="editor-manual">
<a href="#" data-location="${location}">
<span class="name"> ${name}</span>
</a>
</li> </li>
% endif
% endif % endif
% endfor % endfor
</ul> </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" /> <%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 import hashlib
from xmodule.fields import StringyInteger, StringyFloat import copy
import json
hlskey = hashlib.md5(module.location.url()).hexdigest() hlskey = hashlib.md5(module.location.url()).hexdigest()
%> %>
<section class="metadata_edit">
<ul> ## js templates
% for field_name, field_value in editable_metadata_fields.items(): <script id="metadata-editor-tpl" type="text/template">
<li> <%static:include path="js/metadata-editor.underscore" />
% if field_name == 'source_code': </script>
% if field_value['explicitly_set'] is True:
<a href="#hls-modal-${hlskey}" style="color:yellow;" id="hls-trig-${hlskey}" >Edit High Level Source</a> <script id="metadata-number-entry" type="text/template">
% endif <%static:include path="js/metadata-number-entry.underscore" />
% else: </script>
<label>${field_value['field'].display_name}:</label>
<input type='text' data-metadata-name='${field_value["field"].display_name}' <script id="metadata-string-entry" type="text/template">
## This is a hack to keep current behavior for weight and attempts (empty will parse OK as unset). <%static:include path="js/metadata-string-entry.underscore" />
## This hack will go away with our custom editors. </script>
% if field_value["value"] == None and (isinstance(field_value["field"], StringyFloat) or isinstance(field_value["field"], StringyInteger)):
value = '' <script id="metadata-option-entry" type="text/template">
% else: <%static:include path="js/metadata-option-entry.underscore" />
value='${field_value["field"].to_json(field_value["value"])}' </script>
% endif
size='60' /> <% showHighLevelSource='source_code' in editable_metadata_fields and editable_metadata_fields['source_code']['explicitly_set'] %>
## Change to True to see all the information being passed through. <% metadata_field_copy = copy.copy(editable_metadata_fields) %>
% if False: ## Delete 'source_code' field (if it exists) so metadata editor view does not attempt to render it.
<label>Help: ${field_value['field'].help}</label> % if 'source_code' in editable_metadata_fields:
<label>Type: ${type(field_value['field']).__name__}</label> ## source-edit.html needs access to the 'source_code' value, so delete from a copy.
<label>Inheritable: ${field_value['inheritable']}</label> <% del metadata_field_copy['source_code'] %>
<label>Showing inherited value: ${field_value['inheritable'] and not field_value['explicitly_set']}</label> % endif
<label>Explicitly set: ${field_value['explicitly_set']}</label>
<label>Default value: ${field_value['default_value']}</label> % if showHighLevelSource:
% if field_value['field'].values: <div class="launch-latex-compiler">
<label>Possible values:</label> <a href="#hls-modal-${hlskey}" id="hls-trig-${hlskey}">${_("Launch Latex Source Compiler")}</a>
% for value in field_value['field'].values: </div>
<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']:
<%include file="source-edit.html" /> <%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"> <section class="combinedopenended-editor editor">
<div class="row"> <div class="row">
%if enable_markdown: %if enable_markdown:
...@@ -93,3 +93,5 @@ ...@@ -93,3 +93,5 @@
</div> </div>
</article> </article>
</script> </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"> <section class="problem-editor editor">
<div class="row"> <div class="row">
%if enable_markdown: %if enable_markdown:
<div class="editor-bar"> <div class="editor-bar">
<ul class="format-buttons"> <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> 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> 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> 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> 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> 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> 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> class="problem-editor-icon explanation"></span></a></li>
</ul> </ul>
<ul class="editor-tabs"> <ul class="editor-tabs">
<li><a href="#" class="xml-tab advanced-toggle" data-tab="xml">Advanced Editor</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> <li><a href="#" class="cheatsheet-toggle" data-tooltip='${_("Toggle Cheatsheet")}'>?</a></li>
</ul> </ul>
</div> </div>
<textarea class="markdown-box">${markdown | h}</textarea> <textarea class="markdown-box">${markdown | h}</textarea>
...@@ -34,7 +36,7 @@ ...@@ -34,7 +36,7 @@
<article class="simple-editor-cheatsheet"> <article class="simple-editor-cheatsheet">
<div class="cheatsheet-wrapper"> <div class="cheatsheet-wrapper">
<div class="row"> <div class="row">
<h6>Heading 1</h6> <h6>${_("Heading 1")}</h6>
<div class="col sample heading-1"> <div class="col sample heading-1">
<img src="/static/img/header-example.png" /> <img src="/static/img/header-example.png" />
</div> </div>
...@@ -45,7 +47,7 @@ ...@@ -45,7 +47,7 @@
</div> </div>
</div> </div>
<div class="row"> <div class="row">
<h6>Multiple Choice</h6> <h6>${_("Multiple Choice")}</h6>
<div class="col sample multiple-choice"> <div class="col sample multiple-choice">
<img src="/static/img/choice-example.png" /> <img src="/static/img/choice-example.png" />
</div> </div>
...@@ -56,7 +58,7 @@ ...@@ -56,7 +58,7 @@
</div> </div>
</div> </div>
<div class="row"> <div class="row">
<h6>Checkboxes</h6> <h6>${_("Checkboxes")}</h6>
<div class="col sample check-multiple"> <div class="col sample check-multiple">
<img src="/static/img/multi-example.png" /> <img src="/static/img/multi-example.png" />
</div> </div>
...@@ -67,7 +69,7 @@ ...@@ -67,7 +69,7 @@
</div> </div>
</div> </div>
<div class="row"> <div class="row">
<h6>Text Input</h6> <h6>${_("Text Input")}</h6>
<div class="col sample string-response"> <div class="col sample string-response">
<img src="/static/img/string-example.png" /> <img src="/static/img/string-example.png" />
</div> </div>
...@@ -76,7 +78,7 @@ ...@@ -76,7 +78,7 @@
</div> </div>
</div> </div>
<div class="row"> <div class="row">
<h6>Numerical Input</h6> <h6>${_("Numerical Input")}</h6>
<div class="col sample numerical-response"> <div class="col sample numerical-response">
<img src="/static/img/number-example.png" /> <img src="/static/img/number-example.png" />
</div> </div>
...@@ -85,7 +87,7 @@ ...@@ -85,7 +87,7 @@
</div> </div>
</div> </div>
<div class="row"> <div class="row">
<h6>Dropdown</h6> <h6>${_("Dropdown")}</h6>
<div class="col sample option-reponse"> <div class="col sample option-reponse">
<img src="/static/img/select-example.png" /> <img src="/static/img/select-example.png" />
</div> </div>
...@@ -94,7 +96,7 @@ ...@@ -94,7 +96,7 @@
</div> </div>
</div> </div>
<div class="row"> <div class="row">
<h6>Explanation</h6> <h6>${_("Explanation")}</h6>
<div class="col sample explanation"> <div class="col sample explanation">
<img src="/static/img/explanation-example.png" /> <img src="/static/img/explanation-example.png" />
</div> </div>
...@@ -105,3 +107,5 @@ ...@@ -105,3 +107,5 @@
</div> </div>
</article> </article>
</script> </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" /> <%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 @@ ...@@ -29,7 +29,6 @@
</ul> </ul>
</section> </section>
<%include file="metadata-edit.html" />
<div class="content"> <div class="content">
<section class="modules"> <section class="modules">
<ol> <ol>
...@@ -50,5 +49,6 @@ ...@@ -50,5 +49,6 @@
</ol> </ol>
</section> </section>
</div> </div>
<%include file="metadata-edit.html" />
</section> </section>
...@@ -28,4 +28,4 @@ class CmsNamespace(Namespace): ...@@ -28,4 +28,4 @@ class CmsNamespace(Namespace):
""" """
published_date = DateTuple(help="Date when the module was published", scope=Scope.settings) 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) 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): ...@@ -13,6 +13,7 @@ class UserFactory(sf.UserFactory):
""" """
User account for lms / cms User account for lms / cms
""" """
FACTORY_DJANGO_GET_OR_CREATE = ('username',)
pass pass
...@@ -21,6 +22,7 @@ class UserProfileFactory(sf.UserProfileFactory): ...@@ -21,6 +22,7 @@ class UserProfileFactory(sf.UserProfileFactory):
""" """
Demographics etc for the User Demographics etc for the User
""" """
FACTORY_DJANGO_GET_OR_CREATE = ('user',)
pass pass
...@@ -29,6 +31,7 @@ class RegistrationFactory(sf.RegistrationFactory): ...@@ -29,6 +31,7 @@ class RegistrationFactory(sf.RegistrationFactory):
""" """
Activation key for registering the user account Activation key for registering the user account
""" """
FACTORY_DJANGO_GET_OR_CREATE = ('user',)
pass pass
......
...@@ -4,7 +4,7 @@ ...@@ -4,7 +4,7 @@
from lettuce import world from lettuce import world
import time import time
from urllib import quote_plus 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.support import expected_conditions as EC
from selenium.webdriver.common.by import By from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support.ui import WebDriverWait
...@@ -63,7 +63,7 @@ def css_click(css_selector): ...@@ -63,7 +63,7 @@ def css_click(css_selector):
# Occassionally, MathJax or other JavaScript can cover up # Occassionally, MathJax or other JavaScript can cover up
# an element temporarily. # an element temporarily.
# If this happens, wait a second, then try again # If this happens, wait a second, then try again
time.sleep(1) world.wait(1)
world.browser.find_by_css(css_selector).click() world.browser.find_by_css(css_selector).click()
...@@ -80,6 +80,14 @@ def css_click_at(css, x=10, y=10): ...@@ -80,6 +80,14 @@ def css_click_at(css, x=10, y=10):
@world.absorb @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): def css_fill(css_selector, text):
world.browser.find_by_css(css_selector).first.fill(text) world.browser.find_by_css(css_selector).first.fill(text)
...@@ -94,7 +102,12 @@ def css_text(css_selector): ...@@ -94,7 +102,12 @@ def css_text(css_selector):
# Wait for the css selector to appear # Wait for the css selector to appear
if world.is_css_present(css_selector): 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: else:
return "" return ""
......
...@@ -66,22 +66,51 @@ class ComplexEncoder(json.JSONEncoder): ...@@ -66,22 +66,51 @@ class ComplexEncoder(json.JSONEncoder):
class CapaFields(object): class CapaFields(object):
attempts = StringyInteger(help="Number of attempts taken by the student on this problem", default=0, scope=Scope.user_state) 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) 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) 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", showanswer = String(
values=["answered", "always", "attempted", "closed", "never"]) 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) 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) 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={}) 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) 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) 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) 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) 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) 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): class CapaModule(CapaFields, XModule):
......
...@@ -5,7 +5,7 @@ from pkg_resources import resource_string ...@@ -5,7 +5,7 @@ from pkg_resources import resource_string
from xmodule.raw_module import RawDescriptor from xmodule.raw_module import RawDescriptor
from .x_module import XModule 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 xmodule.open_ended_grading_classes.combined_open_ended_modulev1 import CombinedOpenEndedV1Module, CombinedOpenEndedV1Descriptor
from collections import namedtuple from collections import namedtuple
from .fields import Date, StringyFloat, StringyInteger, StringyBoolean from .fields import Date, StringyFloat, StringyInteger, StringyBoolean
...@@ -48,27 +48,49 @@ class VersionInteger(Integer): ...@@ -48,27 +48,49 @@ class VersionInteger(Integer):
class CombinedOpenEndedFields(object): 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) 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) 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", state = String(help="Which step within the current task that the student is on.", default="initial",
scope=Scope.user_state) scope=Scope.user_state)
student_attempts = StringyInteger(help="Number of attempts taken by the student on this problem", default=0, student_attempts = StringyInteger(help="Number of attempts taken by the student on this problem", default=0,
scope=Scope.user_state) scope=Scope.user_state)
ready_to_reset = StringyBoolean(help="If the problem is ready to be reset or not.", default=False, ready_to_reset = StringyBoolean(
scope=Scope.user_state) help="If the problem is ready to be reset or not.", default=False,
attempts = StringyInteger(help="Maximum number of attempts that a student is allowed.", default=1, scope=Scope.settings) scope=Scope.user_state
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, attempts = StringyInteger(
scope=Scope.settings) display_name="Maximum Attempts",
skip_spelling_checks = StringyBoolean(help="Whether or not to skip initial spelling checks.", default=True, help="The number of times the student can try to answer this problem.", default=1,
scope=Scope.settings) 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) 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, graceperiod = String(
scope=Scope.settings) 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) version = VersionInteger(help="Current version number", default=DEFAULT_VERSION, scope=Scope.settings)
data = String(help="XML data for the problem", scope=Scope.content) 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) markdown = String(help="Markdown source of this module", scope=Scope.settings)
...@@ -244,6 +266,6 @@ class CombinedOpenEndedDescriptor(CombinedOpenEndedFields, RawDescriptor): ...@@ -244,6 +266,6 @@ class CombinedOpenEndedDescriptor(CombinedOpenEndedFields, RawDescriptor):
def non_editable_metadata_fields(self): def non_editable_metadata_fields(self):
non_editable_fields = super(CombinedOpenEndedDescriptor, self).non_editable_metadata_fields non_editable_fields = super(CombinedOpenEndedDescriptor, self).non_editable_metadata_fields
non_editable_fields.extend([CombinedOpenEndedDescriptor.due, CombinedOpenEndedDescriptor.graceperiod, non_editable_fields.extend([CombinedOpenEndedDescriptor.due, CombinedOpenEndedDescriptor.graceperiod,
CombinedOpenEndedDescriptor.markdown]) CombinedOpenEndedDescriptor.markdown, CombinedOpenEndedDescriptor.version])
return non_editable_fields return non_editable_fields
...@@ -10,8 +10,6 @@ ...@@ -10,8 +10,6 @@
position: relative; position: relative;
@include linear-gradient(top, #d4dee8, #c9d5e2); @include linear-gradient(top, #d4dee8, #c9d5e2);
padding: 5px; padding: 5px;
border: 1px solid #3c3c3c;
border-radius: 3px 3px 0 0;
border-bottom-color: #a5aaaf; border-bottom-color: #a5aaaf;
@include clearfix; @include clearfix;
......
...@@ -5,7 +5,7 @@ ...@@ -5,7 +5,7 @@
.advanced-toggle { .advanced-toggle {
@include white-button; @include white-button;
height: auto; height: auto;
margin-top: -1px; margin-top: -4px;
padding: 3px 9px; padding: 3px 9px;
font-size: 12px; font-size: 12px;
...@@ -16,7 +16,7 @@ ...@@ -16,7 +16,7 @@
color: $darkGrey !important; color: $darkGrey !important;
pointer-events: none; pointer-events: none;
cursor: none; cursor: none;
&:hover { &:hover {
box-shadow: 0 0 0 0 !important; box-shadow: 0 0 0 0 !important;
} }
...@@ -27,7 +27,7 @@ ...@@ -27,7 +27,7 @@
width: 21px; width: 21px;
height: 21px; height: 21px;
padding: 0; padding: 0;
margin: 0 5px 0 15px; margin: -1px 5px 0 15px;
border-radius: 22px; border-radius: 22px;
border: 1px solid #a5aaaf; border: 1px solid #a5aaaf;
background: #e5ecf3; background: #e5ecf3;
...@@ -99,6 +99,13 @@ ...@@ -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 { .problem-editor-icon {
display: inline-block; display: inline-block;
width: 26px; width: 26px;
......
...@@ -170,7 +170,7 @@ nav.sequence-nav { ...@@ -170,7 +170,7 @@ nav.sequence-nav {
font-family: $sans-serif; font-family: $sans-serif;
line-height: lh(); line-height: lh();
left: 0px; left: 0px;
opacity: 0; opacity: 0.0;
padding: 6px; padding: 6px;
position: absolute; position: absolute;
top: 48px; top: 48px;
...@@ -204,7 +204,7 @@ nav.sequence-nav { ...@@ -204,7 +204,7 @@ nav.sequence-nav {
p { p {
display: block; display: block;
margin-top: 4px; margin-top: 4px;
opacity: 1; opacity: 1.0;
} }
} }
} }
...@@ -248,12 +248,12 @@ nav.sequence-nav { ...@@ -248,12 +248,12 @@ nav.sequence-nav {
} }
&:hover { &:hover {
opacity: .5; opacity: 0.5;
} }
&.disabled { &.disabled {
cursor: normal; cursor: normal;
opacity: .4; opacity: 0.4;
} }
} }
} }
...@@ -320,12 +320,12 @@ nav.sequence-bottom { ...@@ -320,12 +320,12 @@ nav.sequence-bottom {
outline: 0; outline: 0;
&:hover { &:hover {
opacity: .5; opacity: 0.5;
background-position: center 15px; background-position: center 15px;
} }
&.disabled { &.disabled {
opacity: .4; opacity: 0.4;
} }
&:focus { &:focus {
......
...@@ -41,7 +41,7 @@ div.video { ...@@ -41,7 +41,7 @@ div.video {
&:hover { &:hover {
ul, div { ul, div {
opacity: 1; opacity: 1.0;
} }
} }
...@@ -158,7 +158,7 @@ div.video { ...@@ -158,7 +158,7 @@ div.video {
ol.video_speeds { ol.video_speeds {
display: block; display: block;
opacity: 1; opacity: 1.0;
padding: 0; padding: 0;
margin: 0; margin: 0;
list-style: none; list-style: none;
...@@ -208,7 +208,7 @@ div.video { ...@@ -208,7 +208,7 @@ div.video {
} }
&:hover, &:active, &:focus { &:hover, &:active, &:focus {
opacity: 1; opacity: 1.0;
background-color: #444; background-color: #444;
} }
} }
...@@ -221,7 +221,7 @@ div.video { ...@@ -221,7 +221,7 @@ div.video {
border: 1px solid #000; border: 1px solid #000;
bottom: 46px; bottom: 46px;
display: none; display: none;
opacity: 0; opacity: 0.0;
position: absolute; position: absolute;
width: 133px; width: 133px;
z-index: 10; z-index: 10;
...@@ -264,7 +264,7 @@ div.video { ...@@ -264,7 +264,7 @@ div.video {
&.open { &.open {
.volume-slider-container { .volume-slider-container {
display: block; display: block;
opacity: 1; opacity: 1.0;
} }
} }
...@@ -302,7 +302,7 @@ div.video { ...@@ -302,7 +302,7 @@ div.video {
border: 1px solid #000; border: 1px solid #000;
bottom: 46px; bottom: 46px;
display: none; display: none;
opacity: 0; opacity: 0.0;
position: absolute; position: absolute;
width: 45px; width: 45px;
height: 125px; height: 125px;
...@@ -395,7 +395,7 @@ div.video { ...@@ -395,7 +395,7 @@ div.video {
font-weight: 800; font-weight: 800;
line-height: 46px; //height of play pause buttons line-height: 46px; //height of play pause buttons
margin-left: 0; margin-left: 0;
opacity: 1; opacity: 1.0;
padding: 0 lh(.5); padding: 0 lh(.5);
position: relative; position: relative;
text-indent: -9999px; text-indent: -9999px;
...@@ -410,7 +410,7 @@ div.video { ...@@ -410,7 +410,7 @@ div.video {
} }
&.off { &.off {
opacity: .7; opacity: 0.7;
} }
} }
} }
...@@ -418,7 +418,7 @@ div.video { ...@@ -418,7 +418,7 @@ div.video {
&:hover section.video-controls { &:hover section.video-controls {
ul, div { ul, div {
opacity: 1; opacity: 1.0;
} }
div.slider { div.slider {
......
...@@ -41,7 +41,7 @@ div.video { ...@@ -41,7 +41,7 @@ div.video {
&:hover { &:hover {
ul, div { ul, div {
opacity: 1; opacity: 1.0;
} }
} }
...@@ -158,7 +158,7 @@ div.video { ...@@ -158,7 +158,7 @@ div.video {
ol.video_speeds { ol.video_speeds {
display: block; display: block;
opacity: 1; opacity: 1.0;
padding: 0; padding: 0;
margin: 0; margin: 0;
list-style: none; list-style: none;
...@@ -208,7 +208,7 @@ div.video { ...@@ -208,7 +208,7 @@ div.video {
} }
&:hover, &:active, &:focus { &:hover, &:active, &:focus {
opacity: 1; opacity: 1.0;
background-color: #444; background-color: #444;
} }
} }
...@@ -221,7 +221,7 @@ div.video { ...@@ -221,7 +221,7 @@ div.video {
border: 1px solid #000; border: 1px solid #000;
bottom: 46px; bottom: 46px;
display: none; display: none;
opacity: 0; opacity: 0.0;
position: absolute; position: absolute;
width: 133px; width: 133px;
z-index: 10; z-index: 10;
...@@ -264,7 +264,7 @@ div.video { ...@@ -264,7 +264,7 @@ div.video {
&.open { &.open {
.volume-slider-container { .volume-slider-container {
display: block; display: block;
opacity: 1; opacity: 1.0;
} }
} }
...@@ -302,7 +302,7 @@ div.video { ...@@ -302,7 +302,7 @@ div.video {
border: 1px solid #000; border: 1px solid #000;
bottom: 46px; bottom: 46px;
display: none; display: none;
opacity: 0; opacity: 0.0;
position: absolute; position: absolute;
width: 45px; width: 45px;
height: 125px; height: 125px;
...@@ -395,7 +395,7 @@ div.video { ...@@ -395,7 +395,7 @@ div.video {
font-weight: 800; font-weight: 800;
line-height: 46px; //height of play pause buttons line-height: 46px; //height of play pause buttons
margin-left: 0; margin-left: 0;
opacity: 1; opacity: 1.0;
padding: 0 lh(.5); padding: 0 lh(.5);
position: relative; position: relative;
text-indent: -9999px; text-indent: -9999px;
...@@ -410,7 +410,7 @@ div.video { ...@@ -410,7 +410,7 @@ div.video {
} }
&.off { &.off {
opacity: .7; opacity: 0.7;
} }
} }
} }
...@@ -418,7 +418,7 @@ div.video { ...@@ -418,7 +418,7 @@ div.video {
&:hover section.video-controls { &:hover section.video-controls {
ul, div { ul, div {
opacity: 1; opacity: 1.0;
} }
div.slider { div.slider {
......
...@@ -8,8 +8,16 @@ from xblock.core import String, Scope ...@@ -8,8 +8,16 @@ from xblock.core import String, Scope
class DiscussionFields(object): class DiscussionFields(object):
discussion_id = String(scope=Scope.settings) discussion_id = String(scope=Scope.settings)
discussion_category = String(scope=Scope.settings) discussion_category = String(
discussion_target = String(scope=Scope.settings) 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) sort_key = String(scope=Scope.settings)
......
...@@ -10,7 +10,7 @@ from .x_module import XModule ...@@ -10,7 +10,7 @@ from .x_module import XModule
from xmodule.raw_module import RawDescriptor from xmodule.raw_module import RawDescriptor
from xmodule.modulestore.django import modulestore from xmodule.modulestore.django import modulestore
from .timeinfo import TimeInfo 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.fields import Date, StringyFloat, StringyInteger, StringyBoolean
from xmodule.open_ended_grading_classes.peer_grading_service import PeerGradingService, GradingServiceError, MockPeerGradingService from xmodule.open_ended_grading_classes.peer_grading_service import PeerGradingService, GradingServiceError, MockPeerGradingService
...@@ -22,24 +22,43 @@ USE_FOR_SINGLE_LOCATION = False ...@@ -22,24 +22,43 @@ USE_FOR_SINGLE_LOCATION = False
LINK_TO_LOCATION = "" LINK_TO_LOCATION = ""
TRUE_DICT = [True, "True", "true", "TRUE"] TRUE_DICT = [True, "True", "true", "TRUE"]
MAX_SCORE = 1 MAX_SCORE = 1
IS_GRADED = True IS_GRADED = False
EXTERNAL_GRADER_NO_CONTACT_ERROR = "Failed to contact external graders. Please notify course staff." EXTERNAL_GRADER_NO_CONTACT_ERROR = "Failed to contact external graders. Please notify course staff."
class PeerGradingFields(object): class PeerGradingFields(object):
use_for_single_location = StringyBoolean(help="Whether to use this for a single location or as a panel.", use_for_single_location = StringyBoolean(
default=USE_FOR_SINGLE_LOCATION, scope=Scope.settings) display_name="Show Single Problem",
link_to_location = String(help="The location this problem is linked to.", default=LINK_TO_LOCATION, help='When True, only the single problem specified by "Link to Problem Location" is shown. '
scope=Scope.settings) 'When False, a panel is displayed with all problems available for peer grading.',
is_graded = StringyBoolean(help="Whether or not this module is scored.", default=IS_GRADED, scope=Scope.settings) 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) 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) 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, max_grade = StringyInteger(
scope=Scope.settings) help="The maximum grade that a student can receive for this problem.", default=MAX_SCORE,
student_data_for_location = Object(help="Student data for a given peer grading problem.", scope=Scope.settings, values={"min": 0}
scope=Scope.user_state) )
weight = StringyFloat(help="How much to weight this problem by", scope=Scope.settings) 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): class PeerGradingModule(PeerGradingFields, XModule):
...@@ -590,3 +609,11 @@ class PeerGradingDescriptor(PeerGradingFields, RawDescriptor): ...@@ -590,3 +609,11 @@ class PeerGradingDescriptor(PeerGradingFields, RawDescriptor):
#Specify whether or not to pass in open ended interface #Specify whether or not to pass in open ended interface
needs_open_ended_interface = True 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: metadata:
display_name: Open Ended Response display_name: Open Ended Response
attempts: 1
is_graded: False
version: 1
skip_spelling_checks: False
accept_file_upload: False
weight: ""
markdown: "" markdown: ""
data: | data: |
<combinedopenended> <combinedopenended>
......
--- ---
metadata: metadata:
display_name: Blank HTML Page display_name: Blank HTML Page
empty: True
data: | data: |
......
--- ---
metadata: metadata:
display_name: Peer Grading Interface display_name: Peer Grading Interface
use_for_single_location: False
link_to_location: None
is_graded: False
max_grade: 1 max_grade: 1
weight: ""
data: | data: |
<peergrading> <peergrading>
</peergrading> </peergrading>
......
...@@ -3,9 +3,7 @@ ...@@ -3,9 +3,7 @@
metadata: metadata:
display_name: Circuit Schematic Builder display_name: Circuit Schematic Builder
rerandomize: never rerandomize: never
showanswer: always showanswer: finished
weight: ""
attempts: ""
data: | data: |
<problem > <problem >
Please make a voltage divider that splits the provided voltage evenly. Please make a voltage divider that splits the provided voltage evenly.
......
...@@ -2,9 +2,7 @@ ...@@ -2,9 +2,7 @@
metadata: metadata:
display_name: Custom Python-Evaluated Input display_name: Custom Python-Evaluated Input
rerandomize: never rerandomize: never
showanswer: always showanswer: finished
weight: ""
attempts: ""
data: | data: |
<problem> <problem>
<p> <p>
......
...@@ -2,11 +2,8 @@ ...@@ -2,11 +2,8 @@
metadata: metadata:
display_name: Blank Common Problem display_name: Blank Common Problem
rerandomize: never rerandomize: never
showanswer: always showanswer: finished
markdown: "" markdown: ""
weight: ""
empty: True
attempts: ""
data: | data: |
<problem> <problem>
</problem> </problem>
......
...@@ -2,10 +2,7 @@ ...@@ -2,10 +2,7 @@
metadata: metadata:
display_name: Blank Advanced Problem display_name: Blank Advanced Problem
rerandomize: never rerandomize: never
showanswer: always showanswer: finished
weight: ""
attempts: ""
empty: True
data: | data: |
<problem> <problem>
</problem> </problem>
......
...@@ -2,9 +2,7 @@ ...@@ -2,9 +2,7 @@
metadata: metadata:
display_name: Math Expression Input display_name: Math Expression Input
rerandomize: never rerandomize: never
showanswer: always showanswer: finished
weight: ""
attempts: ""
data: | data: |
<problem> <problem>
<p> <p>
......
...@@ -2,9 +2,7 @@ ...@@ -2,9 +2,7 @@
metadata: metadata:
display_name: Image Mapped Input display_name: Image Mapped Input
rerandomize: never rerandomize: never
showanswer: always showanswer: finished
weight: ""
attempts: ""
data: | data: |
<problem> <problem>
<p> <p>
......
...@@ -2,9 +2,7 @@ ...@@ -2,9 +2,7 @@
metadata: metadata:
display_name: Multiple Choice display_name: Multiple Choice
rerandomize: never rerandomize: never
showanswer: always showanswer: finished
weight: ""
attempts: ""
markdown: markdown:
"A multiple choice problem presents radio buttons for student input. Students can only select a single "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 option presented. Multiple Choice questions have been the subject of many areas of research due to the early
......
...@@ -2,9 +2,7 @@ ...@@ -2,9 +2,7 @@
metadata: metadata:
display_name: Numerical Input display_name: Numerical Input
rerandomize: never rerandomize: never
showanswer: always showanswer: finished
weight: ""
attempts: ""
markdown: markdown:
"A numerical input problem accepts a line of text input from the "A numerical input problem accepts a line of text input from the
student, and evaluates the input for correctness based on its student, and evaluates the input for correctness based on its
......
...@@ -2,9 +2,7 @@ ...@@ -2,9 +2,7 @@
metadata: metadata:
display_name: Dropdown display_name: Dropdown
rerandomize: never rerandomize: never
showanswer: always showanswer: finished
weight: ""
attempts: ""
markdown: markdown:
"Dropdown problems give a limited set of options for students to respond with, and present those options "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 in a format that encourages them to search for a specific answer rather than being immediately presented
......
...@@ -2,9 +2,7 @@ ...@@ -2,9 +2,7 @@
metadata: metadata:
display_name: Text Input display_name: Text Input
rerandomize: never rerandomize: never
showanswer: always showanswer: finished
weight: ""
attempts: ""
# Note, the extra newlines are needed to make the yaml parser add blank lines instead of folding # Note, the extra newlines are needed to make the yaml parser add blank lines instead of folding
markdown: markdown:
"A text input problem accepts a line of text from the "A text input problem accepts a line of text from the
......
--- ---
metadata: metadata:
display_name: Word cloud display_name: Word cloud
version: 1
num_inputs: 5
num_top_words: 250
display_student_percents: True
data: {} data: {}
children: [] children: []
...@@ -460,8 +460,8 @@ class ImportTestCase(BaseCourseTestCase): ...@@ -460,8 +460,8 @@ class ImportTestCase(BaseCourseTestCase):
) )
module = modulestore.get_instance(course.id, location) module = modulestore.get_instance(course.id, location)
self.assertEqual(len(module.get_children()), 0) self.assertEqual(len(module.get_children()), 0)
self.assertEqual(module.num_inputs, '5') self.assertEqual(module.num_inputs, 5)
self.assertEqual(module.num_top_words, '250') self.assertEqual(module.num_top_words, 250)
def test_cohort_config(self): def test_cohort_config(self):
""" """
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
generate and view word cloud. generate and view word cloud.
On the client side we show: 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. If student have answered - words he entered and cloud.
""" """
...@@ -14,7 +14,8 @@ from xmodule.raw_module import RawDescriptor ...@@ -14,7 +14,8 @@ from xmodule.raw_module import RawDescriptor
from xmodule.editing_module import MetadataOnlyEditingDescriptor from xmodule.editing_module import MetadataOnlyEditingDescriptor
from xmodule.x_module import XModule 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__) log = logging.getLogger(__name__)
...@@ -31,22 +32,23 @@ def pretty_bool(value): ...@@ -31,22 +32,23 @@ def pretty_bool(value):
class WordCloudFields(object): class WordCloudFields(object):
"""XFields for word cloud.""" """XFields for word cloud."""
display_name = String( num_inputs = StringyInteger(
help="Display name for this module", display_name="Inputs",
scope=Scope.settings help="Number of text boxes available for students to input words/sentences.",
)
num_inputs = Integer(
help="Number of inputs.",
scope=Scope.settings, scope=Scope.settings,
default=5 default=5,
values={"min": 1}
) )
num_top_words = Integer( num_top_words = StringyInteger(
help="Number of max words, which will be displayed.", display_name="Maximum Words",
help="Maximum number of words to be displayed in generated word cloud.",
scope=Scope.settings, scope=Scope.settings,
default=250 default=250,
values={"min": 1}
) )
display_student_percents = Boolean( display_student_percents = StringyBoolean(
help="Display usage percents for each word?", display_name="Show Percents",
help="Statistics are shown for entered words near that word.",
scope=Scope.settings, scope=Scope.settings,
default=True default=True
) )
...@@ -205,7 +207,7 @@ class WordCloudModule(WordCloudFields, XModule): ...@@ -205,7 +207,7 @@ class WordCloudModule(WordCloudFields, XModule):
# Update top_words. # Update top_words.
self.top_words = self.top_dict( self.top_words = self.top_dict(
temp_all_words, temp_all_words,
int(self.num_top_words) self.num_top_words
) )
# Save all_words in database. # Save all_words in database.
...@@ -226,7 +228,7 @@ class WordCloudModule(WordCloudFields, XModule): ...@@ -226,7 +228,7 @@ class WordCloudModule(WordCloudFields, XModule):
'element_id': self.location.html_id(), 'element_id': self.location.html_id(),
'element_class': self.location.category, 'element_class': self.location.category,
'ajax_url': self.system.ajax_url, 'ajax_url': self.system.ajax_url,
'num_inputs': int(self.num_inputs), 'num_inputs': self.num_inputs,
'submitted': self.submitted 'submitted': self.submitted
} }
self.content = self.system.render_template('word_cloud.html', context) self.content = self.system.render_template('word_cloud.html', context)
......
import logging import logging
import copy
import yaml import yaml
import os import os
...@@ -9,7 +10,7 @@ from pkg_resources import resource_listdir, resource_string, resource_isdir ...@@ -9,7 +10,7 @@ from pkg_resources import resource_listdir, resource_string, resource_isdir
from xmodule.modulestore import Location from xmodule.modulestore import Location
from xmodule.modulestore.exceptions import ItemNotFoundError 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__) log = logging.getLogger(__name__)
...@@ -75,12 +76,13 @@ class HTMLSnippet(object): ...@@ -75,12 +76,13 @@ class HTMLSnippet(object):
""" """
raise NotImplementedError( raise NotImplementedError(
"get_html() must be provided by specific modules - not present in {0}" "get_html() must be provided by specific modules - not present in {0}"
.format(self.__class__)) .format(self.__class__))
class XModuleFields(object): class XModuleFields(object):
display_name = String( 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, scope=Scope.settings,
default=None default=None
) )
...@@ -356,7 +358,7 @@ class XModuleDescriptor(XModuleFields, HTMLSnippet, ResourceTemplates, XBlock): ...@@ -356,7 +358,7 @@ class XModuleDescriptor(XModuleFields, HTMLSnippet, ResourceTemplates, XBlock):
metadata_translations = { metadata_translations = {
'slug': 'url_name', 'slug': 'url_name',
'name': 'display_name', 'name': 'display_name',
} }
# ============================= STRUCTURAL MANIPULATION =================== # ============================= STRUCTURAL MANIPULATION ===================
def __init__(self, def __init__(self,
...@@ -458,7 +460,6 @@ class XModuleDescriptor(XModuleFields, HTMLSnippet, ResourceTemplates, XBlock): ...@@ -458,7 +460,6 @@ class XModuleDescriptor(XModuleFields, HTMLSnippet, ResourceTemplates, XBlock):
""" """
return False return False
# ================================= JSON PARSING =========================== # ================================= JSON PARSING ===========================
@staticmethod @staticmethod
def load_from_json(json_data, system, default_class=None): def load_from_json(json_data, system, default_class=None):
...@@ -523,10 +524,10 @@ class XModuleDescriptor(XModuleFields, HTMLSnippet, ResourceTemplates, XBlock): ...@@ -523,10 +524,10 @@ class XModuleDescriptor(XModuleFields, HTMLSnippet, ResourceTemplates, XBlock):
# ================================= XML PARSING ============================ # ================================= XML PARSING ============================
@staticmethod @staticmethod
def load_from_xml(xml_data, def load_from_xml(xml_data,
system, system,
org=None, org=None,
course=None, course=None,
default_class=None): default_class=None):
""" """
This method instantiates the correct subclass of XModuleDescriptor based This method instantiates the correct subclass of XModuleDescriptor based
on the contents of xml_data. on the contents of xml_data.
...@@ -541,7 +542,7 @@ class XModuleDescriptor(XModuleFields, HTMLSnippet, ResourceTemplates, XBlock): ...@@ -541,7 +542,7 @@ class XModuleDescriptor(XModuleFields, HTMLSnippet, ResourceTemplates, XBlock):
class_ = XModuleDescriptor.load_class( class_ = XModuleDescriptor.load_class(
etree.fromstring(xml_data).tag, etree.fromstring(xml_data).tag,
default_class default_class
) )
# leave next line, commented out - useful for low-level debugging # leave next line, commented out - useful for low-level debugging
# log.debug('[XModuleDescriptor.load_from_xml] tag=%s, class_=%s' % ( # log.debug('[XModuleDescriptor.load_from_xml] tag=%s, class_=%s' % (
# etree.fromstring(xml_data).tag,class_)) # etree.fromstring(xml_data).tag,class_))
...@@ -625,7 +626,7 @@ class XModuleDescriptor(XModuleFields, HTMLSnippet, ResourceTemplates, XBlock): ...@@ -625,7 +626,7 @@ class XModuleDescriptor(XModuleFields, HTMLSnippet, ResourceTemplates, XBlock):
""" """
inherited_metadata = getattr(self, '_inherited_metadata', {}) inherited_metadata = getattr(self, '_inherited_metadata', {})
inheritable_metadata = getattr(self, '_inheritable_metadata', {}) inheritable_metadata = getattr(self, '_inheritable_metadata', {})
metadata = {} metadata_fields = {}
for field in self.fields: for field in self.fields:
if field.scope != Scope.settings or field in self.non_editable_metadata_fields: if field.scope != Scope.settings or field in self.non_editable_metadata_fields:
...@@ -641,13 +642,39 @@ class XModuleDescriptor(XModuleFields, HTMLSnippet, ResourceTemplates, XBlock): ...@@ -641,13 +642,39 @@ class XModuleDescriptor(XModuleFields, HTMLSnippet, ResourceTemplates, XBlock):
if field.name in inherited_metadata: if field.name in inherited_metadata:
explicitly_set = False explicitly_set = False
metadata[field.name] = {'field': field, # We support the following editors:
'value': value, # 1. A select editor for fields with a list of possible values (includes Booleans).
'default_value': default_value, # 2. Number editors for integers and floats.
'inheritable': inheritable, # 3. A generic string editor for anything else (editing JSON representation of the value).
'explicitly_set': explicitly_set } type = "Generic"
values = [] if field.values is None else copy.deepcopy(field.values)
return metadata 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): class DescriptorSystem(object):
...@@ -740,7 +767,7 @@ class ModuleSystem(object): ...@@ -740,7 +767,7 @@ class ModuleSystem(object):
s3_interface=None, s3_interface=None,
cache=None, cache=None,
can_execute_unsafe_code=None, can_execute_unsafe_code=None,
): ):
''' '''
Create a closure around the system environment. 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 @@ ...@@ -25,8 +25,8 @@
/* Layout */ /* Layout */
.studioSkin table.mceLayout {border:0;} .studioSkin table.mceLayout {border:0;}
.studioSkin table.mceLayout tr.mceFirst td {border-top: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:1px solid #3c3c3c;} .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 table.mceToolbar, .studioSkin tr.mceFirst .mceToolbar tr td, .studioSkin tr.mceLast .mceToolbar tr td {border:0; margin:0; padding:0;}
.studioSkin td.mceToolbar { .studioSkin td.mceToolbar {
background: -webkit-linear-gradient(top, #d4dee8, #c9d5e2); background: -webkit-linear-gradient(top, #d4dee8, #c9d5e2);
...@@ -36,11 +36,11 @@ ...@@ -36,11 +36,11 @@
background: linear-gradient(top, #d4dee8, #c9d5e2); background: linear-gradient(top, #d4dee8, #c9d5e2);
border: 1px solid #3c3c3c; border: 1px solid #3c3c3c;
border-bottom-color: #a5aaaf; border-bottom-color: #a5aaaf;
border-radius: 3px 3px 0 0; border-radius: 0;
padding: 10px 10px 9px; padding: 10px 10px 9px;
vertical-align: top; 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 {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 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} .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 @@ ...@@ -85,8 +85,8 @@
} }
@-webkit-keyframes fadeIn { @-webkit-keyframes fadeIn {
0% { opacity: 0; } 0% { opacity: 0.0; }
100% { opacity: 1; } 100% { opacity: 1.0; }
} }
...@@ -736,11 +736,11 @@ body.discussion { ...@@ -736,11 +736,11 @@ body.discussion {
&.is-open { &.is-open {
.browse-topic-drop-btn span { .browse-topic-drop-btn span {
opacity: 1; opacity: 1.0;
} }
.browse-topic-drop-icon { .browse-topic-drop-icon {
opacity: 0; opacity: 0.0;
} }
&.is-dropped { &.is-dropped {
...@@ -788,7 +788,7 @@ body.discussion { ...@@ -788,7 +788,7 @@ body.discussion {
&::-webkit-input-placeholder, &::-webkit-input-placeholder,
&:-moz-placeholder, &:-moz-placeholder,
&:-ms-input-placeholder { &:-ms-input-placeholder {
opacity: 1; opacity: 1.0;
} }
} }
} }
...@@ -818,7 +818,7 @@ body.discussion { ...@@ -818,7 +818,7 @@ body.discussion {
line-height: 58px; line-height: 58px;
color: #333; color: #333;
text-shadow: 0 1px 0 rgba(255, 255, 255, .8); text-shadow: 0 1px 0 rgba(255, 255, 255, .8);
opacity: 0; opacity: 0.0;
@include transition(opacity .2s); @include transition(opacity .2s);
} }
} }
...@@ -833,7 +833,7 @@ body.discussion { ...@@ -833,7 +833,7 @@ body.discussion {
height: 16px; height: 16px;
margin-left: -12px; margin-left: -12px;
background: url(../images/browse-icon.png) no-repeat; background: url(../images/browse-icon.png) no-repeat;
opacity: 1; opacity: 1.0;
@include transition(none); @include transition(none);
} }
...@@ -967,7 +967,7 @@ body.discussion { ...@@ -967,7 +967,7 @@ body.discussion {
&::-webkit-input-placeholder, &::-webkit-input-placeholder,
&:-moz-placeholder, &:-moz-placeholder,
&:-ms-input-placeholder { &:-ms-input-placeholder {
opacity: 0; opacity: 0.0;
@include transition(opacity .2s); @include transition(opacity .2s);
} }
...@@ -2454,7 +2454,7 @@ body.discussion { ...@@ -2454,7 +2454,7 @@ body.discussion {
font-style: italic; font-style: italic;
cursor:pointer; cursor:pointer;
margin-right: 10px; margin-right: 10px;
opacity:.8; opacity: 0.8;
span { span {
cursor: pointer; cursor: pointer;
...@@ -2462,7 +2462,7 @@ body.discussion { ...@@ -2462,7 +2462,7 @@ body.discussion {
&:hover { &:hover {
@include transition(opacity .2s); @include transition(opacity .2s);
opacity: 1; opacity: 1.0;
} }
} }
...@@ -2475,7 +2475,7 @@ body.discussion { ...@@ -2475,7 +2475,7 @@ body.discussion {
top:-13px; top:-13px;
margin-right:35px; margin-right:35px;
margin-top:13px; margin-top:13px;
opacity: 1; opacity: 1.0;
} }
.notpinned .icon { .notpinned .icon {
...@@ -2523,11 +2523,11 @@ display:none; ...@@ -2523,11 +2523,11 @@ display:none;
padding-right: 5px; padding-right: 5px;
font-style: italic; font-style: italic;
cursor:pointer; cursor:pointer;
opacity:.8; opacity: 0.8;
&:hover { &:hover {
@include transition(opacity .2s); @include transition(opacity .2s);
opacity: 1; opacity: 1.0;
} }
} }
......
...@@ -9,12 +9,12 @@ ...@@ -9,12 +9,12 @@
@mixin home-header-pop-up-keyframes { @mixin home-header-pop-up-keyframes {
0% { 0% {
opacity: 0; opacity: 0.0;
top: 300px; top: 300px;
//@include transform(scale(0.9)); //@include transform(scale(0.9));
} }
45% { 45% {
opacity: 1; opacity: 1.0;
} }
65% { 65% {
top: -40px; top: -40px;
...@@ -43,19 +43,19 @@ ...@@ -43,19 +43,19 @@
@mixin title-appear-keyframes { @mixin title-appear-keyframes {
0% { 0% {
opacity: 0; opacity: 0.0;
top: 60px; top: 60px;
@include transform(scale(0.9)); @include transform(scale(0.9));
} }
20% { 20% {
opacity: 1; opacity: 1.0;
} }
27% { // this % of total-time should be ~ 1.25s 27% { // this % of total-time should be ~ 1.25s
top: 40px; top: 40px;
@include transform(scale(1)); @include transform(scale(1));
} }
90% { // this % of total-time is when 2nd half of animation starts 90% { // this % of total-time is when 2nd half of animation starts
opacity: 1; opacity: 1.0;
top: 40px; top: 40px;
@include transform(scale(1)); @include transform(scale(1));
} }
...@@ -79,24 +79,24 @@ ...@@ -79,24 +79,24 @@
@mixin home-appear-keyframes { @mixin home-appear-keyframes {
0% { 0% {
opacity: 0; opacity: 0.0;
top: 60px; top: 60px;
@include transform(scale(0.9)); @include transform(scale(0.9));
} }
20% { 20% {
opacity: 1; opacity: 1.0;
} }
30% { // this % of total-time should be ~ 1.25s 30% { // this % of total-time should be ~ 1.25s
top: 40px; top: 40px;
@include transform(scale(1)); @include transform(scale(1));
} }
80% { // this % of total-time is when 2nd half of animation starts 80% { // this % of total-time is when 2nd half of animation starts
opacity: 1; opacity: 1.0;
top: 40px; top: 40px;
@include transform(scale(1)); @include transform(scale(1));
} }
100% { 100% {
opacity: 0; opacity: 0.0;
top: 60px; top: 60px;
@include transform(scale(0.7)); @include transform(scale(0.7));
} }
...@@ -117,10 +117,10 @@ ...@@ -117,10 +117,10 @@
@mixin edx-appear-keyframes { @mixin edx-appear-keyframes {
0% { 0% {
opacity: 0; opacity: 0.0;
} }
100% { 100% {
opacity: 1; opacity: 1.0;
} }
} }
...@@ -231,7 +231,7 @@ ...@@ -231,7 +231,7 @@
opacity: 0.9; opacity: 0.9;
} }
80% { 80% {
opacity: 1; opacity: 1.0;
} }
100% { 100% {
bottom: 0px; bottom: 0px;
......
...@@ -38,7 +38,7 @@ div.book-wrapper { ...@@ -38,7 +38,7 @@ div.book-wrapper {
line-height: 2.1em; line-height: 2.1em;
text-align: right; text-align: right;
color: #9a9a9a; color: #9a9a9a;
opacity: 0; opacity: 0.0;
@include transition(opacity .15s); @include transition(opacity .15s);
} }
...@@ -55,7 +55,7 @@ div.book-wrapper { ...@@ -55,7 +55,7 @@ div.book-wrapper {
background-color: transparent; background-color: transparent;
.page-number { .page-number {
opacity: 1; opacity: 1.0;
} }
} }
} }
...@@ -119,7 +119,7 @@ div.book-wrapper { ...@@ -119,7 +119,7 @@ div.book-wrapper {
@include box-sizing(border-box); @include box-sizing(border-box);
display: table; display: table;
height: 100%; height: 100%;
opacity: 0; opacity: 0.0;
filter: alpha(opacity=0); filter: alpha(opacity=0);
text-indent: -9999px; text-indent: -9999px;
@include transition; @include transition;
...@@ -127,7 +127,7 @@ div.book-wrapper { ...@@ -127,7 +127,7 @@ div.book-wrapper {
width: 100%; width: 100%;
&:hover { &:hover {
opacity: 1; opacity: 1.0;
filter: alpha(opacity=100); filter: alpha(opacity=100);
} }
} }
......
...@@ -61,7 +61,7 @@ section.course-index { ...@@ -61,7 +61,7 @@ section.course-index {
span.ui-icon { span.ui-icon {
left: 0; left: 0;
background-image: url("/static/images/ui-icons_222222_256x240.png"); background-image: url("/static/images/ui-icons_222222_256x240.png");
opacity: .3; opacity: 0.3;
} }
} }
} }
...@@ -146,7 +146,7 @@ section.course-index { ...@@ -146,7 +146,7 @@ section.course-index {
@include box-shadow(inset 0 1px 14px 0 rgba(0,0,0, 0.1)); @include box-shadow(inset 0 1px 14px 0 rgba(0,0,0, 0.1));
&:after { &:after {
opacity: 1; opacity: 1.0;
right: 15px; right: 15px;
} }
} }
...@@ -174,7 +174,7 @@ section.course-index { ...@@ -174,7 +174,7 @@ section.course-index {
background: $sidebar-active-image; background: $sidebar-active-image;
&:after { &:after {
opacity: 1; opacity: 1.0;
right: 15px; right: 15px;
} }
......
...@@ -27,7 +27,7 @@ div.calc-main { ...@@ -27,7 +27,7 @@ div.calc-main {
width: 16px; width: 16px;
&:hover { &:hover {
opacity: .8; opacity: 0.8;
} }
&.closed { &.closed {
...@@ -136,7 +136,7 @@ div.calc-main { ...@@ -136,7 +136,7 @@ div.calc-main {
&.shown { &.shown {
display: block; display: block;
opacity: 1; opacity: 1.0;
} }
dt { dt {
......
...@@ -26,7 +26,7 @@ header.global { ...@@ -26,7 +26,7 @@ header.global {
} }
h2 { h2 {
opacity: 1; opacity: 1.0;
} }
} }
...@@ -51,7 +51,7 @@ header.global { ...@@ -51,7 +51,7 @@ header.global {
text-decoration: none; text-decoration: none;
&::before { &::before {
opacity: 1; opacity: 1.0;
} }
.name { .name {
......
...@@ -429,7 +429,7 @@ ...@@ -429,7 +429,7 @@
&:hover { &:hover {
.sharing-message { .sharing-message {
opacity: 1; opacity: 1.0;
top: 56px; top: 56px;
} }
} }
...@@ -470,7 +470,7 @@ ...@@ -470,7 +470,7 @@
width: 44px; width: 44px;
&:hover { &:hover {
opacity: 1; opacity: 1.0;
} }
img { img {
...@@ -514,7 +514,7 @@ ...@@ -514,7 +514,7 @@
&:hover { &:hover {
.icon { .icon {
opacity: 1; opacity: 1.0;
} }
} }
......
...@@ -74,7 +74,7 @@ ...@@ -74,7 +74,7 @@
&:hover { &:hover {
.title .icon { .title .icon {
opacity: 1; opacity: 1.0;
} }
} }
......
...@@ -53,7 +53,7 @@ ...@@ -53,7 +53,7 @@
@include box-sizing(border-box); @include box-sizing(border-box);
@include inline-block; @include inline-block;
left: 0px; left: 0px;
opacity: 1; opacity: 1.0;
padding: 20px 30px; padding: 20px 30px;
top: 0px; top: 0px;
@include transition(all, 0.2s, linear); @include transition(all, 0.2s, linear);
...@@ -312,7 +312,7 @@ ...@@ -312,7 +312,7 @@
text-decoration: none; text-decoration: none;
&::before { &::before {
opacity: 1; opacity: 1.0;
} }
.name { .name {
......
...@@ -117,7 +117,7 @@ ...@@ -117,7 +117,7 @@
.info-link { .info-link {
color: $link-color; color: $link-color;
opacity: 1; opacity: 1.0;
} }
h2 { 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