Merge pull request #11 from edx/feature/christina/metadata-ui
Feature/christina/metadata ui
This commit is contained in:
@@ -5,8 +5,6 @@ from lettuce import world, step
|
||||
from nose.tools import assert_true
|
||||
from nose.tools import assert_equal
|
||||
|
||||
from xmodule.modulestore.django import _MODULESTORES, modulestore
|
||||
from xmodule.templates import update_templates
|
||||
from auth.authz import get_user_by_email
|
||||
|
||||
from selenium.webdriver.common.keys import Keys
|
||||
@@ -50,31 +48,31 @@ def i_press_the_category_delete_icon(step, category):
|
||||
|
||||
@step('I have opened a new course in Studio$')
|
||||
def i_have_opened_a_new_course(step):
|
||||
open_new_course()
|
||||
|
||||
|
||||
####### HELPER FUNCTIONS ##############
|
||||
def open_new_course():
|
||||
world.clear_courses()
|
||||
log_into_studio()
|
||||
create_a_course()
|
||||
|
||||
|
||||
####### HELPER FUNCTIONS ##############
|
||||
def create_studio_user(
|
||||
uname='robot',
|
||||
email='robot+studio@edx.org',
|
||||
password='test',
|
||||
is_staff=False):
|
||||
studio_user = world.UserFactory.build(
|
||||
studio_user = world.UserFactory(
|
||||
username=uname,
|
||||
email=email,
|
||||
password=password,
|
||||
is_staff=is_staff)
|
||||
studio_user.set_password(password)
|
||||
studio_user.save()
|
||||
|
||||
registration = world.RegistrationFactory(user=studio_user)
|
||||
registration.register(studio_user)
|
||||
registration.activate()
|
||||
|
||||
user_profile = world.UserProfileFactory(user=studio_user)
|
||||
|
||||
|
||||
def fill_in_course_info(
|
||||
name='Robot Super Course',
|
||||
@@ -153,4 +151,13 @@ def set_date_and_time(date_css, desired_date, time_css, desired_time):
|
||||
world.css_fill(time_css, desired_time)
|
||||
e = world.css_find(time_css).first
|
||||
e._element.send_keys(Keys.TAB)
|
||||
time.sleep(float(1))
|
||||
time.sleep(float(1))
|
||||
|
||||
|
||||
@step('I have created a Video component$')
|
||||
def i_created_a_video_component(step):
|
||||
world.create_component_instance(
|
||||
step, '.large-video-icon',
|
||||
'i4x://edx/templates/video/default',
|
||||
'.xmodule_VideoModule'
|
||||
)
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
# 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
|
||||
@@ -0,0 +1,13 @@
|
||||
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
|
||||
23
cms/djangoapps/contentstore/features/discussion-editor.py
Normal file
23
cms/djangoapps/contentstore/features/discussion-editor.py
Normal file
@@ -0,0 +1,23 @@
|
||||
# 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]
|
||||
])
|
||||
13
cms/djangoapps/contentstore/features/html-editor.feature
Normal file
13
cms/djangoapps/contentstore/features/html-editor.feature
Normal file
@@ -0,0 +1,13 @@
|
||||
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
|
||||
17
cms/djangoapps/contentstore/features/html-editor.py
Normal file
17
cms/djangoapps/contentstore/features/html-editor.py
Normal file
@@ -0,0 +1,17 @@
|
||||
# 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]])
|
||||
67
cms/djangoapps/contentstore/features/problem-editor.feature
Normal file
67
cms/djangoapps/contentstore/features/problem-editor.feature
Normal file
@@ -0,0 +1,67 @@
|
||||
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
|
||||
187
cms/djangoapps/contentstore/features/problem-editor.py
Normal file
187
cms/djangoapps/contentstore/features/problem-editor.py
Normal file
@@ -0,0 +1,187 @@
|
||||
# disable missing docstring
|
||||
#pylint: disable=C0111
|
||||
|
||||
from lettuce import world, step
|
||||
from nose.tools import assert_equal
|
||||
|
||||
DISPLAY_NAME = "Display Name"
|
||||
MAXIMUM_ATTEMPTS = "Maximum Attempts"
|
||||
PROBLEM_WEIGHT = "Problem Weight"
|
||||
RANDOMIZATION = 'Randomization'
|
||||
SHOW_ANSWER = "Show Answer"
|
||||
|
||||
|
||||
############### ACTIONS ####################
|
||||
@step('I have created a Blank Common Problem$')
|
||||
def i_created_blank_common_problem(step):
|
||||
world.create_component_instance(
|
||||
step,
|
||||
'.large-problem-icon',
|
||||
'i4x://edx/templates/problem/Blank_Common_Problem',
|
||||
'.xmodule_CapaModule'
|
||||
)
|
||||
|
||||
|
||||
@step('I edit and select Settings$')
|
||||
def i_edit_and_select_settings(step):
|
||||
world.edit_component_and_select_settings()
|
||||
|
||||
|
||||
@step('I see five alphabetized settings and their expected values$')
|
||||
def i_see_five_settings_with_values(step):
|
||||
world.verify_all_setting_entries(
|
||||
[
|
||||
[DISPLAY_NAME, "Blank Common Problem", True],
|
||||
[MAXIMUM_ATTEMPTS, "", False],
|
||||
[PROBLEM_WEIGHT, "", False],
|
||||
[RANDOMIZATION, "Never", True],
|
||||
[SHOW_ANSWER, "Finished", True]
|
||||
])
|
||||
|
||||
|
||||
@step('I can modify the display name')
|
||||
def i_can_modify_the_display_name(step):
|
||||
world.get_setting_entry(DISPLAY_NAME).find_by_css('.setting-input')[0].fill('modified')
|
||||
verify_modified_display_name()
|
||||
|
||||
|
||||
@step('my display name change is persisted on save')
|
||||
def my_display_name_change_is_persisted_on_save(step):
|
||||
world.save_component_and_reopen(step)
|
||||
verify_modified_display_name()
|
||||
|
||||
|
||||
@step('I can specify special characters in the display name')
|
||||
def i_can_modify_the_display_name_with_special_chars(step):
|
||||
world.get_setting_entry(DISPLAY_NAME).find_by_css('.setting-input')[0].fill("updated ' \" &")
|
||||
verify_modified_display_name_with_special_chars()
|
||||
|
||||
|
||||
@step('my special characters and persisted on save')
|
||||
def special_chars_persisted_on_save(step):
|
||||
world.save_component_and_reopen(step)
|
||||
verify_modified_display_name_with_special_chars()
|
||||
|
||||
|
||||
@step('I can revert the display name to unset')
|
||||
def can_revert_display_name_to_unset(step):
|
||||
world.revert_setting_entry(DISPLAY_NAME)
|
||||
verify_unset_display_name()
|
||||
|
||||
|
||||
@step('my display name is unset on save')
|
||||
def my_display_name_is_persisted_on_save(step):
|
||||
world.save_component_and_reopen(step)
|
||||
verify_unset_display_name()
|
||||
|
||||
|
||||
@step('I can select Per Student for Randomization')
|
||||
def i_can_select_per_student_for_randomization(step):
|
||||
world.browser.select(RANDOMIZATION, "Per Student")
|
||||
verify_modified_randomization()
|
||||
|
||||
|
||||
@step('my change to randomization is persisted')
|
||||
def my_change_to_randomization_is_persisted(step):
|
||||
world.save_component_and_reopen(step)
|
||||
verify_modified_randomization()
|
||||
|
||||
|
||||
@step('I can revert to the default value for randomization')
|
||||
def i_can_revert_to_default_for_randomization(step):
|
||||
world.revert_setting_entry(RANDOMIZATION)
|
||||
world.save_component_and_reopen(step)
|
||||
world.verify_setting_entry(world.get_setting_entry(RANDOMIZATION), RANDOMIZATION, "Always", False)
|
||||
|
||||
|
||||
@step('I can set the weight to "(.*)"?')
|
||||
def i_can_set_weight(step, weight):
|
||||
set_weight(weight)
|
||||
verify_modified_weight()
|
||||
|
||||
|
||||
@step('my change to weight is persisted')
|
||||
def my_change_to_weight_is_persisted(step):
|
||||
world.save_component_and_reopen(step)
|
||||
verify_modified_weight()
|
||||
|
||||
|
||||
@step('I can revert to the default value of unset for weight')
|
||||
def i_can_revert_to_default_for_unset_weight(step):
|
||||
world.revert_setting_entry(PROBLEM_WEIGHT)
|
||||
world.save_component_and_reopen(step)
|
||||
world.verify_setting_entry(world.get_setting_entry(PROBLEM_WEIGHT), PROBLEM_WEIGHT, "", False)
|
||||
|
||||
|
||||
@step('if I set the weight to "(.*)", it remains unset')
|
||||
def set_the_weight_to_abc(step, bad_weight):
|
||||
set_weight(bad_weight)
|
||||
# We show the clear button immediately on type, hence the "True" here.
|
||||
world.verify_setting_entry(world.get_setting_entry(PROBLEM_WEIGHT), PROBLEM_WEIGHT, "", True)
|
||||
world.save_component_and_reopen(step)
|
||||
# But no change was actually ever sent to the model, so on reopen, explicitly_set is False
|
||||
world.verify_setting_entry(world.get_setting_entry(PROBLEM_WEIGHT), PROBLEM_WEIGHT, "", False)
|
||||
|
||||
|
||||
@step('if I set the max attempts to "(.*)", it displays initially as "(.*)", and is persisted as "(.*)"')
|
||||
def set_the_max_attempts(step, max_attempts_set, max_attempts_displayed, max_attempts_persisted):
|
||||
world.get_setting_entry(MAXIMUM_ATTEMPTS).find_by_css('.setting-input')[0].fill(max_attempts_set)
|
||||
world.verify_setting_entry(world.get_setting_entry(MAXIMUM_ATTEMPTS), MAXIMUM_ATTEMPTS, max_attempts_displayed, True)
|
||||
world.save_component_and_reopen(step)
|
||||
world.verify_setting_entry(world.get_setting_entry(MAXIMUM_ATTEMPTS), MAXIMUM_ATTEMPTS, max_attempts_persisted, True)
|
||||
|
||||
|
||||
@step('Edit High Level Source is not visible')
|
||||
def edit_high_level_source_not_visible(step):
|
||||
verify_high_level_source(step, False)
|
||||
|
||||
|
||||
@step('Edit High Level Source is visible')
|
||||
def edit_high_level_source_visible(step):
|
||||
verify_high_level_source(step, True)
|
||||
|
||||
|
||||
@step('If I press Cancel my changes are not persisted')
|
||||
def cancel_does_not_save_changes(step):
|
||||
world.cancel_component(step)
|
||||
step.given("I edit and select Settings")
|
||||
step.given("I see five alphabetized settings and their expected values")
|
||||
|
||||
|
||||
@step('I have created a LaTeX Problem')
|
||||
def create_latex_problem(step):
|
||||
world.click_new_component_button(step, '.large-problem-icon')
|
||||
# Go to advanced tab (waiting for the tab to be visible)
|
||||
world.css_find('#ui-id-2')
|
||||
world.css_click('#ui-id-2')
|
||||
world.click_component_from_menu("i4x://edx/templates/problem/Problem_Written_in_LaTeX", '.xmodule_CapaModule')
|
||||
|
||||
|
||||
def verify_high_level_source(step, visible):
|
||||
assert_equal(visible, world.is_css_present('.launch-latex-compiler'))
|
||||
world.cancel_component(step)
|
||||
assert_equal(visible, world.is_css_present('.upload-button'))
|
||||
|
||||
|
||||
def verify_modified_weight():
|
||||
world.verify_setting_entry(world.get_setting_entry(PROBLEM_WEIGHT), PROBLEM_WEIGHT, "3.5", True)
|
||||
|
||||
|
||||
def verify_modified_randomization():
|
||||
world.verify_setting_entry(world.get_setting_entry(RANDOMIZATION), RANDOMIZATION, "Per Student", True)
|
||||
|
||||
|
||||
def verify_modified_display_name():
|
||||
world.verify_setting_entry(world.get_setting_entry(DISPLAY_NAME), DISPLAY_NAME, 'modified', True)
|
||||
|
||||
|
||||
def verify_modified_display_name_with_special_chars():
|
||||
world.verify_setting_entry(world.get_setting_entry(DISPLAY_NAME), DISPLAY_NAME, "updated ' \" &", True)
|
||||
|
||||
|
||||
def verify_unset_display_name():
|
||||
world.verify_setting_entry(world.get_setting_entry(DISPLAY_NAME), DISPLAY_NAME, '', False)
|
||||
|
||||
|
||||
def set_weight(weight):
|
||||
world.get_setting_entry(PROBLEM_WEIGHT).find_by_css('.setting-input')[0].fill(weight)
|
||||
@@ -10,9 +10,7 @@ from nose.tools import assert_equal
|
||||
|
||||
@step('I have opened a new course section in Studio$')
|
||||
def i_have_opened_a_new_course_section(step):
|
||||
world.clear_courses()
|
||||
log_into_studio()
|
||||
create_a_course()
|
||||
open_new_course()
|
||||
add_section()
|
||||
|
||||
|
||||
|
||||
13
cms/djangoapps/contentstore/features/video-editor.feature
Normal file
13
cms/djangoapps/contentstore/features/video-editor.feature
Normal file
@@ -0,0 +1,13 @@
|
||||
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
|
||||
9
cms/djangoapps/contentstore/features/video-editor.py
Normal file
9
cms/djangoapps/contentstore/features/video-editor.py
Normal file
@@ -0,0 +1,9 @@
|
||||
# disable missing docstring
|
||||
#pylint: disable=C0111
|
||||
|
||||
from lettuce import world, step
|
||||
|
||||
|
||||
@step('I see only the video display name setting$')
|
||||
def i_see_only_the_video_display_name(step):
|
||||
world.verify_all_setting_entries([['Display Name', "default", True]])
|
||||
@@ -149,8 +149,7 @@ def edit_unit(request, location):
|
||||
component_templates[category].append((
|
||||
template.display_name_with_default,
|
||||
template.location.url(),
|
||||
hasattr(template, 'markdown') and template.markdown is not None,
|
||||
template.cms.empty,
|
||||
hasattr(template, 'markdown') and template.markdown is not None
|
||||
))
|
||||
|
||||
components = [
|
||||
|
||||
@@ -223,7 +223,8 @@ PIPELINE_JS = {
|
||||
rooted_glob(PROJECT_ROOT / 'static/', 'coffee/src/**/*.js')
|
||||
) + ['js/hesitate.js', 'js/base.js',
|
||||
'js/models/feedback.js', 'js/views/feedback.js',
|
||||
'js/models/section.js', 'js/views/section.js'],
|
||||
'js/models/section.js', 'js/views/section.js',
|
||||
'js/models/metadata_model.js', 'js/views/metadata_editor_view.js'],
|
||||
'output_filename': 'js/cms-application.js',
|
||||
'test_order': 0
|
||||
},
|
||||
|
||||
1
cms/static/coffee/fixtures/metadata-editor.underscore
Symbolic link
1
cms/static/coffee/fixtures/metadata-editor.underscore
Symbolic link
@@ -0,0 +1 @@
|
||||
../../../templates/js/metadata-editor.underscore
|
||||
1
cms/static/coffee/fixtures/metadata-number-entry.underscore
Symbolic link
1
cms/static/coffee/fixtures/metadata-number-entry.underscore
Symbolic link
@@ -0,0 +1 @@
|
||||
../../../templates/js/metadata-number-entry.underscore
|
||||
1
cms/static/coffee/fixtures/metadata-option-entry.underscore
Symbolic link
1
cms/static/coffee/fixtures/metadata-option-entry.underscore
Symbolic link
@@ -0,0 +1 @@
|
||||
../../../templates/js/metadata-option-entry.underscore
|
||||
1
cms/static/coffee/fixtures/metadata-string-entry.underscore
Symbolic link
1
cms/static/coffee/fixtures/metadata-string-entry.underscore
Symbolic link
@@ -0,0 +1 @@
|
||||
../../../templates/js/metadata-string-entry.underscore
|
||||
58
cms/static/coffee/spec/models/metadata_spec.coffee
Normal file
58
cms/static/coffee/spec/models/metadata_spec.coffee
Normal file
@@ -0,0 +1,58 @@
|
||||
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)
|
||||
|
||||
300
cms/static/coffee/spec/views/metadata_edit_spec.coffee
Normal file
300
cms/static/coffee/spec/views/metadata_edit_spec.coffee
Normal file
@@ -0,0 +1,300 @@
|
||||
describe "Test Metadata Editor", ->
|
||||
editorTemplate = readFixtures('metadata-editor.underscore')
|
||||
numberEntryTemplate = readFixtures('metadata-number-entry.underscore')
|
||||
stringEntryTemplate = readFixtures('metadata-string-entry.underscore')
|
||||
optionEntryTemplate = readFixtures('metadata-option-entry.underscore')
|
||||
|
||||
beforeEach ->
|
||||
setFixtures($("<script>", {id: "metadata-editor-tpl", type: "text/template"}).text(editorTemplate))
|
||||
appendSetFixtures($("<script>", {id: "metadata-number-entry", type: "text/template"}).text(numberEntryTemplate))
|
||||
appendSetFixtures($("<script>", {id: "metadata-string-entry", type: "text/template"}).text(stringEntryTemplate))
|
||||
appendSetFixtures($("<script>", {id: "metadata-option-entry", type: "text/template"}).text(optionEntryTemplate))
|
||||
|
||||
genericEntry = {
|
||||
default_value: 'default value',
|
||||
display_name: "Display Name",
|
||||
explicitly_set: true,
|
||||
field_name: "display_name",
|
||||
help: "Specifies the name for this component.",
|
||||
inheritable: false,
|
||||
options: [],
|
||||
type: CMS.Models.Metadata.GENERIC_TYPE,
|
||||
value: "Word cloud"
|
||||
}
|
||||
|
||||
selectEntry = {
|
||||
default_value: "answered",
|
||||
display_name: "Show Answer",
|
||||
explicitly_set: false,
|
||||
field_name: "show_answer",
|
||||
help: "When should you show the answer",
|
||||
inheritable: true,
|
||||
options: [
|
||||
{"display_name": "Always", "value": "always"},
|
||||
{"display_name": "Answered", "value": "answered"},
|
||||
{"display_name": "Never", "value": "never"}
|
||||
],
|
||||
type: CMS.Models.Metadata.SELECT_TYPE,
|
||||
value: "always"
|
||||
}
|
||||
|
||||
integerEntry = {
|
||||
default_value: 5,
|
||||
display_name: "Inputs",
|
||||
explicitly_set: false,
|
||||
field_name: "num_inputs",
|
||||
help: "Number of text boxes for student to input words/sentences.",
|
||||
inheritable: false,
|
||||
options: {min: 1},
|
||||
type: CMS.Models.Metadata.INTEGER_TYPE,
|
||||
value: 5
|
||||
}
|
||||
|
||||
floatEntry = {
|
||||
default_value: 2.7,
|
||||
display_name: "Weight",
|
||||
explicitly_set: true,
|
||||
field_name: "weight",
|
||||
help: "Weight for this problem",
|
||||
inheritable: true,
|
||||
options: {min: 1.3, max:100.2, step:0.1},
|
||||
type: CMS.Models.Metadata.FLOAT_TYPE,
|
||||
value: 10.2
|
||||
}
|
||||
|
||||
# Test for the editor that creates the individual views.
|
||||
describe "CMS.Views.Metadata.Editor creates editors for each field", ->
|
||||
beforeEach ->
|
||||
@model = new CMS.Models.MetadataCollection(
|
||||
[
|
||||
integerEntry,
|
||||
floatEntry,
|
||||
selectEntry,
|
||||
genericEntry,
|
||||
{
|
||||
default_value: null,
|
||||
display_name: "Unknown",
|
||||
explicitly_set: true,
|
||||
field_name: "unknown_type",
|
||||
help: "Mystery property.",
|
||||
inheritable: false,
|
||||
options: [
|
||||
{"display_name": "Always", "value": "always"},
|
||||
{"display_name": "Answered", "value": "answered"},
|
||||
{"display_name": "Never", "value": "never"}],
|
||||
type: "unknown type",
|
||||
value: null
|
||||
}
|
||||
]
|
||||
)
|
||||
|
||||
it "creates child views on initialize, and sorts them alphabetically", ->
|
||||
view = new CMS.Views.Metadata.Editor({collection: @model})
|
||||
childModels = view.collection.models
|
||||
expect(childModels.length).toBe(5)
|
||||
childViews = view.$el.find('.setting-input')
|
||||
expect(childViews.length).toBe(5)
|
||||
|
||||
verifyEntry = (index, display_name, type) ->
|
||||
expect(childModels[index].get('display_name')).toBe(display_name)
|
||||
expect(childViews[index].type).toBe(type)
|
||||
|
||||
verifyEntry(0, 'Display Name', 'text')
|
||||
verifyEntry(1, 'Inputs', 'number')
|
||||
verifyEntry(2, 'Show Answer', 'select-one')
|
||||
verifyEntry(3, 'Unknown', 'text')
|
||||
verifyEntry(4, 'Weight', 'number')
|
||||
|
||||
it "returns its display name", ->
|
||||
view = new CMS.Views.Metadata.Editor({collection: @model})
|
||||
expect(view.getDisplayName()).toBe("Word cloud")
|
||||
|
||||
it "returns an empty string if there is no display name property with a valid value", ->
|
||||
view = new CMS.Views.Metadata.Editor({collection: new CMS.Models.MetadataCollection()})
|
||||
expect(view.getDisplayName()).toBe("")
|
||||
|
||||
view = new CMS.Views.Metadata.Editor({collection: new CMS.Models.MetadataCollection([
|
||||
{
|
||||
default_value: null,
|
||||
display_name: "Display Name",
|
||||
explicitly_set: false,
|
||||
field_name: "display_name",
|
||||
help: "",
|
||||
inheritable: false,
|
||||
options: [],
|
||||
type: CMS.Models.Metadata.GENERIC_TYPE,
|
||||
value: null
|
||||
|
||||
}])
|
||||
})
|
||||
expect(view.getDisplayName()).toBe("")
|
||||
|
||||
it "has no modified values by default", ->
|
||||
view = new CMS.Views.Metadata.Editor({collection: @model})
|
||||
expect(view.getModifiedMetadataValues()).toEqual({})
|
||||
|
||||
it "returns modified values only", ->
|
||||
view = new CMS.Views.Metadata.Editor({collection: @model})
|
||||
childModels = view.collection.models
|
||||
childModels[0].setValue('updated display name')
|
||||
childModels[1].setValue(20)
|
||||
expect(view.getModifiedMetadataValues()).toEqual({
|
||||
display_name : 'updated display name',
|
||||
num_inputs: 20
|
||||
})
|
||||
|
||||
# Tests for individual views.
|
||||
assertInputType = (view, expectedType) ->
|
||||
input = view.$el.find('.setting-input')
|
||||
expect(input.length).toBe(1)
|
||||
expect(input[0].type).toBe(expectedType)
|
||||
|
||||
assertValueInView = (view, expectedValue) ->
|
||||
expect(view.getValueFromEditor()).toBe(expectedValue)
|
||||
|
||||
assertCanUpdateView = (view, newValue) ->
|
||||
view.setValueInEditor(newValue)
|
||||
expect(view.getValueFromEditor()).toBe(newValue)
|
||||
|
||||
assertClear = (view, modelValue, editorValue=modelValue) ->
|
||||
view.clear()
|
||||
expect(view.model.getValue()).toBe(null)
|
||||
expect(view.model.getDisplayValue()).toBe(modelValue)
|
||||
expect(view.getValueFromEditor()).toBe(editorValue)
|
||||
|
||||
assertUpdateModel = (view, originalValue, newValue) ->
|
||||
view.setValueInEditor(newValue)
|
||||
expect(view.model.getValue()).toBe(originalValue)
|
||||
view.updateModel()
|
||||
expect(view.model.getValue()).toBe(newValue)
|
||||
|
||||
describe "CMS.Views.Metadata.String is a basic string input with clear functionality", ->
|
||||
beforeEach ->
|
||||
model = new CMS.Models.Metadata(genericEntry)
|
||||
@view = new CMS.Views.Metadata.String({model: model})
|
||||
|
||||
it "uses a text input type", ->
|
||||
assertInputType(@view, 'text')
|
||||
|
||||
it "returns the intial value upon initialization", ->
|
||||
assertValueInView(@view, 'Word cloud')
|
||||
|
||||
it "can update its value in the view", ->
|
||||
assertCanUpdateView(@view, "updated ' \" &")
|
||||
|
||||
it "has a clear method to revert to the model default", ->
|
||||
assertClear(@view, 'default value')
|
||||
|
||||
it "has an update model method", ->
|
||||
assertUpdateModel(@view, 'Word cloud', 'updated')
|
||||
|
||||
describe "CMS.Views.Metadata.Option is an option input type with clear functionality", ->
|
||||
beforeEach ->
|
||||
model = new CMS.Models.Metadata(selectEntry)
|
||||
@view = new CMS.Views.Metadata.Option({model: model})
|
||||
|
||||
it "uses a select input type", ->
|
||||
assertInputType(@view, 'select-one')
|
||||
|
||||
it "returns the intial value upon initialization", ->
|
||||
assertValueInView(@view, 'always')
|
||||
|
||||
it "can update its value in the view", ->
|
||||
assertCanUpdateView(@view, "never")
|
||||
|
||||
it "has a clear method to revert to the model default", ->
|
||||
assertClear(@view, 'answered')
|
||||
|
||||
it "has an update model method", ->
|
||||
assertUpdateModel(@view, null, 'never')
|
||||
|
||||
it "does not update to a value that is not an option", ->
|
||||
@view.setValueInEditor("not an option")
|
||||
expect(@view.getValueFromEditor()).toBe('always')
|
||||
|
||||
describe "CMS.Views.Metadata.Number supports integer or float type and has clear functionality", ->
|
||||
beforeEach ->
|
||||
integerModel = new CMS.Models.Metadata(integerEntry)
|
||||
@integerView = new CMS.Views.Metadata.Number({model: integerModel})
|
||||
|
||||
floatModel = new CMS.Models.Metadata(floatEntry)
|
||||
@floatView = new CMS.Views.Metadata.Number({model: floatModel})
|
||||
|
||||
it "uses a number input type", ->
|
||||
assertInputType(@integerView, 'number')
|
||||
assertInputType(@floatView, 'number')
|
||||
|
||||
it "returns the intial value upon initialization", ->
|
||||
assertValueInView(@integerView, '5')
|
||||
assertValueInView(@floatView, '10.2')
|
||||
|
||||
it "can update its value in the view", ->
|
||||
assertCanUpdateView(@integerView, "12")
|
||||
assertCanUpdateView(@floatView, "-2.4")
|
||||
|
||||
it "has a clear method to revert to the model default", ->
|
||||
assertClear(@integerView, 5, '5')
|
||||
assertClear(@floatView, 2.7, '2.7')
|
||||
|
||||
it "has an update model method", ->
|
||||
assertUpdateModel(@integerView, null, '90')
|
||||
assertUpdateModel(@floatView, 10.2, '-9.5')
|
||||
|
||||
it "knows the difference between integer and float", ->
|
||||
expect(@integerView.isIntegerField()).toBeTruthy()
|
||||
expect(@floatView.isIntegerField()).toBeFalsy()
|
||||
|
||||
it "sets attribtues related to min, max, and step", ->
|
||||
verifyAttributes = (view, min, step, max=null) ->
|
||||
inputEntry = view.$el.find('input')
|
||||
expect(Number(inputEntry.attr('min'))).toEqual(min)
|
||||
expect(Number(inputEntry.attr('step'))).toEqual(step)
|
||||
if max is not null
|
||||
expect(Number(inputEntry.attr('max'))).toEqual(max)
|
||||
|
||||
verifyAttributes(@integerView, 1, 1)
|
||||
verifyAttributes(@floatView, 1.3, .1, 100.2)
|
||||
|
||||
it "corrects values that are out of range", ->
|
||||
verifyValueAfterChanged = (view, value, expectedResult) ->
|
||||
view.setValueInEditor(value)
|
||||
view.changed()
|
||||
expect(view.getValueFromEditor()).toBe(expectedResult)
|
||||
|
||||
verifyValueAfterChanged(@integerView, '-4', '1')
|
||||
verifyValueAfterChanged(@integerView, '1', '1')
|
||||
verifyValueAfterChanged(@integerView, '0', '1')
|
||||
verifyValueAfterChanged(@integerView, '3001', '3001')
|
||||
|
||||
verifyValueAfterChanged(@floatView, '-4', '1.3')
|
||||
verifyValueAfterChanged(@floatView, '1.3', '1.3')
|
||||
verifyValueAfterChanged(@floatView, '1.2', '1.3')
|
||||
verifyValueAfterChanged(@floatView, '100.2', '100.2')
|
||||
verifyValueAfterChanged(@floatView, '100.3', '100.2')
|
||||
|
||||
it "disallows invalid characters", ->
|
||||
verifyValueAfterKeyPressed = (view, character, reject) ->
|
||||
event = {
|
||||
type : 'keypress',
|
||||
which : character.charCodeAt(0),
|
||||
keyCode: character.charCodeAt(0),
|
||||
preventDefault : () -> 'no op'
|
||||
}
|
||||
spyOn(event, 'preventDefault')
|
||||
view.$el.find('input').trigger(event)
|
||||
if (reject)
|
||||
expect(event.preventDefault).toHaveBeenCalled()
|
||||
else
|
||||
expect(event.preventDefault).not.toHaveBeenCalled()
|
||||
|
||||
verifyDisallowedChars = (view) ->
|
||||
verifyValueAfterKeyPressed(view, 'a', true)
|
||||
verifyValueAfterKeyPressed(view, '.', view.isIntegerField())
|
||||
verifyValueAfterKeyPressed(view, '[', true)
|
||||
verifyValueAfterKeyPressed(view, '@', true)
|
||||
|
||||
for i in [0...9]
|
||||
verifyValueAfterKeyPressed(view, String(i), false)
|
||||
|
||||
verifyDisallowedChars(@integerView)
|
||||
verifyDisallowedChars(@floatView)
|
||||
@@ -73,13 +73,3 @@ describe "CMS.Views.ModuleEdit", ->
|
||||
expect(XModule.loadModule).toHaveBeenCalled()
|
||||
expect(XModule.loadModule.mostRecentCall.args[0]).toBe($('.xmodule_display'))
|
||||
|
||||
describe "changedMetadata", ->
|
||||
it "returns empty if no metadata loaded", ->
|
||||
expect(@moduleEdit.changedMetadata()).toEqual({})
|
||||
|
||||
it "returns only changed values", ->
|
||||
@moduleEdit.originalMetadata = {'foo', 'bar'}
|
||||
spyOn(@moduleEdit, 'metadata').andReturn({'a': '', 'b': 'before', 'c': ''})
|
||||
@moduleEdit.loadEdit()
|
||||
@moduleEdit.metadata.andReturn({'a': '', 'b': 'after', 'd': 'only_after'})
|
||||
expect(@moduleEdit.changedMetadata()).toEqual({'b' : 'after', 'd' : 'only_after'})
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
class CMS.Views.ModuleEdit extends Backbone.View
|
||||
tagName: 'li'
|
||||
className: 'component'
|
||||
editorMode: 'editor-mode'
|
||||
|
||||
events:
|
||||
"click .component-editor .cancel-button": 'clickCancelButton'
|
||||
"click .component-editor .save-button": 'clickSaveButton'
|
||||
"click .component-actions .edit-button": 'clickEditButton'
|
||||
"click .component-actions .delete-button": 'onDelete'
|
||||
"click .mode a": 'clickModeButton'
|
||||
|
||||
initialize: ->
|
||||
@onDelete = @options.onDelete
|
||||
@@ -20,29 +22,30 @@ class CMS.Views.ModuleEdit extends Backbone.View
|
||||
loadEdit: ->
|
||||
if not @module
|
||||
@module = XModule.loadModule(@$el.find('.xmodule_edit'))
|
||||
@originalMetadata = @metadata()
|
||||
# At this point, metadata-edit.html will be loaded, and the metadata (as JSON) is available.
|
||||
metadataEditor = @$el.find('.metadata_edit')
|
||||
metadataData = metadataEditor.data('metadata')
|
||||
models = [];
|
||||
for key of metadataData
|
||||
models.push(metadataData[key])
|
||||
@metadataEditor = new CMS.Views.Metadata.Editor({
|
||||
el: metadataEditor,
|
||||
collection: new CMS.Models.MetadataCollection(models)
|
||||
})
|
||||
|
||||
metadata: ->
|
||||
# cdodge: package up metadata which is separated into a number of input fields
|
||||
# there's probably a better way to do this, but at least this lets me continue to move onwards
|
||||
_metadata = {}
|
||||
# Need to update set "active" class on data editor if there is one.
|
||||
# If we are only showing settings, hide the data editor controls and update settings accordingly.
|
||||
if @hasDataEditor()
|
||||
@selectMode(@editorMode)
|
||||
else
|
||||
@hideDataEditor()
|
||||
|
||||
$metadata = @$component_editor().find('.metadata_edit')
|
||||
|
||||
if $metadata
|
||||
# walk through the set of elments which have the 'xmetadata_name' attribute and
|
||||
# build up a object to pass back to the server on the subsequent POST
|
||||
_metadata[$(el).data("metadata-name")] = el.value for el in $('[data-metadata-name]', $metadata)
|
||||
|
||||
return _metadata
|
||||
title = interpolate(gettext('<em>Editing:</em> %s'),
|
||||
[@metadataEditor.getDisplayName()])
|
||||
@$el.find('.component-name').html(title)
|
||||
|
||||
changedMetadata: ->
|
||||
currentMetadata = @metadata()
|
||||
changedMetadata = {}
|
||||
for key of currentMetadata
|
||||
if currentMetadata[key] != @originalMetadata[key]
|
||||
changedMetadata[key] = currentMetadata[key]
|
||||
return changedMetadata
|
||||
return @metadataEditor.getModifiedMetadataValues()
|
||||
|
||||
cloneTemplate: (parent, template) ->
|
||||
$.post("/clone_item", {
|
||||
@@ -77,7 +80,7 @@ class CMS.Views.ModuleEdit extends Backbone.View
|
||||
@render()
|
||||
@$el.removeClass('editing')
|
||||
).fail( ->
|
||||
showToastMessage("There was an error saving your changes. Please try again.", null, 3)
|
||||
showToastMessage(gettext("There was an error saving your changes. Please try again."), null, 3)
|
||||
)
|
||||
|
||||
clickCancelButton: (event) ->
|
||||
@@ -96,3 +99,38 @@ class CMS.Views.ModuleEdit extends Backbone.View
|
||||
$modalCover.show().addClass('is-fixed')
|
||||
@$component_editor().slideDown(150)
|
||||
@loadEdit()
|
||||
|
||||
clickModeButton: (event) ->
|
||||
event.preventDefault()
|
||||
if not @hasDataEditor()
|
||||
return
|
||||
@selectMode(event.currentTarget.parentElement.id)
|
||||
|
||||
hasDataEditor: =>
|
||||
return @$el.find('.wrapper-comp-editor').length > 0
|
||||
|
||||
selectMode: (mode) =>
|
||||
dataEditor = @$el.find('.wrapper-comp-editor')
|
||||
settingsEditor = @$el.find('.wrapper-comp-settings')
|
||||
editorModeButton = @$el.find('#editor-mode').find("a")
|
||||
settingsModeButton = @$el.find('#settings-mode').find("a")
|
||||
|
||||
if mode == @editorMode
|
||||
# Because of CodeMirror editor, cannot hide the data editor when it is first loaded. Therefore
|
||||
# we have to use a class of is-inactive instead of is-active.
|
||||
dataEditor.removeClass('is-inactive')
|
||||
editorModeButton.addClass('is-set')
|
||||
settingsEditor.removeClass('is-active')
|
||||
settingsModeButton.removeClass('is-set')
|
||||
else
|
||||
dataEditor.addClass('is-inactive')
|
||||
editorModeButton.removeClass('is-set')
|
||||
settingsEditor.addClass('is-active')
|
||||
settingsModeButton.addClass('is-set')
|
||||
|
||||
hideDataEditor: =>
|
||||
editorModeButtonParent = @$el.find('#editor-mode')
|
||||
editorModeButtonParent.addClass('inactive-mode')
|
||||
editorModeButtonParent.removeClass('active-mode')
|
||||
@$el.find('.wrapper-comp-settings').addClass('is-active')
|
||||
@$el.find('#settings-mode').find("a").addClass('is-set')
|
||||
|
||||
113
cms/static/js/models/metadata_model.js
Normal file
113
cms/static/js/models/metadata_model.js
Normal file
@@ -0,0 +1,113 @@
|
||||
/**
|
||||
* 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";
|
||||
312
cms/static/js/views/metadata_editor_view.js
Normal file
312
cms/static/js/views/metadata_editor_view.js
Normal file
@@ -0,0 +1,312 @@
|
||||
if (!CMS.Views['Metadata']) CMS.Views.Metadata = {};
|
||||
|
||||
CMS.Views.Metadata.Editor = Backbone.View.extend({
|
||||
|
||||
// Model is CMS.Models.MetadataCollection,
|
||||
initialize : function() {
|
||||
var tpl = $("#metadata-editor-tpl").text();
|
||||
if(!tpl) {
|
||||
console.error("Couldn't load metadata editor template");
|
||||
}
|
||||
this.template = _.template(tpl);
|
||||
|
||||
this.$el.html(this.template({numEntries: this.collection.length}));
|
||||
var counter = 0;
|
||||
|
||||
var self = this;
|
||||
this.collection.each(
|
||||
function (model) {
|
||||
var data = {
|
||||
el: self.$el.find('.metadata_entry')[counter++],
|
||||
model: model
|
||||
};
|
||||
if (model.getType() === CMS.Models.Metadata.SELECT_TYPE) {
|
||||
new CMS.Views.Metadata.Option(data);
|
||||
}
|
||||
else if (model.getType() === CMS.Models.Metadata.INTEGER_TYPE ||
|
||||
model.getType() === CMS.Models.Metadata.FLOAT_TYPE) {
|
||||
new CMS.Views.Metadata.Number(data);
|
||||
}
|
||||
else {
|
||||
// Everything else is treated as GENERIC_TYPE, which uses String editor.
|
||||
new CMS.Views.Metadata.String(data);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns the just the modified metadata values, in the format used to persist to the server.
|
||||
*/
|
||||
getModifiedMetadataValues: function () {
|
||||
var modified_values = {};
|
||||
this.collection.each(
|
||||
function (model) {
|
||||
if (model.isModified()) {
|
||||
modified_values[model.getFieldName()] = model.getValue();
|
||||
}
|
||||
}
|
||||
);
|
||||
return modified_values;
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns a display name for the component related to this metadata. This method looks to see
|
||||
* if there is a metadata entry called 'display_name', and if so, it returns its value. If there
|
||||
* is no such entry, or if display_name does not have a value set, it returns an empty string.
|
||||
*/
|
||||
getDisplayName: function () {
|
||||
var displayName = '';
|
||||
this.collection.each(
|
||||
function (model) {
|
||||
if (model.get('field_name') === 'display_name') {
|
||||
var displayNameValue = model.get('value');
|
||||
// It is possible that there is no display name value set. In that case, return empty string.
|
||||
displayName = displayNameValue ? displayNameValue : '';
|
||||
}
|
||||
}
|
||||
);
|
||||
return displayName;
|
||||
}
|
||||
});
|
||||
|
||||
CMS.Views.Metadata.AbstractEditor = Backbone.View.extend({
|
||||
|
||||
// Model is CMS.Models.Metadata.
|
||||
initialize : function() {
|
||||
var self = this;
|
||||
var templateName = _.result(this, 'templateName');
|
||||
// Backbone model cid is only unique within the collection.
|
||||
this.uniqueId = _.uniqueId(templateName + "_");
|
||||
|
||||
var tpl = document.getElementById(templateName).text;
|
||||
if(!tpl) {
|
||||
console.error("Couldn't load template: " + templateName);
|
||||
}
|
||||
this.template = _.template(tpl);
|
||||
this.$el.html(this.template({model: this.model, uniqueId: this.uniqueId}));
|
||||
this.listenTo(this.model, 'change', this.render);
|
||||
this.render();
|
||||
},
|
||||
|
||||
/**
|
||||
* The ID/name of the template. Subclasses must override this.
|
||||
*/
|
||||
templateName: '',
|
||||
|
||||
/**
|
||||
* Returns the value currently displayed in the editor/view. Subclasses should implement this method.
|
||||
*/
|
||||
getValueFromEditor : function () {},
|
||||
|
||||
/**
|
||||
* Sets the value currently displayed in the editor/view. Subclasses should implement this method.
|
||||
*/
|
||||
setValueInEditor : function (value) {},
|
||||
|
||||
/**
|
||||
* Sets the value in the model, using the value currently displayed in the view.
|
||||
*/
|
||||
updateModel: function () {
|
||||
this.model.setValue(this.getValueFromEditor());
|
||||
},
|
||||
|
||||
/**
|
||||
* Clears the value currently set in the model (reverting to the default).
|
||||
*/
|
||||
clear: function () {
|
||||
this.model.clear();
|
||||
},
|
||||
|
||||
/**
|
||||
* Shows the clear button, if it is not already showing.
|
||||
*/
|
||||
showClearButton: function() {
|
||||
if (!this.$el.hasClass('is-set')) {
|
||||
this.$el.addClass('is-set');
|
||||
this.getClearButton().removeClass('inactive');
|
||||
this.getClearButton().addClass('active');
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns the clear button.
|
||||
*/
|
||||
getClearButton: function () {
|
||||
return this.$el.find('.setting-clear');
|
||||
},
|
||||
|
||||
/**
|
||||
* Renders the editor, updating the value displayed in the view, as well as the state of
|
||||
* the clear button.
|
||||
*/
|
||||
render: function () {
|
||||
if (!this.template) return;
|
||||
|
||||
this.setValueInEditor(this.model.getDisplayValue());
|
||||
|
||||
if (this.model.isExplicitlySet()) {
|
||||
this.showClearButton();
|
||||
}
|
||||
else {
|
||||
this.$el.removeClass('is-set');
|
||||
this.getClearButton().addClass('inactive');
|
||||
this.getClearButton().removeClass('active');
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
});
|
||||
|
||||
CMS.Views.Metadata.String = CMS.Views.Metadata.AbstractEditor.extend({
|
||||
|
||||
events : {
|
||||
"change input" : "updateModel",
|
||||
"keypress .setting-input" : "showClearButton" ,
|
||||
"click .setting-clear" : "clear"
|
||||
},
|
||||
|
||||
templateName: "metadata-string-entry",
|
||||
|
||||
getValueFromEditor : function () {
|
||||
return this.$el.find('#' + this.uniqueId).val();
|
||||
},
|
||||
|
||||
setValueInEditor : function (value) {
|
||||
this.$el.find('input').val(value);
|
||||
}
|
||||
});
|
||||
|
||||
CMS.Views.Metadata.Number = CMS.Views.Metadata.AbstractEditor.extend({
|
||||
|
||||
events : {
|
||||
"change input" : "updateModel",
|
||||
"keypress .setting-input" : "keyPressed",
|
||||
"change .setting-input" : "changed",
|
||||
"click .setting-clear" : "clear"
|
||||
},
|
||||
|
||||
render: function () {
|
||||
CMS.Views.Metadata.AbstractEditor.prototype.render.apply(this);
|
||||
if (!this.initialized) {
|
||||
var numToString = function (val) {
|
||||
return val.toFixed(4);
|
||||
};
|
||||
var min = "min";
|
||||
var max = "max";
|
||||
var step = "step";
|
||||
var options = this.model.getOptions();
|
||||
if (options.hasOwnProperty(min)) {
|
||||
this.min = Number(options[min]);
|
||||
this.$el.find('input').attr(min, numToString(this.min));
|
||||
}
|
||||
if (options.hasOwnProperty(max)) {
|
||||
this.max = Number(options[max]);
|
||||
this.$el.find('input').attr(max, numToString(this.max));
|
||||
}
|
||||
var stepValue = undefined;
|
||||
if (options.hasOwnProperty(step)) {
|
||||
// Parse step and convert to String. Polyfill doesn't like float values like ".1" (expects "0.1").
|
||||
stepValue = numToString(Number(options[step]));
|
||||
}
|
||||
else if (this.isIntegerField()) {
|
||||
stepValue = "1";
|
||||
}
|
||||
if (stepValue !== undefined) {
|
||||
this.$el.find('input').attr(step, stepValue);
|
||||
}
|
||||
|
||||
// Manually runs polyfill for input number types to correct for Firefox non-support.
|
||||
// inputNumber will be undefined when unit test is running.
|
||||
if ($.fn.inputNumber) {
|
||||
this.$el.find('.setting-input-number').inputNumber();
|
||||
}
|
||||
|
||||
this.initialized = true;
|
||||
}
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
templateName: "metadata-number-entry",
|
||||
|
||||
getValueFromEditor : function () {
|
||||
return this.$el.find('#' + this.uniqueId).val();
|
||||
},
|
||||
|
||||
setValueInEditor : function (value) {
|
||||
this.$el.find('input').val(value);
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns true if this view is restricted to integers, as opposed to floating points values.
|
||||
*/
|
||||
isIntegerField : function () {
|
||||
return this.model.getType() === 'Integer';
|
||||
},
|
||||
|
||||
keyPressed: function (e) {
|
||||
this.showClearButton();
|
||||
// This first filtering if statement is take from polyfill to prevent
|
||||
// non-numeric input (for browsers that don't use polyfill because they DO have a number input type).
|
||||
var _ref, _ref1;
|
||||
if (((_ref = e.keyCode) !== 8 && _ref !== 9 && _ref !== 35 && _ref !== 36 && _ref !== 37 && _ref !== 39) &&
|
||||
((_ref1 = e.which) !== 45 && _ref1 !== 46 && _ref1 !== 48 && _ref1 !== 49 && _ref1 !== 50 && _ref1 !== 51
|
||||
&& _ref1 !== 52 && _ref1 !== 53 && _ref1 !== 54 && _ref1 !== 55 && _ref1 !== 56 && _ref1 !== 57)) {
|
||||
e.preventDefault();
|
||||
}
|
||||
// For integers, prevent decimal points.
|
||||
if (this.isIntegerField() && e.keyCode === 46) {
|
||||
e.preventDefault();
|
||||
}
|
||||
},
|
||||
|
||||
changed: function () {
|
||||
// Limit value to the range specified by min and max (necessary for browsers that aren't using polyfill).
|
||||
var value = this.getValueFromEditor();
|
||||
if ((this.max !== undefined) && value > this.max) {
|
||||
value = this.max;
|
||||
} else if ((this.min != undefined) && value < this.min) {
|
||||
value = this.min;
|
||||
}
|
||||
this.setValueInEditor(value);
|
||||
this.updateModel();
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
CMS.Views.Metadata.Option = CMS.Views.Metadata.AbstractEditor.extend({
|
||||
|
||||
events : {
|
||||
"change select" : "updateModel",
|
||||
"click .setting-clear" : "clear"
|
||||
},
|
||||
|
||||
templateName: "metadata-option-entry",
|
||||
|
||||
getValueFromEditor : function () {
|
||||
var selectedText = this.$el.find('#' + this.uniqueId).find(":selected").text();
|
||||
var selectedValue;
|
||||
_.each(this.model.getOptions(), function (modelValue) {
|
||||
if (modelValue === selectedText) {
|
||||
selectedValue = modelValue;
|
||||
}
|
||||
else if (modelValue['display_name'] === selectedText) {
|
||||
selectedValue = modelValue['value'];
|
||||
}
|
||||
});
|
||||
return selectedValue;
|
||||
},
|
||||
|
||||
setValueInEditor : function (value) {
|
||||
// Value here is the json value as used by the field. The choice may instead be showing display names.
|
||||
// Find the display name matching the value passed in.
|
||||
_.each(this.model.getOptions(), function (modelValue) {
|
||||
if (modelValue['value'] === value) {
|
||||
value = modelValue['display_name'];
|
||||
}
|
||||
});
|
||||
this.$el.find('#' + this.uniqueId + " option").filter(function() {
|
||||
return $(this).text() === value;
|
||||
}).prop('selected', true);
|
||||
}
|
||||
});
|
||||
@@ -814,7 +814,7 @@ hr.divide {
|
||||
line-height: 26px;
|
||||
color: $white;
|
||||
pointer-events: none;
|
||||
opacity: 0;
|
||||
opacity: 0.0;
|
||||
|
||||
&:after {
|
||||
content: '▾';
|
||||
|
||||
@@ -149,11 +149,11 @@ abbr[title] {
|
||||
margin-left: 20px;
|
||||
}
|
||||
li {
|
||||
opacity: .8;
|
||||
opacity: 0.8;
|
||||
|
||||
&:ui-state-active {
|
||||
background-color: rgba(255, 255, 255, .3);
|
||||
opacity: 1;
|
||||
opacity: 1.0;
|
||||
font-weight: 400;
|
||||
}
|
||||
a:focus {
|
||||
|
||||
@@ -95,12 +95,12 @@
|
||||
// bounce in
|
||||
@mixin bounceIn {
|
||||
0% {
|
||||
opacity: 0;
|
||||
opacity: 0.0;
|
||||
@include transform(scale(0.3));
|
||||
}
|
||||
|
||||
50% {
|
||||
opacity: 1;
|
||||
opacity: 1.0;
|
||||
@include transform(scale(1.05));
|
||||
}
|
||||
|
||||
@@ -128,12 +128,12 @@
|
||||
// bounce in
|
||||
@mixin bounceOut {
|
||||
0% {
|
||||
opacity: 0;
|
||||
opacity: 0.0;
|
||||
@include transform(scale(0.3));
|
||||
}
|
||||
|
||||
50% {
|
||||
opacity: 1;
|
||||
opacity: 1.0;
|
||||
@include transform(scale(1.05));
|
||||
}
|
||||
|
||||
@@ -146,12 +146,12 @@
|
||||
}
|
||||
|
||||
50% {
|
||||
opacity: 1;
|
||||
opacity: 1.0;
|
||||
@include transform(scale(1.05));
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 0;
|
||||
opacity: 0.0;
|
||||
@include transform(scale(0.3));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -124,7 +124,6 @@ code {
|
||||
|
||||
.CodeMirror {
|
||||
font-size: 13px;
|
||||
border: 1px solid $darkGrey;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
|
||||
@@ -243,7 +243,7 @@
|
||||
left: -7px;
|
||||
top: 47px;
|
||||
width: 140px;
|
||||
opacity: 0;
|
||||
opacity: 0.0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
@@ -558,7 +558,7 @@ body.signin .nav-not-signedin-signup {
|
||||
|
||||
.wrapper-nav-sub {
|
||||
@include transition (opacity 1.0s ease-in-out 0s);
|
||||
opacity: 0;
|
||||
opacity: 0.0;
|
||||
pointer-events: none;
|
||||
|
||||
&.is-shown {
|
||||
|
||||
@@ -627,7 +627,7 @@
|
||||
pointer-events: none;
|
||||
|
||||
.prompt {
|
||||
opacity: 0;
|
||||
opacity: 0.0;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -254,7 +254,7 @@ body.course.checklists {
|
||||
.task-support {
|
||||
@extend .t-copy-sub2;
|
||||
@include transition(opacity .15s .25s ease-in-out);
|
||||
opacity: 0;
|
||||
opacity: 0.0;
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
@@ -267,7 +267,7 @@ body.course.checklists {
|
||||
float: right;
|
||||
width: flex-grid(2,9);
|
||||
margin: ($baseline/2) 0 0 flex-gutter();
|
||||
opacity: 0;
|
||||
opacity: 0.0;
|
||||
pointer-events: none;
|
||||
text-align: right;
|
||||
|
||||
|
||||
@@ -59,7 +59,7 @@ body.dashboard {
|
||||
top: 15px;
|
||||
right: $baseline;
|
||||
padding: ($baseline/4) ($baseline/2);
|
||||
opacity: 0;
|
||||
opacity: 0.0;
|
||||
pointer-events: none;
|
||||
|
||||
&:hover {
|
||||
|
||||
@@ -162,7 +162,7 @@ body.index {
|
||||
position: absolute;
|
||||
bottom: -30px;
|
||||
right: ($baseline/2);
|
||||
opacity: 0;
|
||||
opacity: 0.0;
|
||||
|
||||
[class^="icon-"] {
|
||||
@include font-size(18);
|
||||
|
||||
@@ -21,7 +21,7 @@ body.course.settings {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.message-status {
|
||||
.message-status {
|
||||
display: none;
|
||||
@include border-top-radius(2px);
|
||||
@include box-sizing(border-box);
|
||||
@@ -386,6 +386,11 @@ body.course.settings {
|
||||
#course-overview {
|
||||
height: ($baseline*20);
|
||||
}
|
||||
|
||||
//adds back in CodeMirror border removed due to Unit page styling of component editors
|
||||
.CodeMirror {
|
||||
border: 1px solid $gray-l2;
|
||||
}
|
||||
}
|
||||
|
||||
// specific fields - video
|
||||
@@ -698,7 +703,7 @@ body.course.settings {
|
||||
|
||||
.tip {
|
||||
@include transition (opacity 0.5s ease-in-out 0s);
|
||||
opacity: 0;
|
||||
opacity: 0.0;
|
||||
position: absolute;
|
||||
bottom: ($baseline*1.25);
|
||||
}
|
||||
@@ -713,7 +718,7 @@ body.course.settings {
|
||||
input.error {
|
||||
|
||||
& + .tip {
|
||||
opacity: 0;
|
||||
opacity: 0.0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,38 +41,23 @@ body.course.static-pages {
|
||||
@include edit-box;
|
||||
@include box-shadow(none);
|
||||
display: none;
|
||||
padding: 20px;
|
||||
padding: 0;
|
||||
border-radius: 2px 2px 0 0;
|
||||
|
||||
.metadata_edit {
|
||||
margin-bottom: 20px;
|
||||
font-size: 13px;
|
||||
//Overrides general edit-box mixin
|
||||
.row {
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
|
||||
li {
|
||||
margin-bottom: 10px;
|
||||
// This duplicates the styling from Unit page editing
|
||||
.module-actions {
|
||||
@include box-shadow(inset 0 1px 1px $shadow);
|
||||
padding: 0px 0 10px 10px;
|
||||
background-color: $gray-l6;
|
||||
|
||||
.save-button {
|
||||
margin: ($baseline/2) 8px 0 0;
|
||||
}
|
||||
|
||||
label {
|
||||
display: inline-block;
|
||||
margin-right: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
h3 {
|
||||
margin-bottom: 10px;
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
h5 {
|
||||
margin-bottom: 8px;
|
||||
color: #fff;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.save-button {
|
||||
margin-top: 10px;
|
||||
margin: 15px 8px 0 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -215,3 +200,4 @@ body.course.static-pages {
|
||||
outline: 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ body.course.unit {
|
||||
border: none;
|
||||
|
||||
.rendered-component {
|
||||
padding: 0 20px;
|
||||
padding: 0 $baseline;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -32,7 +32,7 @@ body.course.unit {
|
||||
.unit-body {
|
||||
|
||||
.unit-name-input {
|
||||
padding: 20px 40px;
|
||||
padding: $baseline 2*$baseline;
|
||||
|
||||
label {
|
||||
display: block;
|
||||
@@ -73,15 +73,15 @@ body.course.unit {
|
||||
letter-spacing: 1px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
// ====================
|
||||
|
||||
// Component List Meta
|
||||
.components {
|
||||
|
||||
> li {
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
margin: 20px 40px;
|
||||
|
||||
|
||||
margin: $baseline 2*$baseline;
|
||||
|
||||
.title {
|
||||
margin: 0 0 15px 0;
|
||||
@@ -91,23 +91,26 @@ body.course.unit {
|
||||
}
|
||||
}
|
||||
|
||||
// ====================
|
||||
|
||||
// New Components
|
||||
&.new-component-item {
|
||||
margin: 20px 0px;
|
||||
margin: $baseline 0px;
|
||||
border-top: 1px solid $mediumGrey;
|
||||
box-shadow: 0 2px 1px rgba(182, 182, 182, 0.75) inset;
|
||||
background-color: $lightGrey;
|
||||
margin-bottom: 0px;
|
||||
padding-bottom: 20px;
|
||||
padding-bottom: $baseline;
|
||||
|
||||
.new-component-button {
|
||||
display: block;
|
||||
padding: 20px;
|
||||
padding: $baseline;
|
||||
text-align: center;
|
||||
color: #edf1f5;
|
||||
}
|
||||
|
||||
h5 {
|
||||
margin: 20px 0px;
|
||||
margin: $baseline 0px;
|
||||
color: #fff;
|
||||
font-weight: 600;
|
||||
font-size: 18px;
|
||||
@@ -132,7 +135,7 @@ body.course.unit {
|
||||
height: 100px;
|
||||
color: #fff;
|
||||
margin-right: 15px;
|
||||
margin-bottom: 20px;
|
||||
margin-bottom: $baseline;
|
||||
border-radius: 8px;
|
||||
font-size: 15px;
|
||||
line-height: 14px;
|
||||
@@ -144,7 +147,7 @@ body.course.unit {
|
||||
bottom: 5px;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
padding: $baseline/2;
|
||||
@include box-sizing(border-box);
|
||||
color: #fff;
|
||||
}
|
||||
@@ -153,7 +156,7 @@ body.course.unit {
|
||||
|
||||
.new-component-templates {
|
||||
display: none;
|
||||
margin: 20px 40px 20px 40px;
|
||||
margin: $baseline 2*$baseline;
|
||||
border-radius: 3px;
|
||||
border: 1px solid $mediumGrey;
|
||||
background-color: #fff;
|
||||
@@ -161,7 +164,7 @@ body.course.unit {
|
||||
@include clearfix;
|
||||
|
||||
.cancel-button {
|
||||
margin: 20px 0px 10px 10px;
|
||||
margin: $baseline 0px $baseline/2 $baseline/2;
|
||||
@include white-button;
|
||||
}
|
||||
|
||||
@@ -171,9 +174,9 @@ body.course.unit {
|
||||
|
||||
// specific menu types
|
||||
&.new-component-problem {
|
||||
padding-bottom:10px;
|
||||
padding-bottom: $baseline/2;
|
||||
|
||||
.ss-icon, .editor-indicator {
|
||||
[class^="icon-"], .editor-indicator {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
@@ -208,7 +211,7 @@ body.course.unit {
|
||||
@include box-shadow(0 1px 0 rgba(255, 255, 255, 0.2) inset, 0 -1px 0 rgba(0, 0, 0, 0.2) inset);
|
||||
|
||||
li:first-child {
|
||||
margin-left: 20px;
|
||||
margin-left: $baseline;
|
||||
}
|
||||
|
||||
li {
|
||||
@@ -219,21 +222,21 @@ body.course.unit {
|
||||
@include linear-gradient(top, rgba(255, 255, 255, .4), rgba(255, 255, 255, 0));
|
||||
background-color: tint($lightBluishGrey, 10%);
|
||||
@include box-shadow(0 1px 0 rgba(255, 255, 255, 0.2) inset, 0 -1px 0 rgba(0, 0, 0, 0.2) inset);
|
||||
opacity:.8;
|
||||
opacity: 0.8;
|
||||
|
||||
&:hover {
|
||||
opacity:1;
|
||||
opacity: 0.9;
|
||||
background-color: tint($lightBluishGrey, 20%);
|
||||
}
|
||||
|
||||
&.ui-state-active {
|
||||
border: 0px;
|
||||
@include active;
|
||||
opacity:1;
|
||||
opacity: 1.0;
|
||||
}
|
||||
}
|
||||
|
||||
a{
|
||||
a {
|
||||
display: block;
|
||||
padding: 15px 25px;
|
||||
font-size: 15px;
|
||||
@@ -280,14 +283,14 @@ body.course.unit {
|
||||
a {
|
||||
@include clearfix();
|
||||
display: block;
|
||||
padding: 7px 20px;
|
||||
padding: 7px $baseline;
|
||||
border-bottom: none;
|
||||
font-weight: 500;
|
||||
|
||||
.name {
|
||||
float: left;
|
||||
|
||||
.ss-icon {
|
||||
[class^="icon-"] {
|
||||
@include transition(opacity .15s);
|
||||
display: inline-block;
|
||||
top: 1px;
|
||||
@@ -308,14 +311,14 @@ body.course.unit {
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
.ss-icon, .editor-indicator {
|
||||
[class^="icon-"], .editor-indicator {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: #fff;
|
||||
|
||||
.ss-icon {
|
||||
[class^="icon-"] {
|
||||
opacity: 1.0;
|
||||
}
|
||||
|
||||
@@ -355,6 +358,9 @@ body.course.unit {
|
||||
}
|
||||
}
|
||||
|
||||
// ====================
|
||||
|
||||
// Component Drag and Drop, Non-Edit Module Rendering, Styling
|
||||
.component {
|
||||
border: 1px solid $lightBluishGrey2;
|
||||
border-radius: 3px;
|
||||
@@ -401,7 +407,7 @@ body.course.unit {
|
||||
}
|
||||
|
||||
.xmodule_display {
|
||||
padding: 40px 20px 20px;
|
||||
padding: 2*$baseline $baseline $baseline;
|
||||
overflow-x: auto;
|
||||
|
||||
h1 {
|
||||
@@ -409,36 +415,24 @@ body.course.unit {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
// ====================
|
||||
|
||||
// Component Editing
|
||||
.wrapper-component-editor {
|
||||
z-index: 9999;
|
||||
position: relative;
|
||||
background: $lightBluishGrey2;
|
||||
background: $white;
|
||||
}
|
||||
|
||||
.component-editor {
|
||||
@include edit-box;
|
||||
@include box-shadow(none);
|
||||
display: none;
|
||||
padding: 20px;
|
||||
padding: 0;
|
||||
border-radius: 2px 2px 0 0;
|
||||
|
||||
.metadata_edit {
|
||||
margin-bottom: 20px;
|
||||
font-size: 13px;
|
||||
|
||||
li {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
label {
|
||||
display: inline-block;
|
||||
margin-right: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
h3 {
|
||||
margin-bottom: 10px;
|
||||
margin-bottom: $baseline/2;
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
}
|
||||
@@ -449,214 +443,286 @@ body.course.unit {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.save-button {
|
||||
margin-top: 10px;
|
||||
margin: 15px 8px 0 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.unit-settings {
|
||||
.window-contents {
|
||||
padding: 10px 20px;
|
||||
}
|
||||
|
||||
.unit-actions {
|
||||
border-bottom: none;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.published-alert {
|
||||
display: none;
|
||||
padding: 10px;
|
||||
border: 1px solid #edbd3c;
|
||||
border-radius: 3px;
|
||||
background: #fbf6e1;
|
||||
font-size: 14px;
|
||||
line-height: 1.4;
|
||||
|
||||
div {
|
||||
margin-top: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
input[type="radio"] {
|
||||
margin-right: 7px;
|
||||
}
|
||||
|
||||
.status {
|
||||
font-size: 12px;
|
||||
|
||||
strong {
|
||||
font-weight: 700;
|
||||
}
|
||||
}
|
||||
|
||||
.preview-button, .view-button {
|
||||
@include white-button;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.publish-button {
|
||||
@include orange-button;
|
||||
}
|
||||
|
||||
.delete-button {
|
||||
@include blue-button;
|
||||
}
|
||||
|
||||
.delete-draft {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.delete-button,
|
||||
.preview-button,
|
||||
.publish-button,
|
||||
.view-button {
|
||||
font-size: 11px;
|
||||
margin-top: 10px;
|
||||
padding: 6px 15px 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.unit-history {
|
||||
&.collapsed {
|
||||
h4 {
|
||||
border-bottom: none;
|
||||
border-radius: 3px;
|
||||
.row {
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
|
||||
.window-contents {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
// Module Actions, also used for Static Pages
|
||||
.module-actions {
|
||||
@include box-shadow(inset 0 1px 1px $shadow);
|
||||
padding: 0 0 $baseline $baseline;
|
||||
background-color: $gray-l6;
|
||||
|
||||
ol {
|
||||
border: 1px solid #ced2db;
|
||||
|
||||
li {
|
||||
display: block;
|
||||
padding: 6px 8px 8px 10px;
|
||||
background: #edf1f5;
|
||||
font-size: 12px;
|
||||
|
||||
&:hover {
|
||||
background: #fffcf1;
|
||||
|
||||
.item-actions {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
&.checked {
|
||||
background: #d1dae3;
|
||||
}
|
||||
|
||||
.item-actions {
|
||||
display: none;
|
||||
}
|
||||
|
||||
input[type="radio"] {
|
||||
margin-right: 7px;
|
||||
.save-button {
|
||||
margin: ($baseline/2) 8px 0 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.unit-location {
|
||||
.url {
|
||||
width: 100%;
|
||||
margin-bottom: 10px;
|
||||
@include box-shadow(none);
|
||||
}
|
||||
|
||||
.draft-tag,
|
||||
.hidden-tag,
|
||||
.private-tag,
|
||||
.has-new-draft-tag {
|
||||
font-size: 8px;
|
||||
}
|
||||
|
||||
.window-contents > ol {
|
||||
@include tree-view;
|
||||
|
||||
.section-item {
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
font-size: 11px;
|
||||
padding: 2px 8px 4px;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
@include box-sizing(border-box);
|
||||
}
|
||||
|
||||
ol {
|
||||
.section-item {
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
.new-unit-item {
|
||||
margin-left: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
ol ol {
|
||||
.section-item {
|
||||
padding-left: 34px;
|
||||
}
|
||||
|
||||
.new-unit-item {
|
||||
margin: 0 0 10px 41px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.edit-state-draft {
|
||||
.visibility,
|
||||
|
||||
.edit-draft-message,
|
||||
.view-button {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.published-alert {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.edit-state-public {
|
||||
.delete-draft,
|
||||
.component-actions,
|
||||
.new-component-item,
|
||||
.editing-draft-alert,
|
||||
.publish-draft-message,
|
||||
.preview-button {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.published-alert {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.drag-handle {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
.edit-state-private {
|
||||
.delete-draft,
|
||||
.publish-draft,
|
||||
.editing-draft-alert,
|
||||
.create-draft,
|
||||
.view-button {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// editing units from courseware
|
||||
// Edit Header (Component Name, Mode-Editor, Mode-Settings)
|
||||
.component-edit-header {
|
||||
@include box-sizing(border-box);
|
||||
padding: 18px 0 18px $baseline;
|
||||
top: 0;
|
||||
right: 0;
|
||||
background-color: $blue;
|
||||
border-bottom: 1px solid $blue-d2;
|
||||
color: $white;
|
||||
|
||||
//Component Name
|
||||
.component-name {
|
||||
@extend .t-copy-sub1;
|
||||
width: 50%;
|
||||
color: $white;
|
||||
font-weight: 600;
|
||||
|
||||
em {
|
||||
display: inline-block;
|
||||
margin-right: ($baseline/4);
|
||||
font-weight: 400;
|
||||
color: $white;
|
||||
}
|
||||
}
|
||||
|
||||
//Nav-Edit Modes
|
||||
.nav-edit-modes {
|
||||
list-style: none;
|
||||
right: 0;
|
||||
top: 0;
|
||||
position: absolute;
|
||||
padding: 12px ($baseline*0.75);
|
||||
|
||||
.mode {
|
||||
display: inline-block;
|
||||
margin-left: 8px;
|
||||
|
||||
&.inactive-mode{
|
||||
display: none;
|
||||
}
|
||||
|
||||
&.active-mode a {
|
||||
|
||||
@include blue-button;
|
||||
|
||||
&.is-set {
|
||||
@include transition(box-shadow 0.5 ease-in-out);
|
||||
@include linear-gradient($blue, $blue);
|
||||
color: $blue-d1;
|
||||
box-shadow: inset 0 1px 2px 1px $shadow-l1;
|
||||
background-color: $blue-d4;
|
||||
cursor: default;
|
||||
|
||||
&:hover {
|
||||
box-shadow: inset 0 1px 2px 1px $shadow;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Editor Wrapper
|
||||
.wrapper-comp-editor {
|
||||
display: block;
|
||||
|
||||
// Because the editor may be a CodeMirror editor (which must be visible at the time it is created
|
||||
// in order for it to properly initialize), we must toggle "is-inactive" instead of the more common "is-active".
|
||||
&.is-inactive {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
// Settings Wrapper
|
||||
.wrapper-comp-settings {
|
||||
display: none;
|
||||
|
||||
&.is-active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
//settings-list
|
||||
.list-input.settings-list {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
|
||||
overflow: auto;
|
||||
max-height: 400px;
|
||||
|
||||
//chrome scrollbar visibility correction
|
||||
&::-webkit-scrollbar {
|
||||
-webkit-appearance: none;
|
||||
width: 11px;
|
||||
height: 11px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
border-radius: 8px;
|
||||
border: 2px solid $gray-l2;
|
||||
background-color: rgba(0, 0, 0, .5);
|
||||
}
|
||||
|
||||
//component-setting-entry
|
||||
.field.comp-setting-entry {
|
||||
background-color: $white;
|
||||
padding: $baseline;
|
||||
border-bottom: 1px solid $gray-l2;
|
||||
opacity: 0.7;
|
||||
|
||||
&:last-child {
|
||||
//margin-bottom: 0;
|
||||
}
|
||||
|
||||
//no required component settings currently
|
||||
&.required {
|
||||
label {
|
||||
//font-weight: 600;
|
||||
}
|
||||
label:after {
|
||||
//margin-left: ($baseline/4);
|
||||
//content: "*";
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
@include transition(opacity 0.25s ease-in-out);
|
||||
opacity: 1.0;
|
||||
}
|
||||
|
||||
&.is-set {
|
||||
opacity: 1.0;
|
||||
background-color: $white;
|
||||
|
||||
.setting-input {
|
||||
color: $blue-l1;
|
||||
}
|
||||
}
|
||||
|
||||
.wrapper-comp-setting{
|
||||
display: inline-block;
|
||||
min-width: 300px;
|
||||
width: 45%;
|
||||
top: 0;
|
||||
vertical-align: top;
|
||||
margin-bottom:5px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
label.setting-label {
|
||||
@extend .t-copy-sub1;
|
||||
@include transition(color, 0.15s, ease-in-out);
|
||||
font-weight: 400;
|
||||
vertical-align: middle;
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
left: 0;
|
||||
min-width: 100px;
|
||||
width: 35%;
|
||||
|
||||
&.is-focused {
|
||||
color: $blue;
|
||||
}
|
||||
}
|
||||
|
||||
input, select, input[type="number"] {
|
||||
@include placeholder($gray-l4);
|
||||
@include font-size(16);
|
||||
@include size(100%,100%);
|
||||
padding: ($baseline/2);
|
||||
min-width: 100px;
|
||||
width: 45%;
|
||||
|
||||
//&.long {
|
||||
//
|
||||
//}
|
||||
|
||||
//&.short {
|
||||
//}
|
||||
|
||||
//&.error {
|
||||
// border-color: $red;
|
||||
//}
|
||||
|
||||
//&:focus {
|
||||
// + .tip {
|
||||
// color: $gray;
|
||||
// }
|
||||
border-radius: 3px;
|
||||
border: 1px solid $gray-l2;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
input[type="number"] {
|
||||
|
||||
width: 38.5%;
|
||||
@include box-shadow(0 1px 2px $shadow-l1 inset);
|
||||
//For webkit browsers which render number fields differently, make input wider.
|
||||
-moz-column-width: {
|
||||
width: 32%;
|
||||
}
|
||||
|
||||
&:active {
|
||||
background-color: #FFFCF1;
|
||||
}
|
||||
}
|
||||
|
||||
select {
|
||||
//@include box-shadow(0 1px 2px $shadow-l1 inset);
|
||||
|
||||
&:focus {
|
||||
@include box-shadow(0 0 1px $shadow);
|
||||
@include transition(opacity 0.25s ease-in-out);
|
||||
background-color: $yellow;
|
||||
}
|
||||
|
||||
&:active {
|
||||
background-color: $yellow;
|
||||
}
|
||||
}
|
||||
|
||||
.action.setting-clear {
|
||||
@include font-size(11);
|
||||
color: $gray;
|
||||
width: 25px;
|
||||
height: 25px;
|
||||
vertical-align: middle;
|
||||
padding: 5px;
|
||||
border-radius: 50%;
|
||||
margin: 0 $baseline/2;
|
||||
box-shadow: none;
|
||||
text-shadow: none;
|
||||
border: 1px solid $gray-l1;
|
||||
background-color: $gray-l4;
|
||||
|
||||
&:hover {
|
||||
@include box-shadow(0 1px 1px $shadow);
|
||||
@include transition(opacity 0.15s ease-in-out);
|
||||
background-color: $blue-s3;
|
||||
border: 1px solid $blue-s3;
|
||||
color: $white;
|
||||
}
|
||||
|
||||
&.inactive {
|
||||
visibility: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
.tip.setting-help {
|
||||
@include font-size(12);
|
||||
display: inline-block;
|
||||
font-color: $gray-l6;
|
||||
min-width: 260px;
|
||||
width: 50%;
|
||||
vertical-align: top;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// ====================
|
||||
|
||||
// Editing Units from Courseware
|
||||
body.unit {
|
||||
|
||||
.component {
|
||||
@@ -678,3 +744,224 @@ body.unit {
|
||||
}
|
||||
}
|
||||
}
|
||||
// ====================
|
||||
|
||||
// Unit Page Sidebar
|
||||
.unit-settings {
|
||||
.window-contents {
|
||||
padding: $baseline/2 $baseline;
|
||||
}
|
||||
|
||||
.unit-actions {
|
||||
border-bottom: none;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.published-alert {
|
||||
display: none;
|
||||
padding: $baseline/2;
|
||||
border: 1px solid #edbd3c;
|
||||
border-radius: 3px;
|
||||
background: #fbf6e1;
|
||||
font-size: 14px;
|
||||
line-height: 1.4;
|
||||
|
||||
div {
|
||||
margin-top: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
input[type="radio"] {
|
||||
margin-right: 7px;
|
||||
}
|
||||
|
||||
.status {
|
||||
font-size: 12px;
|
||||
|
||||
strong {
|
||||
font-weight: 700;
|
||||
}
|
||||
}
|
||||
|
||||
.preview-button, .view-button {
|
||||
@include white-button;
|
||||
margin-bottom: $baseline/2;
|
||||
}
|
||||
|
||||
.publish-button {
|
||||
@include orange-button;
|
||||
}
|
||||
|
||||
.delete-button {
|
||||
@include blue-button;
|
||||
}
|
||||
|
||||
.delete-draft {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.delete-button,
|
||||
.preview-button,
|
||||
.publish-button,
|
||||
.view-button {
|
||||
font-size: 11px;
|
||||
margin-top: $baseline/2;
|
||||
padding: 6px 15px 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.unit-history {
|
||||
&.collapsed {
|
||||
h4 {
|
||||
border-bottom: none;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.window-contents {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
ol {
|
||||
border: 1px solid #ced2db;
|
||||
|
||||
li {
|
||||
display: block;
|
||||
padding: 6px 8px 8px $baseline/2;
|
||||
background: #edf1f5;
|
||||
font-size: 12px;
|
||||
|
||||
&:hover {
|
||||
background: #fffcf1;
|
||||
|
||||
.item-actions {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
&.checked {
|
||||
background: #d1dae3;
|
||||
}
|
||||
|
||||
.item-actions {
|
||||
display: none;
|
||||
}
|
||||
|
||||
input[type="radio"] {
|
||||
margin-right: 7px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.unit-location {
|
||||
.url {
|
||||
@include box-shadow(none);
|
||||
width: 100%;
|
||||
margin-bottom: $baseline/2;
|
||||
}
|
||||
|
||||
.draft-tag,
|
||||
.hidden-tag,
|
||||
.private-tag,
|
||||
.has-new-draft-tag {
|
||||
font-size: 8px;
|
||||
}
|
||||
|
||||
.window-contents > ol {
|
||||
@include tree-view;
|
||||
|
||||
.section-item {
|
||||
@include box-sizing(border-box);
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
font-size: 11px;
|
||||
padding: 2px 8px 4px;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
ol {
|
||||
.section-item {
|
||||
padding-left: $baseline;
|
||||
}
|
||||
|
||||
.new-unit-item {
|
||||
margin-left: $baseline;
|
||||
}
|
||||
}
|
||||
|
||||
ol ol {
|
||||
.section-item {
|
||||
padding-left: 34px;
|
||||
}
|
||||
|
||||
.new-unit-item {
|
||||
margin: 0 0 $baseline 41px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.edit-state-draft {
|
||||
.visibility,
|
||||
|
||||
.edit-draft-message,
|
||||
.view-button {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.published-alert {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.edit-state-public {
|
||||
.delete-draft,
|
||||
.component-actions,
|
||||
.new-component-item,
|
||||
.editing-draft-alert,
|
||||
.publish-draft-message,
|
||||
.preview-button {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.published-alert {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.drag-handle {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
.edit-state-private {
|
||||
.delete-draft,
|
||||
.publish-draft,
|
||||
.editing-draft-alert,
|
||||
.create-draft,
|
||||
.view-button {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
// ====================
|
||||
|
||||
// Latex Compiler
|
||||
.launch-latex-compiler {
|
||||
background-color: $white;
|
||||
padding: $baseline/2 0 $baseline/2 $baseline;
|
||||
border-bottom: 1px solid $gray-l2;
|
||||
opacity: 0.8;
|
||||
|
||||
|
||||
&:hover {
|
||||
@include transition(opacity 0.25s ease-in-out);
|
||||
opacity: 1.0s;
|
||||
}
|
||||
}
|
||||
|
||||
// hides latex compiler button if settings mode is-active
|
||||
div.wrapper-comp-editor.is-inactive + div.launch-latex-compiler{
|
||||
display: none;
|
||||
}
|
||||
|
||||
@@ -212,6 +212,7 @@ body.course.updates {
|
||||
@include edit-box;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
z-index: 10001;
|
||||
width: 800px;
|
||||
padding: 30px;
|
||||
|
||||
@@ -1,19 +1,41 @@
|
||||
<%! from django.utils.translation import ugettext as _ %>
|
||||
<%namespace name='static' file='static_content.html'/>
|
||||
|
||||
<script type="text/javascript" src="${static.url('js/models/metadata_model.js')}"></script>
|
||||
<script type="text/javascript" src="${static.url('js/views/metadata_editor_view.js')}"></script>
|
||||
<script src="${static.url('js/vendor/html5-input-polyfills/number-polyfill.js')}"></script>
|
||||
<link rel="stylesheet" type="text/css" href="${static.url('css/vendor/html5-input-polyfills/number-polyfill.css')}" />
|
||||
|
||||
<div class="wrapper wrapper-component-editor">
|
||||
<div class="component-editor">
|
||||
<div class="module-editor">
|
||||
${editor}
|
||||
</div>
|
||||
<div class="row module-actions">
|
||||
<a href="#" class="save-button">Save</a>
|
||||
<a href="#" class="cancel-button">Cancel</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="component-edit-header">
|
||||
<span class="component-name"></span>
|
||||
<ul class="nav-edit-modes">
|
||||
<li id="editor-mode" class="mode active-mode" aria-controls="editor-tab" role="tab">
|
||||
<a href="#">${_("Editor")}</a>
|
||||
</li>
|
||||
<li id="settings-mode" class="mode active-mode" aria-controls="settings-tab" role="tab">
|
||||
<a href="#">${_("Settings")}</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div> <!-- Editor Header -->
|
||||
|
||||
<div class="component-edit-modes">
|
||||
<div class="module-editor">
|
||||
${editor}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row module-actions">
|
||||
<a href="#" class="save-button">${_("Save")}</a>
|
||||
<a href="#" class="cancel-button">${_("Cancel")}</a>
|
||||
</div> <!-- Module Actions-->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="component-actions">
|
||||
<a href="#" class="edit-button standard"><span class="edit-icon"></span>Edit</a>
|
||||
<a href="#" class="delete-button standard"><span class="delete-icon"></span>Delete</a>
|
||||
<a href="#" class="edit-button standard"><span class="edit-icon"></span>${_("Edit")}</a>
|
||||
<a href="#" class="delete-button standard"><span class="delete-icon"></span>${_("Delete")}</a>
|
||||
</div>
|
||||
<a data-tooltip="Drag to reorder" href="#" class="drag-handle"></a>
|
||||
<a data-tooltip='${_("Drag to reorder")}' href="#" class="drag-handle"></a>
|
||||
${preview}
|
||||
|
||||
|
||||
6
cms/templates/js/metadata-editor.underscore
Normal file
6
cms/templates/js/metadata-editor.underscore
Normal file
@@ -0,0 +1,6 @@
|
||||
<ul class="list-input settings-list">
|
||||
<% _.each(_.range(numEntries), function() { %>
|
||||
<li class="field comp-setting-entry metadata_entry" id="settings-listing">
|
||||
</li>
|
||||
<% }) %>
|
||||
</ul>
|
||||
8
cms/templates/js/metadata-number-entry.underscore
Normal file
8
cms/templates/js/metadata-number-entry.underscore
Normal file
@@ -0,0 +1,8 @@
|
||||
<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>
|
||||
16
cms/templates/js/metadata-option-entry.underscore
Normal file
16
cms/templates/js/metadata-option-entry.underscore
Normal file
@@ -0,0 +1,16 @@
|
||||
<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>
|
||||
8
cms/templates/js/metadata-string-entry.underscore
Normal file
8
cms/templates/js/metadata-string-entry.underscore
Normal file
@@ -0,0 +1,8 @@
|
||||
<div class="wrapper-comp-setting">
|
||||
<label class="label setting-label" for="<%= uniqueId %>"><%= model.get('display_name') %></label>
|
||||
<input class="input setting-input" type="text" id="<%= uniqueId %>" value='<%= model.get("value") %>'/>
|
||||
<button class="action setting-clear inactive" type="button" name="setting-clear" value="<%= gettext("Clear") %>" data-tooltip="<%= gettext("Clear") %>">
|
||||
<i class="icon-undo"></i><span class="sr">"<%= gettext("Clear Value") %>"</span>
|
||||
</button>
|
||||
</div>
|
||||
<span class="tip setting-help"><%= model.get('help') %></span>
|
||||
@@ -78,22 +78,13 @@
|
||||
% endif
|
||||
<div class="tab current" id="tab1">
|
||||
<ul class="new-component-template">
|
||||
% for name, location, has_markdown, is_empty in templates:
|
||||
% for name, location, has_markdown in templates:
|
||||
% if has_markdown or type != "problem":
|
||||
% if is_empty:
|
||||
<li class="editor-md empty">
|
||||
<a href="#" data-location="${location}">
|
||||
<span class="name"> ${name}</span>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
% else:
|
||||
<li class="editor-md">
|
||||
<a href="#" data-location="${location}">
|
||||
<span class="name"> ${name}</span>
|
||||
</a>
|
||||
</li>
|
||||
% endif
|
||||
<li class="editor-md">
|
||||
<a href="#" id="${location}" data-location="${location}">
|
||||
<span class="name"> ${name}</span>
|
||||
</a>
|
||||
</li>
|
||||
% endif
|
||||
|
||||
%endfor
|
||||
@@ -102,23 +93,14 @@
|
||||
% if type == "problem":
|
||||
<div class="tab" id="tab2">
|
||||
<ul class="new-component-template">
|
||||
% for name, location, has_markdown, is_empty in templates:
|
||||
% for name, location, has_markdown in templates:
|
||||
% if not has_markdown:
|
||||
% if is_empty:
|
||||
<li class="editor-manual empty">
|
||||
<a href="#" data-location="${location}">
|
||||
<span class="name">${name}</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="editor-manual">
|
||||
<a href="#" id="${location}" data-location="${location}">
|
||||
<span class="name"> ${name}</span>
|
||||
</a>
|
||||
|
||||
% else:
|
||||
<li class="editor-manual">
|
||||
<a href="#" data-location="${location}">
|
||||
<span class="name"> ${name}</span>
|
||||
</a>
|
||||
|
||||
</li>
|
||||
% endif
|
||||
</li>
|
||||
% endif
|
||||
% endfor
|
||||
</ul>
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
<%! from django.utils.translation import ugettext as _ %>
|
||||
|
||||
<div class="wrapper-comp-editor" id="editor-tab">
|
||||
<section class="html-editor editor">
|
||||
<ul class="editor-tabs">
|
||||
<li><a href="#" class="visual-tab tab current" data-tab="visual">${_("Visual")}</a></li>
|
||||
<li><a href="#" class="html-tab tab" data-tab="advanced">${_("HTML")}</a></li>
|
||||
</ul>
|
||||
|
||||
<div class="row">
|
||||
<textarea class="tiny-mce">${data | h}</textarea>
|
||||
<textarea name="" class="edit-box">${data | h}</textarea>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
<%include file="metadata-edit.html" />
|
||||
<section class="html-editor editor">
|
||||
<ul class="editor-tabs">
|
||||
<li><a href="#" class="visual-tab tab current" data-tab="visual">Visual</a></li>
|
||||
<li><a href="#" class="html-tab tab" data-tab="advanced">HTML</a></li>
|
||||
</ul>
|
||||
|
||||
<div class="row">
|
||||
<textarea class="tiny-mce">${data | h}</textarea>
|
||||
<textarea name="" class="edit-box">${data | h}</textarea>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -1,49 +1,43 @@
|
||||
<%! from django.utils.translation import ugettext as _ %>
|
||||
<%namespace name='static' file='../static_content.html'/>
|
||||
|
||||
<%
|
||||
import hashlib
|
||||
from xmodule.fields import StringyInteger, StringyFloat
|
||||
import copy
|
||||
import json
|
||||
hlskey = hashlib.md5(module.location.url()).hexdigest()
|
||||
%>
|
||||
<section class="metadata_edit">
|
||||
<ul>
|
||||
% for field_name, field_value in editable_metadata_fields.items():
|
||||
<li>
|
||||
% if field_name == 'source_code':
|
||||
% if field_value['explicitly_set'] is True:
|
||||
<a href="#hls-modal-${hlskey}" style="color:yellow;" id="hls-trig-${hlskey}" >Edit High Level Source</a>
|
||||
% endif
|
||||
% else:
|
||||
<label>${field_value['field'].display_name}:</label>
|
||||
<input type='text' data-metadata-name='${field_value["field"].display_name}'
|
||||
## This is a hack to keep current behavior for weight and attempts (empty will parse OK as unset).
|
||||
## This hack will go away with our custom editors.
|
||||
% if field_value["value"] == None and (isinstance(field_value["field"], StringyFloat) or isinstance(field_value["field"], StringyInteger)):
|
||||
value = ''
|
||||
% else:
|
||||
value='${field_value["field"].to_json(field_value["value"])}'
|
||||
% endif
|
||||
size='60' />
|
||||
## Change to True to see all the information being passed through.
|
||||
% if False:
|
||||
<label>Help: ${field_value['field'].help}</label>
|
||||
<label>Type: ${type(field_value['field']).__name__}</label>
|
||||
<label>Inheritable: ${field_value['inheritable']}</label>
|
||||
<label>Showing inherited value: ${field_value['inheritable'] and not field_value['explicitly_set']}</label>
|
||||
<label>Explicitly set: ${field_value['explicitly_set']}</label>
|
||||
<label>Default value: ${field_value['default_value']}</label>
|
||||
% if field_value['field'].values:
|
||||
<label>Possible values:</label>
|
||||
% for value in field_value['field'].values:
|
||||
<label>${value}</label>
|
||||
% endfor
|
||||
% endif
|
||||
% endif
|
||||
% endif
|
||||
</li>
|
||||
% endfor
|
||||
</ul>
|
||||
|
||||
% if 'source_code' in editable_metadata_fields and editable_metadata_fields['source_code']['explicitly_set']:
|
||||
## js templates
|
||||
<script id="metadata-editor-tpl" type="text/template">
|
||||
<%static:include path="js/metadata-editor.underscore" />
|
||||
</script>
|
||||
|
||||
<script id="metadata-number-entry" type="text/template">
|
||||
<%static:include path="js/metadata-number-entry.underscore" />
|
||||
</script>
|
||||
|
||||
<script id="metadata-string-entry" type="text/template">
|
||||
<%static:include path="js/metadata-string-entry.underscore" />
|
||||
</script>
|
||||
|
||||
<script id="metadata-option-entry" type="text/template">
|
||||
<%static:include path="js/metadata-option-entry.underscore" />
|
||||
</script>
|
||||
|
||||
<% showHighLevelSource='source_code' in editable_metadata_fields and editable_metadata_fields['source_code']['explicitly_set'] %>
|
||||
<% metadata_field_copy = copy.copy(editable_metadata_fields) %>
|
||||
## Delete 'source_code' field (if it exists) so metadata editor view does not attempt to render it.
|
||||
% if 'source_code' in editable_metadata_fields:
|
||||
## source-edit.html needs access to the 'source_code' value, so delete from a copy.
|
||||
<% del metadata_field_copy['source_code'] %>
|
||||
% endif
|
||||
|
||||
% if showHighLevelSource:
|
||||
<div class="launch-latex-compiler">
|
||||
<a href="#hls-modal-${hlskey}" id="hls-trig-${hlskey}">${_("Launch Latex Source Compiler")}</a>
|
||||
</div>
|
||||
<%include file="source-edit.html" />
|
||||
% endif
|
||||
% endif
|
||||
|
||||
</section>
|
||||
<div class="wrapper-comp-settings metadata_edit" id="settings-tab" data-metadata='${json.dumps(metadata_field_copy) | h}'/>
|
||||
@@ -1,4 +1,4 @@
|
||||
<%include file="metadata-edit.html" />
|
||||
<div class="wrapper-comp-editor" id="editor-tab">
|
||||
<section class="combinedopenended-editor editor">
|
||||
<div class="row">
|
||||
%if enable_markdown:
|
||||
@@ -93,3 +93,5 @@
|
||||
</div>
|
||||
</article>
|
||||
</script>
|
||||
</div>
|
||||
<%include file="metadata-edit.html" />
|
||||
|
||||
@@ -1,27 +1,29 @@
|
||||
<%include file="metadata-edit.html" />
|
||||
<%! from django.utils.translation import ugettext as _ %>
|
||||
|
||||
<div class="wrapper-comp-editor" id="editor-tab">
|
||||
<section class="problem-editor editor">
|
||||
<div class="row">
|
||||
%if enable_markdown:
|
||||
<div class="editor-bar">
|
||||
<ul class="format-buttons">
|
||||
<li><a href="#" class="header-button" data-tooltip="Heading 1"><span
|
||||
<li><a href="#" class="header-button" data-tooltip='${_("Heading 1")}'><span
|
||||
class="problem-editor-icon heading1"></span></a></li>
|
||||
<li><a href="#" class="multiple-choice-button" data-tooltip="Multiple Choice"><span
|
||||
<li><a href="#" class="multiple-choice-button" data-tooltip='${_("Multiple Choice")}'><span
|
||||
class="problem-editor-icon multiple-choice"></span></a></li>
|
||||
<li><a href="#" class="checks-button" data-tooltip="Checkboxes"><span
|
||||
<li><a href="#" class="checks-button" data-tooltip='${_("Checkboxes")}'><span
|
||||
class="problem-editor-icon checks"></span></a></li>
|
||||
<li><a href="#" class="string-button" data-tooltip="Text Input"><span
|
||||
<li><a href="#" class="string-button" data-tooltip='${_("Text Input")}'><span
|
||||
class="problem-editor-icon string"></span></a></li>
|
||||
<li><a href="#" class="number-button" data-tooltip="Numerical Input"><span
|
||||
<li><a href="#" class="number-button" data-tooltip='${_("Numerical Input")}'><span
|
||||
class="problem-editor-icon number"></span></a></li>
|
||||
<li><a href="#" class="dropdown-button" data-tooltip="Dropdown"><span
|
||||
<li><a href="#" class="dropdown-button" data-tooltip='${_("Dropdown")}'><span
|
||||
class="problem-editor-icon dropdown"></span></a></li>
|
||||
<li><a href="#" class="explanation-button" data-tooltip="Explanation"><span
|
||||
<li><a href="#" class="explanation-button" data-tooltip='${_("Explanation")}'><span
|
||||
class="problem-editor-icon explanation"></span></a></li>
|
||||
</ul>
|
||||
<ul class="editor-tabs">
|
||||
<li><a href="#" class="xml-tab advanced-toggle" data-tab="xml">Advanced Editor</a></li>
|
||||
<li><a href="#" class="cheatsheet-toggle" data-tooltip="Toggle Cheatsheet">?</a></li>
|
||||
<li><a href="#" class="xml-tab advanced-toggle" data-tab="xml">${_("Advanced Editor")}</a></li>
|
||||
<li><a href="#" class="cheatsheet-toggle" data-tooltip='${_("Toggle Cheatsheet")}'>?</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<textarea class="markdown-box">${markdown | h}</textarea>
|
||||
@@ -34,7 +36,7 @@
|
||||
<article class="simple-editor-cheatsheet">
|
||||
<div class="cheatsheet-wrapper">
|
||||
<div class="row">
|
||||
<h6>Heading 1</h6>
|
||||
<h6>${_("Heading 1")}</h6>
|
||||
<div class="col sample heading-1">
|
||||
<img src="/static/img/header-example.png" />
|
||||
</div>
|
||||
@@ -45,7 +47,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<h6>Multiple Choice</h6>
|
||||
<h6>${_("Multiple Choice")}</h6>
|
||||
<div class="col sample multiple-choice">
|
||||
<img src="/static/img/choice-example.png" />
|
||||
</div>
|
||||
@@ -56,7 +58,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<h6>Checkboxes</h6>
|
||||
<h6>${_("Checkboxes")}</h6>
|
||||
<div class="col sample check-multiple">
|
||||
<img src="/static/img/multi-example.png" />
|
||||
</div>
|
||||
@@ -67,7 +69,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<h6>Text Input</h6>
|
||||
<h6>${_("Text Input")}</h6>
|
||||
<div class="col sample string-response">
|
||||
<img src="/static/img/string-example.png" />
|
||||
</div>
|
||||
@@ -76,7 +78,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<h6>Numerical Input</h6>
|
||||
<h6>${_("Numerical Input")}</h6>
|
||||
<div class="col sample numerical-response">
|
||||
<img src="/static/img/number-example.png" />
|
||||
</div>
|
||||
@@ -85,7 +87,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<h6>Dropdown</h6>
|
||||
<h6>${_("Dropdown")}</h6>
|
||||
<div class="col sample option-reponse">
|
||||
<img src="/static/img/select-example.png" />
|
||||
</div>
|
||||
@@ -94,7 +96,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<h6>Explanation</h6>
|
||||
<h6>${_("Explanation")}</h6>
|
||||
<div class="col sample explanation">
|
||||
<img src="/static/img/explanation-example.png" />
|
||||
</div>
|
||||
@@ -105,3 +107,5 @@
|
||||
</div>
|
||||
</article>
|
||||
</script>
|
||||
</div>
|
||||
<%include file="metadata-edit.html" />
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
<div class="wrapper-comp-editor" id="editor-tab">
|
||||
<section class="raw-edit">
|
||||
<textarea name="" class="edit-box" rows="8" cols="40">${data | h}</textarea>
|
||||
</section>
|
||||
</div>
|
||||
<%include file="metadata-edit.html" />
|
||||
<section class="raw-edit">
|
||||
<textarea name="" class="edit-box" rows="8" cols="40">${data | h}</textarea>
|
||||
</section>
|
||||
|
||||
@@ -29,7 +29,6 @@
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<%include file="metadata-edit.html" />
|
||||
<div class="content">
|
||||
<section class="modules">
|
||||
<ol>
|
||||
@@ -50,5 +49,6 @@
|
||||
</ol>
|
||||
</section>
|
||||
</div>
|
||||
<%include file="metadata-edit.html" />
|
||||
</section>
|
||||
|
||||
|
||||
@@ -28,4 +28,4 @@ class CmsNamespace(Namespace):
|
||||
"""
|
||||
published_date = DateTuple(help="Date when the module was published", scope=Scope.settings)
|
||||
published_by = String(help="Id of the user who published this module", scope=Scope.settings)
|
||||
empty = StringyBoolean(help="Whether this is an empty template", scope=Scope.settings, default=False)
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ class UserFactory(sf.UserFactory):
|
||||
"""
|
||||
User account for lms / cms
|
||||
"""
|
||||
FACTORY_DJANGO_GET_OR_CREATE = ('username',)
|
||||
pass
|
||||
|
||||
|
||||
@@ -21,6 +22,7 @@ class UserProfileFactory(sf.UserProfileFactory):
|
||||
"""
|
||||
Demographics etc for the User
|
||||
"""
|
||||
FACTORY_DJANGO_GET_OR_CREATE = ('user',)
|
||||
pass
|
||||
|
||||
|
||||
@@ -29,6 +31,7 @@ class RegistrationFactory(sf.RegistrationFactory):
|
||||
"""
|
||||
Activation key for registering the user account
|
||||
"""
|
||||
FACTORY_DJANGO_GET_OR_CREATE = ('user',)
|
||||
pass
|
||||
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
from lettuce import world
|
||||
import time
|
||||
from urllib import quote_plus
|
||||
from selenium.common.exceptions import WebDriverException
|
||||
from selenium.common.exceptions import WebDriverException, StaleElementReferenceException
|
||||
from selenium.webdriver.support import expected_conditions as EC
|
||||
from selenium.webdriver.common.by import By
|
||||
from selenium.webdriver.support.ui import WebDriverWait
|
||||
@@ -63,7 +63,7 @@ def css_click(css_selector):
|
||||
# Occassionally, MathJax or other JavaScript can cover up
|
||||
# an element temporarily.
|
||||
# If this happens, wait a second, then try again
|
||||
time.sleep(1)
|
||||
world.wait(1)
|
||||
world.browser.find_by_css(css_selector).click()
|
||||
|
||||
|
||||
@@ -79,6 +79,14 @@ def css_click_at(css, x=10, y=10):
|
||||
e.action_chains.perform()
|
||||
|
||||
|
||||
@world.absorb
|
||||
def id_click(elem_id):
|
||||
"""
|
||||
Perform a click on an element as specified by its id
|
||||
"""
|
||||
world.css_click('#%s' % elem_id)
|
||||
|
||||
|
||||
@world.absorb
|
||||
def css_fill(css_selector, text):
|
||||
world.browser.find_by_css(css_selector).first.fill(text)
|
||||
@@ -94,7 +102,12 @@ def css_text(css_selector):
|
||||
|
||||
# Wait for the css selector to appear
|
||||
if world.is_css_present(css_selector):
|
||||
return world.browser.find_by_css(css_selector).first.text
|
||||
try:
|
||||
return world.browser.find_by_css(css_selector).first.text
|
||||
except StaleElementReferenceException:
|
||||
# The DOM was still redrawing. Wait a second and try again.
|
||||
world.wait(1)
|
||||
return world.browser.find_by_css(css_selector).first.text
|
||||
else:
|
||||
return ""
|
||||
|
||||
|
||||
@@ -66,22 +66,51 @@ class ComplexEncoder(json.JSONEncoder):
|
||||
|
||||
class CapaFields(object):
|
||||
attempts = StringyInteger(help="Number of attempts taken by the student on this problem", default=0, scope=Scope.user_state)
|
||||
max_attempts = StringyInteger(help="Maximum number of attempts that a student is allowed", scope=Scope.settings)
|
||||
max_attempts = StringyInteger(
|
||||
display_name="Maximum Attempts",
|
||||
help="Defines the number of times a student can try to answer this problem. If the value is not set, infinite attempts are allowed.",
|
||||
values={"min": 1}, scope=Scope.settings
|
||||
)
|
||||
due = Date(help="Date that this problem is due by", scope=Scope.settings)
|
||||
graceperiod = Timedelta(help="Amount of time after the due date that submissions will be accepted", scope=Scope.settings)
|
||||
showanswer = String(help="When to show the problem answer to the student", scope=Scope.settings, default="closed",
|
||||
values=["answered", "always", "attempted", "closed", "never"])
|
||||
showanswer = String(
|
||||
display_name="Show Answer",
|
||||
help="Defines when to show the answer to the problem. A default value can be set in Advanced Settings.",
|
||||
scope=Scope.settings, default="closed",
|
||||
values=[
|
||||
{"display_name": "Always", "value": "always"},
|
||||
{"display_name": "Answered", "value": "answered"},
|
||||
{"display_name": "Attempted", "value": "attempted"},
|
||||
{"display_name": "Closed", "value": "closed"},
|
||||
{"display_name": "Finished", "value": "finished"},
|
||||
{"display_name": "Past Due", "value": "past_due"},
|
||||
{"display_name": "Never", "value": "never"}]
|
||||
)
|
||||
force_save_button = Boolean(help="Whether to force the save button to appear on the page", scope=Scope.settings, default=False)
|
||||
rerandomize = Randomization(help="When to rerandomize the problem", default="always", scope=Scope.settings)
|
||||
rerandomize = Randomization(
|
||||
display_name="Randomization", help="Defines how often inputs are randomized when a student loads the problem. This setting only applies to problems that can have randomly generated numeric values. A default value can be set in Advanced Settings.",
|
||||
default="always", scope=Scope.settings, values=[{"display_name": "Always", "value": "always"},
|
||||
{"display_name": "On Reset", "value": "onreset"},
|
||||
{"display_name": "Never", "value": "never"},
|
||||
{"display_name": "Per Student", "value": "per_student"}]
|
||||
)
|
||||
data = String(help="XML data for the problem", scope=Scope.content)
|
||||
correct_map = Object(help="Dictionary with the correctness of current student answers", scope=Scope.user_state, default={})
|
||||
input_state = Object(help="Dictionary for maintaining the state of inputtypes", scope=Scope.user_state)
|
||||
student_answers = Object(help="Dictionary with the current student responses", scope=Scope.user_state)
|
||||
done = Boolean(help="Whether the student has answered the problem", scope=Scope.user_state)
|
||||
seed = StringyInteger(help="Random seed for this student", scope=Scope.user_state)
|
||||
weight = StringyFloat(help="How much to weight this problem by", scope=Scope.settings)
|
||||
weight = StringyFloat(
|
||||
display_name="Problem Weight",
|
||||
help="Defines the number of points each problem is worth. If the value is not set, each response field in the problem is worth one point.",
|
||||
values={"min": 0, "step": .1},
|
||||
scope=Scope.settings
|
||||
)
|
||||
markdown = String(help="Markdown source of this module", scope=Scope.settings)
|
||||
source_code = String(help="Source code for LaTeX and Word problems. This feature is not well-supported.", scope=Scope.settings)
|
||||
source_code = String(
|
||||
help="Source code for LaTeX and Word problems. This feature is not well-supported.",
|
||||
scope=Scope.settings
|
||||
)
|
||||
|
||||
|
||||
class CapaModule(CapaFields, XModule):
|
||||
|
||||
@@ -5,7 +5,7 @@ from pkg_resources import resource_string
|
||||
|
||||
from xmodule.raw_module import RawDescriptor
|
||||
from .x_module import XModule
|
||||
from xblock.core import Integer, Scope, String, Boolean, List
|
||||
from xblock.core import Integer, Scope, String, List
|
||||
from xmodule.open_ended_grading_classes.combined_open_ended_modulev1 import CombinedOpenEndedV1Module, CombinedOpenEndedV1Descriptor
|
||||
from collections import namedtuple
|
||||
from .fields import Date, StringyFloat, StringyInteger, StringyBoolean
|
||||
@@ -48,27 +48,49 @@ class VersionInteger(Integer):
|
||||
|
||||
|
||||
class CombinedOpenEndedFields(object):
|
||||
display_name = String(help="Display name for this module", default="Open Ended Grading", scope=Scope.settings)
|
||||
display_name = String(
|
||||
display_name="Display Name",
|
||||
help="This name appears in the horizontal navigation at the top of the page.",
|
||||
default="Open Ended Grading", scope=Scope.settings
|
||||
)
|
||||
current_task_number = StringyInteger(help="Current task that the student is on.", default=0, scope=Scope.user_state)
|
||||
task_states = List(help="List of state dictionaries of each task within this module.", scope=Scope.user_state)
|
||||
state = String(help="Which step within the current task that the student is on.", default="initial",
|
||||
scope=Scope.user_state)
|
||||
student_attempts = StringyInteger(help="Number of attempts taken by the student on this problem", default=0,
|
||||
scope=Scope.user_state)
|
||||
ready_to_reset = StringyBoolean(help="If the problem is ready to be reset or not.", default=False,
|
||||
scope=Scope.user_state)
|
||||
attempts = StringyInteger(help="Maximum number of attempts that a student is allowed.", default=1, scope=Scope.settings)
|
||||
is_graded = StringyBoolean(help="Whether or not the problem is graded.", default=False, scope=Scope.settings)
|
||||
accept_file_upload = StringyBoolean(help="Whether or not the problem accepts file uploads.", default=False,
|
||||
scope=Scope.settings)
|
||||
skip_spelling_checks = StringyBoolean(help="Whether or not to skip initial spelling checks.", default=True,
|
||||
scope=Scope.settings)
|
||||
ready_to_reset = StringyBoolean(
|
||||
help="If the problem is ready to be reset or not.", default=False,
|
||||
scope=Scope.user_state
|
||||
)
|
||||
attempts = StringyInteger(
|
||||
display_name="Maximum Attempts",
|
||||
help="The number of times the student can try to answer this problem.", default=1,
|
||||
scope=Scope.settings, values = {"min" : 1 }
|
||||
)
|
||||
is_graded = StringyBoolean(display_name="Graded", help="Whether or not the problem is graded.", default=False, scope=Scope.settings)
|
||||
accept_file_upload = StringyBoolean(
|
||||
display_name="Allow File Uploads",
|
||||
help="Whether or not the student can submit files as a response.", default=False, scope=Scope.settings
|
||||
)
|
||||
skip_spelling_checks = StringyBoolean(
|
||||
display_name="Disable Quality Filter",
|
||||
help="If False, the Quality Filter is enabled and submissions with poor spelling, short length, or poor grammar will not be peer reviewed.",
|
||||
default=False, scope=Scope.settings
|
||||
)
|
||||
due = Date(help="Date that this problem is due by", default=None, scope=Scope.settings)
|
||||
graceperiod = String(help="Amount of time after the due date that submissions will be accepted", default=None,
|
||||
scope=Scope.settings)
|
||||
graceperiod = String(
|
||||
help="Amount of time after the due date that submissions will be accepted",
|
||||
default=None,
|
||||
scope=Scope.settings
|
||||
)
|
||||
version = VersionInteger(help="Current version number", default=DEFAULT_VERSION, scope=Scope.settings)
|
||||
data = String(help="XML data for the problem", scope=Scope.content)
|
||||
weight = StringyFloat(help="How much to weight this problem by", scope=Scope.settings)
|
||||
weight = StringyFloat(
|
||||
display_name="Problem Weight",
|
||||
help="Defines the number of points each problem is worth. If the value is not set, each problem is worth one point.",
|
||||
scope=Scope.settings, values = {"min" : 0 , "step": ".1"}
|
||||
)
|
||||
markdown = String(help="Markdown source of this module", scope=Scope.settings)
|
||||
|
||||
|
||||
@@ -244,6 +266,6 @@ class CombinedOpenEndedDescriptor(CombinedOpenEndedFields, RawDescriptor):
|
||||
def non_editable_metadata_fields(self):
|
||||
non_editable_fields = super(CombinedOpenEndedDescriptor, self).non_editable_metadata_fields
|
||||
non_editable_fields.extend([CombinedOpenEndedDescriptor.due, CombinedOpenEndedDescriptor.graceperiod,
|
||||
CombinedOpenEndedDescriptor.markdown])
|
||||
CombinedOpenEndedDescriptor.markdown, CombinedOpenEndedDescriptor.version])
|
||||
return non_editable_fields
|
||||
|
||||
|
||||
@@ -10,8 +10,6 @@
|
||||
position: relative;
|
||||
@include linear-gradient(top, #d4dee8, #c9d5e2);
|
||||
padding: 5px;
|
||||
border: 1px solid #3c3c3c;
|
||||
border-radius: 3px 3px 0 0;
|
||||
border-bottom-color: #a5aaaf;
|
||||
@include clearfix;
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
.advanced-toggle {
|
||||
@include white-button;
|
||||
height: auto;
|
||||
margin-top: -1px;
|
||||
margin-top: -4px;
|
||||
padding: 3px 9px;
|
||||
font-size: 12px;
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
color: $darkGrey !important;
|
||||
pointer-events: none;
|
||||
cursor: none;
|
||||
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 0 0 0 !important;
|
||||
}
|
||||
@@ -27,7 +27,7 @@
|
||||
width: 21px;
|
||||
height: 21px;
|
||||
padding: 0;
|
||||
margin: 0 5px 0 15px;
|
||||
margin: -1px 5px 0 15px;
|
||||
border-radius: 22px;
|
||||
border: 1px solid #a5aaaf;
|
||||
background: #e5ecf3;
|
||||
@@ -99,6 +99,13 @@
|
||||
}
|
||||
}
|
||||
|
||||
.problem-editor {
|
||||
// adding padding to simple editor only - adjacent selector is needed since there are no toggles for CodeMirror
|
||||
.markdown-box+.CodeMirror {
|
||||
padding: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.problem-editor-icon {
|
||||
display: inline-block;
|
||||
width: 26px;
|
||||
|
||||
@@ -170,7 +170,7 @@ nav.sequence-nav {
|
||||
font-family: $sans-serif;
|
||||
line-height: lh();
|
||||
left: 0px;
|
||||
opacity: 0;
|
||||
opacity: 0.0;
|
||||
padding: 6px;
|
||||
position: absolute;
|
||||
top: 48px;
|
||||
@@ -204,7 +204,7 @@ nav.sequence-nav {
|
||||
p {
|
||||
display: block;
|
||||
margin-top: 4px;
|
||||
opacity: 1;
|
||||
opacity: 1.0;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -248,12 +248,12 @@ nav.sequence-nav {
|
||||
}
|
||||
|
||||
&:hover {
|
||||
opacity: .5;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
cursor: normal;
|
||||
opacity: .4;
|
||||
opacity: 0.4;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -320,12 +320,12 @@ nav.sequence-bottom {
|
||||
outline: 0;
|
||||
|
||||
&:hover {
|
||||
opacity: .5;
|
||||
opacity: 0.5;
|
||||
background-position: center 15px;
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
opacity: .4;
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
|
||||
@@ -41,7 +41,7 @@ div.video {
|
||||
|
||||
&:hover {
|
||||
ul, div {
|
||||
opacity: 1;
|
||||
opacity: 1.0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -158,7 +158,7 @@ div.video {
|
||||
|
||||
ol.video_speeds {
|
||||
display: block;
|
||||
opacity: 1;
|
||||
opacity: 1.0;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
list-style: none;
|
||||
@@ -208,7 +208,7 @@ div.video {
|
||||
}
|
||||
|
||||
&:hover, &:active, &:focus {
|
||||
opacity: 1;
|
||||
opacity: 1.0;
|
||||
background-color: #444;
|
||||
}
|
||||
}
|
||||
@@ -221,7 +221,7 @@ div.video {
|
||||
border: 1px solid #000;
|
||||
bottom: 46px;
|
||||
display: none;
|
||||
opacity: 0;
|
||||
opacity: 0.0;
|
||||
position: absolute;
|
||||
width: 133px;
|
||||
z-index: 10;
|
||||
@@ -264,7 +264,7 @@ div.video {
|
||||
&.open {
|
||||
.volume-slider-container {
|
||||
display: block;
|
||||
opacity: 1;
|
||||
opacity: 1.0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -302,7 +302,7 @@ div.video {
|
||||
border: 1px solid #000;
|
||||
bottom: 46px;
|
||||
display: none;
|
||||
opacity: 0;
|
||||
opacity: 0.0;
|
||||
position: absolute;
|
||||
width: 45px;
|
||||
height: 125px;
|
||||
@@ -395,7 +395,7 @@ div.video {
|
||||
font-weight: 800;
|
||||
line-height: 46px; //height of play pause buttons
|
||||
margin-left: 0;
|
||||
opacity: 1;
|
||||
opacity: 1.0;
|
||||
padding: 0 lh(.5);
|
||||
position: relative;
|
||||
text-indent: -9999px;
|
||||
@@ -410,7 +410,7 @@ div.video {
|
||||
}
|
||||
|
||||
&.off {
|
||||
opacity: .7;
|
||||
opacity: 0.7;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -418,7 +418,7 @@ div.video {
|
||||
|
||||
&:hover section.video-controls {
|
||||
ul, div {
|
||||
opacity: 1;
|
||||
opacity: 1.0;
|
||||
}
|
||||
|
||||
div.slider {
|
||||
|
||||
@@ -41,7 +41,7 @@ div.video {
|
||||
|
||||
&:hover {
|
||||
ul, div {
|
||||
opacity: 1;
|
||||
opacity: 1.0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -158,7 +158,7 @@ div.video {
|
||||
|
||||
ol.video_speeds {
|
||||
display: block;
|
||||
opacity: 1;
|
||||
opacity: 1.0;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
list-style: none;
|
||||
@@ -208,7 +208,7 @@ div.video {
|
||||
}
|
||||
|
||||
&:hover, &:active, &:focus {
|
||||
opacity: 1;
|
||||
opacity: 1.0;
|
||||
background-color: #444;
|
||||
}
|
||||
}
|
||||
@@ -221,7 +221,7 @@ div.video {
|
||||
border: 1px solid #000;
|
||||
bottom: 46px;
|
||||
display: none;
|
||||
opacity: 0;
|
||||
opacity: 0.0;
|
||||
position: absolute;
|
||||
width: 133px;
|
||||
z-index: 10;
|
||||
@@ -264,7 +264,7 @@ div.video {
|
||||
&.open {
|
||||
.volume-slider-container {
|
||||
display: block;
|
||||
opacity: 1;
|
||||
opacity: 1.0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -302,7 +302,7 @@ div.video {
|
||||
border: 1px solid #000;
|
||||
bottom: 46px;
|
||||
display: none;
|
||||
opacity: 0;
|
||||
opacity: 0.0;
|
||||
position: absolute;
|
||||
width: 45px;
|
||||
height: 125px;
|
||||
@@ -395,7 +395,7 @@ div.video {
|
||||
font-weight: 800;
|
||||
line-height: 46px; //height of play pause buttons
|
||||
margin-left: 0;
|
||||
opacity: 1;
|
||||
opacity: 1.0;
|
||||
padding: 0 lh(.5);
|
||||
position: relative;
|
||||
text-indent: -9999px;
|
||||
@@ -410,7 +410,7 @@ div.video {
|
||||
}
|
||||
|
||||
&.off {
|
||||
opacity: .7;
|
||||
opacity: 0.7;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -418,7 +418,7 @@ div.video {
|
||||
|
||||
&:hover section.video-controls {
|
||||
ul, div {
|
||||
opacity: 1;
|
||||
opacity: 1.0;
|
||||
}
|
||||
|
||||
div.slider {
|
||||
|
||||
@@ -8,8 +8,16 @@ from xblock.core import String, Scope
|
||||
|
||||
class DiscussionFields(object):
|
||||
discussion_id = String(scope=Scope.settings)
|
||||
discussion_category = String(scope=Scope.settings)
|
||||
discussion_target = String(scope=Scope.settings)
|
||||
discussion_category = String(
|
||||
display_name="Category",
|
||||
help="A category name for the discussion. This name appears in the left pane of the discussion forum for the course.",
|
||||
scope=Scope.settings
|
||||
)
|
||||
discussion_target = String(
|
||||
display_name="Subcategory",
|
||||
help="A subcategory name for the discussion. This name appears in the left pane of the discussion forum for the course.",
|
||||
scope=Scope.settings
|
||||
)
|
||||
sort_key = String(scope=Scope.settings)
|
||||
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ from .x_module import XModule
|
||||
from xmodule.raw_module import RawDescriptor
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from .timeinfo import TimeInfo
|
||||
from xblock.core import Object, Integer, Boolean, String, Scope
|
||||
from xblock.core import Object, String, Scope
|
||||
from xmodule.fields import Date, StringyFloat, StringyInteger, StringyBoolean
|
||||
|
||||
from xmodule.open_ended_grading_classes.peer_grading_service import PeerGradingService, GradingServiceError, MockPeerGradingService
|
||||
@@ -22,24 +22,43 @@ USE_FOR_SINGLE_LOCATION = False
|
||||
LINK_TO_LOCATION = ""
|
||||
TRUE_DICT = [True, "True", "true", "TRUE"]
|
||||
MAX_SCORE = 1
|
||||
IS_GRADED = True
|
||||
IS_GRADED = False
|
||||
|
||||
EXTERNAL_GRADER_NO_CONTACT_ERROR = "Failed to contact external graders. Please notify course staff."
|
||||
|
||||
|
||||
class PeerGradingFields(object):
|
||||
use_for_single_location = StringyBoolean(help="Whether to use this for a single location or as a panel.",
|
||||
default=USE_FOR_SINGLE_LOCATION, scope=Scope.settings)
|
||||
link_to_location = String(help="The location this problem is linked to.", default=LINK_TO_LOCATION,
|
||||
scope=Scope.settings)
|
||||
is_graded = StringyBoolean(help="Whether or not this module is scored.", default=IS_GRADED, scope=Scope.settings)
|
||||
use_for_single_location = StringyBoolean(
|
||||
display_name="Show Single Problem",
|
||||
help='When True, only the single problem specified by "Link to Problem Location" is shown. '
|
||||
'When False, a panel is displayed with all problems available for peer grading.',
|
||||
default=USE_FOR_SINGLE_LOCATION, scope=Scope.settings
|
||||
)
|
||||
link_to_location = String(
|
||||
display_name="Link to Problem Location",
|
||||
help='The location of the problem being graded. Only used when "Show Single Problem" is True.',
|
||||
default=LINK_TO_LOCATION, scope=Scope.settings
|
||||
)
|
||||
is_graded = StringyBoolean(
|
||||
display_name="Graded",
|
||||
help='Defines whether the student gets credit for grading this problem. Only used when "Show Single Problem" is True.',
|
||||
default=IS_GRADED, scope=Scope.settings
|
||||
)
|
||||
due_date = Date(help="Due date that should be displayed.", default=None, scope=Scope.settings)
|
||||
grace_period_string = String(help="Amount of grace to give on the due date.", default=None, scope=Scope.settings)
|
||||
max_grade = StringyInteger(help="The maximum grade that a student can receieve for this problem.", default=MAX_SCORE,
|
||||
scope=Scope.settings)
|
||||
student_data_for_location = Object(help="Student data for a given peer grading problem.",
|
||||
scope=Scope.user_state)
|
||||
weight = StringyFloat(help="How much to weight this problem by", scope=Scope.settings)
|
||||
max_grade = StringyInteger(
|
||||
help="The maximum grade that a student can receive for this problem.", default=MAX_SCORE,
|
||||
scope=Scope.settings, values={"min": 0}
|
||||
)
|
||||
student_data_for_location = Object(
|
||||
help="Student data for a given peer grading problem.",
|
||||
scope=Scope.user_state
|
||||
)
|
||||
weight = StringyFloat(
|
||||
display_name="Problem Weight",
|
||||
help="Defines the number of points each problem is worth. If the value is not set, each problem is worth one point.",
|
||||
scope=Scope.settings, values={"min": 0, "step": ".1"}
|
||||
)
|
||||
|
||||
|
||||
class PeerGradingModule(PeerGradingFields, XModule):
|
||||
@@ -590,3 +609,11 @@ class PeerGradingDescriptor(PeerGradingFields, RawDescriptor):
|
||||
|
||||
#Specify whether or not to pass in open ended interface
|
||||
needs_open_ended_interface = True
|
||||
|
||||
@property
|
||||
def non_editable_metadata_fields(self):
|
||||
non_editable_fields = super(PeerGradingDescriptor, self).non_editable_metadata_fields
|
||||
non_editable_fields.extend([PeerGradingFields.due_date, PeerGradingFields.grace_period_string,
|
||||
PeerGradingFields.max_grade])
|
||||
return non_editable_fields
|
||||
|
||||
|
||||
@@ -1,12 +1,6 @@
|
||||
---
|
||||
metadata:
|
||||
display_name: Open Ended Response
|
||||
attempts: 1
|
||||
is_graded: False
|
||||
version: 1
|
||||
skip_spelling_checks: False
|
||||
accept_file_upload: False
|
||||
weight: ""
|
||||
markdown: ""
|
||||
data: |
|
||||
<combinedopenended>
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
---
|
||||
metadata:
|
||||
display_name: Blank HTML Page
|
||||
empty: True
|
||||
|
||||
data: |
|
||||
|
||||
|
||||
@@ -1,11 +1,7 @@
|
||||
---
|
||||
metadata:
|
||||
display_name: Peer Grading Interface
|
||||
use_for_single_location: False
|
||||
link_to_location: None
|
||||
is_graded: False
|
||||
max_grade: 1
|
||||
weight: ""
|
||||
data: |
|
||||
<peergrading>
|
||||
</peergrading>
|
||||
|
||||
@@ -3,9 +3,7 @@
|
||||
metadata:
|
||||
display_name: Circuit Schematic Builder
|
||||
rerandomize: never
|
||||
showanswer: always
|
||||
weight: ""
|
||||
attempts: ""
|
||||
showanswer: finished
|
||||
data: |
|
||||
<problem >
|
||||
Please make a voltage divider that splits the provided voltage evenly.
|
||||
|
||||
@@ -2,9 +2,7 @@
|
||||
metadata:
|
||||
display_name: Custom Python-Evaluated Input
|
||||
rerandomize: never
|
||||
showanswer: always
|
||||
weight: ""
|
||||
attempts: ""
|
||||
showanswer: finished
|
||||
data: |
|
||||
<problem>
|
||||
<p>
|
||||
|
||||
@@ -2,11 +2,8 @@
|
||||
metadata:
|
||||
display_name: Blank Common Problem
|
||||
rerandomize: never
|
||||
showanswer: always
|
||||
showanswer: finished
|
||||
markdown: ""
|
||||
weight: ""
|
||||
empty: True
|
||||
attempts: ""
|
||||
data: |
|
||||
<problem>
|
||||
</problem>
|
||||
|
||||
@@ -2,10 +2,7 @@
|
||||
metadata:
|
||||
display_name: Blank Advanced Problem
|
||||
rerandomize: never
|
||||
showanswer: always
|
||||
weight: ""
|
||||
attempts: ""
|
||||
empty: True
|
||||
showanswer: finished
|
||||
data: |
|
||||
<problem>
|
||||
</problem>
|
||||
|
||||
@@ -2,9 +2,7 @@
|
||||
metadata:
|
||||
display_name: Math Expression Input
|
||||
rerandomize: never
|
||||
showanswer: always
|
||||
weight: ""
|
||||
attempts: ""
|
||||
showanswer: finished
|
||||
data: |
|
||||
<problem>
|
||||
<p>
|
||||
|
||||
@@ -2,9 +2,7 @@
|
||||
metadata:
|
||||
display_name: Image Mapped Input
|
||||
rerandomize: never
|
||||
showanswer: always
|
||||
weight: ""
|
||||
attempts: ""
|
||||
showanswer: finished
|
||||
data: |
|
||||
<problem>
|
||||
<p>
|
||||
|
||||
@@ -2,9 +2,7 @@
|
||||
metadata:
|
||||
display_name: Multiple Choice
|
||||
rerandomize: never
|
||||
showanswer: always
|
||||
weight: ""
|
||||
attempts: ""
|
||||
showanswer: finished
|
||||
markdown:
|
||||
"A multiple choice problem presents radio buttons for student input. Students can only select a single
|
||||
option presented. Multiple Choice questions have been the subject of many areas of research due to the early
|
||||
|
||||
@@ -2,9 +2,7 @@
|
||||
metadata:
|
||||
display_name: Numerical Input
|
||||
rerandomize: never
|
||||
showanswer: always
|
||||
weight: ""
|
||||
attempts: ""
|
||||
showanswer: finished
|
||||
markdown:
|
||||
"A numerical input problem accepts a line of text input from the
|
||||
student, and evaluates the input for correctness based on its
|
||||
|
||||
@@ -2,9 +2,7 @@
|
||||
metadata:
|
||||
display_name: Dropdown
|
||||
rerandomize: never
|
||||
showanswer: always
|
||||
weight: ""
|
||||
attempts: ""
|
||||
showanswer: finished
|
||||
markdown:
|
||||
"Dropdown problems give a limited set of options for students to respond with, and present those options
|
||||
in a format that encourages them to search for a specific answer rather than being immediately presented
|
||||
|
||||
@@ -2,9 +2,7 @@
|
||||
metadata:
|
||||
display_name: Text Input
|
||||
rerandomize: never
|
||||
showanswer: always
|
||||
weight: ""
|
||||
attempts: ""
|
||||
showanswer: finished
|
||||
# Note, the extra newlines are needed to make the yaml parser add blank lines instead of folding
|
||||
markdown:
|
||||
"A text input problem accepts a line of text from the
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
---
|
||||
metadata:
|
||||
display_name: Word cloud
|
||||
version: 1
|
||||
num_inputs: 5
|
||||
num_top_words: 250
|
||||
display_student_percents: True
|
||||
data: {}
|
||||
children: []
|
||||
|
||||
@@ -460,8 +460,8 @@ class ImportTestCase(BaseCourseTestCase):
|
||||
)
|
||||
module = modulestore.get_instance(course.id, location)
|
||||
self.assertEqual(len(module.get_children()), 0)
|
||||
self.assertEqual(module.num_inputs, '5')
|
||||
self.assertEqual(module.num_top_words, '250')
|
||||
self.assertEqual(module.num_inputs, 5)
|
||||
self.assertEqual(module.num_top_words, 250)
|
||||
|
||||
def test_cohort_config(self):
|
||||
"""
|
||||
|
||||
@@ -1,69 +1,141 @@
|
||||
# disable missing docstring
|
||||
#pylint: disable=C0111
|
||||
|
||||
from xmodule.x_module import XModuleFields
|
||||
from xblock.core import Scope, String, Object
|
||||
from xmodule.fields import Date, StringyInteger
|
||||
from xblock.core import Scope, String, Object, Boolean
|
||||
from xmodule.fields import Date, StringyInteger, StringyFloat
|
||||
from xmodule.xml_module import XmlDescriptor
|
||||
import unittest
|
||||
from . import test_system
|
||||
from .import test_system
|
||||
from mock import Mock
|
||||
|
||||
|
||||
class CrazyJsonString(String):
|
||||
def to_json(self, value):
|
||||
return value + " JSON"
|
||||
|
||||
|
||||
class TestFields(object):
|
||||
# Will be returned by editable_metadata_fields.
|
||||
max_attempts = StringyInteger(scope=Scope.settings, default=1000)
|
||||
max_attempts = StringyInteger(scope=Scope.settings, default=1000, values={'min': 1, 'max': 10})
|
||||
# Will not be returned by editable_metadata_fields because filtered out by non_editable_metadata_fields.
|
||||
due = Date(scope=Scope.settings)
|
||||
# Will not be returned by editable_metadata_fields because is not Scope.settings.
|
||||
student_answers = Object(scope=Scope.user_state)
|
||||
# Will be returned, and can override the inherited value from XModule.
|
||||
display_name = String(scope=Scope.settings, default='local default')
|
||||
display_name = String(scope=Scope.settings, default='local default', display_name='Local Display Name',
|
||||
help='local help')
|
||||
# Used for testing select type, effect of to_json method
|
||||
string_select = CrazyJsonString(
|
||||
scope=Scope.settings,
|
||||
default='default value',
|
||||
values=[{'display_name': 'first', 'value': 'value a'},
|
||||
{'display_name': 'second', 'value': 'value b'}]
|
||||
)
|
||||
# Used for testing select type
|
||||
float_select = StringyFloat(scope=Scope.settings, default=.999, values=[1.23, 0.98])
|
||||
# Used for testing float type
|
||||
float_non_select = StringyFloat(scope=Scope.settings, default=.999, values={'min': 0, 'step': .3})
|
||||
# Used for testing that Booleans get mapped to select type
|
||||
boolean_select = Boolean(scope=Scope.settings)
|
||||
|
||||
|
||||
class EditableMetadataFieldsTest(unittest.TestCase):
|
||||
|
||||
def test_display_name_field(self):
|
||||
editable_fields = self.get_xml_editable_fields({})
|
||||
# Tests that the xblock fields (currently tags and name) get filtered out.
|
||||
# Also tests that xml_attributes is filtered out of XmlDescriptor.
|
||||
self.assertEqual(1, len(editable_fields), "Expected only 1 editable field for xml descriptor.")
|
||||
self.assert_field_values(editable_fields, 'display_name', XModuleFields.display_name,
|
||||
explicitly_set=False, inheritable=False, value=None, default_value=None)
|
||||
self.assert_field_values(
|
||||
editable_fields, 'display_name', XModuleFields.display_name,
|
||||
explicitly_set=False, inheritable=False, value=None, default_value=None
|
||||
)
|
||||
|
||||
def test_override_default(self):
|
||||
# Tests that explicitly_set is correct when a value overrides the default (not inheritable).
|
||||
editable_fields = self.get_xml_editable_fields({'display_name': 'foo'})
|
||||
self.assert_field_values(editable_fields, 'display_name', XModuleFields.display_name,
|
||||
explicitly_set=True, inheritable=False, value='foo', default_value=None)
|
||||
self.assert_field_values(
|
||||
editable_fields, 'display_name', XModuleFields.display_name,
|
||||
explicitly_set=True, inheritable=False, value='foo', default_value=None
|
||||
)
|
||||
|
||||
def test_additional_field(self):
|
||||
descriptor = self.get_descriptor({'max_attempts' : '7'})
|
||||
def test_integer_field(self):
|
||||
descriptor = self.get_descriptor({'max_attempts': '7'})
|
||||
editable_fields = descriptor.editable_metadata_fields
|
||||
self.assertEqual(2, len(editable_fields))
|
||||
self.assert_field_values(editable_fields, 'max_attempts', TestFields.max_attempts,
|
||||
explicitly_set=True, inheritable=False, value=7, default_value=1000)
|
||||
self.assert_field_values(editable_fields, 'display_name', XModuleFields.display_name,
|
||||
explicitly_set=False, inheritable=False, value='local default', default_value='local default')
|
||||
self.assertEqual(6, len(editable_fields))
|
||||
self.assert_field_values(
|
||||
editable_fields, 'max_attempts', TestFields.max_attempts,
|
||||
explicitly_set=True, inheritable=False, value=7, default_value=1000, type='Integer',
|
||||
options=TestFields.max_attempts.values
|
||||
)
|
||||
self.assert_field_values(
|
||||
editable_fields, 'display_name', TestFields.display_name,
|
||||
explicitly_set=False, inheritable=False, value='local default', default_value='local default'
|
||||
)
|
||||
|
||||
editable_fields = self.get_descriptor({}).editable_metadata_fields
|
||||
self.assert_field_values(editable_fields, 'max_attempts', TestFields.max_attempts,
|
||||
explicitly_set=False, inheritable=False, value=1000, default_value=1000)
|
||||
self.assert_field_values(
|
||||
editable_fields, 'max_attempts', TestFields.max_attempts,
|
||||
explicitly_set=False, inheritable=False, value=1000, default_value=1000, type='Integer',
|
||||
options=TestFields.max_attempts.values
|
||||
)
|
||||
|
||||
def test_inherited_field(self):
|
||||
model_val = {'display_name' : 'inherited'}
|
||||
model_val = {'display_name': 'inherited'}
|
||||
descriptor = self.get_descriptor(model_val)
|
||||
# Mimic an inherited value for display_name (inherited and inheritable are the same in this case).
|
||||
descriptor._inherited_metadata = model_val
|
||||
descriptor._inheritable_metadata = model_val
|
||||
editable_fields = descriptor.editable_metadata_fields
|
||||
self.assert_field_values(editable_fields, 'display_name', XModuleFields.display_name,
|
||||
explicitly_set=False, inheritable=True, value='inherited', default_value='inherited')
|
||||
self.assert_field_values(
|
||||
editable_fields, 'display_name', TestFields.display_name,
|
||||
explicitly_set=False, inheritable=True, value='inherited', default_value='inherited'
|
||||
)
|
||||
|
||||
descriptor = self.get_descriptor({'display_name' : 'explicit'})
|
||||
descriptor = self.get_descriptor({'display_name': 'explicit'})
|
||||
# Mimic the case where display_name WOULD have been inherited, except we explicitly set it.
|
||||
descriptor._inheritable_metadata = {'display_name' : 'inheritable value'}
|
||||
descriptor._inheritable_metadata = {'display_name': 'inheritable value'}
|
||||
descriptor._inherited_metadata = {}
|
||||
editable_fields = descriptor.editable_metadata_fields
|
||||
self.assert_field_values(editable_fields, 'display_name', XModuleFields.display_name,
|
||||
explicitly_set=True, inheritable=True, value='explicit', default_value='inheritable value')
|
||||
self.assert_field_values(
|
||||
editable_fields, 'display_name', TestFields.display_name,
|
||||
explicitly_set=True, inheritable=True, value='explicit', default_value='inheritable value'
|
||||
)
|
||||
|
||||
def test_type_and_options(self):
|
||||
# test_display_name_field verifies that a String field is of type "Generic".
|
||||
# test_integer_field verifies that a StringyInteger field is of type "Integer".
|
||||
|
||||
descriptor = self.get_descriptor({})
|
||||
editable_fields = descriptor.editable_metadata_fields
|
||||
|
||||
# Tests for select
|
||||
self.assert_field_values(
|
||||
editable_fields, 'string_select', TestFields.string_select,
|
||||
explicitly_set=False, inheritable=False, value='default value', default_value='default value',
|
||||
type='Select', options=[{'display_name': 'first', 'value': 'value a JSON'},
|
||||
{'display_name': 'second', 'value': 'value b JSON'}]
|
||||
)
|
||||
|
||||
self.assert_field_values(
|
||||
editable_fields, 'float_select', TestFields.float_select,
|
||||
explicitly_set=False, inheritable=False, value=.999, default_value=.999,
|
||||
type='Select', options=[1.23, 0.98]
|
||||
)
|
||||
|
||||
self.assert_field_values(
|
||||
editable_fields, 'boolean_select', TestFields.boolean_select,
|
||||
explicitly_set=False, inheritable=False, value=None, default_value=None,
|
||||
type='Select', options=[{'display_name': "True", "value": True}, {'display_name': "False", "value": False}]
|
||||
)
|
||||
|
||||
# Test for float
|
||||
self.assert_field_values(
|
||||
editable_fields, 'float_non_select', TestFields.float_non_select,
|
||||
explicitly_set=False, inheritable=False, value=.999, default_value=.999,
|
||||
type='Float', options={'min': 0, 'step': .3}
|
||||
)
|
||||
|
||||
|
||||
# Start of helper methods
|
||||
def get_xml_editable_fields(self, model_data):
|
||||
@@ -73,7 +145,6 @@ class EditableMetadataFieldsTest(unittest.TestCase):
|
||||
|
||||
def get_descriptor(self, model_data):
|
||||
class TestModuleDescriptor(TestFields, XmlDescriptor):
|
||||
|
||||
@property
|
||||
def non_editable_metadata_fields(self):
|
||||
non_editable_fields = super(TestModuleDescriptor, self).non_editable_metadata_fields
|
||||
@@ -84,10 +155,19 @@ class EditableMetadataFieldsTest(unittest.TestCase):
|
||||
system.render_template = Mock(return_value="<div>Test Template HTML</div>")
|
||||
return TestModuleDescriptor(system=system, location=None, model_data=model_data)
|
||||
|
||||
def assert_field_values(self, editable_fields, name, field, explicitly_set, inheritable, value, default_value):
|
||||
def assert_field_values(self, editable_fields, name, field, explicitly_set, inheritable, value, default_value,
|
||||
type='Generic', options=[]):
|
||||
test_field = editable_fields[name]
|
||||
self.assertEqual(field, test_field['field'])
|
||||
|
||||
self.assertEqual(field.name, test_field['field_name'])
|
||||
self.assertEqual(field.display_name, test_field['display_name'])
|
||||
self.assertEqual(field.help, test_field['help'])
|
||||
|
||||
self.assertEqual(field.to_json(value), test_field['value'])
|
||||
self.assertEqual(field.to_json(default_value), test_field['default_value'])
|
||||
|
||||
self.assertEqual(options, test_field['options'])
|
||||
self.assertEqual(type, test_field['type'])
|
||||
|
||||
self.assertEqual(explicitly_set, test_field['explicitly_set'])
|
||||
self.assertEqual(inheritable, test_field['inheritable'])
|
||||
self.assertEqual(value, test_field['value'])
|
||||
self.assertEqual(default_value, test_field['default_value'])
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
generate and view word cloud.
|
||||
|
||||
On the client side we show:
|
||||
If student does not yet anwered - `num_inputs` numbers of text inputs.
|
||||
If student does not yet answered - `num_inputs` numbers of text inputs.
|
||||
If student have answered - words he entered and cloud.
|
||||
"""
|
||||
|
||||
@@ -14,7 +14,8 @@ from xmodule.raw_module import RawDescriptor
|
||||
from xmodule.editing_module import MetadataOnlyEditingDescriptor
|
||||
from xmodule.x_module import XModule
|
||||
|
||||
from xblock.core import Scope, String, Object, Boolean, List, Integer
|
||||
from xblock.core import Scope, Object, Boolean, List
|
||||
from fields import StringyBoolean, StringyInteger
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
@@ -31,22 +32,23 @@ def pretty_bool(value):
|
||||
|
||||
class WordCloudFields(object):
|
||||
"""XFields for word cloud."""
|
||||
display_name = String(
|
||||
help="Display name for this module",
|
||||
scope=Scope.settings
|
||||
)
|
||||
num_inputs = Integer(
|
||||
help="Number of inputs.",
|
||||
num_inputs = StringyInteger(
|
||||
display_name="Inputs",
|
||||
help="Number of text boxes available for students to input words/sentences.",
|
||||
scope=Scope.settings,
|
||||
default=5
|
||||
default=5,
|
||||
values={"min": 1}
|
||||
)
|
||||
num_top_words = Integer(
|
||||
help="Number of max words, which will be displayed.",
|
||||
num_top_words = StringyInteger(
|
||||
display_name="Maximum Words",
|
||||
help="Maximum number of words to be displayed in generated word cloud.",
|
||||
scope=Scope.settings,
|
||||
default=250
|
||||
default=250,
|
||||
values={"min": 1}
|
||||
)
|
||||
display_student_percents = Boolean(
|
||||
help="Display usage percents for each word?",
|
||||
display_student_percents = StringyBoolean(
|
||||
display_name="Show Percents",
|
||||
help="Statistics are shown for entered words near that word.",
|
||||
scope=Scope.settings,
|
||||
default=True
|
||||
)
|
||||
@@ -205,7 +207,7 @@ class WordCloudModule(WordCloudFields, XModule):
|
||||
# Update top_words.
|
||||
self.top_words = self.top_dict(
|
||||
temp_all_words,
|
||||
int(self.num_top_words)
|
||||
self.num_top_words
|
||||
)
|
||||
|
||||
# Save all_words in database.
|
||||
@@ -226,7 +228,7 @@ class WordCloudModule(WordCloudFields, XModule):
|
||||
'element_id': self.location.html_id(),
|
||||
'element_class': self.location.category,
|
||||
'ajax_url': self.system.ajax_url,
|
||||
'num_inputs': int(self.num_inputs),
|
||||
'num_inputs': self.num_inputs,
|
||||
'submitted': self.submitted
|
||||
}
|
||||
self.content = self.system.render_template('word_cloud.html', context)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import logging
|
||||
import copy
|
||||
import yaml
|
||||
import os
|
||||
|
||||
@@ -9,7 +10,7 @@ from pkg_resources import resource_listdir, resource_string, resource_isdir
|
||||
from xmodule.modulestore import Location
|
||||
from xmodule.modulestore.exceptions import ItemNotFoundError
|
||||
|
||||
from xblock.core import XBlock, Scope, String
|
||||
from xblock.core import XBlock, Scope, String, Integer, Float
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
@@ -75,12 +76,13 @@ class HTMLSnippet(object):
|
||||
"""
|
||||
raise NotImplementedError(
|
||||
"get_html() must be provided by specific modules - not present in {0}"
|
||||
.format(self.__class__))
|
||||
.format(self.__class__))
|
||||
|
||||
|
||||
class XModuleFields(object):
|
||||
display_name = String(
|
||||
help="Display name for this module",
|
||||
display_name="Display Name",
|
||||
help="This name appears in the horizontal navigation at the top of the page.",
|
||||
scope=Scope.settings,
|
||||
default=None
|
||||
)
|
||||
@@ -356,7 +358,7 @@ class XModuleDescriptor(XModuleFields, HTMLSnippet, ResourceTemplates, XBlock):
|
||||
metadata_translations = {
|
||||
'slug': 'url_name',
|
||||
'name': 'display_name',
|
||||
}
|
||||
}
|
||||
|
||||
# ============================= STRUCTURAL MANIPULATION ===================
|
||||
def __init__(self,
|
||||
@@ -458,7 +460,6 @@ class XModuleDescriptor(XModuleFields, HTMLSnippet, ResourceTemplates, XBlock):
|
||||
"""
|
||||
return False
|
||||
|
||||
|
||||
# ================================= JSON PARSING ===========================
|
||||
@staticmethod
|
||||
def load_from_json(json_data, system, default_class=None):
|
||||
@@ -523,10 +524,10 @@ class XModuleDescriptor(XModuleFields, HTMLSnippet, ResourceTemplates, XBlock):
|
||||
# ================================= XML PARSING ============================
|
||||
@staticmethod
|
||||
def load_from_xml(xml_data,
|
||||
system,
|
||||
org=None,
|
||||
course=None,
|
||||
default_class=None):
|
||||
system,
|
||||
org=None,
|
||||
course=None,
|
||||
default_class=None):
|
||||
"""
|
||||
This method instantiates the correct subclass of XModuleDescriptor based
|
||||
on the contents of xml_data.
|
||||
@@ -541,7 +542,7 @@ class XModuleDescriptor(XModuleFields, HTMLSnippet, ResourceTemplates, XBlock):
|
||||
class_ = XModuleDescriptor.load_class(
|
||||
etree.fromstring(xml_data).tag,
|
||||
default_class
|
||||
)
|
||||
)
|
||||
# leave next line, commented out - useful for low-level debugging
|
||||
# log.debug('[XModuleDescriptor.load_from_xml] tag=%s, class_=%s' % (
|
||||
# etree.fromstring(xml_data).tag,class_))
|
||||
@@ -625,7 +626,7 @@ class XModuleDescriptor(XModuleFields, HTMLSnippet, ResourceTemplates, XBlock):
|
||||
"""
|
||||
inherited_metadata = getattr(self, '_inherited_metadata', {})
|
||||
inheritable_metadata = getattr(self, '_inheritable_metadata', {})
|
||||
metadata = {}
|
||||
metadata_fields = {}
|
||||
for field in self.fields:
|
||||
|
||||
if field.scope != Scope.settings or field in self.non_editable_metadata_fields:
|
||||
@@ -641,13 +642,39 @@ class XModuleDescriptor(XModuleFields, HTMLSnippet, ResourceTemplates, XBlock):
|
||||
if field.name in inherited_metadata:
|
||||
explicitly_set = False
|
||||
|
||||
metadata[field.name] = {'field': field,
|
||||
'value': value,
|
||||
'default_value': default_value,
|
||||
'inheritable': inheritable,
|
||||
'explicitly_set': explicitly_set }
|
||||
# We support the following editors:
|
||||
# 1. A select editor for fields with a list of possible values (includes Booleans).
|
||||
# 2. Number editors for integers and floats.
|
||||
# 3. A generic string editor for anything else (editing JSON representation of the value).
|
||||
type = "Generic"
|
||||
values = [] if field.values is None else copy.deepcopy(field.values)
|
||||
if isinstance(values, tuple):
|
||||
values = list(values)
|
||||
if isinstance(values, list):
|
||||
if len(values) > 0:
|
||||
type = "Select"
|
||||
for index, choice in enumerate(values):
|
||||
json_choice = copy.deepcopy(choice)
|
||||
if isinstance(json_choice, dict) and 'value' in json_choice:
|
||||
json_choice['value'] = field.to_json(json_choice['value'])
|
||||
else:
|
||||
json_choice = field.to_json(json_choice)
|
||||
values[index] = json_choice
|
||||
elif isinstance(field, Integer):
|
||||
type = "Integer"
|
||||
elif isinstance(field, Float):
|
||||
type = "Float"
|
||||
metadata_fields[field.name] = {'field_name': field.name,
|
||||
'type': type,
|
||||
'display_name': field.display_name,
|
||||
'value': field.to_json(value),
|
||||
'options': values,
|
||||
'default_value': field.to_json(default_value),
|
||||
'inheritable': inheritable,
|
||||
'explicitly_set': explicitly_set,
|
||||
'help': field.help}
|
||||
|
||||
return metadata
|
||||
return metadata_fields
|
||||
|
||||
|
||||
class DescriptorSystem(object):
|
||||
@@ -740,7 +767,7 @@ class ModuleSystem(object):
|
||||
s3_interface=None,
|
||||
cache=None,
|
||||
can_execute_unsafe_code=None,
|
||||
):
|
||||
):
|
||||
'''
|
||||
Create a closure around the system environment.
|
||||
|
||||
|
||||
87
common/static/css/vendor/html5-input-polyfills/number-polyfill.css
vendored
Normal file
87
common/static/css/vendor/html5-input-polyfills/number-polyfill.css
vendored
Normal file
@@ -0,0 +1,87 @@
|
||||
/* 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%; }
|
||||
298
common/static/js/vendor/html5-input-polyfills/number-polyfill.js
vendored
Normal file
298
common/static/js/vendor/html5-input-polyfills/number-polyfill.js
vendored
Normal file
@@ -0,0 +1,298 @@
|
||||
// Generated by CoffeeScript 1.4.0
|
||||
|
||||
/*
|
||||
HTML5 Number polyfill | Jonathan Stipe | https://github.com/jonstipe/number-polyfill
|
||||
*/
|
||||
|
||||
(function() {
|
||||
|
||||
(function($) {
|
||||
var i;
|
||||
i = document.createElement("input");
|
||||
i.setAttribute("type", "number");
|
||||
if (i.type === "text") {
|
||||
$.fn.inputNumber = function() {
|
||||
var clipValues, decrement, domMouseScrollHandler, extractNumDecimalDigits, getParams, increment, matchStep, mouseWheelHandler;
|
||||
getParams = function(elem) {
|
||||
var $elem, max, min, step, val;
|
||||
$elem = $(elem);
|
||||
step = $elem.attr('step');
|
||||
min = $elem.attr('min');
|
||||
max = $elem.attr('max');
|
||||
val = parseFloat($elem.val());
|
||||
step = /^-?\d+(?:\.\d+)?$/.test(step) ? parseFloat(step) : null;
|
||||
min = /^-?\d+(?:\.\d+)?$/.test(min) ? parseFloat(min) : null;
|
||||
max = /^-?\d+(?:\.\d+)?$/.test(max) ? parseFloat(max) : null;
|
||||
if (isNaN(val)) {
|
||||
val = min || 0;
|
||||
}
|
||||
return {
|
||||
min: min,
|
||||
max: max,
|
||||
step: step,
|
||||
val: val
|
||||
};
|
||||
};
|
||||
clipValues = function(value, min, max) {
|
||||
if ((max != null) && value > max) {
|
||||
return max;
|
||||
} else if ((min != null) && value < min) {
|
||||
return min;
|
||||
} else {
|
||||
return value;
|
||||
}
|
||||
};
|
||||
extractNumDecimalDigits = function(input) {
|
||||
var num, raisedNum;
|
||||
if (input != null) {
|
||||
num = 0;
|
||||
raisedNum = input;
|
||||
while (raisedNum !== Math.round(raisedNum)) {
|
||||
num += 1;
|
||||
raisedNum = input * Math.pow(10, num);
|
||||
}
|
||||
return num;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
};
|
||||
matchStep = function(value, min, max, step) {
|
||||
var mod, raiseTo, raisedMod, raisedStep, raisedStepDown, raisedStepUp, raisedValue, stepDecimalDigits, stepDown, stepUp;
|
||||
stepDecimalDigits = extractNumDecimalDigits(step);
|
||||
if (step == null) {
|
||||
return value;
|
||||
} else if (stepDecimalDigits === 0) {
|
||||
mod = (value - (min || 0)) % step;
|
||||
if (mod === 0) {
|
||||
return value;
|
||||
} else {
|
||||
stepDown = value - mod;
|
||||
stepUp = stepDown + step;
|
||||
if ((stepUp > max) || ((value - stepDown) < (stepUp - value))) {
|
||||
return stepDown;
|
||||
} else {
|
||||
return stepUp;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
raiseTo = Math.pow(10, stepDecimalDigits);
|
||||
raisedStep = step * raiseTo;
|
||||
raisedMod = (value - (min || 0)) * raiseTo % raisedStep;
|
||||
if (raisedMod === 0) {
|
||||
return value;
|
||||
} else {
|
||||
raisedValue = value * raiseTo;
|
||||
raisedStepDown = raisedValue - raisedMod;
|
||||
raisedStepUp = raisedStepDown + raisedStep;
|
||||
if (((raisedStepUp / raiseTo) > max) || ((raisedValue - raisedStepDown) < (raisedStepUp - raisedValue))) {
|
||||
return raisedStepDown / raiseTo;
|
||||
} else {
|
||||
return raisedStepUp / raiseTo;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
increment = function(elem) {
|
||||
var newVal, params, raiseTo;
|
||||
if (!$(elem).is(":disabled")) {
|
||||
params = getParams(elem);
|
||||
raiseTo = Math.pow(10, Math.max(extractNumDecimalDigits(params['val']), extractNumDecimalDigits(params['step'])));
|
||||
newVal = (Math.round(params['val'] * raiseTo) + Math.round((params['step'] || 1) * raiseTo)) / raiseTo;
|
||||
if ((params['max'] != null) && newVal > params['max']) {
|
||||
newVal = params['max'];
|
||||
}
|
||||
newVal = matchStep(newVal, params['min'], params['max'], params['step']);
|
||||
$(elem).val(newVal).change();
|
||||
}
|
||||
return null;
|
||||
};
|
||||
decrement = function(elem) {
|
||||
var newVal, params, raiseTo;
|
||||
if (!$(elem).is(":disabled")) {
|
||||
params = getParams(elem);
|
||||
raiseTo = Math.pow(10, Math.max(extractNumDecimalDigits(params['val']), extractNumDecimalDigits(params['step'])));
|
||||
newVal = (Math.round(params['val'] * raiseTo) - Math.round((params['step'] || 1) * raiseTo)) / raiseTo;
|
||||
if ((params['min'] != null) && newVal < params['min']) {
|
||||
newVal = params['min'];
|
||||
}
|
||||
newVal = matchStep(newVal, params['min'], params['max'], params['step']);
|
||||
$(elem).val(newVal).change();
|
||||
}
|
||||
return null;
|
||||
};
|
||||
domMouseScrollHandler = function(e) {
|
||||
e.preventDefault();
|
||||
if (e.originalEvent.detail < 0) {
|
||||
increment(this);
|
||||
} else {
|
||||
decrement(this);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
mouseWheelHandler = function(e) {
|
||||
e.preventDefault();
|
||||
if (e.originalEvent.wheelDelta > 0) {
|
||||
increment(this);
|
||||
} else {
|
||||
decrement(this);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
$(this).filter('input[type="number"]').each(function() {
|
||||
var $downBtn, $elem, $upBtn, attrMutationCallback, attrObserver, btnContainer, downBtn, elem, halfHeight, upBtn;
|
||||
elem = this;
|
||||
$elem = $(elem);
|
||||
halfHeight = ($elem.outerHeight() / 6) + 'px';
|
||||
upBtn = document.createElement('div');
|
||||
downBtn = document.createElement('div');
|
||||
$upBtn = $(upBtn);
|
||||
$downBtn = $(downBtn);
|
||||
btnContainer = document.createElement('div');
|
||||
$upBtn.addClass('number-spin-btn number-spin-btn-up').css('height', halfHeight);
|
||||
$downBtn.addClass('number-spin-btn number-spin-btn-down').css('height', halfHeight);
|
||||
btnContainer.appendChild(upBtn);
|
||||
btnContainer.appendChild(downBtn);
|
||||
$(btnContainer).addClass('number-spin-btn-container').insertAfter(elem);
|
||||
$elem.on({
|
||||
focus: function(e) {
|
||||
$elem.on({
|
||||
DOMMouseScroll: domMouseScrollHandler,
|
||||
mousewheel: mouseWheelHandler
|
||||
});
|
||||
return null;
|
||||
},
|
||||
blur: function(e) {
|
||||
$elem.off({
|
||||
DOMMouseScroll: domMouseScrollHandler,
|
||||
mousewheel: mouseWheelHandler
|
||||
});
|
||||
return null;
|
||||
},
|
||||
keypress: function(e) {
|
||||
var _ref, _ref1;
|
||||
if (e.keyCode === 38) {
|
||||
increment(this);
|
||||
} else if (e.keyCode === 40) {
|
||||
decrement(this);
|
||||
} else if (((_ref = e.keyCode) !== 8 && _ref !== 9 && _ref !== 35 && _ref !== 36 && _ref !== 37 && _ref !== 39) && ((_ref1 = e.which) !== 45 && _ref1 !== 46 && _ref1 !== 48 && _ref1 !== 49 && _ref1 !== 50 && _ref1 !== 51 && _ref1 !== 52 && _ref1 !== 53 && _ref1 !== 54 && _ref1 !== 55 && _ref1 !== 56 && _ref1 !== 57)) {
|
||||
e.preventDefault();
|
||||
}
|
||||
return null;
|
||||
},
|
||||
change: function(e) {
|
||||
var newVal, params;
|
||||
if (e.originalEvent != null) {
|
||||
params = getParams(this);
|
||||
newVal = clipValues(params['val'], params['min'], params['max']);
|
||||
newVal = matchStep(newVal, params['min'], params['max'], params['step'], params['stepDecimal']);
|
||||
$(this).val(newVal);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
});
|
||||
$upBtn.on("mousedown", function(e) {
|
||||
var releaseFunc, timeoutFunc;
|
||||
increment(elem);
|
||||
timeoutFunc = function(elem, incFunc) {
|
||||
incFunc(elem);
|
||||
$elem.data("timeoutID", window.setTimeout(timeoutFunc, 10, elem, incFunc));
|
||||
return null;
|
||||
};
|
||||
releaseFunc = function(e) {
|
||||
window.clearTimeout($elem.data("timeoutID"));
|
||||
$(document).off('mouseup', releaseFunc);
|
||||
$upBtn.off('mouseleave', releaseFunc);
|
||||
return null;
|
||||
};
|
||||
$(document).on('mouseup', releaseFunc);
|
||||
$upBtn.on('mouseleave', releaseFunc);
|
||||
$elem.data("timeoutID", window.setTimeout(timeoutFunc, 700, elem, increment));
|
||||
return null;
|
||||
});
|
||||
$downBtn.on("mousedown", function(e) {
|
||||
var releaseFunc, timeoutFunc;
|
||||
decrement(elem);
|
||||
timeoutFunc = function(elem, decFunc) {
|
||||
decFunc(elem);
|
||||
$elem.data("timeoutID", window.setTimeout(timeoutFunc, 10, elem, decFunc));
|
||||
return null;
|
||||
};
|
||||
releaseFunc = function(e) {
|
||||
window.clearTimeout($elem.data("timeoutID"));
|
||||
$(document).off('mouseup', releaseFunc);
|
||||
$downBtn.off('mouseleave', releaseFunc);
|
||||
return null;
|
||||
};
|
||||
$(document).on('mouseup', releaseFunc);
|
||||
$downBtn.on('mouseleave', releaseFunc);
|
||||
$elem.data("timeoutID", window.setTimeout(timeoutFunc, 700, elem, decrement));
|
||||
return null;
|
||||
});
|
||||
$elem.css("textAlign", 'left');
|
||||
if ($elem.css("opacity") !== "1") {
|
||||
$(btnContainer).css("opacity", $elem.css("opacity"));
|
||||
}
|
||||
if ($elem.css("visibility") !== "visible") {
|
||||
$(btnContainer).css("visibility", $elem.css("visibility"));
|
||||
}
|
||||
if (elem.style.display !== "") {
|
||||
$(btnContainer).css("display", $elem.css("display"));
|
||||
}
|
||||
if ((typeof WebKitMutationObserver !== "undefined" && WebKitMutationObserver !== null) || (typeof MutationObserver !== "undefined" && MutationObserver !== null)) {
|
||||
attrMutationCallback = function(mutations, observer) {
|
||||
var mutation, _i, _len;
|
||||
for (_i = 0, _len = mutations.length; _i < _len; _i++) {
|
||||
mutation = mutations[_i];
|
||||
if (mutation.type === "attributes") {
|
||||
if (mutation.attributeName === "class") {
|
||||
$(btnContainer).removeClass(mutation.oldValue).addClass(elem.className);
|
||||
} else if (mutation.attributeName === "style") {
|
||||
$(btnContainer).css({
|
||||
"opacity": elem.style.opacity,
|
||||
"visibility": elem.style.visibility,
|
||||
"display": elem.style.display
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
attrObserver = (typeof WebKitMutationObserver !== "undefined" && WebKitMutationObserver !== null) ? new WebKitMutationObserver(attrMutationCallback) : ((typeof MutationObserver !== "undefined" && MutationObserver !== null) ? new MutationObserver(attrMutationCallback) : null);
|
||||
attrObserver.observe(elem, {
|
||||
attributes: true,
|
||||
attributeOldValue: true,
|
||||
attributeFilter: ["class", "style"]
|
||||
});
|
||||
} else if (typeof MutationEvent !== "undefined" && MutationEvent !== null) {
|
||||
$elem.on("DOMAttrModified", function(evt) {
|
||||
if (evt.originalEvent.attrName === "class") {
|
||||
$(btnContainer).removeClass(evt.originalEvent.prevValue).addClass(evt.originalEvent.newValue);
|
||||
} else if (evt.originalEvent.attrName === "style") {
|
||||
$(btnContainer).css({
|
||||
"display": elem.style.display,
|
||||
"visibility": elem.style.visibility,
|
||||
"opacity": elem.style.opacity
|
||||
});
|
||||
}
|
||||
return null;
|
||||
});
|
||||
}
|
||||
return null;
|
||||
});
|
||||
return $(this);
|
||||
};
|
||||
$(function() {
|
||||
$('input[type="number"]').inputNumber();
|
||||
return null;
|
||||
});
|
||||
null;
|
||||
} else {
|
||||
$.fn.inputNumber = function() {
|
||||
return $(this);
|
||||
};
|
||||
null;
|
||||
}
|
||||
return null;
|
||||
})(jQuery);
|
||||
|
||||
}).call(this);
|
||||
@@ -25,8 +25,8 @@
|
||||
|
||||
/* Layout */
|
||||
.studioSkin table.mceLayout {border:0;}
|
||||
.studioSkin table.mceLayout tr.mceFirst td {border-top:1px solid #3c3c3c;}
|
||||
.studioSkin table.mceLayout tr.mceLast td {border-bottom:1px solid #3c3c3c;}
|
||||
.studioSkin table.mceLayout tr.mceFirst td {border-top: 1px solid #D1DCE6; border-left: none; border-right:none;}
|
||||
.studioSkin table.mceLayout tr.mceLast td {border-bottom:none;}
|
||||
.studioSkin table.mceToolbar, .studioSkin tr.mceFirst .mceToolbar tr td, .studioSkin tr.mceLast .mceToolbar tr td {border:0; margin:0; padding:0;}
|
||||
.studioSkin td.mceToolbar {
|
||||
background: -webkit-linear-gradient(top, #d4dee8, #c9d5e2);
|
||||
@@ -36,11 +36,11 @@
|
||||
background: linear-gradient(top, #d4dee8, #c9d5e2);
|
||||
border: 1px solid #3c3c3c;
|
||||
border-bottom-color: #a5aaaf;
|
||||
border-radius: 3px 3px 0 0;
|
||||
border-radius: 0;
|
||||
padding: 10px 10px 9px;
|
||||
vertical-align: top;
|
||||
}
|
||||
.studioSkin .mceIframeContainer {border: 1px solid #3c3c3c; border-top: none;}
|
||||
.studioSkin .mceIframeContainer {border: 1px solid white; border-top: none;}
|
||||
.studioSkin .mceStatusbar {background:#F0F0EE; font-size:9pt; line-height:16px; overflow:visible; color:#000; display:block; height:20px}
|
||||
.studioSkin .mceStatusbar div {float:left; margin:2px}
|
||||
.studioSkin .mceStatusbar a.mceResize {display:block; float:right; background:url(../../img/studio-icons.png) -800px 0; width:20px; height:20px; cursor:se-resize; outline:0}
|
||||
|
||||
@@ -85,8 +85,8 @@
|
||||
}
|
||||
|
||||
@-webkit-keyframes fadeIn {
|
||||
0% { opacity: 0; }
|
||||
100% { opacity: 1; }
|
||||
0% { opacity: 0.0; }
|
||||
100% { opacity: 1.0; }
|
||||
}
|
||||
|
||||
|
||||
@@ -736,11 +736,11 @@ body.discussion {
|
||||
|
||||
&.is-open {
|
||||
.browse-topic-drop-btn span {
|
||||
opacity: 1;
|
||||
opacity: 1.0;
|
||||
}
|
||||
|
||||
.browse-topic-drop-icon {
|
||||
opacity: 0;
|
||||
opacity: 0.0;
|
||||
}
|
||||
|
||||
&.is-dropped {
|
||||
@@ -788,7 +788,7 @@ body.discussion {
|
||||
&::-webkit-input-placeholder,
|
||||
&:-moz-placeholder,
|
||||
&:-ms-input-placeholder {
|
||||
opacity: 1;
|
||||
opacity: 1.0;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -818,7 +818,7 @@ body.discussion {
|
||||
line-height: 58px;
|
||||
color: #333;
|
||||
text-shadow: 0 1px 0 rgba(255, 255, 255, .8);
|
||||
opacity: 0;
|
||||
opacity: 0.0;
|
||||
@include transition(opacity .2s);
|
||||
}
|
||||
}
|
||||
@@ -833,7 +833,7 @@ body.discussion {
|
||||
height: 16px;
|
||||
margin-left: -12px;
|
||||
background: url(../images/browse-icon.png) no-repeat;
|
||||
opacity: 1;
|
||||
opacity: 1.0;
|
||||
@include transition(none);
|
||||
}
|
||||
|
||||
@@ -967,7 +967,7 @@ body.discussion {
|
||||
&::-webkit-input-placeholder,
|
||||
&:-moz-placeholder,
|
||||
&:-ms-input-placeholder {
|
||||
opacity: 0;
|
||||
opacity: 0.0;
|
||||
@include transition(opacity .2s);
|
||||
}
|
||||
|
||||
@@ -2454,7 +2454,7 @@ body.discussion {
|
||||
font-style: italic;
|
||||
cursor:pointer;
|
||||
margin-right: 10px;
|
||||
opacity:.8;
|
||||
opacity: 0.8;
|
||||
|
||||
span {
|
||||
cursor: pointer;
|
||||
@@ -2462,7 +2462,7 @@ body.discussion {
|
||||
|
||||
&:hover {
|
||||
@include transition(opacity .2s);
|
||||
opacity: 1;
|
||||
opacity: 1.0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2475,7 +2475,7 @@ body.discussion {
|
||||
top:-13px;
|
||||
margin-right:35px;
|
||||
margin-top:13px;
|
||||
opacity: 1;
|
||||
opacity: 1.0;
|
||||
}
|
||||
|
||||
.notpinned .icon {
|
||||
@@ -2523,11 +2523,11 @@ display:none;
|
||||
padding-right: 5px;
|
||||
font-style: italic;
|
||||
cursor:pointer;
|
||||
opacity:.8;
|
||||
opacity: 0.8;
|
||||
|
||||
&:hover {
|
||||
@include transition(opacity .2s);
|
||||
opacity: 1;
|
||||
opacity: 1.0;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -9,12 +9,12 @@
|
||||
|
||||
@mixin home-header-pop-up-keyframes {
|
||||
0% {
|
||||
opacity: 0;
|
||||
opacity: 0.0;
|
||||
top: 300px;
|
||||
//@include transform(scale(0.9));
|
||||
}
|
||||
45% {
|
||||
opacity: 1;
|
||||
opacity: 1.0;
|
||||
}
|
||||
65% {
|
||||
top: -40px;
|
||||
@@ -43,19 +43,19 @@
|
||||
|
||||
@mixin title-appear-keyframes {
|
||||
0% {
|
||||
opacity: 0;
|
||||
opacity: 0.0;
|
||||
top: 60px;
|
||||
@include transform(scale(0.9));
|
||||
}
|
||||
20% {
|
||||
opacity: 1;
|
||||
opacity: 1.0;
|
||||
}
|
||||
27% { // this % of total-time should be ~ 1.25s
|
||||
top: 40px;
|
||||
@include transform(scale(1));
|
||||
}
|
||||
90% { // this % of total-time is when 2nd half of animation starts
|
||||
opacity: 1;
|
||||
opacity: 1.0;
|
||||
top: 40px;
|
||||
@include transform(scale(1));
|
||||
}
|
||||
@@ -79,24 +79,24 @@
|
||||
|
||||
@mixin home-appear-keyframes {
|
||||
0% {
|
||||
opacity: 0;
|
||||
opacity: 0.0;
|
||||
top: 60px;
|
||||
@include transform(scale(0.9));
|
||||
}
|
||||
20% {
|
||||
opacity: 1;
|
||||
opacity: 1.0;
|
||||
}
|
||||
30% { // this % of total-time should be ~ 1.25s
|
||||
top: 40px;
|
||||
@include transform(scale(1));
|
||||
}
|
||||
80% { // this % of total-time is when 2nd half of animation starts
|
||||
opacity: 1;
|
||||
opacity: 1.0;
|
||||
top: 40px;
|
||||
@include transform(scale(1));
|
||||
}
|
||||
100% {
|
||||
opacity: 0;
|
||||
opacity: 0.0;
|
||||
top: 60px;
|
||||
@include transform(scale(0.7));
|
||||
}
|
||||
@@ -117,10 +117,10 @@
|
||||
|
||||
@mixin edx-appear-keyframes {
|
||||
0% {
|
||||
opacity: 0;
|
||||
opacity: 0.0;
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
opacity: 1.0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -231,7 +231,7 @@
|
||||
opacity: 0.9;
|
||||
}
|
||||
80% {
|
||||
opacity: 1;
|
||||
opacity: 1.0;
|
||||
}
|
||||
100% {
|
||||
bottom: 0px;
|
||||
|
||||
@@ -38,7 +38,7 @@ div.book-wrapper {
|
||||
line-height: 2.1em;
|
||||
text-align: right;
|
||||
color: #9a9a9a;
|
||||
opacity: 0;
|
||||
opacity: 0.0;
|
||||
@include transition(opacity .15s);
|
||||
}
|
||||
|
||||
@@ -55,7 +55,7 @@ div.book-wrapper {
|
||||
background-color: transparent;
|
||||
|
||||
.page-number {
|
||||
opacity: 1;
|
||||
opacity: 1.0;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -119,7 +119,7 @@ div.book-wrapper {
|
||||
@include box-sizing(border-box);
|
||||
display: table;
|
||||
height: 100%;
|
||||
opacity: 0;
|
||||
opacity: 0.0;
|
||||
filter: alpha(opacity=0);
|
||||
text-indent: -9999px;
|
||||
@include transition;
|
||||
@@ -127,7 +127,7 @@ div.book-wrapper {
|
||||
width: 100%;
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
opacity: 1.0;
|
||||
filter: alpha(opacity=100);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,7 +61,7 @@ section.course-index {
|
||||
span.ui-icon {
|
||||
left: 0;
|
||||
background-image: url("/static/images/ui-icons_222222_256x240.png");
|
||||
opacity: .3;
|
||||
opacity: 0.3;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -146,7 +146,7 @@ section.course-index {
|
||||
@include box-shadow(inset 0 1px 14px 0 rgba(0,0,0, 0.1));
|
||||
|
||||
&:after {
|
||||
opacity: 1;
|
||||
opacity: 1.0;
|
||||
right: 15px;
|
||||
}
|
||||
}
|
||||
@@ -174,7 +174,7 @@ section.course-index {
|
||||
background: $sidebar-active-image;
|
||||
|
||||
&:after {
|
||||
opacity: 1;
|
||||
opacity: 1.0;
|
||||
right: 15px;
|
||||
}
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ div.calc-main {
|
||||
width: 16px;
|
||||
|
||||
&:hover {
|
||||
opacity: .8;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
&.closed {
|
||||
@@ -136,7 +136,7 @@ div.calc-main {
|
||||
|
||||
&.shown {
|
||||
display: block;
|
||||
opacity: 1;
|
||||
opacity: 1.0;
|
||||
}
|
||||
|
||||
dt {
|
||||
|
||||
@@ -26,7 +26,7 @@ header.global {
|
||||
}
|
||||
|
||||
h2 {
|
||||
opacity: 1;
|
||||
opacity: 1.0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,7 +51,7 @@ header.global {
|
||||
text-decoration: none;
|
||||
|
||||
&::before {
|
||||
opacity: 1;
|
||||
opacity: 1.0;
|
||||
}
|
||||
|
||||
.name {
|
||||
|
||||
@@ -429,7 +429,7 @@
|
||||
|
||||
&:hover {
|
||||
.sharing-message {
|
||||
opacity: 1;
|
||||
opacity: 1.0;
|
||||
top: 56px;
|
||||
}
|
||||
}
|
||||
@@ -470,7 +470,7 @@
|
||||
width: 44px;
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
opacity: 1.0;
|
||||
}
|
||||
|
||||
img {
|
||||
@@ -514,7 +514,7 @@
|
||||
|
||||
&:hover {
|
||||
.icon {
|
||||
opacity: 1;
|
||||
opacity: 1.0;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -74,7 +74,7 @@
|
||||
|
||||
&:hover {
|
||||
.title .icon {
|
||||
opacity: 1;
|
||||
opacity: 1.0;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -53,7 +53,7 @@
|
||||
@include box-sizing(border-box);
|
||||
@include inline-block;
|
||||
left: 0px;
|
||||
opacity: 1;
|
||||
opacity: 1.0;
|
||||
padding: 20px 30px;
|
||||
top: 0px;
|
||||
@include transition(all, 0.2s, linear);
|
||||
@@ -312,7 +312,7 @@
|
||||
text-decoration: none;
|
||||
|
||||
&::before {
|
||||
opacity: 1;
|
||||
opacity: 1.0;
|
||||
}
|
||||
|
||||
.name {
|
||||
|
||||
@@ -117,7 +117,7 @@
|
||||
|
||||
.info-link {
|
||||
color: $link-color;
|
||||
opacity: 1;
|
||||
opacity: 1.0;
|
||||
}
|
||||
|
||||
h2 {
|
||||
|
||||
Reference in New Issue
Block a user