Fix merge conflict.
This commit is contained in:
@@ -27,3 +27,59 @@ def click_component_from_menu(instance_id, expected_css):
|
||||
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]])
|
||||
@@ -34,6 +34,8 @@ from xmodule.course_module import CourseDescriptor
|
||||
from xmodule.seq_module import SequenceDescriptor
|
||||
from xmodule.modulestore.exceptions import ItemNotFoundError
|
||||
|
||||
from contentstore.views.component import ADVANCED_COMPONENT_TYPES
|
||||
|
||||
from django_comment_common.utils import are_permissions_roles_seeded
|
||||
|
||||
TEST_DATA_MODULESTORE = copy.deepcopy(settings.MODULESTORE)
|
||||
@@ -75,6 +77,31 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
|
||||
self.client = Client()
|
||||
self.client.login(username=uname, password=password)
|
||||
|
||||
def test_advanced_components_in_edit_unit(self):
|
||||
store = modulestore('direct')
|
||||
import_from_xml(store, 'common/test/data/', ['simple'])
|
||||
|
||||
course = store.get_item(Location(['i4x', 'edX', 'simple',
|
||||
'course', '2012_Fall', None]), depth=None)
|
||||
|
||||
course.advanced_modules = ADVANCED_COMPONENT_TYPES
|
||||
|
||||
store.update_metadata(course.location, own_metadata(course))
|
||||
|
||||
# just pick one vertical
|
||||
descriptor = store.get_items(Location('i4x', 'edX', 'simple', 'vertical', None, None))[0]
|
||||
|
||||
resp = self.client.get(reverse('edit_unit', kwargs={'location': descriptor.location.url()}))
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
# This could be made better, but for now let's just assert that we see the advanced modules mentioned in the page
|
||||
# response HTML
|
||||
self.assertIn('Video Alpha', resp.content)
|
||||
self.assertIn('Word cloud', resp.content)
|
||||
self.assertIn('Annotation', resp.content)
|
||||
self.assertIn('Open Ended Response', resp.content)
|
||||
self.assertIn('Peer Grading Interface', resp.content)
|
||||
|
||||
def check_edit_unit(self, test_course_name):
|
||||
import_from_xml(modulestore('direct'), 'common/test/data/', [test_course_name])
|
||||
|
||||
|
||||
@@ -111,6 +111,18 @@ class AuthTestCase(ContentStoreTestCase):
|
||||
# Now login should work
|
||||
self.login(self.email, self.pw)
|
||||
|
||||
def test_login_link_on_activation_age(self):
|
||||
self.create_account(self.username, self.email, self.pw)
|
||||
# we want to test the rendering of the activation page when the user isn't logged in
|
||||
self.client.logout()
|
||||
resp = self._activate_user(self.email)
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
# check the the HTML has links to the right login page. Note that this is merely a content
|
||||
# check and thus could be fragile should the wording change on this page
|
||||
expected = 'You can now <a href="' + reverse('login') + '">login</a>.'
|
||||
self.assertIn(expected, resp.content)
|
||||
|
||||
def test_private_pages_auth(self):
|
||||
"""Make sure pages that do require login work."""
|
||||
auth_pages = (
|
||||
|
||||
@@ -42,7 +42,7 @@ COMPONENT_TYPES = ['customtag', 'discussion', 'html', 'problem', 'video']
|
||||
|
||||
OPEN_ENDED_COMPONENT_TYPES = ["combinedopenended", "peergrading"]
|
||||
NOTE_COMPONENT_TYPES = ['notes']
|
||||
ADVANCED_COMPONENT_TYPES = ['annotatable', 'word_cloud'] + OPEN_ENDED_COMPONENT_TYPES + NOTE_COMPONENT_TYPES
|
||||
ADVANCED_COMPONENT_TYPES = ['annotatable', 'word_cloud', 'videoalpha'] + OPEN_ENDED_COMPONENT_TYPES + NOTE_COMPONENT_TYPES
|
||||
ADVANCED_COMPONENT_CATEGORY = 'advanced'
|
||||
ADVANCED_COMPONENT_POLICY_KEY = 'advanced_modules'
|
||||
|
||||
@@ -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 = [
|
||||
|
||||
@@ -226,7 +226,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;
|
||||
|
||||
@@ -61,6 +61,8 @@
|
||||
<div class="wrapper wrapper-view">
|
||||
<%include file="widgets/header.html" />
|
||||
|
||||
## remove this block after advanced settings notification is rewritten
|
||||
<%block name="view_alerts"></%block>
|
||||
<div id="page-alert"></div>
|
||||
|
||||
<%block name="content"></%block>
|
||||
@@ -72,9 +74,13 @@
|
||||
<%include file="widgets/footer.html" />
|
||||
<%include file="widgets/tender.html" />
|
||||
|
||||
## remove this block after advanced settings notification is rewritten
|
||||
<%block name="view_notifications"></%block>
|
||||
<div id="page-notification"></div>
|
||||
</div>
|
||||
|
||||
## remove this block after advanced settings notification is rewritten
|
||||
<%block name="view_prompts"></%block>
|
||||
<div id="page-prompt"></div>
|
||||
<%block name="jsextra"></%block>
|
||||
</body>
|
||||
|
||||
@@ -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>
|
||||
@@ -3,6 +3,12 @@
|
||||
|
||||
<%namespace name='static' file='../static_content.html'/>
|
||||
|
||||
%if not user_logged_in:
|
||||
<%block name="bodyclass">
|
||||
not-signedin
|
||||
</%block>
|
||||
%endif
|
||||
|
||||
<%block name="content">
|
||||
<section class="container activation">
|
||||
|
||||
@@ -18,7 +24,7 @@
|
||||
%if user_logged_in:
|
||||
Visit your <a href="/">dashboard</a> to see your courses.
|
||||
%else:
|
||||
You can now <a href="#login-modal" rel="leanModal">login</a>.
|
||||
You can now <a href="${reverse('login')}">login</a>.
|
||||
%endif
|
||||
</p>
|
||||
</section>
|
||||
|
||||
@@ -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}" id="${location}">
|
||||
<span class="name"> ${name}</span>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
% else:
|
||||
<li class="editor-md">
|
||||
<a href="#" data-location="${location}" id="${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,13 @@
|
||||
% 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}" id="${location}">
|
||||
<span class="name">${name}</span>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
% else:
|
||||
<li class="editor-manual">
|
||||
<a href="#" data-location="${location}" id="${location}">
|
||||
<span class="name"> ${name}</span>
|
||||
</a>
|
||||
|
||||
</li>
|
||||
% endif
|
||||
<li class="editor-manual">
|
||||
<a href="#" id="${location}" data-location="${location}">
|
||||
<span class="name"> ${name}</span>
|
||||
</a>
|
||||
</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)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user